Add comprehensive documentation for key organizational aspects
All checks were successful
On Push to Main / test (push) Successful in 1m33s
On Push to Main / publish (push) Successful in 1m31s
On Push to Main / deploy-dev (push) Successful in 2m3s

- Introduced "Pension Scheme & Benefits" detailing secondary employment benefits and pension specifics.
- Created "Roles & Accountabilities" outlining the Holacracy role structure and responsibilities within Respellion.
- Added "Security" section covering GDPR compliance and workplace safety protocols.
- Established "Spending and Contracting" policy detailing expense categories and submission processes.
- Documented "Who We Are" to define Respellion's identity, services, and operational model under Holacracy and ISO 9001.
This commit is contained in:
RaymondVerhoef
2026-05-27 08:24:56 +02:00
parent 7066f881f9
commit 07af2783dc
26 changed files with 2631 additions and 4598 deletions

View File

@@ -2,144 +2,150 @@
Welcome, fellow AI agent! If you are reading this, you are tasked with maintaining or extending the Respellion Learning Platform. This document provides the critical context, architectural patterns, and design standards you need to successfully work on this codebase.
> **Last updated:** 2026-05-18Adds 52-week annual curriculum system (§12). Reflects selective content generation (3 types), ISO week alignment, GitHub sync folder change, and AI extraction token limits.
> **Last updated:** 2026-05-26Per-user curriculum start (employees enroll on first login; week/cycle derived from each user's `curriculum_started_at`, no longer from the ISO calendar week). Documents the real React/Vite + PocketBase stack, TF-IDF retrieval, 3 learning-content types, 3 micro-learning types, and the tiered Claude model setup.
## 1. Architectural Overview
This is a single-page React application built with **Vite**, backed by **PocketBase** as the database and auth layer.
* **Frontend:** React, React Router, Vanilla CSS (via CSS variables) + Tailwind utility classes mapped to those variables.
* **Backend:** PocketBase (self-hosted). All data is stored in PocketBase collections, not localStorage.
* **Animations:** Framer Motion (used extensively for page transitions, podium effects, and gamification feedback).
This is a single-page React application built with **Vite**, backed by **PocketBase** as the database and auth layer. There is no separate backend server — the browser talks directly to PocketBase, and to the Anthropic API through a reverse proxy.
* **Frontend:** React 19, React Router 7, Vanilla CSS (via CSS variables) + Tailwind v4 utilities mapped to those variables.
* **Backend:** PocketBase (self-hosted, SQLite). All data is stored in PocketBase collections, not localStorage.
* **Animations:** Framer Motion (page transitions, podium effects, gamification feedback).
* **Icons:** Lucide React.
* **Visualizations:** D3.js (used strictly for the Admin Knowledge Graph).
* **Retrieval:** A dependency-free TF-IDF index over the knowledge graph (`src/lib/retrieval.js`). There is **no Qdrant and no embeddings API** — older specs that mention them describe a design that was never built.
> The top-level `app/` directory is abandoned Next.js scaffolding from that original design. It is not built or deployed. Ignore it; the real app is `src/`.
## 2. State Management & Storage (Critical)
All persistent data lives in **PocketBase**. The data access layer is in `src/lib/db.js`, which wraps the PocketBase SDK client from `src/lib/pb.js`.
**PocketBase Collections:**
* `topics` — Knowledge graph nodes (`id`, `label`, `type`, `description`).
* `relations` — Knowledge graph edges (`source`, `target`, `type`).
* `content` — AI-generated learning modules per topic (`topic_id`, `data`). The `data` field is a **merged JSON object** containing only the content types that have been generated for that topic (e.g. `{ article: {...}, slides: [...] }`). New types are shallow-merged into the existing object by `learningService.js`; nothing is ever overwritten.
* `quiz_banks` — Banks of AI-generated questions per topic (`topic_id`, `questions[]`).
* `quiz_results` — User test scores (`user_id`, `week_number`, `score`, `total`, `percentage`, etc.).
* `quiz_cache` — Cached weekly quiz for a user (`user_id`, `week_number`, `questions[]`, `meta`).
* `learn_progress` — Whether a user completed the weekly learning session (`user_id`, `week_number`, `done`).
* `leaderboard` — Points ledger (`user_id`, `name`, `points`, `tests_completed`).
* `team_members` — Registered users with PIN auth (`name`, `role`, `pin`).
* `sources` — Uploaded source documents and their extraction status (`name`, `status`, `error`).
* `curriculum` — Annual learning schedule (`year`, `week_number`, `topic_id`, `theme`, `quarter`, `is_review_week`, `sort_order`). One entry per week per year. Managed via the admin Curriculum tab.
* `settings` — Key/value store for app-wide settings (`key`, `value`).
**PocketBase Collections (current):**
* `topics` — Knowledge graph nodes (`id`, `label`, `type`, `description`, `learning_relevance`, `relevance_locked`, `theme`, `complexity_weight`, `difficulty`).
* `relations` — Knowledge graph edges (`source`, `target`, `type``related_to` / `depends_on` / `part_of` / `executed_by`).
* `content` — AI-generated learning modules per topic (`topic_id`, `data`). The `data` field is a **merged JSON object** containing only the content types generated so far (e.g. `{ article: {...}, slides: [...] }`). New types are shallow-merged in by `learningService.js`; nothing is overwritten.
* `micro_learnings` — Generated micro-learning artifacts (`topic_id`, `type`, `content`, `status`). One record per topic per type. `status='published'` items are visible to employees.
* `micro_learning_completions` — Append-only completion events (`team_member_id`, `micro_learning_id`, `topic_id`, `type`, `session_week`).
* `curriculum_versions` — Versioned 26-week schedules (`version_number`, `status`, `generation_reason`, `confirmed_by`, `confirmed_at`, `schedule` JSON, `coverage_stats` JSON). Exactly one `active` at a time.
* `leaderboard` — Points ledger (`user_id`, `name`, `points`, `tests_completed`, `learnings_completed`).
* `team_members` — Registered users with PIN auth (`name`, `role`, `pin`, `curriculum_started_at`, `enrollment_status`).
* `sources` — Uploaded source documents and extraction status (`name`, `status`, `error`, `progress`).
* `settings` — Key/value store (`key`, `value`).
* `llm_calls` — Per-call telemetry (`task`, `model`, `tier`, `duration_ms`, token counts, `stop_reason`, `ok`, `error_msg`).
**Dropped collections:** `quiz_banks`, `quiz_results`, `quiz_cache`, `learn_progress`, and the legacy `curriculum` (v1) collection no longer exist. The matching `db.js` helpers are deprecated stubs — do not build on them.
**localStorage** is only used for **admin browser settings** (not user data):
* `respellion:admin:anthropic_key` — Anthropic API key.
* `respellion:admin:model` — Model override.
* `respellion:admin:use_simulation`Simulation mode toggle.
* `kb:suggestions` — Pending/approved/rejected graph deltas proposed by R42. Always mutated via `kbStore` (see §8).
* `quiz:active:{userId}` — Boolean flag set while the user is mid-quiz. R42's FAB is hidden when this is true (quiz-integrity rule).
* `admin:model:fast` / `admin:model:standard` / `admin:model:reasoning` — per-tier model overrides (legacy `admin:model` still honored for `standard`).
* `admin:use_simulation` — when true, `callLLM` returns stub data instead of calling Anthropic. Useful for UI work without spending tokens.
* `kb:suggestions`Pending/approved/rejected graph deltas proposed by R42. Always mutated via `kbStore` (see §9).
* `quiz:active:{userId}` — Boolean flag set while a user is mid-quiz. R42's launcher is hidden when this is true (quiz-integrity rule).
* `chat:thread:{userId}` — Persisted R42 conversation, capped at 50 messages.
**Session:** User login is PIN-based. The logged-in user's ID is stored in `sessionStorage` under `respellion_session` and resolved against `team_members` on app load (see `src/store/AppContext.jsx`).
**Session:** Login is PIN-based. The logged-in user's ID is stored in `sessionStorage` under `respellion_session` and resolved against `team_members` on app load (`src/store/AppContext.jsx`).
**Week Number:** The current ISO-8601 week number is calculated dynamically on app load via `getWeekNumber(new Date())` in `src/store/AppContext.jsx`. It is **not** stored in the database. The `ADVANCE_WEEK` action still exists for admin use, but initial state always reflects the real calendar week.
**Per-user curriculum position (important — changed):** Each employee starts the curriculum when *they* choose. On first login a blocking onboarding screen (`/onboarding`) records `curriculum_started_at` and flips `enrollment_status` to `active`. `AppContext` derives `state.weekNumber` — an absolute counter starting at 1 — from `getPersonalWeekNumber(curriculum_started_at)` (= `floor(days_since_start / 7) + 1`). The 26-week slot and cycle come from `getCurriculumWeek(n)` (`((n-1) % 26) + 1`) and `getCurriculumCycle(n)` (`floor((n-1)/26)+1`) in `curriculumService.js`. The cycle is **detached from the ISO calendar** — week 1 is simply the first 7 days after the user's start. After week 26 the cycle restarts at week 1 with the same content. `state.weekNumber` is `0` until the user enrolls.
**Curriculum Year:** The curriculum year is derived from `new Date().getFullYear()` via `getCurriculumYear()` in `src/lib/curriculumService.js`. It is not storedalways computed.
> Do **not** reintroduce ISO-week-based scheduling or a shared `admin:current_week`. There is no global "current week" anymore — every employee has their own.
**Important:** All `db.js` functions are `async`. Always `await` them — omitting `await` will silently pass a Promise where data is expected.
**Auto-Cancellation:** The PocketBase JS SDK has auto-cancellation enabled by default, which aborts concurrent identical requests (common under React StrictMode and `Promise.all`) with `ClientResponseError 0`. It is **globally disabled** via `pb.autoCancellation(false)` in `src/lib/pb.js`. Never re-enable it.
**Auto-Cancellation:** The PocketBase JS SDK has auto-cancellation enabled by default. This causes concurrent identical requests (like `db.getTopics()` during React StrictMode renders or concurrent Promise.all) to abort with `ClientResponseError 0`. This feature is **globally disabled** in `src/lib/pb.js` via `pb.autoCancellation(false)` to prevent UI crashes during concurrent fetching.
**PocketBase URL:** Resolved from `VITE_PB_URL`, else `window.location.origin` (`src/lib/pb.js`). In production Caddy proxies `/api/*` and `/_/*` to the PocketBase container.
**Important:** All `db.js` functions are `async`. Always `await` them — omitting `await` silently passes a Promise where data is expected.
## 3. The AI Integration (Anthropic)
The application calls the Anthropic API via a proxy to avoid CORS issues.
* **Location:** `src/lib/llm.js`.
* **Proxy:** In Docker, `/api/anthropic` is proxied via Caddy to the Anthropic API endpoint. In local dev, configure `vite.config.js` to proxy the same path. The API key is injected server-side by Caddy; there is **no client-side API key**.
* **Token limit:** `generateContent` uses `max_tokens: 8192`. Do not lower this — large knowledge-base files need the headroom.
* **JSON Enforcement:** Prompts *must* strictly enforce that Claude returns *only* raw JSON. Do not let the AI wrap the response in markdown blocks — a regex strip via `.match(/\{[\s\S]*\}/)` is already applied in all service files.
All Anthropic calls go through one wrapper.
* **Location:** `src/lib/llm.js`, function `callLLM(...)`. Callers must never reach `/api/anthropic` directly.
* **Proxy:** Requests hit `/api/anthropic/v1/messages`. In Docker, Caddy proxies this to `https://api.anthropic.com` and injects the `x-api-key` header server-side. In local dev, `vite.config.js` does the same using `process.env.ANTHROPIC_API_KEY`. There is **no client-side API key**.
* **Model tiers:** `fast` = `claude-haiku-4-5-20251001`, `standard` = `claude-sonnet-4-6`, `reasoning` = `claude-opus-4-7`. Choose a tier per task; admins can override per tier from Settings.
* **Structured output:** Prefer Anthropic **tool use** with a forced `toolChoice`. Tool inputs are validated against Zod schemas in `src/lib/llmSchemas.js` (auto-looked-up via `toolSchemaRegistry`). For text responses, `parseStructuredText` strips code fences and extracts the outermost balanced JSON.
* **Prompt caching:** Wrap stable system text with `cachedSystem(text)` to attach `cache_control: ephemeral`.
* **Retry/limits:** `src/lib/llmRetry.js` handles exponential backoff with jitter on retryable statuses (408/425/429/5xx/529), honors `Retry-After`, and provides rate limiters (e.g. the extraction limiter caps ~20 req/min, burst 2). Default `maxTokens` is 4096; extraction and long-form content use 8192.
* **Telemetry:** Every call is logged (best-effort, non-blocking) to the `llm_calls` collection.
## 4. Design System & Aesthetics
Respellion relies on a premium, modern aesthetic.
* **CSS Variables:** Rely heavily on the variables defined in `src/index.css`.
* Colors: `var(--color-bg)`, `var(--color-paper)`, `var(--color-teal)`, `var(--color-accent)`.
* Radii: `var(--r-sm)`, `var(--r-lg)`, `var(--r-org)` (an organic, pill-like shape used for badges and podiums).
* **Tailwind:** Tailwind is mapped to these CSS variables. Avoid hardcoding random hex codes. Use the existing Tailwind classes like `bg-teal`, `text-fg-muted`, and `border-bg-warm`.
* **Components:** Always reuse the UI primitives in `src/components/ui/` (`Card.jsx`, `Button.jsx`, `Tag.jsx`, `Input.jsx`).
* **CSS Variables:** Rely on the variables in `src/index.css``var(--color-bg)`, `var(--color-paper)`, `var(--color-teal)`, `var(--color-accent)`, radii `var(--r-sm)`, `var(--r-lg)`, `var(--r-org)`.
* **Tailwind:** Tailwind v4 utilities are mapped to these variables. Use classes like `bg-teal`, `text-fg-muted`, `border-bg-warm`. Avoid raw hex codes.
* **`stylesheet.css`** (repo root) is the authoritative visual reference and is frozen — do not edit it.
* **Components:** Reuse the UI primitives in `src/components/ui/` (`Card.jsx`, `Button.jsx`, `Tag.jsx`, `Input.jsx`).
## 5. Learning Content Types
`src/lib/learningService.js` supports **three** content types for selective generation:
## 5. Learning Content Types (on-demand, per topic)
`src/lib/learningService.js` generates **three** long-form content types into the `content` collection, on demand:
| Type | Schema key | Description |
|---|---|---|
| `article` | `content.article` | Title, intro, sections, key takeaways |
| `slides` | `content.slides` | Array of slides with bullets and speaker notes |
| `slides` | `content.slides` | Slides with bullets and speaker notes |
| `infographic` | `content.infographic` | Headline, tagline, stats, steps, quote |
**There is no podcast type.** It was removed. Do not re-add it.
`generateLearningContent(topic, force, selectedType)` accepts one of the three types, or `'all'` (admin regeneration). Cache-hit logic checks `content[selectedType]` directly; new data is shallow-merged so other types are preserved. **There is no podcast type.**
`generateLearningContent(topic, force, selectedType)` accepts one of the three types above, or `'all'` (admin regeneration). Cache-hit logic checks `content[selectedType]` directly. On generation, the new data is shallow-merged into the existing cached object so other types are preserved.
Article refinement uses targeted patch tools (`set_intro`, `set_section`, `add_section`, `remove_section`, `replace_takeaways`) so the model edits only what changed.
The `LearningContentViewer` tab bar reflects exactly these three modes. The empty-state for an un-generated tab shows a "Generate [Type]" button that calls `onGenerate(activeMode)` passed from the parent.
## 6. Micro-Learnings (the weekly session)
`src/lib/microLearningService.js` generates short, single-topic interactions into the `micro_learnings` collection. **Three** types are currently active (a former `reflection_prompt` type was dropped):
| Type | Tier | Shape |
|---|---|---|
| `concept_explainer` | standard | `{ sections: [{ title, content (HTML) }] }` (≥3 sections) |
| `scenario_quiz` | standard | `{ scenario, options: [{ text, isCorrect, explanation }] }` (34 options, 1 correct) |
| `flashcard_set` | fast (Haiku) | `{ cards: [{ front, back }] }` (510 cards) |
## 6. Gamification Rules
If you are extending the Gamification system (`src/pages/Leaderboard.jsx`):
* Tests grant **+2 points** per correct answer (handled in `testService.js → saveTestResult`).
* A 100% score grants the **Perfectionist** badge (computed at render time in `Leaderboard.jsx`).
* Completing 1 test grants **First Steps**, completing 5 grants **Veteran**.
* Points are accumulated in the `leaderboard` collection via `db.upsertLeaderboardEntry`.
`getOrGenerateMicroLearning(topicId, type)` returns a cached published record if present, else generates and stores one. Completions are recorded via `useMicroLearningCompletions` into `micro_learning_completions` (append-only). A weekly session is "done" when every required topic has at least one completed micro-learning.
## 7. Docker & Deployment
The app is fully containerized. PocketBase runs as a sidecar service.
* **Build:** `docker build -t respellion-app:latest .`
* **Run:** `docker compose up -d` (see `docker-compose.yml`).
* **Caddy (reverse proxy):** Handles SPA fallback, injects the Anthropic API key via a `Authorization` header on `/api/anthropic/*` requests, and proxies `/pb/*` to the PocketBase service. Config lives in `Caddyfile`.
* **PocketBase URL:** Resolved from `VITE_PB_URL` env var, or falls back to `window.location.origin + '/pb'` (see `src/lib/pb.js`).
## 7. Weekly Test
`src/lib/testService.js` builds a 5-question multiple-choice quiz for the user's current week.
* Primary topic comes from the active curriculum week (else a deterministic hash fallback), plus a few review topics for breadth.
* Generated in **one** `fast`-tier batch call (`emit_quiz_questions`), with quality gates: no duplicate options, no banned fillers ("all of the above"), explanations ≥20 chars, and a check that `correctIndex` isn't dominated by one position.
* Scoring: **+2 points per correct answer** (`saveTestResult``score * 2`), written to the `leaderboard` collection.
## 8. Local File Upload
The Admin upload panel (`src/components/admin/UploadZone.jsx`) allows admins to manually upload markdown/text files via a drag-and-drop interface.
* **Extraction pipeline (`src/lib/extractionPipeline.js`):** Calls `callLLM` with a strict JSON-only system prompt. To prevent truncated responses on large files, the prompt limits extraction to **max 15 topics** and their most important relations. If the AI returns non-JSON, the file is marked `failed` in the `sources` collection.
* **Deduplication:** A file already in `sources` with `status: completed` will throw and not be re-processed. Delete the source record first to force a re-analysis.
* **Do not increase topic cap beyond 15** without also verifying the `max_tokens: 8192` budget is sufficient for the expected file size.
## 8. Gamification
If you are extending gamification (`src/pages/Leaderboard.jsx`):
* Tests grant **+2 points** per correct answer (`testService.js → saveTestResult`).
* Badges are computed at render time: **First Steps** (1 test), **Veteran** (5 tests), **Perfectionist** (a 100% score).
* Points accumulate in `leaderboard` via `db.upsertLeaderboardEntry`. Admins are excluded from the public board.
## 9. R42 Chatbot
The platform ships a global chatbot avatar called **R42**, rendered as the Respellion `{ r }` brand mark in three states (idle / typing / error).
* **Mark component:** `src/components/ui/Mark.jsx`. Pure SVG; renders the brand mark with `state`, `size`, `theme`, `showFrame`, `brace`, `letter` props. Requires the `BallPill` font (loaded via `@font-face` in `src/index.css`, served from `public/fonts/BallPill-light.otf`). Falls back to JetBrains Mono (already loaded).
* **Mark component:** `src/components/ui/Mark.jsx` — pure SVG with `state`, `size`, `theme`, `brace`, `letter` props. Uses the `BallPill` font (`@font-face` in `src/index.css`), falling back to JetBrains Mono.
* **Chat module:** `src/components/chat/`.
* `ChatLauncher.jsx` — global FAB; auto-hides when `quiz:active:{userId}` is set. Listens to the `respellion:quiz-state` window event for fast updates.
* `ChatWindow.jsx` 380×480 chat panel; Esc closes; renders messages from `useChat`.
* `useChat.js` — owns the message list, persists to `chat:thread:{userId}`, calls `anthropicApi.chat()`. `buildKbContext` is async (PocketBase), so `send()` is fully async.
* `prompts.js` — system prompt, greeting, and the `propose_graph_delta` tool spec.
* `rag.js`fetches `kb:topics` + `kb:relations` from PocketBase; lazy-loads `kb:content:{id}` only when a topic is mentioned. Returns `{ context, topics }` so `validateDelta` can reuse the fetched topics without a second round-trip.
* **Multi-turn API:** `callLLM({ task, system, messages, tools })` in `src/lib/llm.js`. Returns a structured response containing extracted `toolUses` and text. No API key header — Caddy proxy injects it server-side.
* **Quiz-integrity rule:** `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on start and clears it on every non-quiz phase + unmount, plus dispatches a `respellion:quiz-state` event. Never bypass this — letting users ask R42 mid-quiz would break scoring.
* **Graph refinement:** when R42's `tool_use` block proposes a `propose_graph_delta`, `rag.js` validates (no duplicate ids, no orphan relations, caps 3 topics / 5 relations) and surfaces a confirmation chip inline.
* **Admin user clicks Ja**`kbStore.applyDelta` writes to PocketBase via `db.upsertTopic` / `db.addRelation` immediately.
* **Non-admin clicks Ja**`kbStore.appendSuggestion` queues an entry in `kb:suggestions` localStorage (status `pending`).
* **Admin approval UI:** `src/components/admin/SuggestionsQueue.jsx`, mounted at the top of the Knowledge Graph admin panel. Approve calls `kbStore.approveSuggestion(id)` which runs the same `applyDelta` merge (async, PocketBase) and flips status to `approved`. Reject flips to `rejected` for audit.
* **kbStore:** `src/lib/kbStore.js` is the single source of truth for KB mutations from the chatbot path. Topics/relations go to PocketBase; suggestions queue goes to localStorage. Dispatches `respellion:kb-updated` after any write so the D3 graph and the queue panel refresh without a reload.
* `ChatLauncher.jsx` — global FAB; auto-hides when `quiz:active:{userId}` is set; listens to the `respellion:quiz-state` window event.
* `ChatWindow.jsx` — chat panel; renders messages from `useChat`; surfaces graph-delta confirmation chips.
* `useChat.js` — owns the message list, persists to `chat:thread:{userId}` (cap 50; only the last ~12 turns are sent to the API), calls `callLLM`.
* `prompts.js` the cacheable system prompt blocks, greeting, and the `propose_graph_delta` tool spec (max 3 topics / 5 relations).
* `rag.js`builds KB context using the TF-IDF index from `src/lib/retrieval.js` (top-K topics + verbatim mentions), filters relations to retrieved topics, and validates proposed deltas (dedupe by id/label, no orphan/self relations, hard caps).
* **Model:** R42 runs on the `fast`/standard Claude tier (Haiku/Sonnet); low latency matters for chat.
* **Quiz-integrity rule:** `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on start and clears it on every non-quiz phase + unmount, dispatching `respellion:quiz-state`. Never bypass this — letting users ask R42 mid-quiz would break scoring.
* **Graph refinement:** when R42 proposes a `propose_graph_delta`, `rag.js` validates it and a confirmation chip appears inline. **Admin clicks Yes**`kbStore.applyDelta` writes to PocketBase immediately. **Non-admin clicks Yes**`kbStore.appendSuggestion` queues a `pending` entry in `kb:suggestions`.
* **Admin approval UI:** `src/components/admin/SuggestionsQueue.jsx` lets admins approve (re-runs `applyDelta`) or reject queued suggestions.
* **kbStore:** `src/lib/kbStore.js` is the single source of truth for chatbot-path KB mutations. It dispatches `respellion:kb-updated` after writes so the D3 graph and queue refresh.
## 10. How to Add New Features
1. **Define Schema:** Add a new PocketBase collection via the Admin UI or the init script (`scripts/setup-pb-collections.mjs`).
2. **Add DB Helpers:** Add async CRUD functions in `src/lib/db.js`.
3. **Build UI:** Create components using `Card`, `Button`, and `framer-motion` for smooth entry (`animate-in fade-in slide-in-from-bottom-4`).
4. **Wire Logic:** Connect to `src/store/AppContext.jsx` if it requires global user context, otherwise manage state locally.
5. **Test:** Run the Docker stack (`docker compose up`) to ensure no build errors exist.
## 10. Admin Panel
`src/pages/Admin/index.jsx` is tabbed: **Sources** (upload + extraction), **Content** (review/refine generated content), **Quizzes**, **Curriculum** (generate/preview/activate a 26-week schedule), **Graph** (D3 knowledge graph + suggestions queue), **Team** (manage members), **Settings** (model overrides, simulation toggle, smoke-test reset). Source upload lives in `src/components/admin/UploadZone.jsx` (`.txt` / `.md`, ≤5 MB).
## 11. Known Gotchas & Decisions
* **No podcast type.** The podcast learning type was deliberately removed. The three supported types are `article`, `slides`, and `infographic`.
* **Week number is computed, not stored.** Do not add a `admin:current_week` setting — the ISO week is always derived from `new Date()`.
* **Caddy, not Nginx.** Older docs/comments may reference Nginx. The reverse proxy is Caddy. Update any references you encounter.
* **AI token budget.** If you see `[Pipeline] AI returned non-JSON response` in the logs, the response was truncated. Increase the topic cap prompt constraint before raising `max_tokens`.
* **PocketBase auto-cancellation is OFF.** `pb.autoCancellation(false)` is set globally in `src/lib/pb.js`. Never re-enable it — it causes abort errors during concurrent fetches in React StrictMode.
## 11. How to Add New Features
1. **Schema:** add a PocketBase collection via the PB Admin UI or a migration in `pb_migrations/` (and mirror it in `scripts/setup-pb-collections.mjs`).
2. **DB helpers:** add async CRUD in `src/lib/db.js`.
3. **UI:** build with `Card`, `Button`, `Tag`, and Framer Motion entry animations.
4. **Logic:** connect to `src/store/AppContext.jsx` for global user/week context; otherwise keep state local.
5. **Verify:** `npm test`, `npm run lint`, `npm run build`.
## 12. 26-Week Versioned Curriculum System
The platform uses a **26-week perpetual curriculum cycle** so every employee covers the knowledge base in focused, thematic weekly blocks. Cycle 1 covers weeks 1-26, Cycle 2 replays the same schedule, and so on.
## 12. Known Gotchas & Decisions
* **Per-user curriculum start.** Week/cycle derive from each user's `curriculum_started_at`, not the calendar. There is no shared current week. New users are gated through `/onboarding` until enrolled.
* **No podcast type.** Three content types only: `article`, `slides`, `infographic`.
* **Three micro-learning types.** `concept_explainer`, `scenario_quiz`, `flashcard_set`. `reflection_prompt` was dropped.
* **No Qdrant / no embeddings.** Retrieval is local TF-IDF (`src/lib/retrieval.js`).
* **Caddy, not Nginx.** The reverse proxy is Caddy (`Caddyfile`).
* **PocketBase auto-cancellation is OFF.** Set in `src/lib/pb.js`; never re-enable.
* **Go through `callLLM`.** Never call the Anthropic proxy directly; you lose retry, schema validation, and telemetry.
* **AI token budget.** Truncation surfaces as `LLMTruncatedError` (`stop_reason: max_tokens`). For extraction, tighten the prompt's topic cap before raising `max_tokens`.
* **Service:** `src/lib/curriculumService.js` — curriculum engine, week resolution, AI generation, version lifecycle.
* **DB Collections:** `curriculum_versions` holds the generated JSON schedules. `topics` includes `theme`, `complexity_weight`, and `difficulty` for generation input.
* **Version Lifecycle:** `draft` -> `active` -> `superseded`. Only one active version exists at a time.
* **Admin UI:** `src/components/admin/CurriculumManager.jsx` — accessed via the "Curriculum" tab. Admins trigger an AI-generated draft, preview the 26-week schedule, and confirm it to activate it globally.
* **Calendar-Driven Weeks:** The current week (1-26) and cycle (1, 2, 3...) are derived via modulo arithmetic from the absolute ISO calendar week in `curriculumService.js`. All employees share the same schedule.
* **Topic Enrichment:** The Curriculum Manager includes a one-off AI enrichment step to assign `theme` and `complexity_weight` to topics missing them before curriculum generation.
* **Progress tracking:** `getYearProgress(userId, isoWeekNumber)` computes completion for the *current 26-week cycle*.
* **Fallback:** If no active curriculum version exists, `getAssignedTopic()` falls back to the legacy hash-based assignment. Do not remove the hash fallback.
* **Deferred Features (V2):** Per-employee staggered start dates, cycle 2+ personalization, and prerequisite DAG sorting are deferred to future iterations to keep the MVP lean.
## 13. 26-Week Per-User Curriculum System
The platform uses a **26-week perpetual curriculum cycle**. Every employee covers the knowledge base in focused, thematic weekly blocks, **starting whenever they enroll**.
* **Service:** `src/lib/curriculumService.js` — week/cycle math, AI schedule generation, version lifecycle, progress.
* **DB:** `curriculum_versions` holds generated JSON schedules; `topics` carry `theme`, `complexity_weight`, `difficulty` as generation input.
* **Version lifecycle:** `draft``active``superseded`. Only one active version at a time (`CurriculumManager.jsx`).
* **Per-user weeks:** `getPersonalWeekNumber(startedAt)` yields an absolute week counter; `getCurriculumWeek`/`getCurriculumCycle` map it to the 126 slot and cycle. Same content each cycle.
* **Topic enrichment:** a one-off AI step assigns `theme`/`complexity_weight`/`difficulty` to topics missing them before generation (`enrichTopicsForCurriculum`, batches of 20).
* **Progress:** `getYearProgress(userId, personalWeekNumber)` computes completion for the current cycle.
* **Fallback:** if no active version exists, `getAssignedTopic()` falls back to deterministic hash-based assignment. Keep the fallback.
Good luck! You are building a platform that empowers continuous learning. Keep it fast, keep it beautiful, and keep the user engaged.