Add genlocke list and detail pages
Add GET /genlockes and GET /genlockes/{id} endpoints with aggregate
encounter/death stats per leg, and a frontend list page at /genlockes
plus a detail page at /genlockes/:genlockeId showing progress timeline,
cumulative stats, configuration, retired families, and quick actions.
Update nav link to point to the list page instead of /genlockes/new.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -10,12 +10,148 @@ from app.models.evolution import Evolution
|
||||
from app.models.game import Game
|
||||
from app.models.genlocke import Genlocke, GenlockeLeg
|
||||
from app.models.nuzlocke_run import NuzlockeRun
|
||||
from app.schemas.genlocke import GenlockeCreate, GenlockeResponse
|
||||
from app.models.pokemon import Pokemon
|
||||
from app.schemas.genlocke import (
|
||||
GenlockeCreate,
|
||||
GenlockeDetailResponse,
|
||||
GenlockeLegDetailResponse,
|
||||
GenlockeListItem,
|
||||
GenlockeResponse,
|
||||
GenlockeStatsResponse,
|
||||
RetiredPokemonResponse,
|
||||
)
|
||||
from app.services.families import build_families
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=list[GenlockeListItem])
|
||||
async def list_genlockes(session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(
|
||||
select(Genlocke)
|
||||
.options(
|
||||
selectinload(Genlocke.legs).selectinload(GenlockeLeg.run),
|
||||
)
|
||||
.order_by(Genlocke.created_at.desc())
|
||||
)
|
||||
genlockes = result.scalars().all()
|
||||
|
||||
items = []
|
||||
for g in genlockes:
|
||||
completed_legs = 0
|
||||
current_leg_order = None
|
||||
for leg in g.legs:
|
||||
if leg.run and leg.run.status == "completed":
|
||||
completed_legs += 1
|
||||
elif leg.run and leg.run.status == "active":
|
||||
current_leg_order = leg.leg_order
|
||||
|
||||
items.append(
|
||||
GenlockeListItem(
|
||||
id=g.id,
|
||||
name=g.name,
|
||||
status=g.status,
|
||||
created_at=g.created_at,
|
||||
total_legs=len(g.legs),
|
||||
completed_legs=completed_legs,
|
||||
current_leg_order=current_leg_order,
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
@router.get("/{genlocke_id}", response_model=GenlockeDetailResponse)
|
||||
async def get_genlocke(
|
||||
genlocke_id: int, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
result = await session.execute(
|
||||
select(Genlocke)
|
||||
.where(Genlocke.id == genlocke_id)
|
||||
.options(
|
||||
selectinload(Genlocke.legs).selectinload(GenlockeLeg.game),
|
||||
selectinload(Genlocke.legs).selectinload(GenlockeLeg.run),
|
||||
)
|
||||
)
|
||||
genlocke = result.scalar_one_or_none()
|
||||
if genlocke is None:
|
||||
raise HTTPException(status_code=404, detail="Genlocke not found")
|
||||
|
||||
# Collect run IDs for aggregate query
|
||||
run_ids = [leg.run_id for leg in genlocke.legs if leg.run_id is not None]
|
||||
|
||||
stats_by_run: dict[int, tuple[int, int]] = {}
|
||||
if run_ids:
|
||||
stats_result = await session.execute(
|
||||
select(
|
||||
Encounter.run_id,
|
||||
func.count().label("encounter_count"),
|
||||
func.count(Encounter.faint_level).label("death_count"),
|
||||
)
|
||||
.where(Encounter.run_id.in_(run_ids))
|
||||
.group_by(Encounter.run_id)
|
||||
)
|
||||
for row in stats_result:
|
||||
stats_by_run[row.run_id] = (row.encounter_count, row.death_count)
|
||||
|
||||
legs = []
|
||||
total_encounters = 0
|
||||
total_deaths = 0
|
||||
legs_completed = 0
|
||||
for leg in genlocke.legs:
|
||||
run_status = leg.run.status if leg.run else None
|
||||
enc_count, death_count = stats_by_run.get(leg.run_id, (0, 0)) if leg.run_id else (0, 0)
|
||||
total_encounters += enc_count
|
||||
total_deaths += death_count
|
||||
if run_status == "completed":
|
||||
legs_completed += 1
|
||||
|
||||
legs.append(
|
||||
GenlockeLegDetailResponse(
|
||||
id=leg.id,
|
||||
leg_order=leg.leg_order,
|
||||
game=leg.game,
|
||||
run_id=leg.run_id,
|
||||
run_status=run_status,
|
||||
encounter_count=enc_count,
|
||||
death_count=death_count,
|
||||
retired_pokemon_ids=leg.retired_pokemon_ids,
|
||||
)
|
||||
)
|
||||
|
||||
# Fetch retired Pokemon data
|
||||
retired_pokemon: dict[int, RetiredPokemonResponse] = {}
|
||||
all_retired_ids: set[int] = set()
|
||||
for leg in genlocke.legs:
|
||||
if leg.retired_pokemon_ids:
|
||||
all_retired_ids.update(leg.retired_pokemon_ids)
|
||||
|
||||
if all_retired_ids:
|
||||
pokemon_result = await session.execute(
|
||||
select(Pokemon).where(Pokemon.id.in_(all_retired_ids))
|
||||
)
|
||||
for p in pokemon_result.scalars().all():
|
||||
retired_pokemon[p.id] = RetiredPokemonResponse(
|
||||
id=p.id, name=p.name, sprite_url=p.sprite_url
|
||||
)
|
||||
|
||||
return GenlockeDetailResponse(
|
||||
id=genlocke.id,
|
||||
name=genlocke.name,
|
||||
status=genlocke.status,
|
||||
genlocke_rules=genlocke.genlocke_rules,
|
||||
nuzlocke_rules=genlocke.nuzlocke_rules,
|
||||
created_at=genlocke.created_at,
|
||||
legs=legs,
|
||||
stats=GenlockeStatsResponse(
|
||||
total_encounters=total_encounters,
|
||||
total_deaths=total_deaths,
|
||||
legs_completed=legs_completed,
|
||||
total_legs=len(genlocke.legs),
|
||||
),
|
||||
retired_pokemon=retired_pokemon,
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=GenlockeResponse, status_code=201)
|
||||
async def create_genlocke(
|
||||
data: GenlockeCreate, session: AsyncSession = Depends(get_session)
|
||||
|
||||
Reference in New Issue
Block a user