From d535433583ce2f541172ef8f5cb8f52d9cbbc127 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Feb 2026 16:59:54 +0100 Subject: [PATCH 1/4] Archive 23 completed beans Co-Authored-By: Claude Opus 4.6 --- ...tegration-tests-for-runs-encounters-api.md | 31 ----- ...onent-tests-for-key-frontend-components.md | 21 --- ...--fix-wcag-aa-color-contrast-violations.md | 9 -- ...er-49xj--overhaul-nuzlocke-rules-system.md | 53 -------- ...6i--replace-ci-pipeline-with-test-suite.md | 25 ---- ...egration-tests-for-genlockes-bosses-api.md | 31 ----- ...rebrand-to-another-nuzlocke-tracker-ant.md | 25 ---- ...y--add-type-restriction-rules-monolocke.md | 33 ----- ...-integration-tests-for-games-routes-api.md | 31 ----- ...cp--set-up-frontend-test-infrastructure.md | 33 ----- ...ernize-website-design-and-look-and-feel.md | 127 ------------------ ...-tests-for-frontend-utilities-and-hooks.md | 27 ---- ...glocke-wonderlocke-and-randomizer-rules.md | 10 -- ...-tracker-fv7w--add-team-size-limit-rule.md | 29 ---- ...for-pydantic-schemas-and-model-validati.md | 29 ---- ...ker-iam7--unit-tests-for-services-layer.md | 28 ---- ...r-knnc--add-staticlegendary-clause-rule.md | 38 ------ ...cker-o7r8--remove-unused-nuzlocke-rules.md | 26 ---- ...rcf--set-up-backend-test-infrastructure.md | 35 ----- ...ocke-tracker-sij8--add-gift-clause-rule.md | 26 ---- ...ration-tests-for-pokemon-evolutions-api.md | 26 ---- ...flow-failures-for-backend-and-e2e-tests.md | 21 --- ...-yzpb--implement-unit-integration-tests.md | 28 ---- 23 files changed, 742 deletions(-) delete mode 100644 .beans/nuzlocke-tracker-0arz--integration-tests-for-runs-encounters-api.md delete mode 100644 .beans/nuzlocke-tracker-1guz--component-tests-for-key-frontend-components.md delete mode 100644 .beans/nuzlocke-tracker-1qzo--fix-wcag-aa-color-contrast-violations.md delete mode 100644 .beans/nuzlocke-tracker-49xj--overhaul-nuzlocke-rules-system.md delete mode 100644 .beans/nuzlocke-tracker-4a6i--replace-ci-pipeline-with-test-suite.md delete mode 100644 .beans/nuzlocke-tracker-9c66--integration-tests-for-genlockes-bosses-api.md delete mode 100644 .beans/nuzlocke-tracker-9c8d--rebrand-to-another-nuzlocke-tracker-ant.md delete mode 100644 .beans/nuzlocke-tracker-bs0y--add-type-restriction-rules-monolocke.md delete mode 100644 .beans/nuzlocke-tracker-ch77--integration-tests-for-games-routes-api.md delete mode 100644 .beans/nuzlocke-tracker-d8cp--set-up-frontend-test-infrastructure.md delete mode 100644 .beans/nuzlocke-tracker-dpw7--modernize-website-design-and-look-and-feel.md delete mode 100644 .beans/nuzlocke-tracker-ee9s--unit-tests-for-frontend-utilities-and-hooks.md delete mode 100644 .beans/nuzlocke-tracker-fitk--add-egglocke-wonderlocke-and-randomizer-rules.md delete mode 100644 .beans/nuzlocke-tracker-fv7w--add-team-size-limit-rule.md delete mode 100644 .beans/nuzlocke-tracker-hjkk--unit-tests-for-pydantic-schemas-and-model-validati.md delete mode 100644 .beans/nuzlocke-tracker-iam7--unit-tests-for-services-layer.md delete mode 100644 .beans/nuzlocke-tracker-knnc--add-staticlegendary-clause-rule.md delete mode 100644 .beans/nuzlocke-tracker-o7r8--remove-unused-nuzlocke-rules.md delete mode 100644 .beans/nuzlocke-tracker-rrcf--set-up-backend-test-infrastructure.md delete mode 100644 .beans/nuzlocke-tracker-sij8--add-gift-clause-rule.md delete mode 100644 .beans/nuzlocke-tracker-ugb7--integration-tests-for-pokemon-evolutions-api.md delete mode 100644 .beans/nuzlocke-tracker-wtbk--fix-ci-workflow-failures-for-backend-and-e2e-tests.md delete mode 100644 .beans/nuzlocke-tracker-yzpb--implement-unit-integration-tests.md diff --git a/.beans/nuzlocke-tracker-0arz--integration-tests-for-runs-encounters-api.md b/.beans/nuzlocke-tracker-0arz--integration-tests-for-runs-encounters-api.md deleted file mode 100644 index a303812..0000000 --- a/.beans/nuzlocke-tracker-0arz--integration-tests-for-runs-encounters-api.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -# nuzlocke-tracker-0arz -title: Integration tests for Runs & Encounters API -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:33:21Z -updated_at: 2026-02-21T11:54:42Z -parent: nuzlocke-tracker-yzpb ---- - -Write integration tests for the core run tracking and encounter API endpoints. This is the heart of the application. - -## Checklist - -- [x] Test run CRUD operations (create, list, get, update, delete) -- [x] Test run creation with rules configuration (JSONB field) -- [x] Test encounter logging on a run (create encounter on a route) -- [x] Test encounter status changes (alive → dead, faintLevel, deathCause) -- [x] Test route-lock enforcement (duplicate sibling encounter → 409) -- [x] Test shiny encounter handling (shinyClause bypasses route-lock) -- [x] Test gift clause bypass (giftClause=true, origin=gift bypasses route-lock) -- [x] Test ending a run (completion/failure, completed_at set, 400 on double-end) -- [x] Test error cases (404 for invalid run/route/pokemon, 400 for parent route, 422 for missing fields) - -## Notes - -- Run endpoints: `backend/src/app/api/runs.py` -- Encounter endpoints: `backend/src/app/api/encounters.py` -- This is the most critical area — Nuzlocke rules enforcement should be thoroughly tested -- Tests need game + pokemon + route fixtures as prerequisites \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-1guz--component-tests-for-key-frontend-components.md b/.beans/nuzlocke-tracker-1guz--component-tests-for-key-frontend-components.md deleted file mode 100644 index a05eb8c..0000000 --- a/.beans/nuzlocke-tracker-1guz--component-tests-for-key-frontend-components.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -# nuzlocke-tracker-1guz -title: Component tests for key frontend components -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:33:45Z -updated_at: 2026-02-21T12:53:51Z -parent: nuzlocke-tracker-yzpb ---- - -Write component tests for key frontend React components, focusing on user interactions and rendering correctness. - -Test components with no external hook dependencies directly; mock `useTheme` where needed. Use @testing-library/user-event for interactions. - -## Checklist - -- [x] Test `EndRunModal` — Victory/Defeat/Cancel button callbacks, genlocke description text, disabled state -- [x] Test `GameGrid` — renders games, generation filter, region filter, onSelect callback -- [x] Test `RulesConfiguration` — renders rule sections, toggle calls onChange, type restriction toggle, reset button -- [x] Test `Layout` — nav links present, mobile menu toggle, theme toggle button diff --git a/.beans/nuzlocke-tracker-1qzo--fix-wcag-aa-color-contrast-violations.md b/.beans/nuzlocke-tracker-1qzo--fix-wcag-aa-color-contrast-violations.md deleted file mode 100644 index 0e46fd9..0000000 --- a/.beans/nuzlocke-tracker-1qzo--fix-wcag-aa-color-contrast-violations.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -# nuzlocke-tracker-1qzo -title: Fix WCAG AA color contrast violations -status: completed -type: bug -priority: high -created_at: 2026-02-20T19:19:32Z -updated_at: 2026-02-20T19:20:25Z ---- diff --git a/.beans/nuzlocke-tracker-49xj--overhaul-nuzlocke-rules-system.md b/.beans/nuzlocke-tracker-49xj--overhaul-nuzlocke-rules-system.md deleted file mode 100644 index 2d142dc..0000000 --- a/.beans/nuzlocke-tracker-49xj--overhaul-nuzlocke-rules-system.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -# nuzlocke-tracker-49xj -title: Overhaul Nuzlocke Rules System -status: completed -type: epic -priority: normal -created_at: 2026-02-20T13:22:23Z -updated_at: 2026-02-21T11:23:31Z ---- - -Audit and overhaul the nuzlocke rules configuration. The current rules are a flat collection of boolean settings, some of which don't meaningfully affect tracker behavior. This epic cleans up existing rules and adds new rules for popular variants with actual tracker logic. - -## Scope - -### Rules to REMOVE (5) -These rules either define what a nuzlocke is (always true) or don't affect tracker behavior at all: -- `firstEncounterOnly` — implicit; it's a nuzlocke tracker -- `permadeath` — implicit; it's a nuzlocke tracker -- `nicknameRequired` — not enforced or tracked -- `setModeOnly` — not enforced or tracked -- `postGameCompletion` — not enforced or tracked - -### Rules to KEEP (5) -These actively affect tracker logic: -- `duplicatesClause` — used in encounter creation and bulk randomization -- `shinyClause` — used in encounter creation (bypass route-lock) -- `pinwheelClause` — used for zone-based encounter logic -- `hardcoreMode` — used in BossDefeatModal (auto-win, 1 attempt) -- `levelCaps` — displayed in sticky bar on encounters page - -### New rules to ADD (4) -These are boolean flags with real tracker logic: - -- `egglocke` — all caught Pokemon are replaced with traded eggs. When enabled, encounter Pokemon selection should allow picking from ALL Pokemon (not just the game's regional dex), similar to the admin panel encounter creation / boss team creation flow. -- `wonderlocke` — all caught Pokemon are Wonder Traded away. Same as egglocke: encounter Pokemon selection allows picking from ALL Pokemon. -- `randomizer` — the run uses a randomized ROM. Same behavior: encounter Pokemon selection allows picking from ALL Pokemon since the dex is randomized. -- `giftClause` — in-game gift Pokemon are free and do not count against the area's encounter limit. When enabled, gift-origin encounters should bypass the route-lock check (similar to how shinyClause bypasses it for shinies). - -### Complex rules (need design work) -These need more complex logic and are tracked as draft sub-tasks: -- Type Restrictions (Monolocke) — bs0y -- Team Size Limit — fv7w -- Static/Legendary Clause — knnc - -## Children - -Work is tracked in sub-tasks: -- **o7r8** — Remove unused nuzlocke rules -- **fitk** — Add egglocke, wonderlocke, and randomizer rules -- **sij8** — Add gift clause rule -- **bs0y** — Add type restriction rules (monolocke) *(draft)* -- **fv7w** — Add team size limit rule *(draft)* -- **knnc** — Add static/legendary clause rule *(draft)* \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-4a6i--replace-ci-pipeline-with-test-suite.md b/.beans/nuzlocke-tracker-4a6i--replace-ci-pipeline-with-test-suite.md deleted file mode 100644 index 2663547..0000000 --- a/.beans/nuzlocke-tracker-4a6i--replace-ci-pipeline-with-test-suite.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -# nuzlocke-tracker-4a6i -title: Replace CI pipeline with test suite -status: completed -type: task -priority: normal -created_at: 2026-02-21T13:01:01Z -updated_at: 2026-02-21T13:10:15Z ---- - -Replace the current `.github/workflows/ci.yml` with a workflow that runs the actual test suites. The existing jobs (lint, format, type check) are already enforced by pre-commit hooks (prek), so CI should focus on test execution instead. - -## Context - -- **Backend integration tests**: pytest with `TEST_DATABASE_URL` pointing at a postgres service container. Default URL: `postgresql+asyncpg://postgres:postgres@localhost:5433/nuzlocke_test`. Tests live in `backend/tests/`. -- **Frontend unit tests**: vitest (`npm run test -- --run`). No external services needed. -- **E2e tests**: Playwright. `e2e/global-setup.ts` uses `docker compose -p nuzlocke-test -f docker-compose.test.yml up -d --build` to start a test API + DB, then seeds data via the API. `playwright.config.ts` spins up `npm run dev` as the webServer. Need to install Chromium via `npx playwright install --with-deps chromium`. - -## Checklist - -- [x] Add `backend-tests` job: postgres service container (image postgres:16-alpine, user/pass/db matching conftest defaults), install deps with `uv`, run `pytest backend/tests/ -q` -- [x] Add `frontend-tests` job: node 24, `npm ci` in `frontend/`, run `npm run test -- --run` -- [x] Add `e2e-tests` job: install Docker Compose, install Playwright + Chromium deps, run `npx playwright test` from `frontend/`; upload HTML report as artifact on failure -- [x] Keep the `actions-lint` job (actionlint + zizmor); remove `backend-lint` and `frontend-lint` jobs -- [x] Pin all action versions to SHA with version comments; pass `zizmor` audit \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-9c66--integration-tests-for-genlockes-bosses-api.md b/.beans/nuzlocke-tracker-9c66--integration-tests-for-genlockes-bosses-api.md deleted file mode 100644 index 2f51ddb..0000000 --- a/.beans/nuzlocke-tracker-9c66--integration-tests-for-genlockes-bosses-api.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -# nuzlocke-tracker-9c66 -title: Integration tests for Genlockes & Bosses API -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:33:26Z -updated_at: 2026-02-21T12:20:37Z -parent: nuzlocke-tracker-yzpb ---- - -Write integration tests for the genlocke challenge and boss battle API endpoints. - -## Checklist - -- [x] Test genlocke CRUD operations (create, list, get, update, delete) -- [x] Test leg management (add/remove legs to a genlocke) -- [x] Test Pokemon transfers between genlocke legs -- [x] Test boss battle CRUD (create, list, update, delete per game) -- [x] Test boss battle results per run (record win/loss) -- [x] Test stats endpoint for run statistics -- [x] Test export endpoint -- [x] Test error cases (invalid transfers, boss results for wrong game, etc.) - -## Notes - -- Genlocke endpoints: `backend/src/app/api/genlockes.py` -- Boss endpoints: `backend/src/app/api/bosses.py` -- Stats endpoints: `backend/src/app/api/stats.py` -- Export endpoints: `backend/src/app/api/export.py` -- Genlocke tests require multiple runs as fixtures \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-9c8d--rebrand-to-another-nuzlocke-tracker-ant.md b/.beans/nuzlocke-tracker-9c8d--rebrand-to-another-nuzlocke-tracker-ant.md deleted file mode 100644 index fb0a289..0000000 --- a/.beans/nuzlocke-tracker-9c8d--rebrand-to-another-nuzlocke-tracker-ant.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -# nuzlocke-tracker-9c8d -title: Rebrand to Another Nuzlocke Tracker (ANT) -status: completed -type: task -priority: normal -created_at: 2026-02-10T14:46:09Z -updated_at: 2026-02-17T19:17:22Z ---- - -Adopt the new branding: **Another Nuzlocke Tracker**, abbreviated **ANT**. - -## Context - -- No existing Nuzlocke tracker uses this name or acronym. -- The name is self-deprecating/playful ("yet another...") and the acronym opens up mascot/logo possibilities (ant character). -- **Durant** (Steel/Bug, Gen V) is the mascot Pokémon — an actual ant Pokémon that ties the ANT acronym directly into the Pokémon universe. - -## Checklist - -- [x] Update project name in package.json / config files -- [x] Update page titles, meta tags, and any visible app name references -- [x] Update README and any documentation with the new name -- [x] Design or source a Durant-themed logo/icon -- [x] Update favicon and app icons \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-bs0y--add-type-restriction-rules-monolocke.md b/.beans/nuzlocke-tracker-bs0y--add-type-restriction-rules-monolocke.md deleted file mode 100644 index 7278de4..0000000 --- a/.beans/nuzlocke-tracker-bs0y--add-type-restriction-rules-monolocke.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -# nuzlocke-tracker-bs0y -title: Add type restriction rules (monolocke) -status: completed -type: feature -priority: normal -created_at: 2026-02-20T19:56:16Z -updated_at: 2026-02-21T11:22:16Z -parent: nuzlocke-tracker-49xj ---- - -Restrict team composition to specific types (monolocke and similar variants). - -## Design Decisions - -**Type selection:** Multi-select from the 18 standard Pokemon types. A monolocke selects one type; multi-type variants (e.g., "fire and water only") select multiple. - -**Dual-type matching:** A Pokemon qualifies if at least one of its types is in the allowed set. This matches the community standard for monolocke — e.g., in a Fire monolocke, Charizard (Fire/Flying) is allowed because it has Fire. - -**Enforcement:** Soft enforcement via UI warnings, not hard blocks. The tracker warns when a caught Pokemon doesn't match the allowed types but doesn't prevent logging it. Reason: players sometimes need to use HM slaves or have edge cases the tracker shouldn't block. - -**Data model:** Add `allowedTypes: string[]` to `NuzlockeRules`. Empty array means no restriction (disabled). This keeps it in the existing JSONB rules blob on the run. - -**UI:** Add a "Type Restrictions" section to `RulesConfiguration` with a multi-select type picker (reuse the type badge styling from `TypeBadge`). Show a warning badge on encounters that don't match. - -## Checklist - -- [x] Add `allowedTypes: string[]` to `NuzlockeRules` interface (default: `[]`) -- [x] Add a new `BooleanRuleKeys` type to `RuleDefinition` to exclude non-boolean fields -- [x] Add type multi-select UI to `RulesConfiguration` (shown when allowedTypes toggle is on) -- [x] Show warning indicator on `PokemonCard` and encounter list for Pokemon that don't match allowed types -- [x] Add `RuleBadge` display for active type restriction (e.g., "Monolocke: Fire") -- [x] Update `RuleBadges` to handle `allowedTypes` separately from boolean rules diff --git a/.beans/nuzlocke-tracker-ch77--integration-tests-for-games-routes-api.md b/.beans/nuzlocke-tracker-ch77--integration-tests-for-games-routes-api.md deleted file mode 100644 index 1c6186b..0000000 --- a/.beans/nuzlocke-tracker-ch77--integration-tests-for-games-routes-api.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -# nuzlocke-tracker-ch77 -title: Integration tests for Games & Routes API -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:33:13Z -updated_at: 2026-02-21T11:48:10Z -parent: nuzlocke-tracker-yzpb ---- - -Write integration tests for the games and routes API endpoints in backend/src/app/api/games.py. - -## Key behaviors to test - -- Game CRUD: create (201), list, get with routes, update, delete (204) -- Slug uniqueness enforced at create and update (409) -- 404 for missing games -- 422 for invalid request bodies -- Route operations require version_group_id on the game (need VersionGroup fixture via db_session) -- list_game_routes only returns routes with encounters (or parents of routes with encounters) -- Game detail (GET /{id}) returns all routes regardless -- Route create, update, delete, reorder - -## Checklist - -- [x] Test CRUD operations for games (create, list, get, update, delete) -- [x] Test route management within a game (create, list, update, delete, reorder) -- [x] Test error cases (404, 409 duplicate slug, 422 validation) -- [x] Test list_game_routes filtering behavior (empty routes excluded) -- [x] Test by-region endpoint structure \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-d8cp--set-up-frontend-test-infrastructure.md b/.beans/nuzlocke-tracker-d8cp--set-up-frontend-test-infrastructure.md deleted file mode 100644 index 5b65610..0000000 --- a/.beans/nuzlocke-tracker-d8cp--set-up-frontend-test-infrastructure.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -# nuzlocke-tracker-d8cp -title: Set up frontend test infrastructure -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:33:33Z -updated_at: 2026-02-21T12:32:34Z -parent: nuzlocke-tracker-yzpb -blocking: - - nuzlocke-tracker-ee9s - - nuzlocke-tracker-1guz ---- - -Set up the test infrastructure for the React/TypeScript frontend. No testing tooling currently exists. - -## Checklist - -- [x] Install Vitest, @testing-library/react, @testing-library/jest-dom, @testing-library/user-event, jsdom -- [x] Configure Vitest in `vite.config.ts` or a dedicated `vitest.config.ts` -- [x] Set up jsdom as the test environment -- [x] Create a test setup file (e.g. `src/test/setup.ts`) that imports @testing-library/jest-dom matchers -- [x] Create test utility helpers (e.g. render wrapper with providers — QueryClientProvider, BrowserRouter) -- [x] Add a \`test\` script to package.json -- [x] Verify the setup by writing a simple smoke test -- [x] Set up MSW (Mock Service Worker) or a similar API mocking strategy for hook/component tests — using `vi.mock` instead; MSW deferred until needed - -## Notes - -- Vitest integrates natively with Vite, which the project already uses -- React Testing Library is the standard for testing React components -- The app uses React Query (TanStack Query) and React Router — the test wrapper needs to provide these contexts -- MSW is recommended for mocking API calls in hook and component tests, but simpler approaches (vi.mock) may suffice initially \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-dpw7--modernize-website-design-and-look-and-feel.md b/.beans/nuzlocke-tracker-dpw7--modernize-website-design-and-look-and-feel.md deleted file mode 100644 index 6b70016..0000000 --- a/.beans/nuzlocke-tracker-dpw7--modernize-website-design-and-look-and-feel.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -# nuzlocke-tracker-dpw7 -title: Modernize website design and look-and-feel -status: completed -type: feature -priority: normal -created_at: 2026-02-17T19:16:39Z -updated_at: 2026-02-20T19:05:21Z ---- - -Overhaul the UI to a dark-first, techy aesthetic with a cohesive brand identity derived from the ANT steel ant logo. - -## Design direction - -**Dark & techy** — dark-first surfaces, subtle glow/accent effects, code-editor-influenced aesthetic. Think GitHub dark, Discord, or Linear dark mode. Light mode becomes the secondary theme. - -## 1. Brand palette + Tailwind theme - -Define custom Tailwind v4 theme tokens in `index.css` using `@theme`: - -- **Surfaces:** dark navy/charcoal base (`#0f1117`, `#161b22`, `#1c2128`) with layered elevation (darker = further back, lighter = elevated) -- **Accent:** steel blue from the logo (`#395E73`, `#7EB0CE`) as the primary interactive color -- **Text:** off-white primary (`#e6edf3`), muted gray secondary (`#7d8590`) -- **Status colors:** keep green/red/blue semantics but shift to darker, more saturated variants that work on dark surfaces -- **Borders:** subtle (`rgba(255,255,255,0.08)`) instead of gray-200/700 - -Replace ad-hoc Tailwind color classes throughout all components with theme tokens. - -## 2. Typography - -Self-host **Geist** (or Inter/JetBrains Mono pairing): - -- Geist Sans for UI text (headings, labels, body) -- Geist Mono for data-heavy elements (stats numbers, encounter rates, levels) -- Set up via `@font-face` in `index.css`, configure in Tailwind `@theme` -- Establish clear size/weight hierarchy: page titles (2xl bold), section headers (lg semibold), body (sm regular), labels (xs medium) - -## 3. Navigation redesign - -- Add the ant SVG logo mark next to "ANT" in the nav -- Active route indicator (accent-colored underline or background highlight) -- Subtle bottom border glow or gradient accent line -- Slightly translucent/backdrop-blur nav background for depth -- Better mobile menu transitions (slide or fade instead of instant toggle) - -## 4. Home page hero - -- Full-width dark gradient hero section with the ant logo as a subtle watermark/background element -- Tagline with stronger typography hierarchy -- Stats summary (total runs, completion rate) as glowing stat pills if the user has data -- CTA button with accent glow/gradient - -## 5. Cards & surfaces - -- Dark elevated cards (`bg-[#161b22]`) with subtle border (`border-white/[0.06]`) -- Hover state: slight border brightness increase + subtle shadow glow in accent color -- Active/selected states with accent border -- Pokemon cards: dark backgrounds make sprites pop better, accent ring on hover -- Stat cards: accent-colored left border or top gradient -- Modals: dark overlay with backdrop-blur, card-style modal surface - -## 6. Status indicators & badges - -- Status badges: more vibrant on dark backgrounds (alive=emerald glow, dead=red glow, caught=blue) -- Type badges: use the established Pokemon type colors but tuned for dark surfaces -- Encounter method badges: same treatment -- Pulse animation on active run indicators - -## 7. Micro-interactions - -- Smooth transitions on all interactive elements (`transition-all duration-150`) -- Hover lift on cards (`hover:-translate-y-0.5`) -- Button press feedback (`active:scale-[0.98]`) -- Loading spinners in accent color -- Skeleton loading states for data-heavy pages - -## 8. Dark/light mode - -- Dark is the default and primary design target -- Light mode: invert surfaces to white/gray-50, keep accent colors, adjust contrast -- Toggle in nav (sun/moon icon) -- Persist preference in localStorage, respect `prefers-color-scheme` - -## Checklist - -- [x] Define Tailwind v4 `@theme` tokens (colors, fonts, spacing) in `index.css` -- [x] Self-host Geist font family, configure in theme -- [x] Redesign nav bar (logo mark, active states, backdrop blur, dark surface) -- [x] Redesign home page hero section -- [x] Update card/surface styles globally (Layout, PokemonCard, StatCard, GameCard) -- [x] Update all page-level backgrounds and containers -- [x] Update modal styles (EncounterModal, StatusChangeModal, etc.) -- [x] Update badge/indicator styles (TypeBadge, RuleBadges, EncounterMethodBadge) -- [x] Add dark/light mode toggle to nav -- [x] Polish hover states and transitions across all interactive elements -- [x] Add automated Playwright accessibility and mobile layout tests -- [x] Verify accessibility (contrast ratios, focus indicators) -- [x] Verify mobile layout and touch targets - -## Automated verification approach - -Add a Playwright test suite that covers both accessibility and mobile layout: - -### Accessibility (axe-core + Playwright) -- Install `@axe-core/playwright` as a dev dependency -- Write a test that visits each major page and runs axe-core -- Pages to cover: Home, RunList, RunDashboard, RunEncounters, Stats, NewRun, GenlockeList, GenlockeDetail, NewGenlocke, admin pages -- Check for: color contrast (WCAG AA), missing ARIA labels, heading hierarchy, focus indicators, form label associations -- Run as part of CI - -### Mobile layout (Playwright viewports) -- Test each major page at 3 viewport sizes: mobile (375x667), tablet (768x1024), desktop (1280x800) -- Assert no horizontal overflow (`document.documentElement.scrollWidth <= window.innerWidth`) -- Assert touch targets are at least 44x44px (axe-core `target-size` rule) -- Screenshot each page at each viewport for visual review - -### Implementation -- Add test file: `frontend/e2e/accessibility.spec.ts` -- Add Playwright config if not present -- Add `test:a11y` script to `package.json` - -## Constraints - -- Tailwind-only (no additional CSS frameworks or component libraries) -- Self-hosted fonts only (no Google Fonts CDN) -- Maintain accessibility (WCAG AA contrast ratios, visible focus indicators) -- No performance regression (fonts loaded with `font-display: swap`, no layout shift) \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-ee9s--unit-tests-for-frontend-utilities-and-hooks.md b/.beans/nuzlocke-tracker-ee9s--unit-tests-for-frontend-utilities-and-hooks.md deleted file mode 100644 index 5ea9dcc..0000000 --- a/.beans/nuzlocke-tracker-ee9s--unit-tests-for-frontend-utilities-and-hooks.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -# nuzlocke-tracker-ee9s -title: Unit tests for frontend utilities and hooks -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:33:38Z -updated_at: 2026-02-21T12:47:19Z -parent: nuzlocke-tracker-yzpb ---- - -Write unit tests for the frontend utility functions and custom React hooks. - -All API modules are mocked with `vi.mock`. Hooks are tested with `renderHook` from @testing-library/react, wrapped in `QueryClientProvider`. Mutation tests spy on `queryClient.invalidateQueries` to verify cache invalidation. - -## Checklist - -- [x] Test `utils/formatEvolution.ts` — done in smoke test -- [x] Test `utils/download.ts` — blob URL creation, filename, cleanup -- [x] Test `hooks/useGames.ts` — query hooks and disabled state -- [x] Test `hooks/useRuns.ts` — query hooks + mutations with cache invalidation -- [x] Test `hooks/useEncounters.ts` — mutations and conditional queries -- [x] Test `hooks/usePokemon.ts` — conditional queries -- [x] Test `hooks/useGenlockes.ts` — queries and mutations -- [x] Test `hooks/useBosses.ts` — queries and mutations -- [x] Test `hooks/useStats.ts` — single query hook -- [x] Test `hooks/useAdmin.ts` — representative subset (usePokemonList, useCreateGame, useDeleteGame) diff --git a/.beans/nuzlocke-tracker-fitk--add-egglocke-wonderlocke-and-randomizer-rules.md b/.beans/nuzlocke-tracker-fitk--add-egglocke-wonderlocke-and-randomizer-rules.md deleted file mode 100644 index 6f0d54f..0000000 --- a/.beans/nuzlocke-tracker-fitk--add-egglocke-wonderlocke-and-randomizer-rules.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -# nuzlocke-tracker-fitk -title: Add egglocke, wonderlocke, and randomizer rules -status: completed -type: feature -priority: normal -created_at: 2026-02-20T19:56:05Z -updated_at: 2026-02-20T20:31:29Z -parent: nuzlocke-tracker-49xj ---- diff --git a/.beans/nuzlocke-tracker-fv7w--add-team-size-limit-rule.md b/.beans/nuzlocke-tracker-fv7w--add-team-size-limit-rule.md deleted file mode 100644 index 1e939a6..0000000 --- a/.beans/nuzlocke-tracker-fv7w--add-team-size-limit-rule.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -# nuzlocke-tracker-fv7w -title: Add boss team match rule -status: completed -type: feature -priority: normal -created_at: 2026-02-20T19:56:22Z -updated_at: 2026-02-20T21:03:20Z -parent: nuzlocke-tracker-49xj ---- - -When enabled, hint to the player that they should limit their active party to the same number of Pokemon as the next boss fight. This is a self-imposed difficulty rule — the tracker cannot enforce it since it doesn't track the active party, but it can surface the information. - -## Design - -**Rule:** Add `bossTeamMatch: boolean` to `NuzlockeRules` (default: `false`, category: `playstyle`). - -**Display:** When enabled and the sticky boss banner is shown, add a hint next to the boss name showing their team size, e.g. "Next: Brock (2 Pokemon — match their team)". This reuses the existing `nextBoss` and its `pokemon` array. - -**Variant bosses:** Some bosses have conditional teams (e.g. rival starter choice). Use the same logic as `BossTeamPreview`: count pokemon without a `conditionLabel` plus those matching the auto-detected variant (via `matchVariant`). Falls back to first variant if no match is detected. - -**Scope:** Frontend-only. No backend or data model changes needed. - -## Checklist - -- [x] Add `bossTeamMatch: boolean` to `NuzlockeRules` interface and `DEFAULT_RULES` (default: false) -- [x] Add `RuleDefinition` entry (category: `playstyle`) -- [x] Show boss team size hint in the sticky level cap banner when the rule is enabled -- [x] Handle variant boss teams (use auto-matched variant count when available) \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-hjkk--unit-tests-for-pydantic-schemas-and-model-validati.md b/.beans/nuzlocke-tracker-hjkk--unit-tests-for-pydantic-schemas-and-model-validati.md deleted file mode 100644 index 2c26c91..0000000 --- a/.beans/nuzlocke-tracker-hjkk--unit-tests-for-pydantic-schemas-and-model-validati.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -# nuzlocke-tracker-hjkk -title: Unit tests for Pydantic schemas and model validation -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:33:03Z -updated_at: 2026-02-21T11:39:58Z -parent: nuzlocke-tracker-yzpb ---- - -Write unit tests for the Pydantic schemas in `backend/src/app/schemas/`. These are pure validation logic and can be tested without a database. - -## Checklist - -- [x] Test `CamelModel` base class (snake_case → camelCase alias generation) -- [x] Test run schemas — creation validation, required fields, optional fields, serialization -- [x] Test game schemas — validation rules, field constraints -- [x] Test encounter schemas — status enum validation, field dependencies -- [x] Test boss schemas — nested model validation -- [x] Test genlocke schemas — complex nested structures -- [x] Test evolution schemas — validation of evolution chain data -- [x] Test Pokemon create schema (types list, required fields) - -## Notes - -- Focus on: valid input acceptance, invalid input rejection, serialization output format -- The `CamelModel` base class does alias generation — verify both input (camelCase) and output (camelCase) work -- Test edge cases like empty strings, negative numbers, missing required fields \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-iam7--unit-tests-for-services-layer.md b/.beans/nuzlocke-tracker-iam7--unit-tests-for-services-layer.md deleted file mode 100644 index 3ff9e10..0000000 --- a/.beans/nuzlocke-tracker-iam7--unit-tests-for-services-layer.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -# nuzlocke-tracker-iam7 -title: Unit tests for services layer -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:33:08Z -updated_at: 2026-02-21T12:01:23Z -parent: nuzlocke-tracker-yzpb ---- - -Write unit tests for the business logic in `backend/src/app/services/`. Currently this is the `families.py` service which handles Pokemon evolution family resolution. - -## Checklist - -- [x] Test family resolution with simple linear evolution chains (e.g. A → B → C) -- [x] Test family resolution with branching evolutions (e.g. Eevee / Shedinja) -- [x] Test disjoint chains remain separate families -- [x] Test edge cases: empty list, single-stage Pokemon, base form, middle form -- [x] Test resolve_base_form: linear, branching, Shedinja, not-in-any-evolution -- [x] Test to_roman: parametrized 1–100, genlocke sequence I–V -- [x] Test strip_roman_suffix: II/III/IV/X, no suffix, round-trip with to_roman - -## Notes - -- `services/families.py` contains the core logic for resolving Pokemon evolution families -- These tests may need mock database sessions or in-memory data depending on how the service queries data -- If the service methods take a DB session, mock it; if they operate on data objects, pass test data directly \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-knnc--add-staticlegendary-clause-rule.md b/.beans/nuzlocke-tracker-knnc--add-staticlegendary-clause-rule.md deleted file mode 100644 index 7690365..0000000 --- a/.beans/nuzlocke-tracker-knnc--add-staticlegendary-clause-rule.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -# nuzlocke-tracker-knnc -title: Add static encounter filter rule -status: completed -type: feature -priority: normal -created_at: 2026-02-20T19:56:27Z -updated_at: 2026-02-21T11:04:45Z -parent: nuzlocke-tracker-49xj ---- - -Control whether static encounters are available in the encounter selector. Static encounters already exist in the route encounter tables (e.g., Zapdos in Power Plant, Snorlax on Route 7 in X/Y). This rule acts as a display filter, not a route-lock bypass like gift clause. - -## Motivation - -Static encounters can feel unfair in nuzlockes because they are deterministic — the player is forced to pick a specific Pokemon rather than getting the randomness that makes nuzlockes fun. Example: Snorlax blocks Route 7 in X/Y. By definition it is the first encounter, but being forced to take it reduces variety. - -Some static encounters are also overpowered (legendaries), which some players want to avoid. - -## Design - -**Rule:** `staticClause: boolean` (default: true — static encounters enabled by default). When disabled, encounters with a `static` encounter method are hidden or grayed out in the encounter selector, so the player skips them and gets a different first encounter. - -**This is NOT like gift clause.** There is no dual-encounter per route. Disabling static encounters simply filters them out of the available encounter pool for a location. The player still gets one encounter per area — just not the static one. - -**Encounter method:** The existing encounter tables already include static encounters (e.g., Zapdos in Power Plant). The `static` encounter method may already exist in seed data — verify before adding. If not present, add it to seed data and `METHOD_CONFIG` / `METHOD_ORDER`. - -**Frontend behavior:** -- When `staticClause` is **enabled** (default): static encounters appear normally in the encounter selector -- When `staticClause` is **disabled**: static encounters are hidden or visually grayed out in the encounter selector, preventing the player from selecting them - -## Checklist - -- [x] Verify `static` encounter method exists in seed data; add to `METHOD_CONFIG` / `METHOD_ORDER` if missing -- [x] Add `staticClause` to `NuzlockeRules` interface and `DEFAULT_RULES` (default: true) -- [x] Add `RuleDefinition` entry under `core` category -- [x] Frontend: filter or gray out static encounters in encounter selector when `staticClause` is disabled -- [x] Backend seed data: add `staticClause` to `DEFAULT_RULES` in `inject_test_data.py` diff --git a/.beans/nuzlocke-tracker-o7r8--remove-unused-nuzlocke-rules.md b/.beans/nuzlocke-tracker-o7r8--remove-unused-nuzlocke-rules.md deleted file mode 100644 index 9f03b92..0000000 --- a/.beans/nuzlocke-tracker-o7r8--remove-unused-nuzlocke-rules.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -# nuzlocke-tracker-o7r8 -title: Remove unused nuzlocke rules -status: completed -type: feature -priority: normal -created_at: 2026-02-20T19:55:59Z -updated_at: 2026-02-20T20:04:33Z -parent: nuzlocke-tracker-49xj ---- - -Remove 5 rules that either define what a nuzlocke is (always true) or don't affect tracker behavior: -- `firstEncounterOnly` — implicit; it's a nuzlocke tracker -- `permadeath` — implicit; it's a nuzlocke tracker -- `nicknameRequired` — not enforced or tracked -- `setModeOnly` — not enforced or tracked -- `postGameCompletion` — not enforced or tracked - -## Checklist - -- [x] Remove from `NuzlockeRules` interface and `DEFAULT_RULES` -- [x] Remove their entries from `RULE_DEFINITIONS` -- [x] Update `RulesConfiguration`, `RuleToggle`, and `RuleBadges` components as needed -- [x] Update `NewRun.tsx` and `NewGenlocke.tsx` if they reference removed rules -- [x] Verify backend encounter logic still works (uses `.get()` with defaults) -- [x] Update backend test seed data if it references removed rules \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-rrcf--set-up-backend-test-infrastructure.md b/.beans/nuzlocke-tracker-rrcf--set-up-backend-test-infrastructure.md deleted file mode 100644 index 439c46a..0000000 --- a/.beans/nuzlocke-tracker-rrcf--set-up-backend-test-infrastructure.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -# nuzlocke-tracker-rrcf -title: Set up backend test infrastructure -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:32:57Z -updated_at: 2026-02-21T11:33:32Z -parent: nuzlocke-tracker-yzpb -blocking: - - nuzlocke-tracker-hjkk - - nuzlocke-tracker-iam7 - - nuzlocke-tracker-ch77 - - nuzlocke-tracker-ugb7 - - nuzlocke-tracker-0arz - - nuzlocke-tracker-9c66 ---- - -Set up the foundational test infrastructure for the FastAPI backend so that all subsequent test tasks can build on it. - -## Approach - -- Session-scoped async engine: creates all tables once via `Base.metadata.create_all()`, drops them after all tests finish -- Function-scoped `db_session` fixture: provides a fresh `AsyncSession`, overrides the `get_session` FastAPI dependency, and truncates all tables after each test for isolation -- Function-scoped `client` fixture: `httpx.AsyncClient` with `ASGITransport` — hits the real app stack including middleware and routing -- `asyncio_default_fixture_loop_scope = "session"` and `asyncio_default_test_loop_scope = "session"` added to pyproject.toml so all fixtures and tests share the same session event loop (required to avoid asyncpg "Future attached to different loop" errors) -- Test database URL read from `TEST_DATABASE_URL` env var (default: `postgresql+asyncpg://postgres:postgres@localhost:5433/nuzlocke_test`) -- The test DB is provided by `docker-compose.test.yml` (postgres on port 5433, `nuzlocke_test` DB created automatically) - -## Checklist - -- [x] Add `asyncio_default_fixture_loop_scope = "session"` and `asyncio_default_test_loop_scope = "session"` to `pyproject.toml` -- [x] Create `backend/tests/conftest.py` with `engine`, `db_session`, and `client` fixtures -- [x] Write a smoke test in `backend/tests/test_smoke.py` to verify the setup -- [x] Verify all tests pass (`pytest` from backend dir) diff --git a/.beans/nuzlocke-tracker-sij8--add-gift-clause-rule.md b/.beans/nuzlocke-tracker-sij8--add-gift-clause-rule.md deleted file mode 100644 index 418af54..0000000 --- a/.beans/nuzlocke-tracker-sij8--add-gift-clause-rule.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -# nuzlocke-tracker-sij8 -title: Add gift clause rule -status: completed -type: feature -priority: normal -created_at: 2026-02-20T19:56:10Z -updated_at: 2026-02-20T20:55:23Z -parent: nuzlocke-tracker-49xj ---- - -Add a new giftClause boolean rule: in-game gift Pokemon are free and do not count against the area's encounter limit. When enabled, a location with a gift allows both the gift encounter and a regular encounter, in any order. - -## Checklist - -- [x] Add giftClause to NuzlockeRules interface and DEFAULT_RULES (default: false) -- [x] Add RuleDefinition entry with core category -- [x] Add origin column to Encounter model + alembic migration -- [x] Add origin to EncounterResponse schema and frontend Encounter type -- [x] Persist origin when creating encounters (frontend sends, backend stores) -- [x] Backend: gift-origin encounters bypass route-lock check (skip_route_lock) -- [x] Backend: existing gift encounters excluded from route-lock query -- [x] Frontend: split encounterByRoute into regular and gift maps -- [x] Frontend: routes with only gift encounters remain clickable for new encounters -- [x] Frontend: gift encounters displayed on route cards with (gift) label -- [x] Frontend: route filtering accounts for gift encounters \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-ugb7--integration-tests-for-pokemon-evolutions-api.md b/.beans/nuzlocke-tracker-ugb7--integration-tests-for-pokemon-evolutions-api.md deleted file mode 100644 index efb1882..0000000 --- a/.beans/nuzlocke-tracker-ugb7--integration-tests-for-pokemon-evolutions-api.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -# nuzlocke-tracker-ugb7 -title: Integration tests for Pokemon & Evolutions API -status: completed -type: task -priority: normal -created_at: 2026-02-10T09:33:16Z -updated_at: 2026-02-21T12:14:39Z -parent: nuzlocke-tracker-yzpb ---- - -Write integration tests for the Pokemon and evolutions API endpoints. - -## Checklist - -- [x] Test Pokemon CRUD operations (create, list, search, update, delete) -- [x] Test Pokemon filtering and search -- [x] Test evolution chain CRUD (create, list, get, update, delete) -- [x] Test evolution family resolution endpoint -- [x] Test error cases (invalid Pokemon references, circular evolutions, etc.) - -## Notes - -- Pokemon endpoints are in `backend/src/app/api/pokemon.py` -- Evolution endpoints are in `backend/src/app/api/evolutions.py` -- Evolution tests should cover multi-stage and branching chains \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-wtbk--fix-ci-workflow-failures-for-backend-and-e2e-tests.md b/.beans/nuzlocke-tracker-wtbk--fix-ci-workflow-failures-for-backend-and-e2e-tests.md deleted file mode 100644 index 0fb0cc8..0000000 --- a/.beans/nuzlocke-tracker-wtbk--fix-ci-workflow-failures-for-backend-and-e2e-tests.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -# nuzlocke-tracker-wtbk -title: Fix CI workflow failures for backend and e2e tests -status: completed -type: bug -priority: normal -created_at: 2026-02-21T15:26:22Z -updated_at: 2026-02-21T15:29:08Z ---- - -Two failures in CI: - -1. **backend-tests**: `astral-sh/setup-uv@v6.8.0` requires Node.js 20+ but the act runner has Node.js 18. The `File` global doesn't exist in Node 18, causing a ReferenceError. Fix: install uv directly via curl instead of using the GitHub Action. - -2. **e2e-tests**: Port 8000 is already allocated on the runner host. The docker-compose.test.yml binds test-api to host port 8000 which conflicts with whatever else runs on the CI machine. Fix: use port 8100 for the test API container. - -## Checklist -- [x] Replace `astral-sh/setup-uv` action with direct curl install of uv + `uv python install 3.14` -- [x] Change e2e test API host port from 8000 to 8100 in docker-compose.test.yml -- [x] Update global-setup.ts to use port 8100 -- [x] Verify no other references to the test API port \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-yzpb--implement-unit-integration-tests.md b/.beans/nuzlocke-tracker-yzpb--implement-unit-integration-tests.md deleted file mode 100644 index 72f920e..0000000 --- a/.beans/nuzlocke-tracker-yzpb--implement-unit-integration-tests.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -# nuzlocke-tracker-yzpb -title: Implement Unit & Integration Tests -status: completed -type: epic -priority: high -created_at: 2026-02-10T09:32:47Z -updated_at: 2026-02-21T13:00:44Z ---- - -Add comprehensive unit and integration test coverage to both the backend (FastAPI/Python) and frontend (React/TypeScript). The project currently has zero tests — pytest is configured in pyproject.toml with pytest-asyncio and httpx, but no actual test files exist. The frontend has no test tooling at all. - -**Scope:** -- Unit tests for isolated logic (schemas, services, utilities) -- Integration tests for API endpoints (using httpx AsyncClient against a test database) -- Frontend unit/component tests (using Vitest + React Testing Library) - -**Explicitly out of scope:** -- End-to-end / browser tests (e.g. Selenium, Playwright) — requires specialised infrastructure - -## Success Criteria - -- [x] Backend test infrastructure is set up (conftest, fixtures, test DB) -- [x] Backend schemas and services have unit test coverage -- [x] Backend API endpoints have integration test coverage -- [x] Frontend test infrastructure is set up (Vitest, RTL) -- [x] Frontend utilities and hooks have unit test coverage -- [x] Frontend components have basic render/interaction tests \ No newline at end of file -- 2.49.1 From efa0b5f8553f2732c96998f592bd54a657cf4375 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Feb 2026 17:33:54 +0100 Subject: [PATCH 2/4] Add --prune flag to seed command to remove stale data Without --prune, seeds continue to only upsert (add/update). With --prune, routes, encounters, and bosses not present in the seed JSON files are deleted from the database. Co-Authored-By: Claude Opus 4.6 --- ...3--prune-stale-seed-data-during-seeding.md | 19 +++++ backend/src/app/seeds/__main__.py | 4 +- backend/src/app/seeds/loader.py | 73 ++++++++++++++++++- backend/src/app/seeds/run.py | 21 ++++-- 4 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 .beans/nuzlocke-tracker-ecn3--prune-stale-seed-data-during-seeding.md diff --git a/.beans/nuzlocke-tracker-ecn3--prune-stale-seed-data-during-seeding.md b/.beans/nuzlocke-tracker-ecn3--prune-stale-seed-data-during-seeding.md new file mode 100644 index 0000000..9e74e1c --- /dev/null +++ b/.beans/nuzlocke-tracker-ecn3--prune-stale-seed-data-during-seeding.md @@ -0,0 +1,19 @@ +--- +# nuzlocke-tracker-ecn3 +title: Prune stale seed data during seeding +status: completed +type: bug +priority: normal +created_at: 2026-02-21T16:28:37Z +updated_at: 2026-02-21T16:29:43Z +--- + +Seeds only upsert (add/update), they never remove routes, encounters, or bosses that no longer exist in the seed JSON. When routes are renamed, old route names persist in production. + +## Fix + +After upserting each entity type, delete rows not present in the seed data: + +1. **Routes**: After upserting all routes for a version group, delete routes whose names are not in the seed set. FK cascades handle child routes and encounters. +2. **Encounters**: After upserting encounters for a route+game, delete encounters not in the seed data for that route+game pair. +3. **Bosses**: After upserting bosses for a version group, delete bosses with order values beyond what the seed provides. \ No newline at end of file diff --git a/backend/src/app/seeds/__main__.py b/backend/src/app/seeds/__main__.py index 501de18..d5ca62a 100644 --- a/backend/src/app/seeds/__main__.py +++ b/backend/src/app/seeds/__main__.py @@ -2,6 +2,7 @@ Usage: python -m app.seeds # Run seed + python -m app.seeds --prune # Run seed and remove stale data not in seed files python -m app.seeds --verify # Run seed + verification python -m app.seeds --export # Export all seed data from DB to JSON files """ @@ -21,7 +22,8 @@ async def main(): await export_all() return - await seed() + prune = "--prune" in sys.argv + await seed(prune=prune) if "--verify" in sys.argv: await verify() diff --git a/backend/src/app/seeds/loader.py b/backend/src/app/seeds/loader.py index 0965a06..efa1730 100644 --- a/backend/src/app/seeds/loader.py +++ b/backend/src/app/seeds/loader.py @@ -124,11 +124,14 @@ async def upsert_routes( session: AsyncSession, version_group_id: int, routes: list[dict], + *, + prune: bool = False, ) -> dict[str, int]: """Upsert route records for a version group, return {name: id} mapping. Handles hierarchical routes: routes with 'children' are parent routes, and their children get parent_route_id set accordingly. + When prune is True, deletes routes not present in the seed data. """ # First pass: upsert all parent routes (without parent_route_id) for route in routes: @@ -185,6 +188,27 @@ async def upsert_routes( await session.flush() + if prune: + seed_names: set[str] = set() + for route in routes: + seed_names.add(route["name"]) + for child in route.get("children", []): + seed_names.add(child["name"]) + + pruned = await session.execute( + delete(Route) + .where( + Route.version_group_id == version_group_id, + Route.name.not_in(seed_names), + ) + .returning(Route.id) + ) + pruned_count = len(pruned.all()) + if pruned_count: + print(f" Pruned {pruned_count} stale route(s)") + + await session.flush() + # Return full mapping including children result = await session.execute( select(Route.name, Route.id).where(Route.version_group_id == version_group_id) @@ -233,8 +257,15 @@ async def upsert_route_encounters( encounters: list[dict], dex_to_id: dict[int, int], game_id: int, + *, + prune: bool = False, ) -> int: - """Upsert encounters for a route and game, return count of upserted rows.""" + """Upsert encounters for a route and game, return count of upserted rows. + + When prune is True, deletes encounters not present in the seed data. + """ + seed_keys: set[tuple[int, str, str]] = set() + count = 0 for enc in encounters: pokemon_id = dex_to_id.get(enc["pokeapi_id"]) @@ -245,6 +276,7 @@ async def upsert_route_encounters( conditions = enc.get("conditions") if conditions: for condition_name, rate in conditions.items(): + seed_keys.add((pokemon_id, enc["method"], condition_name)) await _upsert_single_encounter( session, route_id, @@ -258,6 +290,7 @@ async def upsert_route_encounters( ) count += 1 else: + seed_keys.add((pokemon_id, enc["method"], "")) await _upsert_single_encounter( session, route_id, @@ -270,6 +303,23 @@ async def upsert_route_encounters( ) count += 1 + if prune: + existing = await session.execute( + select(RouteEncounter).where( + RouteEncounter.route_id == route_id, + RouteEncounter.game_id == game_id, + ) + ) + stale_ids = [ + row.id + for row in existing.scalars() + if (row.pokemon_id, row.encounter_method, row.condition) not in seed_keys + ] + if stale_ids: + await session.execute( + delete(RouteEncounter).where(RouteEncounter.id.in_(stale_ids)) + ) + return count @@ -280,8 +330,13 @@ async def upsert_bosses( dex_to_id: dict[int, int], route_name_to_id: dict[str, int] | None = None, slug_to_game_id: dict[str, int] | None = None, + *, + prune: bool = False, ) -> int: - """Upsert boss battles for a version group, return count of bosses upserted.""" + """Upsert boss battles for a version group, return count of bosses upserted. + + When prune is True, deletes boss battles not present in the seed data. + """ count = 0 for boss in bosses: # Resolve after_route_name to an ID @@ -364,6 +419,20 @@ async def upsert_bosses( count += 1 + if prune: + seed_orders = {boss["order"] for boss in bosses} + pruned = await session.execute( + delete(BossBattle) + .where( + BossBattle.version_group_id == version_group_id, + BossBattle.order.not_in(seed_orders), + ) + .returning(BossBattle.id) + ) + pruned_count = len(pruned.all()) + if pruned_count: + print(f" Pruned {pruned_count} stale boss battle(s)") + await session.flush() return count diff --git a/backend/src/app/seeds/run.py b/backend/src/app/seeds/run.py index f140904..3bc43cd 100644 --- a/backend/src/app/seeds/run.py +++ b/backend/src/app/seeds/run.py @@ -38,9 +38,12 @@ def load_json(filename: str): return json.load(f) -async def seed(): - """Run the full seed process.""" - print("Starting seed...") +async def seed(*, prune: bool = False): + """Run the full seed process. + + When prune is True, removes DB rows not present in seed data. + """ + print("Starting seed..." + (" (with pruning)" if prune else "")) async with async_session() as session, session.begin(): # 1. Upsert version groups @@ -88,7 +91,7 @@ async def seed(): continue # Upsert routes once per version group - route_map = await upsert_routes(session, vg_id, routes_data) + route_map = await upsert_routes(session, vg_id, routes_data, prune=prune) route_maps_by_vg[vg_id] = route_map total_routes += len(route_map) print(f" {vg_slug}: {len(route_map)} routes") @@ -119,6 +122,7 @@ async def seed(): route["encounters"], dex_to_id, game_id, + prune=prune, ) total_encounters += enc_count @@ -137,6 +141,7 @@ async def seed(): child["encounters"], dex_to_id, game_id, + prune=prune, ) total_encounters += enc_count @@ -160,7 +165,13 @@ async def seed(): route_name_to_id = route_maps_by_vg.get(vg_id, {}) boss_count = await upsert_bosses( - session, vg_id, bosses_data, dex_to_id, route_name_to_id, slug_to_id + session, + vg_id, + bosses_data, + dex_to_id, + route_name_to_id, + slug_to_id, + prune=prune, ) total_bosses += boss_count print(f" {vg_slug}: {boss_count} bosses") -- 2.49.1 From dde20c932b9dd3ea33df97950591bfffca474bc1 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Feb 2026 17:37:29 +0100 Subject: [PATCH 3/4] Update Brock and Misty boss sprites Co-Authored-By: Claude Opus 4.6 --- frontend/public/boss-sprites/firered/brock.png | Bin 2839 -> 758 bytes frontend/public/boss-sprites/firered/misty.png | Bin 3041 -> 707 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/public/boss-sprites/firered/brock.png b/frontend/public/boss-sprites/firered/brock.png index b6febf07c898d9b532d5e7f58b6da016e958f931..220c01e3191c31da5eff20ce0c91854b83651b23 100644 GIT binary patch literal 758 zcmV5!p=N;R~Q z$)F*S#Y-qhhIR@Z9eUiN`5=SghGxlub4!*hL_7i2Pr zOQ79q;dyfcrm&)>l=EXlhQ?eEvd9d( zb#xsiE#qt?NrUq~(gm?k< zSz>R+sSsz3R)GW#Wt1^W0a0QHz#NBBU6DFtTjnT`Ijo@r4Mqzw$6&q1K(pnx6D0(}iT1M@$CKeAZA0c6|l;@~07*qoM6N<$f_~9T4gdfE literal 2839 zcmZ{mcTm&W7RP^LK|p*ih(MGgQlx|gNGOrsTLRL9h)GBxU;6e|(S;b!&B-3vN@4YMh$4wdXLK|GNXc&7 z4xsRs0s!pIbg{Sb33ysMG))b98N26;z^twN&pBG_SlU4c(HgBu2Y03&8Rh%XX5W8Q zsN`v)lGS#n9STiq)=pkgIsT&-EF&*1xoa<|Nn&c(CH#cf$bqvj8}!~?KRv4UB1^5i z#9>rB?O=XT)_IiGm(bYkpw{>fJ!WsgVvqT}(Y(1)5=%KLnc(-1J%!%6`sR<#yEbS} zhH~%O&?!pztMQj%`I%QY(JQ{C|AO^?pk)zx@o5wBh%j5 zeW2<-^#h5uq`@gOiOgqw=1BQTbH>5@;7&mmG_O@yF|1_rODu{is_gx z4=b1xxPgci%Xdeg;f$e`3@fVk%5ZDHh&L-PF6r$(-W8zDMaO~I3Y*{U(z~L^4Uhvl z*((+59yo9phVF-C)!X_pK7@64Px=K_6F@?@Q5LnO8zqMbY0$WwWkzi3JZ5N4vX)~9 zovF_lZz50g?Lv_&kY<>0ymqRDc(?a_50@SB+|{+Q0(8@1_F`I2$*O$Knx?~Tjl+94 zmaD|`(|6h4u~xONu90mHWm>jFwEq`>0Pm-mMA?e=RrUjyP9!)Rf|dQiXtZ zH8{`Zo-$kVB9(M?)bSaq(?*I$NyhD{fXnSN*Hy)Op)2STcAMEa;99Kic9o(~P z3Hi5WxG60b%VVv({gU?SOXk2;Ay8t_FNHk3N;}Gq`G>_0HE2zi|OVTPcX8;*pP+p$h z1xM~PC)C^34UNW@nX6iJxR1E}SrwHvr-^bK&9MnVHS)vrgA@ClC4_8WpWP)Fs{&9b zaFF3Uz6nW@W$OU~%QfQrF5UdT;0`HlgZxZ1`VMMz62A)-`#5o!0QR~(kxF`UuYK`qqHJH(HQsz( zFPl_5xcc=*mf!8m>+erE5=#4n-b}RIwX7c?LeSL?VJKg>*mp53kDo(+>@!~Mw=+Qn z$Ko9}ppg!bBQn(3OOwnuP38&;J@^jesc2Gh)7r8tw>Td^HJ{OwiqS;vS87d-c4fMPud(f0 zu>6Knl-;C6d3%}qC6}$aC#w$(PLYiaKo&8UmgUPu2d4I04X~llxHJN~%g2c1zqA+^zUhuo3@4x6$UbgeS_cN6?d8&7G&D7-J?RGEIe#;xW zIv)teNh6Lm^15U+^ywjOswr+OX_A^DIIPY@3|p;FeIEXr>4t3m6-`ujc5q}ht(bX; zY2S>U&pb@s%^(R@|N3ByQfnR0^MNE;rWcMYNwWBHlYR8~sf=>c!EN@(=Cj{>;y| zYNBppzdxGzO%Xc**sO)SdEEN)ynO|<}w{Z>5Z7S+;4(8 zzqEuC%PExeD*E$H?v5(T@>kx|hojN<@+h+mb$^3lPrJ@hYTA9)@94{J z4e2AI00S5tZfpWGF@aq}7ix(RnZF_4i_@% diff --git a/frontend/public/boss-sprites/firered/misty.png b/frontend/public/boss-sprites/firered/misty.png index 9e4a5ef52dee503c88809daa72adc6014cd71094..44e0dfcf3a9ac5f991a8ffb1909655b6d18ec67e 100644 GIT binary patch literal 707 zcmV;!0zCbRP)Yt%p#$ESv6+e<(X{sEC)@KOSC+4ZgmJ;-)tZ5E`5 zJp_9zc$m_I6c=$8f{4q)4y0cEu_ctAdMGRur7k$(>BW;!A@o!b^Ea5p#M8XDcrd4s z_sN?#-}ikpjQy8qcR_r4z5u9qjZjNvzSH8j5?;`?8g#a~q zO_&K3$c>mX%cnbID$Y#zc1$~VBcbh9>4+K$Ses9;za-+9JkQ%xyEG6`Q?XR{`XT^< z16JLN8=$hmaI{jtveHgBYm*ug)WuK^IP49-qj0H-*x z2W|F{>h%|8XM2sqc@7lu_9f8&<62t(W}-tNBD0>6F~fH^zT p0zYRbB{0(hW>S7ml7G4Ae*-lF%qU~L@ZbOd002ovPDHLkV1mc6F|Ggr literal 3041 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 Date: Sat, 21 Feb 2026 17:39:14 +0100 Subject: [PATCH 4/4] Update remaining FireRed boss sprites Co-Authored-By: Claude Opus 4.6 --- .../public/boss-sprites/firered/blaine.png | Bin 4738 -> 905 bytes frontend/public/boss-sprites/firered/erika.png | Bin 5672 -> 696 bytes .../public/boss-sprites/firered/giovanni.png | Bin 3525 -> 703 bytes frontend/public/boss-sprites/firered/koga.png | Bin 3624 -> 774 bytes .../public/boss-sprites/firered/lt-surge.png | Bin 5218 -> 826 bytes .../public/boss-sprites/firered/sabrina.png | Bin 4650 -> 754 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/public/boss-sprites/firered/blaine.png b/frontend/public/boss-sprites/firered/blaine.png index 41613d40a38a10cc6ab72151a0ff1d14fdb86fa9..38b3d8b654a05aa638735dfd63a4d55bbd589cd2 100644 GIT binary patch literal 905 zcmV;419tq0P)L;#2d9Y_EG010qNS#tmY3ljhU3ljkVnw%H_000McNliru z)CeB~9~ssL;FbUY0@6uDK~#9!wbe0b6Hyok@Jo8&;E+5$rO?pG!69^sQfY+NlGG<; zI8r>grArAF*H$xRI3&7d2q`Y^7P{Q(kl_#sEi}U;K^!cUvlKgID9wcn%RTP>^1lD~?uAp7|8N!Z_pY3Ge7`C`y|z)f9DVcsgAED1N`4uQuKOJ1$LA;V zEZH7W|D*4Gkm@eJ>yMUAdQQQUKt4COUK|b=`m@a`sgF>$6ED6V6g#}AvMH&CdSo^W zEg>fS1JCyg<0;i?^6N8gl|;y|p667h5Qy~=rL?5r3MG3^QQphU>?9SUve+tj{!JaezpttkQE@!LGLXHvK87vNSn*Wd(TM~62PhGm~&^JKh+N*X8HlD*=(hHac-ECG{{8eVc; z0W1y(2%hlyZNt_DsEt=3MB^C;z~(8%YO(Y5rT6-F5uFYy!v%>j6i&uv?5 z1OU>vIOt#l1iVcY6&X9|Mi7W99Be5{n1i?`0hL64!kKtRT=Hg1{B55zwuG z2%{f%bv*%qb9>mt6~MR}BrYrPjbL2>boImtL>RO}WZ%e(HY%QB#DCP!%jO00000NkvXXu0mjfgLjBZ literal 4738 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 e3=mX68h-(}JXGX!bbW9D00008OM&InUc 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 diff --git a/frontend/public/boss-sprites/firered/giovanni.png b/frontend/public/boss-sprites/firered/giovanni.png index 60a9fa292afe5a0945b15c022381f44065532419..4ee36c48886702e766ec7f9cf37261ac83f7e47d 100644 GIT binary patch delta 698 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 oJ0G7f%0LAFfwa^uUbH9Xt0ctKNKL7v# diff --git a/frontend/public/boss-sprites/firered/koga.png b/frontend/public/boss-sprites/firered/koga.png index 74fa06ba7707bcb3c41ab3f6c645f3af6c839f6c..82eff988dd44dfac7112f27c0a9767c6303c5951 100644 GIT binary patch literal 774 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 literal 4650 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 -- 2.49.1