From a6d5fd37419785077125d01492f947eda11f80d7 Mon Sep 17 00:00:00 2001 From: timlohse1104 Date: Sun, 15 Mar 2026 16:34:48 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs/=20folder=20with=20st?= =?UTF-8?q?ructured=20feature=20and=20shared=20infrastructure=20documentat?= =?UTF-8?q?ion=20for=20AI=20context.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + CLAUDE.md | 37 ++++++++ docs/_template.md | 122 ++++++++++++++++++++++++ docs/features/about.md | 44 +++++++++ docs/features/admin.md | 107 +++++++++++++++++++++ docs/features/catch-em-all.md | 39 ++++++++ docs/features/food-scan.md | 91 ++++++++++++++++++ docs/features/hitstar.md | 140 ++++++++++++++++++++++++++++ docs/features/jokes.md | 106 +++++++++++++++++++++ docs/features/memorandum.md | 99 ++++++++++++++++++++ docs/features/settings.md | 46 +++++++++ docs/features/todo.md | 119 +++++++++++++++++++++++ docs/features/uno-sort.md | 48 ++++++++++ docs/shared/application-routes.md | 104 +++++++++++++++++++++ docs/shared/auth-guard.md | 95 +++++++++++++++++++ docs/shared/i18n.md | 111 ++++++++++++++++++++++ docs/shared/identifiers.md | 81 ++++++++++++++++ docs/shared/keystore-persistence.md | 77 +++++++++++++++ docs/shared/nx-library-scaffold.md | 107 +++++++++++++++++++++ docs/shared/route-page-pattern.md | 82 ++++++++++++++++ docs/shared/stores.md | 90 ++++++++++++++++++ docs/shared/toggle-system.md | 110 ++++++++++++++++++++++ 22 files changed, 1856 insertions(+) create mode 100644 docs/_template.md create mode 100644 docs/features/about.md create mode 100644 docs/features/admin.md create mode 100644 docs/features/catch-em-all.md create mode 100644 docs/features/food-scan.md create mode 100644 docs/features/hitstar.md create mode 100644 docs/features/jokes.md create mode 100644 docs/features/memorandum.md create mode 100644 docs/features/settings.md create mode 100644 docs/features/todo.md create mode 100644 docs/features/uno-sort.md create mode 100644 docs/shared/application-routes.md create mode 100644 docs/shared/auth-guard.md create mode 100644 docs/shared/i18n.md create mode 100644 docs/shared/identifiers.md create mode 100644 docs/shared/keystore-persistence.md create mode 100644 docs/shared/nx-library-scaffold.md create mode 100644 docs/shared/route-page-pattern.md create mode 100644 docs/shared/stores.md create mode 100644 docs/shared/toggle-system.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 43294dbe..80f3096c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [docs] Add `docs/` folder with structured feature and shared infrastructure documentation for AI context (20 files: 1 template, 10 feature docs, 9 shared docs). - [hitstar] Show current game mode (Classic / Range) as small grey label top-left, only visible during an active game (hidden in the main menu). ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index de4d4602..4bb9ee1a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,6 +68,43 @@ Feature modules follow NX library pattern with `@backend/` path aliases: - `/lib/types/` - TypeScript definitions - `/lib/util/` - Helper functions +## Feature Documentation + +Structured docs live in `docs/`. Use these as context when working on or extending a feature. + +**Template**: `docs/_template.md` + +### Features + +| Feature | Doc | Architecture Pattern | Has Backend | +|---------|-----|---------------------|-------------| +| Hitstar | [docs/features/hitstar.md](docs/features/hitstar.md) | Spotify API proxy, state machine, localStorage | Yes | +| Memorandum | [docs/features/memorandum.md](docs/features/memorandum.md) | Keystore-backed bookmarks, per-user identifier | Yes | +| Todo | [docs/features/todo.md](docs/features/todo.md) | 3-tier NX, shared lists, optimistic locking | Yes | +| Jokes | [docs/features/jokes.md](docs/features/jokes.md) | MongoDB CRUD, nightly cron, external API | Yes | +| Admin | [docs/features/admin.md](docs/features/admin.md) | Identity verify, 5 sub-routes, aggregator | Yes | +| Food Scan | [docs/features/food-scan.md](docs/features/food-scan.md) | Stateless OCR proxy, file upload | Yes | +| Settings | [docs/features/settings.md](docs/features/settings.md) | Frontend-only, localStorage stores | No | +| Uno Sort | [docs/features/uno-sort.md](docs/features/uno-sort.md) | Frontend-only card sorting | No | +| Catch-em-all | [docs/features/catch-em-all.md](docs/features/catch-em-all.md) | Stub/placeholder | No | +| About | [docs/features/about.md](docs/features/about.md) | Static page, i18n only | No | + +### Shared Infrastructure + +| Topic | Doc | +|-------|-----| +| NX library scaffold | [docs/shared/nx-library-scaffold.md](docs/shared/nx-library-scaffold.md) | +| Frontend route pattern | [docs/shared/route-page-pattern.md](docs/shared/route-page-pattern.md) | +| Toggle system | [docs/shared/toggle-system.md](docs/shared/toggle-system.md) | +| Auth guard (`@Public()`) | [docs/shared/auth-guard.md](docs/shared/auth-guard.md) | +| Keystore persistence | [docs/shared/keystore-persistence.md](docs/shared/keystore-persistence.md) | +| Identifiers | [docs/shared/identifiers.md](docs/shared/identifiers.md) | +| i18n system | [docs/shared/i18n.md](docs/shared/i18n.md) | +| Application routes | [docs/shared/application-routes.md](docs/shared/application-routes.md) | +| localStorage stores | [docs/shared/stores.md](docs/shared/stores.md) | + +--- + ## Tech Stack **Backend**: NestJS 11, Fastify, MongoDB/Mongoose, Socket.io, Swagger/OpenAPI, Pino logging, Prometheus metrics diff --git a/docs/_template.md b/docs/_template.md new file mode 100644 index 00000000..a9d148e2 --- /dev/null +++ b/docs/_template.md @@ -0,0 +1,122 @@ +# Feature Name + +> One-liner: What this feature does in one sentence. + +## Architecture Pattern + +2-3 sentences describing the data flow, persistence strategy, and any external APIs involved. + +--- + +## Backend + +**NX Library**: `backend/libs//` +**Path alias**: `@backend/` + +### Module Structure + +| File | Purpose | +|------|---------| +| `src/lib/.module.ts` | NestJS module definition | +| `src/lib/.controller.ts` | HTTP endpoints | +| `src/lib/.service.ts` | Business logic | +| `src/lib/-mongodb.service.ts` | MongoDB layer (if applicable) | +| `src/lib/schema/.schema.ts` | Mongoose schema | + +### API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/v1/` | Public / Bearer | ... | +| POST | `/v1/` | Public / Bearer | ... | +| PUT | `/v1//:id` | Public / Bearer | ... | +| DELETE | `/v1//:id` | Bearer | ... | + +### DTOs + +- `InputDto` — ... +- `OutputDto` — ... + +### Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `VAR_NAME` | Yes | Description | + +### Cron Jobs + +| Schedule | Timezone | Method | Description | +|----------|----------|--------|-------------| +| `0 3 * * *` | Europe/Berlin | `methodName` | What it does | + +--- + +## Frontend + +**Route**: `/feature-name` +**Route files**: `frontend/src/routes/feature-name/` + +### Components + +| Component | Path | Purpose | +|-----------|------|---------| +| `+page.svelte` | `routes/feature-name/` | Main page | +| `+page.ts` | `routes/feature-name/` | Prerender config + toggle gate | + +### API Client + +`frontend/src/lib/api/.api.ts` + +| Function | Description | +|----------|-------------| +| `getResource()` | ... | +| `createResource()` | ... | + +### Types + +`frontend/src/lib/types/.dto.ts` + +| Type/Interface | Description | +|----------------|-------------| +| `ResourceDto` | ... | + +### Stores + +`frontend/src/lib/util/stores/store-.ts` + +| Store | localStorage key | Description | +|-------|-----------------|-------------| +| `featureStore` | `feature.key` | ... | + +### i18n Key Prefix + +Keys live under `feature.` in `frontend/src/lib/config/en.json` and `de.json`. + +--- + +## Toggle Configuration + +| Property | Value | +|----------|-------| +| Toggle key | `TOGGLE_NAV_FEATURE_NAME` | +| Seed default | `'true'` | +| Frontend enum | `TogglesEnum.featureName` | +| Route config | `applicationRoutes.featureName` | + +--- + +## Shared Infrastructure Dependencies + +- [toggle-system.md](../shared/toggle-system.md) — toggle seed + frontend config +- [route-page-pattern.md](../shared/route-page-pattern.md) — `+page.ts` toggle gate +- [auth-guard.md](../shared/auth-guard.md) — `@Public()` decorator + `AdminGuard` +- [i18n.md](../shared/i18n.md) — translation system +- [stores.md](../shared/stores.md) — localStorage store pattern +- [application-routes.md](../shared/application-routes.md) — route registry + +--- + +## Key Implementation Notes + +- Note any gotchas, non-obvious design decisions, or patterns specific to this feature. +- Mention if this feature deviates from common patterns. diff --git a/docs/features/about.md b/docs/features/about.md new file mode 100644 index 00000000..0fbae205 --- /dev/null +++ b/docs/features/about.md @@ -0,0 +1,44 @@ +# About + +> Static "About me" page — frontend-only, no backend. + +## Architecture Pattern + +Static page with personal information. No backend API calls, no stores. + +--- + +## Frontend + +**Route**: `/about` +**Route files**: `frontend/src/routes/about/+page.svelte`, `+page.ts` + +### i18n Key Prefix + +Keys under `about.*` in translation files. + +--- + +## Toggle Configuration + +| Property | Value | +|----------|-------| +| Toggle key | `TOGGLE_NAV_ABOUT` | +| Seed default | `'true'` | +| Frontend enum | `TogglesEnum.about` | +| Route config | `applicationRoutes.about` | + +--- + +## Shared Infrastructure Dependencies + +- [toggle-system.md](../shared/toggle-system.md) +- [route-page-pattern.md](../shared/route-page-pattern.md) +- [i18n.md](../shared/i18n.md) + +--- + +## Key Implementation Notes + +- No backend library, no API client, no stores. +- Content is static and localized via i18n keys. diff --git a/docs/features/admin.md b/docs/features/admin.md new file mode 100644 index 00000000..a7ad9e17 --- /dev/null +++ b/docs/features/admin.md @@ -0,0 +1,107 @@ +# Admin + +> Admin dashboard with 5 sub-routes for managing identifiers, jokes, link presets, toggles, and activities. + +## Architecture Pattern + +The admin feature has a thin NestJS backend (single `POST /verify` endpoint) and a rich frontend with nested routes. The backend aggregates data from the `IdentifiersService`. Frontend admin pages call the respective resource APIs directly (identifiers, jokes, keystore). + +--- + +## Backend + +**NX Library**: `backend/libs/admin/` +**Path alias**: `@backend/admin` + +### Module Structure + +| File | Purpose | +|------|---------| +| `src/lib/admin.module.ts` | NestJS module | +| `src/lib/admin.controller.ts` | HTTP endpoints | +| `src/lib/admin.service.ts` | Verify admin/user identity | + +### API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| POST | `/v1/admin/verify` | Public | Verify if ID is admin or registered user | + +**Rate limit**: 300 requests per 5 minutes (stricter than default 500). + +### `verifyAdmin` Logic + +```ts +// type = 'admin' → compare against ADMIN_IDENTIFIER env var +// type = 'user' → check if identifier exists in MongoDB +{ id: string, type: 'admin' | 'user' } → { isVerified: boolean } +``` + +--- + +## Frontend + +**Route**: `/admin` + sub-routes +**Route files**: `frontend/src/routes/admin/` + +### Sub-Routes + +| Sub-route | Path | Toggle | Description | +|-----------|------|--------|-------------| +| Activities | `/admin` | `TOGGLE_ADMIN_ACTIVITIES` | Main dashboard / activity log | +| Identifiers | `/admin/identifiers` | `TOGGLE_ADMIN_IDENTIFIERS` | Manage user identifiers | +| Jokes | `/admin/jokes` | `TOGGLE_ADMIN_JOKES` | Manage jokes CRUD | +| Presets | `/admin/presets` | `TOGGLE_ADMIN_LINK_PRESETS` | Manage memorandum link presets | +| Toggles | `/admin/toggles` | — | Manage feature toggles via Keystore | + +### Navigation + +Sub-routes defined in `adminSubRoutes` in `frontend/src/lib/config/applications.ts`. Note: there is a typo in the code — key is `idenfiers` (missing 't') but it's not user-facing. + +### API Clients Used + +- `frontend/src/lib/api/admin.api.ts` — verify admin identity +- `frontend/src/lib/api/identifiers.api.ts` — identifiers management +- `frontend/src/lib/api/jokes.api.ts` — jokes management +- `frontend/src/lib/api/keystore.api.ts` — toggle management + +### Types + +`frontend/src/lib/types/admin.dto.ts` + +### i18n Key Prefix + +Keys under `admin.*` in translation files. + +--- + +## Toggle Configuration + +Admin routes use their own toggles (not a single nav toggle): + +| Toggle key | Enum | Controls | +|------------|------|---------| +| `TOGGLE_ADMIN_DASHBOARD` | `TogglesEnum.adminDashboard` | Admin area visibility | +| `TOGGLE_ADMIN_ACTIVITIES` | `TogglesEnum.adminActivities` | Activities tab | +| `TOGGLE_ADMIN_IDENTIFIERS` | `TogglesEnum.adminIdentifiers` | Identifiers tab | +| `TOGGLE_ADMIN_JOKES` | `TogglesEnum.adminJokes` | Jokes tab | +| `TOGGLE_ADMIN_LINK_PRESETS` | `TogglesEnum.adminLinkPreset` | Presets tab | + +Admin is accessible via `utilityRoutes.admin` (always shown, no global toggle gate). + +--- + +## Shared Infrastructure Dependencies + +- [auth-guard.md](../shared/auth-guard.md) — admin verification + ADMIN_IDENTIFIER +- [identifiers.md](../shared/identifiers.md) — user identity management +- [keystore-persistence.md](../shared/keystore-persistence.md) — toggle management UI +- [toggle-system.md](../shared/toggle-system.md) +- [application-routes.md](../shared/application-routes.md) — adminSubRoutes + +--- + +## Key Implementation Notes + +- The admin area is not toggle-gated at the route level (no `+page.ts` toggle redirect). Auth is enforced by the verify flow in the frontend. +- `ADMIN_IDENTIFIER` is the same secret used by `AdminGuard` for Bearer token auth on protected endpoints. diff --git a/docs/features/catch-em-all.md b/docs/features/catch-em-all.md new file mode 100644 index 00000000..cdf90b14 --- /dev/null +++ b/docs/features/catch-em-all.md @@ -0,0 +1,39 @@ +# Catch-em-all + +> Stub / placeholder — feature not yet implemented. + +## Status + +This is a placeholder route. The navigation toggle and route entry exist, but the feature has no meaningful functionality. + +--- + +## Frontend + +**Route**: `/catch-em-all` +**Route files**: `frontend/src/routes/catch-em-all/+page.svelte`, `+page.ts` + +--- + +## Toggle Configuration + +| Property | Value | +|----------|-------| +| Toggle key | `TOGGLE_NAV_CATCH_EM_ALL` | +| Seed default | `'true'` | +| Frontend enum | `TogglesEnum.catchEmAll` | +| Route config | `applicationRoutes['catch-em-all']` | + +--- + +## Backend + +**NX Library**: `backend/libs/catch-em-all/` (exists in path aliases but may be minimal) +**Path alias**: `@backend/catch-em-all` + +--- + +## Shared Infrastructure Dependencies + +- [toggle-system.md](../shared/toggle-system.md) +- [route-page-pattern.md](../shared/route-page-pattern.md) diff --git a/docs/features/food-scan.md b/docs/features/food-scan.md new file mode 100644 index 00000000..d1162e4c --- /dev/null +++ b/docs/features/food-scan.md @@ -0,0 +1,91 @@ +# Food Scan + +> OCR image text extraction — upload a food label image and get the extracted text via OCR.space API. + +## Architecture Pattern + +The frontend uploads an image file to the NestJS backend, which proxies it to the external OCR.space API. The result (extracted text + structured data) is returned to the frontend. No database persistence — stateless proxy. + +--- + +## Backend + +**NX Library**: `backend/libs/ocr/` +**Path alias**: `@backend/ocr` + +The OCR module wraps the shared OCR provider (`@backend/shared-ocr`). + +### Module Structure + +| File | Purpose | +|------|---------| +| `src/lib/ocr.module.ts` | NestJS module | +| `src/lib/ocr.controller.ts` | File upload endpoint | + +Shared provider: `backend/libs/shared/provider/ocr/` +**Path alias**: `@backend/shared-ocr` + +### API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| POST | `/v1/ocr` | Public | Upload image, returns OCR text extraction | + +**File upload**: Uses `@nest-lab/fastify-multer` with `FileInterceptor('file')`. The `fastifyMultipart` plugin is registered in `main.ts`. + +### Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `OCR_SPACE_URL` | Yes | OCR.space API endpoint (`https://api.ocr.space/parse/image`) | +| `OCR_SPACE_API_KEY` | Yes | OCR.space API key | + +--- + +## Frontend + +**Route**: `/food-scan` +**Route files**: `frontend/src/routes/food-scan/+page.svelte`, `+page.ts` + +### API Client + +`frontend/src/lib/api/ocr.api.ts` + +Sends image as `multipart/form-data` to `POST /v1/ocr`. + +### Types + +`frontend/src/lib/types/ocr-space.dto.ts` + +`OcrSpaceResponseDto` — structured OCR result from OCR.space. + +### i18n Key Prefix + +Keys under `foodScan.*` in translation files. + +--- + +## Toggle Configuration + +| Property | Value | +|----------|-------| +| Toggle key | `TOGGLE_NAV_FOOD_SCAN` | +| Seed default | `'true'` | +| Frontend enum | `TogglesEnum.foodScan` | +| Route config | `applicationRoutes['food-scan']` | + +--- + +## Shared Infrastructure Dependencies + +- [toggle-system.md](../shared/toggle-system.md) +- [route-page-pattern.md](../shared/route-page-pattern.md) +- [auth-guard.md](../shared/auth-guard.md) + +--- + +## Key Implementation Notes + +- The backend acts as a pure proxy — it only forwards the file to OCR.space and returns the response. +- `fastifyMultipart` must be registered in `main.ts` for file uploads to work with Fastify. +- The endpoint is `@Public()` — no auth required to scan images. diff --git a/docs/features/hitstar.md b/docs/features/hitstar.md new file mode 100644 index 00000000..54dd8cad --- /dev/null +++ b/docs/features/hitstar.md @@ -0,0 +1,140 @@ +# Hitstar + +> Music guessing game — players hear a Spotify track and guess its release year (Classic or Range mode). + +## Architecture Pattern + +The backend fetches a random track from the Spotify API using Client Credentials flow (token cached in memory). The frontend drives a state machine (MENU → LOADING → GUESSING → REVEAL → RESULTS) with 3D flip card UI. Game state and best scores are persisted per game mode in localStorage. + +--- + +## Backend + +**NX Library**: `backend/libs/hitstar/` +**Path alias**: `@backend/hitstar` + +### Module Structure + +| File | Purpose | +|------|---------| +| `src/lib/hitstar.module.ts` | NestJS module | +| `src/lib/hitstar.controller.ts` | HTTP endpoints | +| `src/lib/hitstar.service.ts` | Spotify API integration + token caching | + +### API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/v1/hitstar/random-track` | Public | Returns a random Spotify track | + +### Random Track Logic + +1. Pick a random year (1955–2025) and offset (0–99). +2. Search Spotify: `q=* year: genre:pop&type=track&market=DE&limit=10&offset=` +3. Filter tracks that have an `album.release_date`. +4. Return a random valid track as `SpotifyTrackDto`. +5. Retry up to 10 times if no valid tracks found. + +### Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `SPOTIFY_CLIENT_ID` | Yes | Spotify app client ID | +| `SPOTIFY_CLIENT_SECRET` | Yes | Spotify app client secret | + +### Token Caching + +Access token is cached in-memory on the service instance. Refreshed automatically 60 seconds before expiry using Spotify's Client Credentials flow (`POST https://accounts.spotify.com/api/token`). + +--- + +## Frontend + +**Route**: `/hitstar` +**Route files**: `frontend/src/routes/hitstar/+page.svelte`, `+page.ts` + +### Game Modes + +| Mode | Description | +|------|-------------| +| **Classic** | Guess the exact year of the track | +| **Range** | Guess a year range (e.g., 1990–2000) | + +### State Machine + +``` +MENU → LOADING → GUESSING → REVEAL → RESULTS + ↑__________________________| + (next round) +``` + +- **MENU**: Start screen, select mode +- **LOADING**: Fetching random track from backend +- **GUESSING**: Player inputs year/range; Spotify embed available +- **REVEAL**: Show actual year, animate 3D flip card +- **RESULTS**: Show all round results after final round + +### API Client + +`frontend/src/lib/api/spotify.api.ts` + +| Function | Description | +|----------|-------------| +| `getRandomTrack()` | Fetch random track from `/v1/hitstar/random-track` | + +### Types + +`frontend/src/lib/types/spotify.dto.ts` + +| Type | Description | +|------|-------------| +| `SpotifyTrackDto` | `{ id, name, artist, album, albumCover, releaseYear, spotifyUrl, previewUrl }` | +| `HitstarRoundResult` | Per-round result including guess, actual year, points | +| `HitstarBestRound` | `{ score, date }` — persisted best score | +| `HitstarGameState` | `{ currentRound, score, currentTrack, roundResults }` — full in-progress state | + +### Stores + +`frontend/src/lib/util/stores/store-hitstar.ts` + +| Store | localStorage key | Description | +|-------|-----------------|-------------| +| `hitstarClassicBestRoundStore` | `hitstar.classic.bestRound` | Best score for Classic mode | +| `hitstarClassicGameStateStore` | `hitstar.classic.gameState` | Saved Classic game state | +| `hitstarRangeBestRoundStore` | `hitstar.range.bestRound` | Best score for Range mode | +| `hitstarRangeGameStateStore` | `hitstar.range.gameState` | Saved Range game state | + +### i18n Key Prefix + +Keys under `hitstar.*` in translation files. + +--- + +## Toggle Configuration + +| Property | Value | +|----------|-------| +| Toggle key | `TOGGLE_NAV_HITSTAR` | +| Seed default | `'true'` | +| Frontend enum | `TogglesEnum.hitstar` | +| Route config | `applicationRoutes.hitstar` | + +--- + +## Shared Infrastructure Dependencies + +- [toggle-system.md](../shared/toggle-system.md) +- [route-page-pattern.md](../shared/route-page-pattern.md) +- [stores.md](../shared/stores.md) — localStorage pattern for game state +- [nx-library-scaffold.md](../shared/nx-library-scaffold.md) + +--- + +## Key Implementation Notes + +- Game state is intentionally NOT auto-resumed on mount — user must click "New Game" to start. +- The `previewUrl` can be `null` (Spotify doesn't provide previews for all tracks). The UI must handle this gracefully. +- localStorage keys were migrated in 2026-03: `hitstar.bestRound` → `hitstar.classic.bestRound`. Migration code in `store-hitstar.ts`. +- The Spotify search uses `genre:pop` filter to surface more recognizable tracks, and randomizes the year per request for variety. +- CSS 3D flip card + shake animation used in REVEAL state. +- `celebrate()` from `stores-global` triggers confetti on high scores. diff --git a/docs/features/jokes.md b/docs/features/jokes.md new file mode 100644 index 00000000..edb0596c --- /dev/null +++ b/docs/features/jokes.md @@ -0,0 +1,106 @@ +# Jokes + +> Daily joke display with automatic nightly fetch from external API and MongoDB persistence. + +## Architecture Pattern + +Jokes are fetched from `witzapi.de` (German joke API) every night at 03:00 via a cron job, stored in MongoDB, and served to the frontend. The frontend shows one joke at a time (daily or random). + +--- + +## Backend + +**NX Library**: `backend/libs/jokes/` +**Path alias**: `@backend/jokes` + +This is the **reference library** for NX scaffold (see [nx-library-scaffold.md](../shared/nx-library-scaffold.md)). + +### Module Structure + +| File | Purpose | +|------|---------| +| `src/lib/jokes.module.ts` | NestJS module | +| `src/lib/jokes.controller.ts` | HTTP endpoints | +| `src/lib/jokes.service.ts` | Business logic + cron jobs | +| `src/lib/jokes-mongodb.service.ts` | MongoDB CRUD layer | +| `src/lib/schema/jokes.schema.ts` | Mongoose schema | + +### API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/v1/jokes/random` | Public | Random verified joke | +| GET | `/v1/jokes/daily` | Public | Today's joke (rotated nightly) | +| GET | `/v1/jokes` | Bearer | List all jokes (filterable) | +| GET | `/v1/jokes/:id` | Bearer | Get joke by ID | +| POST | `/v1/jokes` | Public | Create new joke | +| PUT | `/v1/jokes/:id` | Bearer | Update joke | +| DELETE | `/v1/jokes/:id` | Bearer | Delete joke | + +### Cron Jobs + +| Schedule | Timezone | Method | Description | +|----------|----------|--------|-------------| +| `0 3 * * *` | Europe/Berlin | `persistDailyJoke()` | Fetch + store joke from `witzapi.de` | +| `30 3 * * *` | Europe/Berlin | `cleanupDuplicateJokes()` | Remove duplicate jokes | + +External API: `https://witzapi.de/api/joke/?limit=1&language=de` + +Uses `@nestjs/axios` + `firstValueFrom` RxJS pattern. + +### DTOs + +- `JokeDto` — full joke object +- `ModifyJokeDto` — create/update payload + +--- + +## Frontend + +**Route**: `/jokes` +**Route files**: `frontend/src/routes/jokes/+page.svelte`, `+page.ts` + +### API Client + +`frontend/src/lib/api/jokes.api.ts` + +### Types + +`frontend/src/lib/types/jokes.dto.ts` + +### Toggle for Random Joke + +There is a separate toggle `TOGGLE_RANDOM_JOKE` (distinct from the nav toggle `TOGGLE_NAV_JOKES`) that controls whether a random joke appears on the home page. + +### i18n Key Prefix + +Keys under `jokes.*` in translation files. + +--- + +## Toggle Configuration + +| Property | Value | +|----------|-------| +| Nav toggle key | `TOGGLE_NAV_JOKES` | +| Random joke toggle | `TOGGLE_RANDOM_JOKE` | +| Seed default | `'true'` | +| Frontend enum | `TogglesEnum.jokes` / `TogglesEnum.randomJoke` | +| Route config | `applicationRoutes.jokes` | + +--- + +## Shared Infrastructure Dependencies + +- [toggle-system.md](../shared/toggle-system.md) +- [route-page-pattern.md](../shared/route-page-pattern.md) +- [auth-guard.md](../shared/auth-guard.md) +- [nx-library-scaffold.md](../shared/nx-library-scaffold.md) + +--- + +## Key Implementation Notes + +- `persistDailyJoke()` sets `verified: true` on all auto-fetched jokes. +- The POST endpoint is `@Public()` to allow admin tools to submit jokes manually without auth. +- `findJokeOfTheDay()` in the MongoDB service retrieves the most recent verified joke (implementation detail in schema query). diff --git a/docs/features/memorandum.md b/docs/features/memorandum.md new file mode 100644 index 00000000..4185d2e5 --- /dev/null +++ b/docs/features/memorandum.md @@ -0,0 +1,99 @@ +# Memorandum + +> Bookmark and link manager — per-user folder hierarchy stored in the Keystore (MongoDB). + +## Architecture Pattern + +Memorandum stores bookmarks and folder structures as Keystore entries scoped to a user's identifier UUID. There is no separate Memorandum MongoDB collection — all data is stored in the shared Keystore. User identity is managed via the Identifiers service. + +--- + +## Backend + +**NX Library**: `backend/libs/memorandum/` +**Path alias**: `@backend/memorandum` + +The Memorandum module does not define its own services — it re-exports the shared `KeystoreMongoDbService` and `IdentifiersService` via two controllers. + +### Module Structure + +| File | Purpose | +|------|---------| +| `src/lib/memorandum.module.ts` | NestJS module (imports KeystorePersistence + Identifiers) | +| `src/lib/keystore.controller.ts` | Exposes Keystore CRUD at `/keystore` | +| `src/lib/identifiers.controller.ts` | Exposes Identifiers CRUD at `/identifiers` | + +### API Endpoints — Keystore (`/keystore`) + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/v1/keystore` | Bearer | List all keys (admin) | +| GET | `/v1/keystore/:identifier/:key` | Public | Get a single key | +| POST | `/v1/keystore` | Public | Create a key | +| PUT | `/v1/keystore/:identifier/:key` | Public | Update a key's value | +| DELETE | `/v1/keystore/:identifier/:key` | Bearer | Delete a key | + +### API Endpoints — Identifiers (`/identifiers`) + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/v1/identifiers` | Bearer | List all identifiers | +| GET | `/v1/identifiers/:id` | Public | Get by ID | +| POST | `/v1/identifiers` | Bearer | Create new | +| PUT | `/v1/identifiers/:id` | Public | Update name | +| DELETE | `/v1/identifiers/:id` | Bearer | Delete | + +--- + +## Frontend + +**Route**: `/memorandum` +**Route files**: `frontend/src/routes/memorandum/+page.svelte`, `+page.ts` + +### API Clients + +- `frontend/src/lib/api/keystore.api.ts` — CRUD for bookmark/folder data +- `frontend/src/lib/api/identifiers.api.ts` — user identity management + +### Types + +| Type file | Description | +|-----------|-------------| +| `memorandum.dto.ts` | Link/bookmark DTOs | +| `memorandum-folder.ts` | Folder structure types | +| `keystore.dto.ts` | Keystore entry types | +| `identifiers.dto.ts` | Identifier types | + +### i18n Key Prefix + +Keys under `memorandum.*` in translation files. + +--- + +## Toggle Configuration + +| Property | Value | +|----------|-------| +| Toggle key | `TOGGLE_NAV_MEMORANDUM` | +| Seed default | `'true'` | +| Frontend enum | `TogglesEnum.memorandum` | +| Route config | `applicationRoutes.memorandum` | + +--- + +## Shared Infrastructure Dependencies + +- [keystore-persistence.md](../shared/keystore-persistence.md) — data storage +- [identifiers.md](../shared/identifiers.md) — user namespacing +- [toggle-system.md](../shared/toggle-system.md) +- [route-page-pattern.md](../shared/route-page-pattern.md) +- [auth-guard.md](../shared/auth-guard.md) + +--- + +## Key Implementation Notes + +- Memorandum has **no dedicated MongoDB schema** — all data lives in the shared Keystore collection. +- Each user's data is scoped by their identifier UUID as the `identifier` field in Keystore entries. +- The user's identifier UUID is stored in localStorage and sent with every Keystore request. +- Folder structure and bookmark list are stored as separate Keystore keys under the same identifier. diff --git a/docs/features/settings.md b/docs/features/settings.md new file mode 100644 index 00000000..63db4a6b --- /dev/null +++ b/docs/features/settings.md @@ -0,0 +1,46 @@ +# Settings + +> User settings page — frontend-only, no backend. Controls UI preferences stored in localStorage. + +## Architecture Pattern + +Settings are managed entirely in the frontend. No backend API calls. User preferences (locale, theme, etc.) are stored in Svelte stores backed by localStorage. + +--- + +## Frontend + +**Route**: `/settings` +**Route files**: `frontend/src/routes/settings/+page.svelte` + +No `+page.ts` toggle gate — settings is always accessible (part of `utilityRoutes`). + +### Key Functionality + +- Language selection (de / en) via `setLocale()` from the i18n system +- Any other user preferences managed via localStorage stores + +### i18n Key Prefix + +Keys under `settings.*` in translation files. + +--- + +## Toggle Configuration + +Settings is in `utilityRoutes`, not `applicationRoutes` — no toggle gate, always visible. + +--- + +## Shared Infrastructure Dependencies + +- [stores.md](../shared/stores.md) — localStorage store pattern +- [i18n.md](../shared/i18n.md) — locale switching +- [application-routes.md](../shared/application-routes.md) — `utilityRoutes.settings` + +--- + +## Key Implementation Notes + +- No `+page.ts` needed — no toggle to check, no prerender restrictions specific to this route. +- Locale changes via `setLocale()` take effect immediately without page reload. diff --git a/docs/features/todo.md b/docs/features/todo.md new file mode 100644 index 00000000..93a0e1ec --- /dev/null +++ b/docs/features/todo.md @@ -0,0 +1,119 @@ +# Todo + +> Shared, collaborative todo lists with real-time optimistic locking — persisted in MongoDB. + +## Architecture Pattern + +Todo lists are stored in MongoDB and exposed via REST. The frontend supports both local lists (localStorage only) and shared lists (synced to backend). Updates use optimistic locking via a `version` field to prevent concurrent-write data loss. + +--- + +## Backend + +**NX Library** (3-tier): `backend/libs/todo/` +**Path aliases**: +- `@backend/todo-controller` → `libs/todo/todo-controller/` +- `@backend/todo-provider` → `libs/todo/todo-provider/` +- `@backend/todo-persistence` → `libs/todo/todo-persistence/` + +### Module Structure + +| File | Purpose | +|------|---------| +| `todo-controller/src/lib/todo.controller.ts` | HTTP endpoints | +| `todo-provider/src/lib/todo-provider.service.ts` | Business logic | +| `todo-persistence/src/lib/todo-persistence.service.ts` | MongoDB layer (optimistic locking) | +| `todo-persistence/src/lib/schema/todo.schema.ts` | Mongoose schema (`SharedTodoList`) | + +### Schema + +```ts +SharedTodoList { + name: string; + emoji: string; + todos: Todo[]; // { id, title, done?, category? } + history: HistoryEntry[]; // { title, category } + categories: string[]; + version: number; // incremented on every update + created: Date; + updated: Date; +} +``` + +### API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/v1/shared-todo-lists` | Bearer | List all shared lists | +| GET | `/v1/shared-todo-lists/:id` | Public | Get a single list | +| POST | `/v1/shared-todo-lists` | Public | Create new list | +| PUT | `/v1/shared-todo-lists/:id` | Public | Update list (version required) | +| DELETE | `/v1/shared-todo-lists/:id` | Public | Delete list | + +### Optimistic Locking + +`PUT /v1/shared-todo-lists/:id` requires `version` in the body. The persistence layer uses an atomic MongoDB `findOneAndUpdate` with `{ _id: id, version: currentVersion }` as the filter. If no document is updated: +- If the document doesn't exist → 404 `NotFoundException` +- If it exists but version differs → 409 `ConflictException` + +--- + +## Frontend + +**Route**: `/todo` +**Route files**: `frontend/src/routes/todo/+page.svelte`, `+page.ts` + +### API Client + +`frontend/src/lib/api/todo.api.ts` + +| Function | Description | +|----------|-------------| +| `getSharedTodoList(id)` | Fetch a shared list by ID | +| `createSharedTodoList(name, emoji)` | Create new shared list | +| `updateSharedTodoList(id, data)` | Update list (includes `version`) | +| `deleteSharedTodoList(id)` | Delete shared list | + +### Types + +`frontend/src/lib/types/todo.ts` + +| Type | Description | +|------|-------------| +| `Todo` | `{ id, title, done?, category? }` | +| `TodoList` | Frontend local list (includes `isShared`, `sharedId`, `version`) | +| `SharedTodoListResponse` | Backend response shape (includes `_id`, `version`, timestamps) | +| `HistoryEntry` | `{ title, category }` — completed item history | + +### i18n Key Prefix + +Keys under `todo.*` in translation files. + +--- + +## Toggle Configuration + +| Property | Value | +|----------|-------| +| Toggle key | `TOGGLE_NAV_TODO` | +| Seed default | `'true'` | +| Frontend enum | `TogglesEnum.todo` | +| Route config | `applicationRoutes.todo` | + +--- + +## Shared Infrastructure Dependencies + +- [toggle-system.md](../shared/toggle-system.md) +- [route-page-pattern.md](../shared/route-page-pattern.md) +- [auth-guard.md](../shared/auth-guard.md) +- [nx-library-scaffold.md](../shared/nx-library-scaffold.md) + +--- + +## Key Implementation Notes + +- Local lists are stored in localStorage only and never hit the backend. +- Shared lists are identified by a `sharedId` (MongoDB `_id`) stored in localStorage alongside the local list. +- The version conflict (409) must be handled on the frontend — typically by re-fetching and re-applying changes. +- The 3-tier split (controller / provider / persistence) is unique to Todo; simpler features use a single library. diff --git a/docs/features/uno-sort.md b/docs/features/uno-sort.md new file mode 100644 index 00000000..ab9844cd --- /dev/null +++ b/docs/features/uno-sort.md @@ -0,0 +1,48 @@ +# Uno Sort + +> Frontend-only UNO card sorting utility — no backend. + +## Architecture Pattern + +Pure frontend feature. Players input their UNO hand and the app sorts the cards by color and value. No persistence, no API calls. + +--- + +## Frontend + +**Route**: `/uno-sort` +**Route files**: `frontend/src/routes/uno-sort/+page.svelte`, `+page.ts` + +### Types + +`frontend/src/lib/types/uno-sort.ts` — UNO card type definitions +`frontend/src/lib/types/cards.ts` — generic card type definitions + +### i18n Key Prefix + +Keys under `unoSort.*` in translation files. + +--- + +## Toggle Configuration + +| Property | Value | +|----------|-------| +| Toggle key | `TOGGLE_NAV_UNO_SORT` | +| Seed default | `'true'` | +| Frontend enum | `TogglesEnum.unoSort` | +| Route config | `applicationRoutes['uno-sort']` | + +--- + +## Shared Infrastructure Dependencies + +- [toggle-system.md](../shared/toggle-system.md) +- [route-page-pattern.md](../shared/route-page-pattern.md) + +--- + +## Key Implementation Notes + +- No backend library — frontend-only feature. +- No API client or stores needed. diff --git a/docs/shared/application-routes.md b/docs/shared/application-routes.md new file mode 100644 index 00000000..da0b8aa6 --- /dev/null +++ b/docs/shared/application-routes.md @@ -0,0 +1,104 @@ +# Application Routes + +> Centralized route registry — `ApplicationRoutes` type + `loadApplications()` + reactive writable store. + +## Overview + +All navigable routes are defined in `applications.ts`. The file exports a typed route map, a function that hydrates toggles from the backend, and a reactive Svelte store. The navigation sidebar reads from the derived `activeApplicationRoutes` store. + +--- + +## File + +`frontend/src/lib/config/applications.ts` + +--- + +## Types + +```ts +export type ApplicationRoutes = { + home: Route; + memorandum: Route; + jokes: Route; + todo: Route; + 'food-scan': Route; + 'catch-em-all': Route; + 'uno-sort': Route; + hitstar: Route; + about: Route; +}; +``` + +`Route` type is defined in `frontend/src/lib/types/route.ts`: + +```ts +export type Route = { + id: string; + name: { en: string; de: string }; + path: string; + icon: ComponentType; + toggle: boolean; +}; +``` + +--- + +## Stores + +```ts +// Writable store — starts with all toggles = false (default) +export const applicationRoutes = writable(defaultApplicationRoutes); + +// Derived store — only routes where toggle is true +export const activeApplicationRoutes = derived(applicationRoutes, + ($routes) => Object.values($routes).filter((route) => route.toggle) +); +``` + +On module load, `loadApplications()` fires and updates the store with actual toggle values from the backend. + +--- + +## Utility Routes (not in ApplicationRoutes) + +```ts +export const utilityRoutes = { + settings: { path: '/settings', ... }, + admin: { path: '/admin', ... }, +}; +``` + +Always shown, no toggle gate. + +--- + +## Admin Sub-Routes + +```ts +export const adminSubRoutes = { + activities: { path: '/admin', ... }, + idenfiers: { path: '/admin/identifiers', ... }, // typo in code: "idenfiers" + jokes: { path: '/admin/jokes', ... }, + presets: { path: '/admin/presets', ... }, + toggles: { path: '/admin/toggles', ... }, +}; +``` + +--- + +## Adding a New Route + +1. Add to `ApplicationRoutes` type +2. Add `getToggleValue(TogglesEnum.newFeature)` in `loadApplications()` +3. Add entry in `loadApplications()` return object +4. Add entry in `defaultApplicationRoutes` (with `toggle: false`) +5. Add toggle key to `TOGGLE_SEED_CONFIG` (see [toggle-system.md](toggle-system.md)) +6. Add `TogglesEnum.newFeature` enum entry (see [toggle-system.md](toggle-system.md)) + +--- + +## Related + +- [toggle-system.md](toggle-system.md) — toggle lifecycle +- [route-page-pattern.md](route-page-pattern.md) — `+page.ts` toggle gate per route diff --git a/docs/shared/auth-guard.md b/docs/shared/auth-guard.md new file mode 100644 index 00000000..804e48b3 --- /dev/null +++ b/docs/shared/auth-guard.md @@ -0,0 +1,95 @@ +# Auth Guard + +> Global `AdminGuard` protects all endpoints by default. Use `@Public()` to opt out. + +## Overview + +The backend uses a single global guard (`AdminGuard`) registered in `AppModule`. All routes require a valid Bearer token unless explicitly marked with `@Public()`. + +--- + +## Implementation + +**File**: `backend/libs/shared/util/src/lib/admin-auth.guard.ts` + +```ts +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); + +@Injectable() +export class AdminGuard implements CanActivate { + constructor( + private configService: ConfigService, + private reflector: Reflector, + ) {} + + async canActivate(context: ExecutionContext): Promise { + // 1. Check for @Public() metadata → allow immediately + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) return true; + + // 2. Extract Bearer token from Authorization header + const token = this.extractTokenFromHeader(request); + if (!token) throw new UnauthorizedException(...); + + // 3. Compare token against ADMIN_IDENTIFIER env var + const adminIdentifier = this.configService.get('ADMIN_IDENTIFIER'); + return token === adminIdentifier; + } +} +``` + +--- + +## Registration in AppModule + +```ts +// backend/apps/tilloh-dev/src/main.ts +providers: [ + { + provide: APP_GUARD, + useClass: AdminGuard, + }, +], +``` + +--- + +## Usage in Controllers + +```ts +// Mark a single endpoint as public (no auth required) +@Public() +@Get('/random') +getRandomJoke() { ... } + +// Leave off @Public() for admin-only endpoints (requires Bearer token) +@ApiBearerAuth() +@Get('/') +listJokes() { ... } +``` + +--- + +## Environment Variable + +| Variable | Description | +|----------|-------------| +| `ADMIN_IDENTIFIER` | The secret Bearer token checked against incoming requests | + +--- + +## Throttler Guard + +A second global guard `ThrottlerGuard` is also registered. Default: **500 requests per 5 minutes**. The `admin` POST `/verify` endpoint uses a stricter limit: 300 per 5 minutes. + +--- + +## Key Notes + +- Token comparison is a simple string equality check against `ADMIN_IDENTIFIER` — no JWT, no sessions. +- `@Public()` is a NestJS metadata decorator, not a real security skip; the guard still runs but short-circuits. +- Export path: `@backend/util` → `backend/libs/shared/util/src/index.ts` diff --git a/docs/shared/i18n.md b/docs/shared/i18n.md new file mode 100644 index 00000000..fe70203d --- /dev/null +++ b/docs/shared/i18n.md @@ -0,0 +1,111 @@ +# i18n (Internationalization) + +> `sveltekit-i18n` library — lazy-loaded JSON translation files for `en` and `de` locales. + +## Overview + +The app supports English (`en`) and German (`de`). Translations are loaded from JSON config files. The active locale is auto-detected from `navigator.language` on first load. + +--- + +## Core Files + +| File | Purpose | +|------|---------| +| `frontend/src/lib/util/translations.ts` | i18n instance + config + `Params` interface | +| `frontend/src/lib/config/en.json` | English translations | +| `frontend/src/lib/config/de.json` | German translations | + +--- + +## Setup (`translations.ts`) + +```ts +import i18n, { type Config } from 'sveltekit-i18n'; + +interface Params { + error: unknown; + folderName: string; + linkName: string; + amount: number; + // ... all interpolation params must be declared here +} + +const config: Config> = { + initLocale: 'en', + loaders: [ + { locale: 'en', key: '', loader: async () => (await import('../config/en.json')).default }, + { locale: 'de', key: '', loader: async () => (await import('../config/de.json')).default }, + ], +}; + +export const { t, loading, locales, locale, initialized, translations, loadTranslations, setLocale } = new i18n(config); +``` + +--- + +## Using Translations in Components + +```svelte + + + +

{$t('jokes.title')}

+ + +

{$t('memorandum.folderCreated', { folderName: 'My Folder' })}

+``` + +--- + +## Adding New Keys + +1. Add the key + value to both `en.json` and `de.json` +2. If the key uses interpolation params, add the param name to the `Params` interface in `translations.ts` +3. All params must be strings in `$t()` calls — use `String(value)` for numbers + +--- + +## Locale Detection + +```ts +export const getlocale = () => { + if (navigator.language.startsWith('de')) return 'de'; + return 'en'; +}; +``` + +Called in the root layout to set the initial locale. Stored in the `locale` store. + +--- + +## Convention: Key Naming + +Keys are nested by feature: + +```json +{ + "jokes": { + "title": "Jokes", + "random": "Random Joke" + }, + "memorandum": { + "title": "Memorandum" + }, + "hitstar": { + "title": "Hitstar" + } +} +``` + +Use the feature name as the top-level key prefix. + +--- + +## Key Notes + +- `Params` interface uses `unknown` for `error` (handles any thrown value). +- All interpolation values passed to `$t()` must be strings — Carbon NumberInput, for example, returns numbers that need `String()` conversion. +- `Carbon NumberInput` uses `labelText` (not `label`) as its label prop. diff --git a/docs/shared/identifiers.md b/docs/shared/identifiers.md new file mode 100644 index 00000000..9152d837 --- /dev/null +++ b/docs/shared/identifiers.md @@ -0,0 +1,81 @@ +# Identifiers + +> User identity provider — named UUIDs stored in MongoDB, used as namespaces in the Keystore. + +## Overview + +An "identifier" is a named, persistent UUID that represents a user or a logical namespace. The Memorandum feature uses identifiers to scope bookmarks/links per user. The Admin feature uses identifiers for verification. + +--- + +## Backend + +**NX Library**: `backend/libs/shared/provider/identifiers/` +**Path alias**: `@backend/shared-identifiers` + +### Files + +| File | Purpose | +|------|---------| +| `src/lib/identifier.service.ts` | Business logic (listIdentifiers, createIdentifier, ...) | +| `src/lib/identifiers-mongodb.service.ts` | MongoDB CRUD layer | +| `src/lib/schema/identifiers.schema.ts` | Mongoose schema | + +### Schema + +```ts +{ + _id: string; // UUID + name: string; // human-readable name + created: Date; + updated: Date; +} +``` + +### Service Methods + +| Method | Description | +|--------|-------------| +| `listIdentifiers(filter?)` | Get all identifiers | +| `getIdentifier({ id })` | Get by ID (throws 404) | +| `createIdentifier({ name })` | Create new identifier | +| `updateIdentifier(id, dto)` | Update name | +| `deleteIdentifier(id)` | Delete (throws 404) | + +--- + +## HTTP API (via MemorandumModule) + +Endpoints exposed under `/identifiers` by `IdentifiersController` in `backend/libs/memorandum/`. + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/v1/identifiers` | Bearer | List all identifiers | +| GET | `/v1/identifiers/:id` | Public | Get by ID | +| POST | `/v1/identifiers` | Bearer | Create new | +| PUT | `/v1/identifiers/:id` | Public | Update name | +| DELETE | `/v1/identifiers/:id` | Bearer | Delete | + +--- + +## Frontend API Client + +**File**: `frontend/src/lib/api/identifiers.api.ts` + +**Type**: `frontend/src/lib/types/identifiers.dto.ts` + +--- + +## Usage Pattern + +1. Admin creates an identifier (gives it a name, gets back UUID). +2. UUID is stored in localStorage on the user's device. +3. All Keystore calls for that user use their UUID as the `identifier` parameter. +4. Admin verification: `POST /v1/admin/verify` checks if a submitted UUID exists as an identifier. + +--- + +## Related + +- [keystore-persistence.md](keystore-persistence.md) — uses identifier UUIDs as namespaces +- [auth-guard.md](auth-guard.md) — admin verify endpoint diff --git a/docs/shared/keystore-persistence.md b/docs/shared/keystore-persistence.md new file mode 100644 index 00000000..08e3e44d --- /dev/null +++ b/docs/shared/keystore-persistence.md @@ -0,0 +1,77 @@ +# Keystore Persistence + +> Generic key-value store backed by MongoDB. Used for feature toggles, memorandum links/folders, and settings. + +## Overview + +The Keystore is a generic `{ identifier, key, value }` store. Any part of the system can use it by choosing a unique `identifier` namespace. The same MongoDB collection stores all keys from all features. + +--- + +## Backend Service + +**File**: `backend/libs/shared/provider/keystore-persistence/src/lib/keystore-mongodb.service.ts` +**Path alias**: `@backend/shared-keystore-persistence` + +### Schema + +```ts +{ + _id: string; // randomUUID() + identifier: string; // namespace (e.g. 'tilloh-toggles', user identifier ID) + key: string; // the key name + value: string; // the value (always stored as string) + created: Date; + updated: Date; +} +``` + +### Methods + +| Method | Signature | Description | +|--------|-----------|-------------| +| `findAll` | `(filter?) → KeystoreDto[]` | List all keys, optionally filtered | +| `findOne` | `(identifier, key) → KeystoreDto` | Get a single key (throws 404 if missing) | +| `create` | `(identifier, key, value) → KeystoreDto` | Create a new key | +| `update` | `(identifier, key, body) → KeystoreDto` | Update value (throws 404 if missing) | +| `remove` | `(identifier, key) → KeystoreDto` | Delete a key (throws 404 if missing) | + +--- + +## HTTP API (via MemorandumModule) + +The Keystore endpoints are exposed under `/keystore` by the `KeystoreController` in `backend/libs/memorandum/`. + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/v1/keystore` | Bearer | List all keys (filterable by query params) | +| GET | `/v1/keystore/:identifier/:key` | Public | Get a single key | +| POST | `/v1/keystore` | Public | Create a key | +| PUT | `/v1/keystore/:identifier/:key` | Public | Update a key's value | +| DELETE | `/v1/keystore/:identifier/:key` | Bearer | Delete a key | + +--- + +## Frontend API Client + +**File**: `frontend/src/lib/api/keystore.api.ts` + +Key functions: +- `getKeystoreEntry(identifier, key)` — used by `getToggleValue()` +- `updateKeystoreEntry(identifier, key, value)` — used by admin toggle UI + +--- + +## Known Identifiers / Namespaces + +| Identifier | Used by | Description | +|------------|---------|-------------| +| `tilloh-toggles` | Toggle system | All feature toggle flags | +| `` | Memorandum | Per-user links and folder structure | + +--- + +## Related + +- [toggle-system.md](toggle-system.md) — toggle lifecycle built on top of Keystore +- [identifiers.md](identifiers.md) — user identity provider (provides the `identifier` values for Memorandum) diff --git a/docs/shared/nx-library-scaffold.md b/docs/shared/nx-library-scaffold.md new file mode 100644 index 00000000..691d7b8b --- /dev/null +++ b/docs/shared/nx-library-scaffold.md @@ -0,0 +1,107 @@ +# NX Library Scaffold + +> How to create a new backend NX library in the `backend/` workspace. + +## Reference Library + +Use `backend/libs/jokes/` as the scaffold reference. It is the simplest feature library with full structure. + +--- + +## File Structure + +``` +backend/libs// +├── project.json # NX project definition +├── jest.config.ts # Jest config (references tsconfig.spec.json) +├── tsconfig.json # Base TS config (extends root tsconfig.base.json) +├── tsconfig.lib.json # Library build config +├── tsconfig.spec.json # Test config +├── .eslintrc.json # ESLint (extends root) +└── src/ + ├── index.ts # Public API barrel (export * from './lib/...') + └── lib/ + ├── .module.ts + ├── .controller.ts + ├── .service.ts + ├── -mongodb.service.ts # (if using MongoDB) + └── schema/ + └── .schema.ts # (if using MongoDB) +``` + +--- + +## Step-by-step + +### 1. Create library directory and files + +Copy the `jokes` library structure and rename all references. + +### 2. Register path alias in `backend/tsconfig.base.json` + +```json +// backend/tsconfig.base.json → compilerOptions.paths +"@backend/": ["libs//src/index.ts"] +``` + +### 3. Export public API in `src/index.ts` + +```ts +export * from './lib/.module'; +// export other public symbols +``` + +### 4. Register NestJS module in `backend/apps/tilloh-dev/src/main.ts` + +```ts +import { NameModule } from '@backend/'; + +@Module({ + imports: [ + // ...existing modules + NameModule, + ], +}) +export class AppModule { ... } +``` + +### 5. Add environment variables (if needed) + +Edit `backend/apps/tilloh-dev/src/env.validation.ts`: + +```ts +export class EnvironmentVariables { + // ...existing vars + @IsString() + NEW_VAR: string; +} +``` + +--- + +## NX Project Config (`project.json`) + +Minimal structure based on the jokes library: + +```json +{ + "name": "@backend/", + "sourceRoot": "libs//src", + "projectType": "library", + "targets": { + "build": { "executor": "@nx/js:tsc" }, + "test": { + "executor": "@nx/jest:jest", + "options": { "jestConfig": "libs//jest.config.ts" } + } + } +} +``` + +--- + +## Naming Conventions + +- **Service functions** (business logic layer): descriptive names — `listJokes`, `createJoke`, `deleteJoke` +- **MongoDB functions** (DB layer): generic CRUD — `findAll`, `findOne`, `create`, `update`, `remove` +- Module class: `NameModule`; Controller: `NameController`; Service: `NameService`; DB service: `NameMongoDbService` diff --git a/docs/shared/route-page-pattern.md b/docs/shared/route-page-pattern.md new file mode 100644 index 00000000..76f69c71 --- /dev/null +++ b/docs/shared/route-page-pattern.md @@ -0,0 +1,82 @@ +# Route Page Pattern + +> Every feature route uses a two-file pattern: `+page.ts` for prerender + toggle gate, `+page.svelte` for the UI. + +## File Pair + +``` +frontend/src/routes// +├── +page.ts # prerender:true + toggle redirect +└── +page.svelte # main page component (Svelte 5 runes) +``` + +--- + +## `+page.ts` — Prerender + Toggle Gate + +```ts +// frontend/src/routes/jokes/+page.ts +import { TogglesEnum } from '$lib/types/toggle.dto'; +import { getToggleValue } from '$lib/util/toggle'; +import { redirect } from '@sveltejs/kit'; + +export const prerender = true; + +export const load = async () => { + const jokeToggle = await getToggleValue(TogglesEnum.jokes); + if (!jokeToggle) { + throw redirect(307, '/'); + } +}; +``` + +**Purpose**: If the feature's toggle is `false` in the backend keystore, redirect to `/` so the page is never accessible. + +--- + +## `+page.svelte` — Svelte 5 Runes Component + +Follows the Svelte 5 component order from `CLAUDE.md`: + +```svelte + + + +``` + +--- + +## Toggle Utility + +**File**: `frontend/src/lib/util/toggle.ts` + +```ts +import { getKeystoreEntry } from '$lib/api/keystore.api'; + +export const getToggleValue = async (toggleKey: string): Promise => { + try { + const entry = await getKeystoreEntry('tilloh-toggles', toggleKey); + return entry?.value === 'true'; + } catch { + return false; + } +}; +``` + +--- + +## Key Notes + +- `prerender = true` enables SvelteKit static generation for that route. +- The toggle check happens at load time; if `false`, the user is silently redirected to home. +- The `TogglesEnum` maps feature names to their backend keystore keys (see `frontend/src/lib/types/toggle.dto.ts`). +- See [toggle-system.md](toggle-system.md) for the full toggle lifecycle (seed → storage → frontend). diff --git a/docs/shared/stores.md b/docs/shared/stores.md new file mode 100644 index 00000000..1fa69018 --- /dev/null +++ b/docs/shared/stores.md @@ -0,0 +1,90 @@ +# Svelte Stores (localStorage Pattern) + +> Writable stores that automatically sync to `localStorage` for persistence across sessions. + +## Pattern + +The standard pattern for persistent stores (used by Hitstar, settings, etc.): + +```ts +// frontend/src/lib/util/stores/store-.ts +import { browser } from '$app/environment'; +import { writable } from 'svelte/store'; + +const STORAGE_KEY = 'feature.subkey'; + +const getInitialValue = (): MyType | null => { + if (!browser) return null; + const stored = localStorage.getItem(STORAGE_KEY); + if (!stored) return null; + try { + return JSON.parse(stored) as MyType; + } catch { + return null; + } +}; + +export const myStore = writable(getInitialValue()); + +// Sync store changes → localStorage +if (browser) { + myStore.subscribe((val) => { + if (val === null) { + localStorage.removeItem(STORAGE_KEY); + } else { + localStorage.setItem(STORAGE_KEY, JSON.stringify(val)); + } + }); +} +``` + +--- + +## Key Principles + +1. **Guard with `browser`**: `$app/environment`'s `browser` flag prevents `localStorage` access during SSR. +2. **Null = deleted**: Setting the store to `null` removes the key from localStorage. +3. **JSON serialization**: Objects are stored as JSON strings. +4. **Subscribe for sync**: The `subscribe` call in the `if (browser)` block wires store changes to storage. + +--- + +## Existing Stores + +| Store file | Keys | Used by | +|------------|------|---------| +| `store-hitstar.ts` | `hitstar.classic.bestRound`, `hitstar.classic.gameState`, `hitstar.range.bestRound`, `hitstar.range.gameState` | Hitstar game | +| `stores-global.ts` | Various global state | App-wide (e.g. `celebrate()`) | + +--- + +## Migration Pattern + +When renaming localStorage keys, check for old keys on `browser` init: + +```ts +if (browser) { + const oldValue = localStorage.getItem('old.key'); + if (oldValue) { + localStorage.setItem('new.key', oldValue); + localStorage.removeItem('old.key'); + } +} +``` + +This pattern is used in `store-hitstar.ts` to migrate from `hitstar.bestRound` → `hitstar.classic.bestRound`. + +--- + +## Global Utilities + +**File**: `frontend/src/lib/util/stores/stores-global.ts` + +Exports `celebrate()` — triggers a confetti animation (used by Hitstar, potentially others). + +--- + +## Related + +- [route-page-pattern.md](route-page-pattern.md) — Svelte component structure +- [i18n.md](i18n.md) — locale store also uses this writable pattern diff --git a/docs/shared/toggle-system.md b/docs/shared/toggle-system.md new file mode 100644 index 00000000..55d2266b --- /dev/null +++ b/docs/shared/toggle-system.md @@ -0,0 +1,110 @@ +# Toggle System + +> Feature flags stored in MongoDB via the Keystore, seeded on startup, consumed by both backend guards and frontend route gates. + +## Overview + +Toggles control which features appear in the navigation and are accessible. They are stored as Keystore entries under the `tilloh-toggles` identifier and seeded automatically on application boot. + +--- + +## Backend: Seed Config + +**File**: `backend/libs/shared/provider/keystore-persistence/src/lib/toggle-seed.config.ts` + +```ts +export const TOGGLE_IDENTIFIER = 'tilloh-toggles'; + +export const TOGGLE_SEED_CONFIG: ToggleSeedEntry[] = [ + { key: 'TOGGLE_RANDOM_JOKE', defaultValue: 'true' }, + { key: 'TOGGLE_NAV_MEMORANDUM', defaultValue: 'true' }, + { key: 'TOGGLE_NAV_TODO', defaultValue: 'true' }, + { key: 'TOGGLE_NAV_FOOD_SCAN', defaultValue: 'true' }, + { key: 'TOGGLE_NAV_JOKES', defaultValue: 'true' }, + { key: 'TOGGLE_NAV_CATCH_EM_ALL', defaultValue: 'true' }, + { key: 'TOGGLE_NAV_UNO_SORT', defaultValue: 'true' }, + { key: 'TOGGLE_NAV_ABOUT', defaultValue: 'true' }, + { key: 'TOGGLE_ADMIN_DASHBOARD', defaultValue: 'true' }, + { key: 'TOGGLE_ADMIN_ACTIVITIES', defaultValue: 'true' }, + { key: 'TOGGLE_ADMIN_IDENTIFIERS', defaultValue: 'true' }, + { key: 'TOGGLE_ADMIN_JOKES', defaultValue: 'true' }, + { key: 'TOGGLE_ADMIN_LINK_PRESETS',defaultValue: 'true' }, + { key: 'TOGGLE_NAV_HITSTAR', defaultValue: 'true' }, +]; +``` + +**File**: `backend/libs/shared/provider/keystore-persistence/src/lib/toggle-seed.service.ts` + +`ToggleSeedService` implements `OnApplicationBootstrap`. On startup it reads all existing toggle keys and only creates missing ones — idempotent seeding. + +--- + +## Backend: Reading Toggles + +The frontend reads toggles via the public Keystore endpoint: + +``` +GET /v1/keystore/tilloh-toggles/:toggleKey +``` + +Returns `{ identifier, key, value }` where `value` is `"true"` or `"false"`. + +Admin users can change toggle values via: + +``` +PUT /v1/keystore/tilloh-toggles/:toggleKey +Body: { "value": "false" } +``` + +The admin UI at `/admin/toggles` provides a UI for this. + +--- + +## Frontend: Toggle Enum + +**File**: `frontend/src/lib/types/toggle.dto.ts` + +```ts +export enum TogglesEnum { + randomJoke = 'TOGGLE_RANDOM_JOKE', + memorandum = 'TOGGLE_NAV_MEMORANDUM', + todo = 'TOGGLE_NAV_TODO', + foodScan = 'TOGGLE_NAV_FOOD_SCAN', + jokes = 'TOGGLE_NAV_JOKES', + catchEmAll = 'TOGGLE_NAV_CATCH_EM_ALL', + unoSort = 'TOGGLE_NAV_UNO_SORT', + about = 'TOGGLE_NAV_ABOUT', + adminDashboard = 'TOGGLE_ADMIN_DASHBOARD', + adminActivities = 'TOGGLE_ADMIN_ACTIVITIES', + adminIdentifiers= 'TOGGLE_ADMIN_IDENTIFIERS', + adminJokes = 'TOGGLE_ADMIN_JOKES', + adminLinkPreset = 'TOGGLE_ADMIN_LINK_PRESETS', + hitstar = 'TOGGLE_NAV_HITSTAR', +} +``` + +--- + +## Frontend: Navigation Toggle + +**File**: `frontend/src/lib/config/applications.ts` + +`loadApplications()` calls `getToggleValue()` for each nav toggle, then sets the `applicationRoutes` writable store. The `activeApplicationRoutes` derived store filters to only routes with `toggle: true`. + +--- + +## Adding a New Toggle + +1. Add entry to `TOGGLE_SEED_CONFIG` in `toggle-seed.config.ts` +2. Add enum value to `TogglesEnum` in `frontend/src/lib/types/toggle.dto.ts` +3. Add `getToggleValue(TogglesEnum.newFeature)` call in `loadApplications()` in `applications.ts` +4. Add the route to `ApplicationRoutes` type and both `loadApplications()` return and `defaultApplicationRoutes` +5. Use it in `+page.ts` toggle gate (see [route-page-pattern.md](route-page-pattern.md)) + +--- + +## Related + +- [keystore-persistence.md](keystore-persistence.md) — the underlying Keystore service +- [application-routes.md](application-routes.md) — route registry +- [route-page-pattern.md](route-page-pattern.md) — frontend toggle gate