From a3f332f82b5bb01e398a4afbcd18c5818787a841 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Mar 2026 13:38:08 +0100 Subject: [PATCH] feat: show owner info in admin pages - Add Owner column to AdminRuns.tsx and AdminGenlockes.tsx - Add owner filter dropdown to both admin pages - Add owner field to GenlockeListItem schema (resolved from first leg's run) - Update frontend types for GenlockeListItem Co-Authored-By: Claude Opus 4.6 --- ...er-2fp1--show-owner-info-in-admin-pages.md | 43 ++++++++++++++ backend/src/app/api/genlockes.py | 16 ++++- backend/src/app/schemas/genlocke.py | 7 +++ frontend/src/pages/admin/AdminGenlockes.tsx | 58 +++++++++++++++++-- frontend/src/pages/admin/AdminRuns.tsx | 49 +++++++++++++++- frontend/src/types/game.ts | 6 ++ 6 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 .beans/nuzlocke-tracker-2fp1--show-owner-info-in-admin-pages.md diff --git a/.beans/nuzlocke-tracker-2fp1--show-owner-info-in-admin-pages.md b/.beans/nuzlocke-tracker-2fp1--show-owner-info-in-admin-pages.md new file mode 100644 index 0000000..78ff184 --- /dev/null +++ b/.beans/nuzlocke-tracker-2fp1--show-owner-info-in-admin-pages.md @@ -0,0 +1,43 @@ +--- +# nuzlocke-tracker-2fp1 +title: Show owner info in admin pages +status: in-progress +type: feature +priority: normal +created_at: 2026-03-21T12:18:51Z +updated_at: 2026-03-21T12:37:36Z +parent: nuzlocke-tracker-wwnu +--- + +## Problem + +Admin pages (`AdminRuns.tsx`, `AdminGenlockes.tsx`) don't show which user owns each run or genlocke. This makes it hard for admins to manage content. + +## Approach + +### Backend +- The `/api/runs` list endpoint already returns run data — verify it includes `owner` (id + email). If not, add it to the response schema. +- For genlockes, ownership is inferred from the first leg's run owner. Add an `owner` field to the genlocke list response that resolves from the first leg's run. + +### Frontend +- `AdminRuns.tsx`: Add an "Owner" column showing the owner's email (or "No owner" for legacy runs) +- `AdminGenlockes.tsx`: Add an "Owner" column showing the inferred owner from the first leg's run +- Add owner filter dropdown to both pages + +## Files to modify + +- `backend/src/app/api/runs.py` — verify owner is included in list response +- `backend/src/app/api/genlockes.py` — add owner resolution to list endpoint +- `backend/src/app/schemas/genlocke.py` — add owner field to `GenlockeListItem` +- `frontend/src/pages/admin/AdminRuns.tsx` — add Owner column + filter +- `frontend/src/pages/admin/AdminGenlockes.tsx` — add Owner column + filter +- `frontend/src/types/game.ts` — update types if needed + +## Checklist + +- [x] Verify runs list API includes owner info; add if missing +- [x] Add owner resolution to genlocke list endpoint (from first leg's run) +- [x] Update `GenlockeListItem` schema to include owner +- [x] Add Owner column to `AdminRuns.tsx` +- [x] Add Owner column to `AdminGenlockes.tsx` +- [x] Add owner filter to both admin pages diff --git a/backend/src/app/api/genlockes.py b/backend/src/app/api/genlockes.py index 3222b98..a7881d9 100644 --- a/backend/src/app/api/genlockes.py +++ b/backend/src/app/api/genlockes.py @@ -28,6 +28,7 @@ from app.schemas.genlocke import ( GenlockeLegDetailResponse, GenlockeLineageResponse, GenlockeListItem, + GenlockeOwnerResponse, GenlockeResponse, GenlockeStatsResponse, GenlockeUpdate, @@ -81,7 +82,9 @@ async def list_genlockes(session: AsyncSession = Depends(get_session)): result = await session.execute( select(Genlocke) .options( - selectinload(Genlocke.legs).selectinload(GenlockeLeg.run), + selectinload(Genlocke.legs) + .selectinload(GenlockeLeg.run) + .selectinload(NuzlockeRun.owner), ) .order_by(Genlocke.created_at.desc()) ) @@ -91,6 +94,16 @@ async def list_genlockes(session: AsyncSession = Depends(get_session)): for g in genlockes: completed_legs = 0 current_leg_order = None + owner = None + + # Find first leg (leg_order == 1) to get owner + first_leg = next((leg for leg in g.legs if leg.leg_order == 1), None) + if first_leg and first_leg.run and first_leg.run.owner: + owner = GenlockeOwnerResponse( + id=first_leg.run.owner.id, + display_name=first_leg.run.owner.display_name, + ) + for leg in g.legs: if leg.run and leg.run.status == "completed": completed_legs += 1 @@ -106,6 +119,7 @@ async def list_genlockes(session: AsyncSession = Depends(get_session)): total_legs=len(g.legs), completed_legs=completed_legs, current_leg_order=current_leg_order, + owner=owner, ) ) return items diff --git a/backend/src/app/schemas/genlocke.py b/backend/src/app/schemas/genlocke.py index 54cdc58..40d242d 100644 --- a/backend/src/app/schemas/genlocke.py +++ b/backend/src/app/schemas/genlocke.py @@ -1,10 +1,16 @@ from datetime import datetime +from uuid import UUID from app.schemas.base import CamelModel from app.schemas.game import GameResponse from app.schemas.pokemon import PokemonResponse +class GenlockeOwnerResponse(CamelModel): + id: UUID + display_name: str | None = None + + class GenlockeCreate(CamelModel): name: str game_ids: list[int] @@ -92,6 +98,7 @@ class GenlockeListItem(CamelModel): total_legs: int completed_legs: int current_leg_order: int | None = None + owner: GenlockeOwnerResponse | None = None class GenlockeDetailResponse(CamelModel): diff --git a/frontend/src/pages/admin/AdminGenlockes.tsx b/frontend/src/pages/admin/AdminGenlockes.tsx index b68e301..45081be 100644 --- a/frontend/src/pages/admin/AdminGenlockes.tsx +++ b/frontend/src/pages/admin/AdminGenlockes.tsx @@ -13,14 +13,46 @@ export function AdminGenlockes() { const [deleting, setDeleting] = useState(null) const [statusFilter, setStatusFilter] = useState('') + const [ownerFilter, setOwnerFilter] = useState('') const filtered = useMemo(() => { - if (!statusFilter) return genlockes - return genlockes.filter((g) => g.status === statusFilter) - }, [genlockes, statusFilter]) + let result = genlockes + if (statusFilter) result = result.filter((g) => g.status === statusFilter) + if (ownerFilter) { + if (ownerFilter === '__none__') { + result = result.filter((g) => !g.owner) + } else { + result = result.filter((g) => g.owner?.id === ownerFilter) + } + } + return result + }, [genlockes, statusFilter, ownerFilter]) + + const genlockeOwners = useMemo(() => { + const owners = new Map() + let hasUnowned = false + for (const g of genlockes) { + if (g.owner) { + owners.set(g.owner.id, g.owner.displayName ?? g.owner.id) + } else { + hasUnowned = true + } + } + const sorted = [...owners.entries()].sort((a, b) => a[1].localeCompare(b[1])) + return { owners: sorted, hasUnowned } + }, [genlockes]) const columns: Column[] = [ { header: 'Name', accessor: (g) => g.name, sortKey: (g) => g.name }, + { + header: 'Owner', + accessor: (g) => ( + + {g.owner?.displayName ?? g.owner?.id ?? 'No owner'} + + ), + sortKey: (g) => g.owner?.displayName ?? g.owner?.id ?? '', + }, { header: 'Status', accessor: (g) => ( @@ -67,9 +99,25 @@ export function AdminGenlockes() { - {statusFilter && ( + + {(statusFilter || ownerFilter) && (