Skip to content

Commit ed7604d

Browse files
authored
Merge pull request #3581 from rommapp/claude/sweet-gates-z5rlgn
docs: split v2 constitution into focused agent skills, make CLAUDE.md holistic
2 parents e7dd013 + 0cf0647 commit ed7604d

9 files changed

Lines changed: 612 additions & 451 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
name: backend-development
3+
description: Working on the RomM Python backend (backend/) — a FastAPI app with SQLAlchemy 2.0, Alembic, RQ/Redis, and Socket.IO. Use when adding or changing API endpoints, handlers, ORM models, response schemas, metadata-provider adapters, background tasks, database migrations, or backend tests. Covers the layered architecture, conventions, auth/scopes, the OpenAPI→frontend type pipeline, and the uv/pytest/alembic/trunk workflow. Trigger on any work under backend/.
4+
---
5+
6+
# RomM Backend — FastAPI / SQLAlchemy
7+
8+
Python 3.13+, FastAPI, SQLAlchemy 2.0 (MariaDB default; MySQL/PostgreSQL supported), Alembic, Redis + RQ for jobs/cache/sessions, Socket.IO for real-time. Managed with **uv**.
9+
10+
Full reference: **`docs/BACKEND_ARCHITECTURE.md`** (directory map, ER diagram, every endpoint, auth flows). Read it before non-trivial changes.
11+
12+
---
13+
14+
## Layered architecture — where code goes
15+
16+
```txt
17+
endpoints/ FastAPI routers: request validation, response schemas, @protected_route scopes
18+
endpoints/responses/ Pydantic response schemas (these shape the OpenAPI → frontend types)
19+
endpoints/sockets/ Socket.IO event handlers
20+
handler/ Business logic, decoupled from HTTP
21+
├ auth/ HybridAuthBackend (session/basic/bearer/OIDC/client-token), scopes, CSRF/session middleware
22+
├ database/ Per-entity CRUD handlers (db_rom_handler, db_user_handler, …), engine/session factory
23+
├ metadata/ One handler per provider; normalizes + ranks by priority
24+
└ filesystem/ ROM/asset/firmware file I/O, hashing, archive extraction
25+
adapters/services/ Typed external API clients (igdb.py + igdb_types.py, screenscraper.py, …)
26+
models/ SQLAlchemy ORM models (BaseModel adds created_at/updated_at)
27+
tasks/ RQ jobs — scheduled/ (cron) and manual/ (on-demand); base classes in tasks.py
28+
config/ Env-var loading (__init__.py) + YAML config manager (singleton)
29+
decorators/ @begin_session (DB session), @protected_route (auth + scopes)
30+
exceptions/ Custom exception hierarchy
31+
utils/ logger/ Shared helpers, structured logging
32+
alembic/ Migrations (env.py + versions/)
33+
```
34+
35+
**Endpoint → handler → (database | metadata | filesystem) → models/adapters.** Endpoints stay thin: validate, enforce scopes, call handlers, serialize via a response schema. Don't put business logic or raw queries in endpoints.
36+
37+
## Conventions
38+
39+
- **Naming:** Classes `PascalCase`; functions/vars `snake_case`; constants `UPPER_SNAKE_CASE`; private `_prefixed`.
40+
- **DB sessions:** decorate handler methods with `@begin_session`; it injects and manages the SQLAlchemy session/transaction. Don't open sessions ad hoc.
41+
- **Async:** I/O-bound endpoints and tasks use `async/await`. Per-request `httpx`/`aiohttp` clients come from context vars (`utils/context.py`), not new clients per call.
42+
- **Imports:** stdlib → third-party → local; explicit (no wildcards); `TYPE_CHECKING` blocks to break circular imports.
43+
- **Errors:** raise the custom exceptions in `exceptions/` (e.g. `RomNotFoundInDatabaseException`), not bare `HTTPException`, where a typed one exists.
44+
- **Validation/SSRF:** sanitize filenames/paths before filesystem use (`utils/`); paths are rooted at `LIBRARY_BASE_PATH`/`RESOURCES_BASE_PATH`/`ASSETS_BASE_PATH` from config.
45+
46+
## Auth & scopes
47+
48+
- Roles: `VIEWER` (read), `EDITOR` (+write roms/platforms/assets), `ADMIN` (+users/tasks/logs). Defined on `models/user.py`; scope tiers in `handler/auth/constants.py`.
49+
- Granular scopes: `me.read/write`, `roms.read/write`, `platforms.*`, `assets.*`, `devices.*`, `firmware.*`, `collections.*`, `users.*`, `tasks.run`, `logs.read`.
50+
- Protect routes with `@protected_route(router.<method>, "<path>", [Scope.X])`. The frontend mirrors these scopes — keep them aligned.
51+
52+
## Adding things
53+
54+
- **Endpoint:** add the route in the right `endpoints/*` router, a response schema in `endpoints/responses/`, enforce scopes, delegate to a handler. If the response shape changes, the frontend must regenerate types (below).
55+
- **Model / schema change:** edit `models/`, then create a migration (below). Update the matching response schema so OpenAPI stays accurate.
56+
- **Metadata provider:** add a typed client in `adapters/services/<name>.py` (+ `<name>_types.py`) and a `handler/metadata/<name>_handler.py` that normalizes into the common shape and slots into the priority order.
57+
- **Background job:** subclass `Task`/`PeriodicTask` in `tasks/scheduled/` or `tasks/manual/`; register scheduled jobs in `startup.py`.
58+
59+
## Database migrations (Alembic)
60+
61+
Migrations must work on **MariaDB, MySQL, and PostgreSQL** (CI runs `alembic upgrade head` on Postgres and MariaDB — `.github/workflows/migrations.yml`). Use batch mode / DB-specific SQL where needed; mirror existing migrations in `alembic/versions/`.
62+
63+
```bash
64+
cd backend
65+
uv run alembic revision --autogenerate -m "short description" # generate, then HAND-REVIEW the file
66+
uv run alembic upgrade head # apply
67+
uv run alembic downgrade -1 # verify the downgrade works
68+
```
69+
70+
Always review autogenerated migrations — they miss server-default/enum/index nuances and cross-dialect differences. The `virtual_collections` DB view is excluded from migrations.
71+
72+
## OpenAPI → frontend types
73+
74+
FastAPI serves the schema at `GET /openapi.json`. The frontend regenerates its TypeScript types from it:
75+
76+
```bash
77+
# backend running on :3000, then in frontend/
78+
npm run generate # writes src/__generated__/ via openapi-typescript-codegen
79+
```
80+
81+
**Any change to a response schema or route signature should be followed by `npm run generate` + a frontend typecheck.**
82+
83+
## Run, test, lint
84+
85+
```bash
86+
cd backend
87+
uv run python3 main.py # run (migrations auto-apply on startup)
88+
uv run pytest [path/file] # tests (subset by path); uv run pytest -vv for all
89+
```
90+
91+
- Tests: pytest + pytest-asyncio, isolated per `pytest-xdist` worker (per-worker DBs); `fakeredis`; `pytest-recording` VCR cassettes mock external APIs; Hypothesis for property tests. Mirror the `backend/<area>/` layout under `backend/tests/`. First-time test DB setup: `docker exec -i romm-db-dev mariadb -uroot -p<pw> < backend/romm_test/setup.sql`.
92+
- **Lint / format / type-check run through Trunk** (ruff, black, isort, mypy, bandit): `trunk fmt && trunk check`. CI enforces Trunk on every PR. Never bypass with `--no-verify`.
93+
- New/changed logic needs a test; new endpoints need endpoint tests.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
name: frontend-i18n
3+
description: Internationalization for the RomM frontend (both v1 and v2). Use whenever adding, renaming, or removing any user-visible string / translation key under frontend/src/locales/. Covers the en_US-is-source rule, the requirement to add every new key to ALL locale directories in the same change, namespace layout, and the check_i18n_locales.py validator enforced in CI. Trigger on any change to frontend/src/locales/**.
4+
---
5+
6+
# RomM Frontend — i18n / Localization
7+
8+
User-visible strings are **never hard-coded** in components — they come from locale files via `vue-i18n` (`$t(...)` in templates/composites; utils may call `i18n.global.t(...)`; **v2 lib primitives must not call `$t` at all** — text passes via props/slots).
9+
10+
## Structure
11+
12+
- Locales live in `frontend/src/locales/<locale>/<namespace>.json`, loaded by dynamic glob import in `src/locales/index.ts`.
13+
- **18 locales**: `en_US` (default + fallback), `en_GB`, `bg_BG`, `cs_CZ`, `de_DE`, `es_ES`, `fr_FR`, `hu_HU`, `it_IT`, `ja_JP`, `ko_KR`, `pl_PL`, `pt_BR`, `ro_RO`, `ru_RU`, `zh_CN`, `zh_TW`.
14+
- Namespaces are per-feature files (e.g. `collection`, `common`, `console`, `detail`, `emulator`, `gallery`, `home`, `library`, `login`, `navigation`, `patcher`, `platform`, `scan`, `settings`, `task`).
15+
16+
## The rule (enforced in CI)
17+
18+
- **`en_US` is the source of truth**, but **every key added to `en_US` must be added to all other locale directories in the same change.** Never leave a key English-only.
19+
- Translate where you can; otherwise copy the English value as a placeholder so the key exists.
20+
- Removing or renaming a key means doing it across **every** locale.
21+
22+
## Verify before handoff
23+
24+
```bash
25+
python3 frontend/src/locales/check_i18n_locales.py
26+
```
27+
28+
It compares every non-English locale against `en_US` and **fails on any missing file, missing key, or extra key**. CI runs the same script (`.github/workflows/i18n.yml`) on any change under `frontend/src/locales/**`. It must pass with zero missing/extra keys.
29+
30+
## Adding a new language
31+
32+
Create a new folder under `frontend/src/locales/` mirroring `en_US/`'s files, then translate. Open the PR against `master` (see the docs/Contributing flow). This is the one i18n change where a new locale directory is expected.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---
2+
name: frontend-v2-components
3+
description: Building or modifying components in the RomM v2 frontend (frontend/src/v2/). Use when creating/editing v2 primitives (R* components in src/v2/lib/), shared composites, or feature composites — covers the three-tier model, file/folder conventions, SFC structure, import order, barrels, Storybook requirements, and v2 anti-patterns. Trigger on any work under frontend/src/v2/.
4+
---
5+
6+
# RomM Frontend v2 — Component Constitution
7+
8+
This governs work inside `frontend/src/v2/`. **v1 is frozen** (`src/views/`, `src/components/`, `src/console/`, `src/layouts/`) — never refactor it; it will be deleted wholesale in a final wave. v2 is gated by `user.ui_settings.uiVersion`.
9+
10+
> Official language for all code, comments, identifiers, `.md`, and commit/PR messages: **English**.
11+
12+
Related skills: `frontend-v2-theming` (tokens/colors), `frontend-v2-input` (focus/gamepad/responsive), `frontend-v2-patterns` (errors/loading/forms/permissions/confirmations), `frontend-i18n`, `pre-pr-verification`.
13+
14+
---
15+
16+
## Premises (stable)
17+
18+
1. **v1 is frozen.** Don't touch `src/views/`, `src/components/`, `src/console/`, `src/layouts/`. When coexistence forces a v2 fork of a store/composable/util, annotate the v1 export with `@deprecated` pointing at the v2 replacement.
19+
2. **Three component tiers** (below).
20+
3. **Shared resources are canonical.** Pinia stores, API services, OpenAPI types (`src/__generated__/`), locales, utils — v2 _imports_ them, never forks them. Additive changes to shared resources are allowed; changing a shared store API to work around a v2 call-site issue is not.
21+
4. **TypeScript strict.** Zero `any` (justify with a comment if unavoidable). No `as unknown as ...`; fix the source or define an intermediate type.
22+
5. **Universal substitution.** When an `R*` primitive exists, use it. If it doesn't, create or extend it. Never drop to raw HTML or raw Vuetify when a primitive applies.
23+
6. **Wrapper contract.** Wrappers around Vuetify use `defineOptions({ inheritAttrs: false })` + `v-bind="$attrs"` + slot passthrough, and accept every prop/slot of the wrapped component.
24+
7. **Layout via Vuetify utility classes + our wrappers, not plain CSS.** Use `d-flex`, `pa-4`, `align-center` directly. Vuetify layout components (`v-row`, `v-col`, `v-container`, `v-spacer`, `v-app`, `v-main`) get wrapped as `R*` on first use (lazy).
25+
8. **Accessibility & performance** are requirements: semantic HTML, focus management, contrast, ARIA on icon-only controls; lazy-load heavy views, virtualize large lists, stable `:key` on every `v-for`.
26+
27+
---
28+
29+
## The three tiers
30+
31+
| Tier | Path | Prefix | Stores/services/router/emitter/i18n | Story | Domain knowledge |
32+
| --------------------- | ------------------------------ | -------------- | ----------------------------------- | --------- | --------------------------------- |
33+
| **Primitive** | `src/v2/lib/` | `R*` mandatory | **No** | Mandatory | None |
34+
| **Shared composite** | `src/v2/components/shared/` | no prefix | Yes | Optional | Cross-feature, no specific domain |
35+
| **Feature composite** | `src/v2/components/<feature>/` | no prefix | Yes | Optional | Feature-specific |
36+
37+
### A component is a primitive only if all three hold
38+
39+
1. Does not depend on stores, services, router, or emitter.
40+
2. No knowledge of a product domain (ROM, Platform, Collection, User…). `RAvatar` yes, `UserAvatar` no.
41+
3. Its API can be described without naming features — generic props/slots/events.
42+
43+
If any fails: **shared composite** if generic across features, **feature composite** if owned by one feature. Edge cases get raised to the user, not auto-decided. Consumer count never demotes a primitive.
44+
45+
### Primitive boundaries
46+
47+
- **Can use**: tokens, other primitives, Vue/Vuetify, generic composables (`useInput*`, `useFocus*`).
48+
- **Cannot use**: Pinia stores, API services, `emitter`, `router` (a `RouterLink` may be accepted as a prop), `i18n` directly. **No `$t()` in primitives** — text comes via props or slots.
49+
50+
---
51+
52+
## File & folder conventions
53+
54+
- **Primitive**: one per folder — `RFoo/RFoo.vue`, `RFoo/RFoo.stories.ts`, `RFoo/index.ts`, optional `RFoo/types.ts`.
55+
- **Composite**: flat `.vue` if one file suffices; a folder with the same internal structure (no story required) if it has sub-pieces.
56+
- **Barrel**: `src/v2/lib/index.ts` re-exports every primitive — update it when a new primitive ships. Composites are imported directly by path; no barrel. No single-file `index.ts` that just re-exports to shorten a path.
57+
58+
### SFC structure
59+
60+
- `<script setup lang="ts">` always.
61+
- `defineOptions({ inheritAttrs: false })` on every wrapper, paired with `v-bind="$attrs"` and slot passthrough (without the bind, attrs vanish silently).
62+
- Props via `defineProps<Props>()` (interface), never runtime declarations. Emits via `defineEmits<{...}>()`. Slots with payload via `defineSlots<{}>()`.
63+
- Order: `<script setup>``<template>``<style scoped>`. Unscoped `<style>` (teleport overrides only) goes after the scoped block.
64+
65+
### Import order & aliases
66+
67+
```ts
68+
// 1. External
69+
// 2. v2 primitives
70+
import { RBtn, RDialog } from "@v2/lib";
71+
import { computed, ref } from "vue";
72+
import type { SimpleRom } from "@/__generated__";
73+
// 5. Canonical shared resources
74+
import storeAuth from "@/stores/auth";
75+
// 4. v2 feature siblings
76+
import GameCard from "@/v2/components/GameCard.vue";
77+
// 3. v2 composables / shared
78+
import { useCan } from "@/v2/composables/useCan";
79+
```
80+
81+
- `@v2/lib` — primitives barrel. `@/v2/...` — anything else under v2. `@/...` — canonical shared resources. Never relative paths (`../../foo`) when an alias exists.
82+
- Shared v2 types live in `src/v2/types/`; backend types come from `src/__generated__/`; not `src/types/` (legacy).
83+
84+
### Composables
85+
86+
- `use` prefix; single named export from `composables/useFoo/index.ts`; fully typed args/return; no side effects on module load (init on first call). Creating a v2-only composable when a v1 equivalent exists is allowed.
87+
88+
### Console logging
89+
90+
- `console.error` allowed for production-visible errors. `console.log`/`console.warn` must not ship. `console.debug` is dev-only — remove before PR.
91+
92+
---
93+
94+
## Storybook (mandatory for `/lib`)
95+
96+
- Every primitive ships at least one story with controls and at least one variant per theme.
97+
- A new interactive primitive that warrants gamepad navigation ships a `play()` interaction.
98+
- Modified primitive: existing story must still render and its interactions still pass.
99+
- `npm run test` runs Vitest **and** every `/lib` story's `play()` via `composeStories`. Don't duplicate coverage between Vitest (pure logic) and Storybook `play()` (components).
100+
101+
---
102+
103+
## Anti-patterns (beyond what the premises already say)
104+
105+
1. Don't change shared store APIs to work around a v2 call-site issue. (Fix the call site; the Gallery lesson was calling `romsStore.reset()` from the view, not adding `_fetchSeq` to the store.)
106+
2. Don't drop to inline role checks — always go through `useCan` (see `frontend-v2-patterns`).
107+
3. Don't reinvent a surface — dialog/menu/popover/card all go through their primitive; special cases become a new prop, not a parallel surface.
108+
4. Don't use `v-form` directly — use `RForm`.
109+
5. Don't add backwards-compat shims inside v2: delete removed code; no `// removed`, no renamed-but-unused exports, no deprecated wrappers that just call the new function.
110+
6. Don't write redundant tests; don't touch v1; never `--no-verify` on commits.
111+
112+
**Allowed (often misread):** modifying shared stores/services/utils _additively_; creating v2-only composables; importing from `src/__generated__/`.
113+
114+
---
115+
116+
## Known debt (focused follow-ups)
117+
118+
- **Virtualisation migration**`RVirtualScroller` (`src/v2/lib/structural/`) needs to absorb `GameGrid`/`LetterGroupedGrid` (structural refactor of `Platform.vue`/`Search.vue`/`Collection.vue`); `useLetterGroups` must become index-based for AlphaStrip scroll-spy.
119+
- **`useGalleryFilterUrl`** — sync `galleryFilter` store fields to URL query params for bookmarkable links; mark v1 store usage `@deprecated`.
120+
- **Vue Router scroll restoration** — galleries scroll custom containers (`.r-v2-plat__scroll`), not window; add a Pinia `routeFullPath → offsetTop` map with per-view hooks. Bundle with the virtualisation migration.
121+
- **`useSocketEvent` composable** — typed socket subscriptions with mount/unmount cleanup (consumers currently wire `socket.on/off` by hand).
122+
- **When v1 dies**: move `uiVersion` into `UI_SETTINGS_KEYS`; drop `.r-v2-*` scope classes (tokens move to `:root`); simplify `useUISettings` sync; delete `useGameAnimation`; drop the color-string→tone collapser in `NotificationHost`; remove the Vuetify rule arrays in `stores/users.ts`.
123+
124+
Full reference: `docs/FRONTEND_ARCHITECTURE.md`.

0 commit comments

Comments
 (0)