Compare commits

..

157 Commits

Author SHA1 Message Date
b1d3686d35 Merge pull request 'Onboarding-track: 5-daagse thema-introductie vanuit de "New here?"-kaart (#30)' (#31) from feat/onboarding-track into main
All checks were successful
On Push to Main / test (push) Successful in 34s
On Push to Main / publish (push) Successful in 1m11s
On Push to Main / deploy-dev (push) Successful in 3m19s
Reviewed-on: #31
2026-07-13 07:20:00 +00:00
RaymondVerhoef
b1330ebe3e ci: validate the production Caddyfile inside the built image (#26)
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 41s
On Pull Request to Main / publish (pull_request) Successful in 1m10s
On Pull Request to Main / deploy-dev (pull_request) Successful in 3m15s
The caddy-validate step added for #26 (and merged to main via #29) used a
`-v "$PWD":/workspace` bind mount, which does not propagate to the sibling
Docker daemon in the Gitea act runner — the container saw an empty dir and
every PR failed with "open Caddyfile: no such file or directory".

Validate instead inside the freshly built image, where the production
Caddyfile lives at /etc/caddy/Caddyfile: no bind mount, and it checks the
exact file that ships. Caddyfile.test keeps being covered by the homepage
smoke test (its container must serve 200).

Closes #26

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 09:13:12 +02:00
RaymondVerhoef
5214c9db3b feat: 5-day theme-level onboarding track from the "New here?" card (#30)
Some checks failed
On Pull Request to Main / test (pull_request) Failing after 24s
On Pull Request to Main / publish (pull_request) Has been skipped
On Pull Request to Main / deploy-dev (pull_request) Has been skipped
A self-paced onboarding track that introduces a new employee to every KB
theme in breadth (not depth), so they grasp how Respellion works day to
day and week to week. Offered as a CTA inside the Dashboard "New here?"
explainer card; always available regardless of enrollment.

Design:
- Theme is the trackable unit; the 5 "days" are a read-time presentation
  grouping, so re-chunking never loses progress. Completion is stored per
  theme in onboarding_completions.
- Per-theme overview generated lazily on first open (fast-tier
  emit_onboarding_overview tool), cached in onboarding_overviews keyed by
  theme + a topics_fingerprint that triggers regeneration when the theme's
  topic set changes.
- Reachable via /onboarding-track using the existing skipEnrollmentGate
  prop, decoupled from the 26-week curriculum (distinct from /onboarding,
  the enrollment page).

Backend:
- pb_migrations/1781200000_created_onboarding.js: two collections with
  authenticated-only rules and unique indexes; TEXT team_member_id (no
  relation) per the post-#18/#27 convention. Mirrored in
  scripts/setup-pb-collections.mjs.
- src/lib/onboardingService.js: pure helpers (orderThemes,
  distributeThemesIntoDays, computeTopicsFingerprint,
  computeOnboardingProgress, buildOnboardingPlan) + generation + I/O.
- db.js onboarding helpers use pb.filter() bindings (theme is free text).
- LLM tool + Zod schema + registry + simulation stub.

Frontend:
- src/pages/OnboardingTrack.jsx (day list, per-theme overview, completion
  banner, progress ring/day bar).
- Dashboard "New here?" card CTA + X/5-days progress chip (hidden when the
  KB has no themes).

Docs: data-model, generation-spec (§D), frontend-spec updated.

Verified: 22 new unit tests (npm test 134/134), eslint clean on changed
files, npm run build OK, PocketBase v0.30.4 boot applies the migration
(collections + unique indexes + authed rules confirmed), and a backend
contract check (upsert idempotency, unique-index guard, special-char
theme filtering).

Closes #30

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 09:08:38 +02:00
48715df147 Merge pull request 'Fix #27: TeamManager-rosterbeheer, role-escalatie gedicht, escalate-only resync' (#29) from fix/issue-27-teammanager-admin-rules into main
Some checks failed
On Push to Main / test (push) Failing after 25s
On Push to Main / publish (push) Has been skipped
On Push to Main / deploy-dev (push) Has been skipped
Reviewed-on: #29
2026-07-12 19:04:43 +00:00
85204938df Merge pull request 'CI: caddy validate van beide Caddyfiles in de test-job' (#28) from fix/issue-26-ci-caddy-validate into main
Some checks failed
On Push to Main / publish (push) Has been cancelled
On Push to Main / deploy-dev (push) Has been cancelled
On Push to Main / test (push) Has been cancelled
Reviewed-on: #28
2026-07-12 19:04:29 +00:00
RaymondVerhoef
cbce4555ff fix: TeamManager admin rules, close role self-escalation, escalate-only resync (#27)
Some checks failed
On Pull Request to Main / test (pull_request) Failing after 24s
On Pull Request to Main / publish (pull_request) Has been skipped
On Pull Request to Main / deploy-dev (pull_request) Has been skipped
TeamManager could not manage the roster: updateRule allowed self-update
only and deleteRule was superuser-only. The same self-update rule also
left a privilege escalation open — it did not restrict fields, so any
authenticated user could PATCH their own role to "admin".

- team_members rules (migration 1781000003 + base migration + setup
  script mirror):
    update: (@request.auth.id = id && @request.body.role:isset = false)
            || @request.auth.role = "admin"
    delete: @request.auth.role = "admin"
  Self-service onboarding keeps working (it never touches role); the
  role field is admin-only; deletion is admin-only.
- pb_hooks/team_members.pb.js: the ENTRA_ADMIN_EMAILS resync is now
  escalate-only — it guarantees admin for allow-list members on every
  login but no longer demotes, otherwise every TeamManager promotion
  would revert on the member's next sign-in (ADR-004 amended).
- TeamManager.jsx: info text updated to the new behaviour.

Verified with a two-identity mock-OIDC matrix (11/11): allow-list vs
regular login, self-escalation blocked while onboarding self-update
still works, cross-user update/delete blocked, admin promote/demote/
delete work, promotion survives the member's next login, allow-list
admin stays admin. Regression: #22 matrix 9/9, #24 matrix 8/8, #18
harness 15/15, npm test 112/112, eslint clean.

Closes #27

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:01:28 +02:00
RaymondVerhoef
6bf8bad5fb ci: validate both Caddyfiles in the test job (#26)
Some checks failed
On Pull Request to Main / test (pull_request) Failing after 26s
On Pull Request to Main / publish (pull_request) Has been skipped
On Pull Request to Main / deploy-dev (pull_request) Has been skipped
The test image swaps in Caddyfile.test, so the production Caddyfile was
never exercised by CI: the parse error from issue #20 sailed through
every check and only surfaced when the deploy gate failed — after the
broken container had already replaced the healthy one. Running
`caddy validate` (same caddy:2-alpine base as the Dockerfile) on both
files up front fails the PR check in seconds instead.

Touches the frozen .github/workflows/ per explicit product-owner
approval in #26.

Closes #26

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:55:35 +02:00
RaymondVerhoef
54137122a7 fix: read Entra claims from id_token + UPN e-mail fallback (#24)
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 38s
On Pull Request to Main / publish (pull_request) Successful in 1m10s
On Pull Request to Main / deploy-dev (pull_request) Successful in 3m7s
First Microsoft logins failed with 400 "email: Cannot be blank": Graph's
/oidc/userinfo endpoint omits the email claim for accounts without a
mail attribute, and team_members requires an e-mail (it also drives the
ENTRA_ADMIN_EMAILS role mapping). Reproduced against a mock Entra with
PocketBase v0.30.4; the user-supplied response body matched the
missing-email fingerprint exactly.

- provider config (reconciler + migration fast-path): userInfoURL is now
  empty, so PocketBase reads the claims from the Entra id_token, which
  always carries preferred_username (UPN) and email when available. The
  #18 reconciler flips the already-deployed Labs provider automatically
  on the next boot — no migration needed.
- pb_hooks/entra_oidc.pb.js: onRecordAuthWithOAuth2Request hook falls
  back to the lowercased UPN when the email claim is absent. Guest UPNs
  (ext_user#EXT#@tenant...) are excluded explicitly — "#" is RFC-valid
  in an e-mail local part, so both a naive regex AND PocketBase's own
  validation would accept them (caught by test V5). The hook also logs
  every failed OAuth2 attempt with the underlying error to the container
  log, which PocketBase otherwise only writes to its internal logs db.

Verified with a switchable mock-Entra matrix (8/8): no-email→UPN e-mail
+ allow-list role, email claim wins when present, no duplicate on
re-login, guest UPN yields a clean validation error, anonymous REST
create stays rejected, both log lines present. Regression: issue-22
matrix 9/9 (baseline pinned to 1a1351d now that #23 is merged), DoD
harness 15/15, npm test 112/112.

Closes #24

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:26:25 +02:00
8decdd454f Merge pull request 'Fix #22: OAuth2 sign-up toegestaan via @request.context-rule (eerste Microsoft-login werkt weer)' (#23) from fix/issue-22-oauth2-signup-rule into main
All checks were successful
On Push to Main / test (push) Successful in 33s
On Push to Main / publish (push) Successful in 1m4s
On Push to Main / deploy-dev (push) Successful in 3m3s
Reviewed-on: #23
2026-07-12 15:11:24 +00:00
RaymondVerhoef
897b46d4a1 fix: allow OAuth2 sign-up on team_members via @request.context rule (#22)
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 39s
On Pull Request to Main / publish (pull_request) Successful in 1m9s
On Pull Request to Main / deploy-dev (pull_request) Successful in 3m9s
Every first Microsoft login failed with 403 "Only superusers can perform
this action": PocketBase applies the collection createRule to the
automatic record creation during OAuth2 sign-up, and team_members was
created with createRule null (superuser-only) on the wrong assumption
that the OAuth2 flow bypasses it. Since the pre-Azure records were
deliberately dropped, every user was a first login and nobody could get
in. Reproduced locally against a mock OIDC provider (PocketBase v0.30.4).

createRule becomes '@request.context = "oauth2"': record creation is
allowed exclusively from within the OAuth2 flow. Anonymous REST creates
(which could otherwise pre-seed rogue admin profiles) remain rejected —
covered by an explicit test.

- pb_migrations/1781000002_allow_oauth2_signup.js: applies the rule on
  already-migrated environments (Labs); idempotent, guarded, with down
- pb_migrations/1781000000_team_members_to_auth.js: same rule for fresh
  environments (filename unchanged — ledger-safe)
- scripts/setup-pb-collections.mjs: fallback entry mirrored
- docs/auth-spec.md: ADR 009 + files table

Verified with a mock-OIDC matrix (9/9): baseline reproduces the 403 on
the old rule; upgrade path heals (first login 200, role/enrollment/name
defaults, second login no duplicate, anonymous create still rejected);
fresh chain + admin allow-list mapping OK. DoD harness regression 15/15,
npm test 112/112.

Closes #22

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:15:54 +02:00
RaymondVerhoef
80f738ddcb fix: valid Caddyfile handle_errors + frontend health-gate in deploys (#20)
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 39s
On Pull Request to Main / publish (pull_request) Successful in 1m7s
On Pull Request to Main / deploy-dev (pull_request) Successful in 3m6s
The learning-platform (Caddy) container crash-looped since this morning's
89d3395: `Content-Type application/json` is not a valid subdirective of
`respond` — a Caddyfile parse error aborts Caddy at startup, taking the
whole app offline for authenticated users while every CI run stayed green.
Verified with caddy v2.10.0: "unrecognized subdirective 'Content-Type',
at Caddyfile:53"; the fixed file validates clean.

CI was blind to this twice over: test.yml swaps the real Caddyfile for
Caddyfile.test in the test image, and the deploy health-gate from #18
only covers PocketBase.

- Caddyfile: set Content-Type via a `header @api` directive before the
  `respond`; behaviour of the JSON error response is unchanged
- infra/*/site/deploy-playbook.yml: frontend health-gate (docker exec
  wget --spider on the container) with log dump + abort on failure, and
  an always-on post-deploy smoke report (compose ps, proxy health,
  team_members auth-methods, PocketBase log tail) for visibility behind
  the auth perimeter

Closes #20

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:20:15 +02:00
1a1351ddcb Merge pull request 'Fix #18: migratie-ledger-sync + deploy-proof Azure SSO' (#19) from fix/issue-18-migration-ledger-sso into main
All checks were successful
On Push to Main / test (push) Successful in 32s
On Push to Main / publish (push) Successful in 1m8s
On Push to Main / deploy-dev (push) Successful in 3m0s
Reviewed-on: #19
2026-07-11 11:54:37 +00:00
RaymondVerhoef
d79e69aad2 fix: heal migration-ledger mismatch and make Azure SSO deploy-proof (#18)
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 39s
On Pull Request to Main / publish (pull_request) Successful in 1m9s
On Pull Request to Main / deploy-dev (pull_request) Successful in 3m1s
PocketBase on Labs crash-looped since the SSO deploy (PR #17): mounting
--migrationsDir for the first time replayed the entire migration history
against a database that was provisioned out-of-band (empty _migrations
ledger) and died on 1778948471_created_content.js. On top of that the
team_members->auth migration had its own crash paths and trapped OAuth2
config inside a one-shot, env-dependent migration.

- pb_migrations/1000000000_baseline_ledger_sync.js: detects an
  out-of-band provisioned DB (schema exists, ledger empty) and marks the
  79 historical migrations as applied; no-op on fresh or already-synced DBs
- pb_migrations/1781000000_team_members_to_auth.js: idempotency guard,
  relation->text conversion WITH data preservation (PocketBase diffs
  fields by id, so the column is backed up and restored via SQL), unique
  index rebuild, no silent catches, env only as fast-path
- pb_hooks/entra_oidc.pb.js + pb_hooks/utils.js: reconcile the Entra OIDC
  provider from ENTRA_* env on every bootstrap + cron tick
  (compare-before-save, warn-once); heals environments that migrated
  without secrets and supports secret rotation without re-apply
- pb_hooks/team_members.pb.js: require() pattern — JSVM runs callbacks as
  isolated programs, top-level helpers are not in scope (adopts Leroy's
  fix from the fix-sso branch)
- infra/*/site/deploy-playbook.yml: health-gate after compose up — the
  deploy fails loudly with container logs when PocketBase does not become
  healthy (runs #83-#88 were green while PB crash-looped)
- docker-compose.yml: .env.local is optional again
- docs/auth-spec.md + AI_AGENT.md: ledger/reconciler documentation, ADRs
  006-008, never-rename-applied-migrations warning

Verified locally against PocketBase v0.30.4 with a 7-scenario DoD matrix
(fresh DB +/- env, out-of-band DB with data incl. data preservation,
populated-ledger upgrade, late-secrets healing, re-run guard, hook
provisioning): 15/15 pass. npm test 112/112.

Closes #18

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:14:09 +02:00
RaymondVerhoef
89d3395a62 feat: enhance OIDC configuration for Azure login and improve error handling in API responses
All checks were successful
On Push to Main / test (push) Successful in 56s
On Push to Main / publish (push) Successful in 1m24s
On Push to Main / deploy-dev (push) Successful in 2m58s
2026-07-11 10:19:54 +02:00
RaymondVerhoef
d5191073f0 feat: migrate team_members to OIDC-based auth collection and update docker-compose environment configuration
All checks were successful
On Push to Main / test (push) Successful in 57s
On Push to Main / publish (push) Successful in 1m16s
On Push to Main / deploy-dev (push) Successful in 3m0s
2026-06-25 18:05:39 +02:00
fab7a12f7b Merge pull request 'fix/issue-16-azure-login' (#17) from fix/issue-16-azure-login into main
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 1m15s
On Push to Main / deploy-dev (push) Successful in 2m54s
Reviewed-on: #17
2026-06-24 14:00:40 +00:00
RaymondVerhoef
fe99381cb7 ci: pass Entra (Azure) secrets to deploy playbooks (#16)
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 55s
On Pull Request to Main / publish (pull_request) Successful in 1m16s
On Pull Request to Main / deploy-dev (pull_request) Successful in 3m2s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:57:33 +02:00
RaymondVerhoef
3af105bccd feat: Azure (Entra ID) login as user login (#16)
Replaces the client-side PIN login with real authentication against Azure
Entra ID via PocketBase's built-in OAuth2 (OIDC) flow. Every Azure user is
auto-provisioned as a team_member on first login.

- pb_migrations: team_members becomes a PocketBase auth collection (OIDC
  provider configured from env); all collections require @request.auth.id
- pb_hooks: provisioning of role (ENTRA_ADMIN_EMAILS allow-list) and
  enrollment_status, with admin-role re-sync on every login
- frontend: "Sign in with Microsoft" login, pb.authStore-based session,
  TeamManager manages roster/roles (no PIN/manual create)
- infra: Entra env wiring + pb_hooks mount for dev (Labs) and prod
- src/lib/azureAuth.js + tests for the canonical role/allow-list logic
- docs/auth-spec.md

Knowledge base, generated tests and micro-learnings are left untouched.
Existing PIN users are dropped (no migration required, per sign-off).

Closes #16

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:41:08 +02:00
5f34a6f825 Merge pull request 'Bescherm excluded en locked topics tegen AI-deletion/merge' (#15) from fix/protect-excluded-locked-topics into main
All checks were successful
On Push to Main / test (push) Successful in 36s
On Push to Main / publish (push) Successful in 1m19s
On Push to Main / deploy-dev (push) Successful in 1m45s
Reviewed-on: #15
2026-06-04 06:29:33 +00:00
RaymondVerhoef
9395ea11fe fix: bescherm excluded en locked topics tegen AI-deletion/merge
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 46s
On Pull Request to Main / publish (pull_request) Successful in 1m26s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m49s
Topics met learning_relevance="exclude" of relevance_locked=true werden
door de Full Analysis stilletjes verwijderd: het model zag exclude als
"irrelevant" en stelde ze voor in actions.deletions / actions.merges,
en analyzeGraph paste die zonder filter toe. Het locked-vlaggetje werd
in full-scope payload niet eens meegestuurd, dus zelfs een nieuwe prompt
kon niet helpen.

Defense-in-depth fixes:

1. Pure graphGuard.filterAiActions strips elke deletion/merge waarvan de
   target excluded of locked is, vóór bulkSave. Merges waarin een
   protected topic juist de keepId is (canonical survivor) blijven door.

2. SYSTEM_PROMPTS.full krijgt een expliciete "PROTECTED TOPICS" sectie
   die exclude en locked topics als nooit-te-verwijderen markeert.

3. relevance_locked wordt nu ook in de full-scope compactTopics payload
   meegestuurd, zodat het model de vlag überhaupt ziet.

4. UI-feedback: GraphControls toont een ShieldCheck banner met aantal
   geblokkeerde acties na de analyze, en het excluded-aantal naast de
   "Show Excluded Nodes" toggle (visible/hidden).

5. 13 nieuwe Vitest cases dekken protected detection en alle drop/keep
   paden van filterAiActions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 08:22:12 +02:00
e310b6d85b Merge pull request 'Knowledge graph table view met sort, group en bulk edit' (#11) from fix/issue-10-knowledge-graph-bulk-changes into main
All checks were successful
On Push to Main / test (push) Successful in 44s
On Push to Main / publish (push) Successful in 1m21s
On Push to Main / deploy-dev (push) Successful in 1m46s
Reviewed-on: #11
2026-06-03 20:09:07 +00:00
2b2921f6ed Merge pull request 'fix: stop warning about merged themes in curriculum schedule' (#13) from fix/issue-12-curriculum-merge-warning into main
Some checks failed
On Push to Main / test (push) Has been cancelled
On Push to Main / publish (push) Has been cancelled
On Push to Main / deploy-dev (push) Has been cancelled
Reviewed-on: #13
2026-06-03 20:08:57 +00:00
RaymondVerhoef
2274de4de7 fix: stop warning about merged themes in curriculum schedule
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 44s
On Pull Request to Main / publish (pull_request) Successful in 1m22s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m40s
When the KB has more than 26 themes the curriculum AI is required to
merge — so theme labels not appearing as week names are expected, not
warnings. The previous validation surfaced this as a console.warn that
read like an error in the learning station console.

- validateSchedule now only flags missing theme labels when themes_kb <= 26
- adds a real coverage check: warns when learning topics are absent from
  every week (the actual signal we care about)
- adds vitest coverage for both behaviours

Closes #12

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 22:05:34 +02:00
RaymondVerhoef
eb08c4ad96 feat: knowledge graph table view met sort, group en bulk edit
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 48s
On Pull Request to Main / publish (pull_request) Successful in 1m20s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m52s
Voegt een Graph/Table toggle toe aan de admin Knowledge Graph. De tabel
ondersteunt sorteren per kolom, groeperen op type/relevance/theme/difficulty,
filteren op label, multi-select en bulk-wijzigen van type, learning_relevance
en relevance_locked.

Closes #10

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 17:08:51 +02:00
RaymondVerhoef
218f6e7d64 feat: theme-grouped pickers, theme-session content panel, theme/topic naming
All checks were successful
On Push to Main / test (push) Successful in 42s
On Push to Main / publish (push) Successful in 1m17s
On Push to Main / deploy-dev (push) Successful in 1m56s
- Admin Topic Quizzes (was Quizzes): topics are now grouped under
  collapsible theme headers with per-theme counts. Topics without a
  theme fall into a "No theme" bucket rendered last.
- /topic-test picker: eligible topics grouped by theme so learners can
  scan a curriculum theme and drill its topics on demand.
- Fix admin Theme Content panel (was Learning Content): it was empty
  because content moved to the theme_sessions collection in an earlier
  refactor while the panel still queried the legacy per-topic content
  table. Rewrites ContentManager to read db.getAllThemeSessions and
  render the emit_theme_session schema (title, intro, topic sections,
  connections, key takeaways). Adds db.getAllThemeSessions and
  db.deleteThemeSession.
- Disambiguate theme vs topic across the app: the weekly curriculum
  test is now "Theme Test" (nav, page heading, Dashboard card, button,
  explainer); the on-demand bank test stays "Topic Test". Admin nav
  also clarified to "Theme Content" / "Topic Quizzes".

No schema changes. 85/85 tests still pass, build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 16:37:50 +02:00
RaymondVerhoef
43a71e2110 feat: on-demand topic tests with shared question bank
All checks were successful
On Push to Main / test (push) Successful in 1m1s
On Push to Main / publish (push) Successful in 1m22s
On Push to Main / deploy-dev (push) Successful in 2m18s
Adds a /topic-test route where learners can take a 5-question test on any
eligible topic at any time. Questions come from a shared, admin-curated
question_bank — no LLM calls on the user path. Points feed the existing
leaderboard with a 10pt/topic/week cap (per ISO week) so the bank can't be
farmed, and repeats from a thin bank yield 0 points.

- New PB collections: question_bank, on_demand_attempts (+ migrations and
  setup-pb-collections.mjs entries).
- db.js: un-deprecates getQuizBank/setQuizBank against question_bank so the
  existing admin TestManager panel becomes functional again; adds
  saveOnDemandAttempt, getOnDemandPointsThisWeek, getUserSeenQuestionIds,
  getAllOnDemandAttempts, isoWeekKey.
- testService.js: getEligibleTopicsForOnDemand, startOnDemandTest (unseen-
  first draw), finishOnDemandTest (cap enforcement + leaderboard upsert).
- New TopicTest page, route, and nav entry; weekly test flow untouched.
- Leaderboard folds on-demand 100%s into perfect-score count and merges
  on-demand attempts into the recent activity feed.
- Spec: docs/on-demand-tests-spec.md with ADRs and extension points.
- Tests: 5 new vitest cases covering eligibility, draw fallback, scoring,
  partial cap, and exhausted cap (85/85 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 15:06:43 +02:00
RaymondVerhoef
d1a1cc913c Make Dashboard explainer always accessible via help icon
All checks were successful
On Push to Main / test (push) Successful in 44s
On Push to Main / publish (push) Successful in 1m26s
On Push to Main / deploy-dev (push) Successful in 1m54s
Adds a HelpCircle toggle button in the Dashboard header so users can
re-open the platform explainer after dismissing it. The per-user
dismissed state is preserved as the initial-open default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:43:20 +02:00
RaymondVerhoef
8160242be5 feat: update loading message duration and add caching information; increase timeout for theme session generation
All checks were successful
On Push to Main / test (push) Successful in 43s
On Push to Main / publish (push) Successful in 1m25s
On Push to Main / deploy-dev (push) Successful in 1m56s
2026-05-28 12:03:55 +02:00
RaymondVerhoef
d38c75beb1 feat: theme-level weekly sessions and theme-grouped knowledge library
All checks were successful
On Push to Main / test (push) Successful in 46s
On Push to Main / publish (push) Successful in 1m28s
On Push to Main / deploy-dev (push) Successful in 2m9s
- New theme_sessions + theme_session_completions PocketBase collections
- Generate a detailed per-week theme summary covering every topic in the
  week's curriculum slot via EMIT_THEME_SESSION_TOOL; cache per
  (curriculum_version, week_number)
- Replace Leren.jsx "This Week's Topic" card with "This Week's Theme
  Session" that opens the new ThemeSessionView; per-topic micro-learnings
  remain reachable as optional practice from inside the session
- Group the Knowledge Library by topic.theme, pin the current week's
  theme on top, and badge topics already covered by the active session
- Mark week complete when the user finishes reading the theme session

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:44:10 +02:00
RaymondVerhoef
6a1f5556a9 Add dismissible platform explainer to Dashboard
All checks were successful
On Push to Main / test (push) Successful in 39s
On Push to Main / publish (push) Successful in 1m8s
On Push to Main / deploy-dev (push) Successful in 1m38s
Introduces a per-user, dismissible "How Respellion works" card on the
Dashboard so new users understand the learn/test/leaderboard loop and
where to find R42 and the main navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:24:44 +02:00
RaymondVerhoef
ad5be01b70 feat: enhance ConceptExplainer to handle content extraction and display; add normalization for PocketBase records
All checks were successful
On Push to Main / test (push) Successful in 39s
On Push to Main / publish (push) Successful in 1m8s
On Push to Main / deploy-dev (push) Successful in 1m34s
2026-05-27 21:18:17 +02:00
RaymondVerhoef
ce276f0296 feat: enhance KnowledgeGraph with edge styles, node navigation, and AI analysis scopes; update GraphControls and NodeDetailPanel for improved relation management 2026-05-27 21:09:05 +02:00
RaymondVerhoef
47a738fde3 feat: migrate graph snapshot handling to localStorage for improved persistence
All checks were successful
On Push to Main / test (push) Successful in 39s
On Push to Main / publish (push) Successful in 1m7s
On Push to Main / deploy-dev (push) Successful in 1m36s
2026-05-27 20:27:16 +02:00
RaymondVerhoef
9fb22b8090 feat: update getQuizResult to handle no records gracefully by using getList 2026-05-27 20:27:08 +02:00
RaymondVerhoef
6309ae716b feat: implement snapshot restore functionality and enhance graph state management
All checks were successful
On Push to Main / test (push) Successful in 40s
On Push to Main / publish (push) Successful in 1m10s
On Push to Main / deploy-dev (push) Successful in 1m35s
2026-05-27 17:43:18 +02:00
RaymondVerhoef
6ea8860b96 feat: refactor KnowledgeGraph component and add NodeDetailPanel for enhanced node management
Some checks failed
On Push to Main / test (push) Successful in 47s
On Push to Main / publish (push) Failing after 31m8s
On Push to Main / deploy-dev (push) Has been cancelled
2026-05-27 15:18:01 +02:00
RaymondVerhoef
7b6a5b4bf0 feat: add GraphControls component and useGraphData hook for knowledge graph management 2026-05-27 15:05:26 +02:00
RaymondVerhoef
3aa32c383e feat: implement quiz results tracking and caching for user tests
All checks were successful
On Push to Main / test (push) Successful in 47s
On Push to Main / publish (push) Successful in 1m27s
On Push to Main / deploy-dev (push) Successful in 2m7s
2026-05-27 10:05:56 +02:00
RaymondVerhoef
07af2783dc 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.
2026-05-27 08:24:56 +02:00
RaymondVerhoef
7066f881f9 feat: implement onboarding process and enrollment status tracking for users 2026-05-26 21:33:20 +02:00
RaymondVerhoef
febc9dc7f2 Merge branch 'main' of https://git.labs.respellion.tech/rve/learning-platform
All checks were successful
On Push to Main / test (push) Successful in 41s
On Push to Main / publish (push) Successful in 1m12s
On Push to Main / deploy-dev (push) Successful in 1m39s
2026-05-26 14:23:15 +02:00
RaymondVerhoef
46d2735237 Merge branch 'main' of https://git.labs.respellion.tech/rve/learning-platform
Some checks failed
On Push to Main / test (push) Successful in 41s
On Push to Main / deploy-dev (push) Has been cancelled
On Push to Main / publish (push) Has been cancelled
2026-05-26 14:22:08 +02:00
RaymondVerhoef
daec9cf5b5 Add hot update files for layout and webpack with source maps
- Created new hot update JSON and JS files for app layout and webpack.
- Included eval-source-map for development purposes in the layout hot update.
- Added full hash retrieval function in the webpack hot update.
2026-05-26 14:22:01 +02:00
RaymondVerhoef
9b3d7fde8c fix: end micro-learning sessions explicitly and route to test
Every micro-learning format used to call onComplete implicitly — scroll
to the bottom of the explainer, flip the last flashcard, or pick any
scenario option — which yanked users out of the content before they
were done with it.

Replace the implicit triggers with a Finish session button on each
format. The scenario quiz exposes the button only after an option is
chosen so explanations still get read; the flashcard set exposes a
viewed-cards counter next to it.

The topic-reviewed landing screen now offers Back to Station, Try
another format, and Go to test, so finishing a session leads somewhere
useful instead of dead-ending. Drop the unreachable completion box that
the container was rendering underneath the parent's success screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:20:41 +02:00
RaymondVerhoef
84e7468841 feat: drop reflection_prompt type and flag cached micro-learnings
Remove the reflection_prompt micro-learning format end-to-end: type
config, tool definition, container case, selector tile, and the
ReflectionPrompt component file. The format wasn't pulling its weight as
a learning surface.

Add a Beschikbaar badge to selector tiles whose topic already has a
published micro-learning of that type, so users know which formats open
instantly instead of triggering a fresh generation. Cached records are
fetched once per topic via the new getExistingTypesForTopic helper, and
re-fetched after a generation returns so newly-created formats light up
without a manual refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:41:20 +02:00
RaymondVerhoef
9ea5d5444d fix: generate weekly quiz in a single batch LLM call
The weekly quiz path made up to six sequential LLM calls (one per topic)
with three retries each, blowing past the 30s budget. Worse, the
quiz_banks collection has been dropped, so getQuizBank/setQuizBank are
no-ops and every generated batch was thrown away — the assembled quiz
ended up empty and surfaced as "Could not assemble enough questions."

Replace the per-topic loop with a single batched call on the fast tier
that emits all five questions in one round-trip, using the review topics
as prompt context instead of separate calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:22:44 +02:00
RaymondVerhoef
a653812cd8 feat: implement micro-learning generation service and UI components for interactive topic-based learning
All checks were successful
On Push to Main / test (push) Successful in 32s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m33s
2026-05-25 22:17:26 +02:00
RaymondVerhoef
f16438c1bc feat: implement micro learning generation service with cached LLM content delivery and UI components
All checks were successful
On Push to Main / test (push) Successful in 32s
On Push to Main / publish (push) Successful in 1m2s
On Push to Main / deploy-dev (push) Successful in 1m32s
2026-05-25 22:14:00 +02:00
RaymondVerhoef
7164317908 feat: implement quiz generation service with topic selection, LLM integration, and question bank management 2026-05-25 22:08:18 +02:00
RaymondVerhoef
3c04bab1b9 fix
All checks were successful
On Push to Main / test (push) Successful in 31s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m31s
2026-05-25 21:38:53 +02:00
RaymondVerhoef
80532b6d1b fix 2026-05-25 20:51:11 +02:00
RaymondVerhoef
e4030868b4 fix 2026-05-25 19:46:04 +02:00
RaymondVerhoef
7d1fe83f72 fix 2026-05-25 19:41:20 +02:00
RaymondVerhoef
b24ddc5490 fix
All checks were successful
On Push to Main / test (push) Successful in 31s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m31s
2026-05-25 19:35:52 +02:00
RaymondVerhoef
74e18d2638 fix 2026-05-25 19:23:06 +02:00
RaymondVerhoef
9ba9ec94ad fix 2026-05-25 19:18:24 +02:00
RaymondVerhoef
8291a52ba9 fix test 2026-05-25 19:09:17 +02:00
RaymondVerhoef
6968bebbc2 fix
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 1m1s
On Push to Main / deploy-dev (push) Successful in 1m31s
2026-05-25 18:50:53 +02:00
RaymondVerhoef
ac88c95b46 Bug fix 2026-05-25 18:43:12 +02:00
RaymondVerhoef
d4066a8f61 fix 2026-05-25 18:39:21 +02:00
RaymondVerhoef
042dfb2d92 Feat: microlearning implementation 2026-05-25 18:32:45 +02:00
RaymondVerhoef
18e98380d9 feat: implement micro-learning collections and completion tracking hook 2026-05-24 23:50:08 +02:00
RaymondVerhoef
cd151aace4 feat: implement core micro-learning components and hooks for diverse educational formats 2026-05-24 23:45:26 +02:00
RaymondVerhoef
55bcb689e7 feat: implement micro-learning system with content delivery components, completion tracking hooks, and database schema migrations 2026-05-24 23:40:59 +02:00
RaymondVerhoef
8930ac03ae rename micro-learning-generation-spec.md to micro-learning-spec.md 2026-05-24 23:29:18 +02:00
RaymondVerhoef
f55ec950aa feat: implement curriculum service for auto-generating, validating, and managing 26-week learning schedules
All checks were successful
On Push to Main / test (push) Successful in 32s
On Push to Main / publish (push) Successful in 1m2s
On Push to Main / deploy-dev (push) Successful in 1m32s
2026-05-24 23:16:01 +02:00
RaymondVerhoef
967c68d27d feat: add curriculum management admin dashboard with AI generation and draft approval workflows 2026-05-24 23:09:58 +02:00
RaymondVerhoef
b07c4808a6 feat: implement knowledge extraction pipeline and centralized LLM client service
All checks were successful
On Push to Main / test (push) Successful in 31s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m32s
2026-05-24 22:38:46 +02:00
RaymondVerhoef
e9f37056b6 feat: implement text extraction pipeline and centralized LLM interface for knowledge graph generation 2026-05-24 22:26:13 +02:00
RaymondVerhoef
5965793f11 feat: add specification for micro learning generation and remove deprecated pipeline planning files 2026-05-24 21:14:32 +02:00
RaymondVerhoef
10d5066be8 feat: add curriculum management service and database integration for 26-week schedule generation
All checks were successful
On Push to Main / test (push) Successful in 32s
On Push to Main / publish (push) Successful in 1m2s
On Push to Main / deploy-dev (push) Successful in 1m32s
2026-05-24 19:58:18 +02:00
RaymondVerhoef
c5e23c77cd feat: implement curriculum management system including automated generation, enrichment, and versioning workflows 2026-05-24 19:50:20 +02:00
RaymondVerhoef
8e01b21a50 feat: implement RAG-enabled chat hook and admin file upload component 2026-05-24 16:59:11 +02:00
RaymondVerhoef
7d8999255f Add hot-update files for layout and webpack with eval-source-map configuration
- Created multiple hot-update JavaScript files for app layout and webpack.
- Each file includes a warning about the use of "eval-source-map" for development purposes.
- Added source mapping for CSS files in the app layout.
- Generated corresponding hot-update JSON files to manage module updates.
2026-05-24 11:16:21 +02:00
RaymondVerhoef
815cf0f673 feat: add status field to themes collection and update migration scripts
- Added a new "status" field to the themes collection with options: draft, published, and rejected.
- Updated the migration script to include the new field and its options.
- Created a new ingestion migration script to ensure the "status" field includes "rejected" as an option if not already present.
- Added multiple hot-update files for webpack to support the new changes in the frontend.
2026-05-24 10:56:45 +02:00
RaymondVerhoef
8684ffa35b feat: add new page and migration scripts for access rules and collections
- Introduced new page component for library topics with type checks.
- Added migration scripts to update access rules for various collections including badges, curriculum versions, and themes.
- Implemented PocketBase integration for managing collection access rules dynamically.
- Ensured proper type validation for page props and metadata generation functions.
2026-05-23 22:26:40 +02:00
RaymondVerhoef
14286d6cb1 Add TypeScript type definitions for app components and layout
- Created type definitions for `auth`, `layout`, and `page` components to ensure type safety and consistency.
- Implemented checks for entry validity and prop types using utility types.
- Added a `package.json` file to specify module type for TypeScript compatibility.
2026-05-23 22:10:58 +02:00
RaymondVerhoef
f4d0c85c55 Refactor code structure for improved readability and maintainability 2026-05-23 19:37:17 +02:00
RaymondVerhoef
472685f0d7 Add specifications for gamification, generation, and R42 chat services
- Introduced gamification service spec detailing responsibilities, API surface, XP calculation, levels, streaks, badges, milestone cards, and heatmap data.
- Added generation service spec outlining the process for generating micro learning content, including API endpoints, AI call configuration, prompt strategies, and error handling.
- Created R42 chat service spec covering chatbot interactions, retrieval pipeline, prompt construction, response generation, and stateless design principles.
2026-05-23 18:13:08 +02:00
RaymondVerhoef
dda20612e9 Add comprehensive documentation for employee learning platform
- Created handover document outlining design decisions and application functionality.
- Developed implementation plan detailing phased approach for service development.
- Specified ingestion service responsibilities, API surface, and processing pipeline.
2026-05-23 15:38:09 +02:00
RaymondVerhoef
881148357e refactor: remove Diagnostics component and related LLM call telemetry
All checks were successful
On Push to Main / test (push) Successful in 31s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m30s
2026-05-22 20:00:47 +02:00
RaymondVerhoef
7b6ae265db feat: implement pbUpsert helper for streamlined database operations and update related functions 2026-05-22 19:56:23 +02:00
RaymondVerhoef
ca8945ea5b refactor: remove handbook sync state and related functionality
All checks were successful
On Push to Main / test (push) Successful in 31s
On Push to Main / publish (push) Successful in 1m4s
On Push to Main / deploy-dev (push) Successful in 1m31s
2026-05-22 19:45:14 +02:00
RaymondVerhoef
dc628644b6 feat: add caveman skill with detailed README and SKILL documentation
All checks were successful
On Push to Main / test (push) Successful in 47s
On Push to Main / publish (push) Successful in 1m11s
On Push to Main / deploy-dev (push) Successful in 1m37s
2026-05-22 16:47:44 +02:00
RaymondVerhoef
3626cc0525 feat: add "Reset for Smoke Test" button in admin settings
Truncates sources, curriculum, content, quiz banks/results/cache, topics
and relations in dependency order so AI-generated state can be wiped
between smoke runs without leaving dangling references. Handbook sync
state is cleared by default (otherwise re-sync is a no-op); user
progress and leaderboard are opt-in. Team members, settings, and LLM
telemetry are preserved.

UI lives in Admin → Settings → Danger Zone and requires typing RESET
before the button enables. Per-collection deletion counts are reported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:42:54 +02:00
RaymondVerhoef
25cae2fea9 fix: speed up handbook sync and stop llm_calls 404 noise
Handbook sync ran files sequentially under a 5 req/min limiter with a
hardcoded 60s LLM timeout, causing long syncs and AbortError timeouts on
large files. Now: limiter at 20 req/min, files processed with concurrency
4, handbook extraction timeout raised to 180s, and near-empty files skip
the LLM call.

callLLM gains a timeoutMs option; passing a signal no longer silently
disables the per-request timeout.

llm_calls telemetry self-disables after the first 404 so deploys without
the migration applied don't spam the console.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:35:56 +02:00
a38ad5d1e0 Merge pull request 'feat: phase 5 of AI pipeline hardening — R42 retrieval & telemetry' (#7) from feat/ai-pipeline-hardening-phase-5 into main
All checks were successful
On Push to Main / test (push) Successful in 25s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m31s
Reviewed-on: #7
2026-05-20 19:38:34 +00:00
RaymondVerhoef
229246f7b6 feat: phase 5 of AI pipeline hardening — R42 retrieval & telemetry
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 32s
On Pull Request to Main / publish (pull_request) Successful in 57s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m34s
- Add dependency-free TF-IDF retrieval (src/lib/retrieval.js) with NL+EN
  stopwords and a WeakMap-cached index.
- Rewrite buildKbContext to ship the top-K relevant topics + verbatim-
  mentioned ids only, filter relations to the included set, and append a
  [kb_hash: <8 hex>] suffix so the ephemeral prompt cache busts when the
  graph changes. Returns { context, retrievedTopics, allTopics }.
- Add LOOKUP_TOPIC_TOOL and drive useChat through callLLM directly with a
  multi-hop tool_result loop capped at 3 hops; preserve Anthropic-provided
  tool_use ids through callLLM so the loop can echo correct tool_use_id.
- Truncate R42 history to the last 12 turns and prepend a single
  "(earlier conversation truncated)" assistant message.
- Set R42 chat defaults: temperature 0.3, maxTokens 2048.
- Add pb_migrations/1780500002_created_llm_calls.js (the best-effort
  logger in callLLM was already wired) and a new Admin → Diagnostics
  view showing the last 100 calls with token usage, cache-hit rate, and
  USD cost from a local Anthropic price table.
- Finalize AI_PIPELINE_HARDENING_PLAN.md: mark Phases 1–5 shipped and
  Phase 6 (eval harness) explicitly out of scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:36:40 +02:00
70db8b5c2d Merge pull request 'feat: phase 4 of AI pipeline hardening — quiz & content quality' (#6) from feat/ai-pipeline-hardening-phase-4 into main
All checks were successful
On Push to Main / test (push) Successful in 25s
On Push to Main / publish (push) Successful in 1m1s
On Push to Main / deploy-dev (push) Successful in 1m30s
Reviewed-on: #6
2026-05-20 17:23:32 +00:00
RaymondVerhoef
66e0c275da feat: phase 4 of AI pipeline hardening — quiz & content quality
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 31s
On Pull Request to Main / publish (pull_request) Successful in 1m1s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m31s
- src/lib/random.js: Fisher–Yates shuffle/sample/pickInt; replace every
  biased .sort(() => 0.5 - Math.random()) site in testService.
- testService: debias correctIndex via prompt + runtime re-roll (up to 2x
  when one position holds >50%); quality gate rejecting <4 distinct
  options, banned filler ("all of the above" etc) and explanations
  shorter than 20 chars; dedup new questions against the existing bank
  via normalised question text.
- Quiz schema/tool/prompt require difficulty ('easy'|'medium'|'hard');
  db.getQuizBank defaults legacy records to 'medium' on read.
- learningService.generateCustomTopic: kebab-case slug ID from the
  polished label with collision suffixes; default learning_relevance
  'standard' when the model omits it.
- Tests for random helpers, dedup/quality-gate behaviour and the
  extended quiz schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:22:10 +02:00
RaymondVerhoef
c82e4fc3a1 feat: reduce initial question batch size for a topic to 5
All checks were successful
On Push to Main / test (push) Successful in 32s
On Push to Main / publish (push) Successful in 1m0s
On Push to Main / deploy-dev (push) Successful in 1m33s
When a topic's quiz bank is empty (or below the requested count), we
previously seeded it with a fresh batch of 10 questions. That meant the
first weekly quiz for any new topic triggered a 10-question LLM call —
heavy for what's ultimately a 1-question sample for review topics, and
overkill for the typical 5-question primary topic.

- forceGenerateTopicQuestions default count: 10 → 5
- getOrGenerateTopicQuestions seed amount: 10 → 5
- TestManager "Generate" defaults + empty-state button copy: 10 → 5
- QUIZ_SYSTEM difficulty hint: rewritten for a 5-question batch (2 easy
  / 2 medium / 1 hard) with explicit "scale proportionally for larger
  batches" so admins can still generate 10+ via TestManager when they
  want more depth.

Tests 61/61 pass, lint clean (0 errors), build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:12:16 +02:00
fd3b849c19 Merge pull request 'feat: phase 3 of AI pipeline hardening — extraction quality' (#5) from feat/ai-pipeline-hardening-phase-3 into main
All checks were successful
On Push to Main / test (push) Successful in 25s
On Push to Main / publish (push) Successful in 1m10s
On Push to Main / deploy-dev (push) Successful in 1m31s
Reviewed-on: #5
2026-05-20 15:57:40 +00:00
RaymondVerhoef
aeb197d5f4 feat: phase 3 of AI pipeline hardening — extraction quality
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 31s
On Pull Request to Main / publish (pull_request) Successful in 1m1s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m32s
Replace stateless one-shot extraction with a stateful, paced, cancellable
pipeline. Six subtasks:

- 3.1 Sentence-aware chunking with 800-char overlap (was paragraph-only
  at 4000 chars). Hard-split fallback for runaway sentences.
- 3.2 Stateful extraction: chunks 2+ receive an "already-extracted topic
  IDs" hint capped at 200 IDs, so the model reuses IDs instead of
  inventing variants like software-developer vs software-engineer.
- 3.3 Token-bucket limiter in llmRetry.js (extractionLimiter, 5 req/min).
  callLLM awaits the limiter before fetch; 429+Retry-After calls
  pauseUntil. Replaces hard setTimeout(12000) and setTimeout(15000).
- 3.4 relevance_locked column on topics — admin edits to relevance are
  sticky across re-extraction. Migration + merge respects the flag +
  unlock checkbox in KnowledgeGraph edit form.
- 3.5 Unify relation vocabulary — handbook prompt no longer mentions
  legacy "executes"; one-shot migration rewrites existing executes rows
  to executed_by with source/target swapped.
- 3.6 Cancellation — Cancel button on UploadZone wired to an
  AbortController threaded into callLLM; aborted runs persist status =
  "cancelled" rather than "failed".

Tests: 16 new unit tests for chunkText, buildKnownIdsHint, and
createLimiter. All 61 tests pass, 0 lint errors, build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:56:45 +02:00
9771928926 Merge pull request 'Fix: exclude temperature parameter for reasoning-tier models' (#4) from feat/ai-pipeline-hardening-phase-2 into main
All checks were successful
On Push to Main / test (push) Successful in 25s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m33s
Reviewed-on: #4
2026-05-20 15:18:00 +00:00
RaymondVerhoef
40eff976b4 Fix: exclude temperature parameter for reasoning-tier models
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 31s
On Pull Request to Main / publish (pull_request) Successful in 1m3s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m33s
Anthropic has deprecated the temperature parameter for their reasoning
models (claude-opus-4-7). This was causing a 400 error when analyzeGraph
called callLLM with tier: 'reasoning'.

Solution: conditionally exclude temperature from the request body when
tier === 'reasoning'. Fast and standard tiers retain their temperature
parameter.

This unblocks the "Analyse and Optimize" button in the Knowledge Graph
admin panel post-Phase-2 deployment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:14:17 +02:00
33529dfb2b Merge pull request 'feat: phase 2 of AI pipeline hardening — tool-based structured outputs + prompt caching' (#3) from feat/ai-pipeline-hardening-phase-2 into main
All checks were successful
On Push to Main / test (push) Successful in 25s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m32s
Reviewed-on: #3
2026-05-20 13:48:08 +00:00
RaymondVerhoef
f838755991 feat: phase 2 of AI pipeline hardening — tool-based structured outputs + prompt caching
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 31s
On Pull Request to Main / publish (pull_request) Successful in 1m1s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m31s
Every structured-output call now uses an Anthropic tool instead of
parsing JSON out of free-form prose, and stable system prompts are
sent as cacheable blocks. Behaviour-equivalent to phase 1 from the
caller's point of view; the savings show up in token usage and in the
absence of "AI returned non-JSON response" failure modes.

* src/lib/llmTools.js — single source of truth for tool definitions:
  emit_knowledge_graph, emit_handbook_delta, emit_learning_article /
  _slides / _infographic / _all, emit_custom_topic, emit_quiz_questions,
  emit_graph_actions, plus five article-patch tools (set_intro,
  set_section, add_section, remove_section, replace_takeaways).
* src/lib/articlePatches.js — pure applyArticlePatches +
  applyAndValidate; rebuilds the article from a sequence of patch tool
  calls and re-validates against learningArticleSchema. set_section
  falls back to appending when no matching heading exists so the
  model's intent is preserved rather than silently dropped.
* src/lib/llmSchemas.js — Zod schemas for the five patch ops,
  registered in toolSchemaRegistry so callLLM validates them
  automatically.
* src/lib/llm.js — simulation mode now returns a tool_use stub matching
  toolChoice.name, so the UI keeps working with Simulation Mode on
  after the structured-output migration.
* src/lib/extractionPipeline.js — processSourceText and
  analyzeHandbookDelta migrated to callLLM + tool use. System prompts
  sent as { cache_control: ephemeral } blocks. Handbook results pass
  through normalizeHandbookResult to collapse legacy "executes"
  relations into executed_by with swapped source/target.
* src/lib/learningService.js — generateLearningContent picks the right
  tool per selectedType; generateCustomTopic uses emit_custom_topic;
  refineLearningContent now drives the five patch tools with
  toolChoice 'any' and rejects the whole turn if the patched article
  fails validation. Article-only refinement is intentional for phase 2;
  refining a topic without an article surfaces a clear error.
* src/lib/testService.js — quiz generation via emit_quiz_questions.
* src/components/admin/KnowledgeGraph.jsx — analyzeGraph routed through
  the reasoning tier (Opus) since graph-wide consolidation benefits
  from a stronger reasoner.
* src/components/chat/prompts.js — buildSystemPrompt now returns three
  text blocks: stable preamble (cached), KB context (cached, hash-bust
  deferred to phase 5), per-turn user/admin tail (uncached).
* src/lib/__tests__/ — 13 new tests covering each patch op, multi-op
  sequencing, post-patch validation failure, and tool/registry shape.

Acceptance: lint and 45/45 tests green; build succeeds; no
`match(/\{[\s\S]*\}/)` JSON extraction left in src/. Live verification
of cache hits on a second extraction within 5 minutes is deferred to
manual smoke testing — needs real `/api/anthropic` traffic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:47:20 +02:00
RaymondVerhoef
8a8745fad2 feat: show build SHA + timestamp in a small footer for deploy verification
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m33s
Adds a BuildStamp component pinned to the bottom-right of every page
(desktop only, dim monospace text) so it's obvious at a glance which
build is currently running.

The git short SHA and an ISO build timestamp are injected at build time
via Vite's `define`, falling back to 'unknown' if git is not available.
ESLint config declares the two compile-time constants as readonly
globals so no per-file disable comments are needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:20:46 +02:00
a5c18ccd0f Merge pull request 'feat/ai-pipeline-hardening-plan' (#2) from feat/ai-pipeline-hardening-plan into main
All checks were successful
On Push to Main / test (push) Successful in 25s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m33s
Reviewed-on: #2
2026-05-20 12:50:34 +00:00
RaymondVerhoef
b85785a0cb chore: ignore .claude/ local agent state
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 44s
On Pull Request to Main / publish (pull_request) Successful in 1m0s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m33s
Adds Claude Code's per-user .claude/ directory (settings.local.json
allowlists, etc.) to .gitignore so it doesn't get accidentally
committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:54:57 +02:00
RaymondVerhoef
4a8dbee7df feat: phase 1 of AI pipeline hardening — single LLM client + tier-aware models
Implements phase 1 of AI_PIPELINE_HARDENING_PLAN.md. Every Anthropic call
now goes through one module that owns retry, timeout, abort, structured-
output parsing, schema validation, and best-effort call telemetry.

* src/lib/llm.js — single callLLM entry point. Resolves model per tier
  (fast / standard / reasoning) with admin:model legacy fallback for the
  standard tier; 60s default timeout via AbortController; balanced-brace
  JSON extraction; LLMHttpError, LLMTruncatedError, LLMOutputError, and
  LLMValidationError surface clearly distinct failure modes.
* src/lib/llmRetry.js — exponential backoff with full jitter, retries
  only on transient HTTP statuses, honours Retry-After up to 60s, never
  retries on AbortError.
* src/lib/llmSchemas.js — Zod schemas for every structured task plus
  normalizeHandbookResult (collapses legacy "executes" relations into
  the canonical "executed_by" vocabulary).
* src/lib/api.js — thin shim over callLLM so existing callers (extraction
  pipeline, learning, quiz, R42, knowledge graph) keep working unchanged.
* src/lib/__tests__/ — 32 Vitest cases covering parse paths, error
  surfaces, simulation mode, model resolution, and schema validation.
* src/pages/Admin/index.jsx — three model inputs (fast / standard /
  reasoning) replacing the single legacy field; legacy value falls back
  for the standard tier so existing overrides survive.

Adds Zod and Vitest, plus an "npm run test" script.

Also cleans up the pre-existing repo-wide ESLint failures so phase 1's
"npm run lint passes" acceptance criterion can be checked: drops unused
React imports across the JSX tree (React 19 JSX runtime auto-imports),
attaches cause to rethrown errors in the service modules, ignores
pb_migrations in the ESLint config (PocketBase JSVM globals), and
removes one dead handleCreateCustom function in Leren.jsx. A real
behaviour bug surfaced in Testen.jsx — the quiz timer captured a stale
finishQuiz via setInterval closure; now updated via finishQuizRef so the
timer always invokes the latest callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:50:09 +02:00
d07d15b2a7 Merge pull request 'docs: AI pipeline hardening plan + rename giteaService -> githubService' (#1) from feat/ai-pipeline-hardening-plan into main
All checks were successful
On Push to Main / test (push) Successful in 26s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m40s
Reviewed-on: #1
2026-05-20 10:09:39 +00:00
RaymondVerhoef
db5bb854c3 docs: add AI pipeline hardening plan; rename giteaService to githubService
All checks were successful
On Pull Request to Main / test (pull_request) Successful in 30s
On Pull Request to Main / publish (pull_request) Successful in 58s
On Pull Request to Main / deploy-dev (pull_request) Successful in 1m35s
Adds AI_PIPELINE_HARDENING_PLAN.md — a phased, self-contained plan an AI
agent can execute to harden the Anthropic integration (central LLM
client, tool-based structured outputs, prompt caching, retrieval-based
R42 context, eval harness).

Renames src/lib/giteaService.js to src/lib/githubService.js. The module
calls api.github.com and raw.githubusercontent.com; the previous name
was misleading. No behaviour change. Updates the single import site in
src/components/admin/KnowledgeGraph.jsx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:03:25 +02:00
RaymondVerhoef
6fe66bbd93 feat: implement curriculum service and learning automation infrastructure
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m33s
2026-05-20 11:44:58 +02:00
RaymondVerhoef
2752fb95d9 feat: implement AI-driven knowledge extraction pipeline for company documentation
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m32s
2026-05-20 09:35:57 +02:00
RaymondVerhoef
caaf2b9eba feat: implement extraction pipeline for knowledge graph generation from text and handbook updates 2026-05-20 09:29:21 +02:00
RaymondVerhoef
d6c1813f75 feat: add extractionPipeline to process source text and handbook updates into a structured knowledge graph 2026-05-20 09:29:16 +02:00
RaymondVerhoef
d5655d2232 feat: implement automated knowledge graph extraction pipeline and visualization component 2026-05-20 08:55:27 +02:00
RaymondVerhoef
08aaed591f feat: add KnowledgeGraph component for visualizing and managing learning topics and relationships
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m37s
2026-05-19 12:33:12 +02:00
RaymondVerhoef
8529def748 feat: add UploadZone component and database utility for file processing
All checks were successful
On Push to Main / test (push) Successful in 31s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m35s
2026-05-19 12:00:14 +02:00
RaymondVerhoef
d23b0b6b16 feat: add learning_relevance field to topics and implement KnowledgeGraph UI with handbook synchronization capabilities
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m39s
2026-05-19 08:40:52 +02:00
RaymondVerhoef
fa5dcaeb01 feat: add docker-compose configuration and ansible playbook for production deployment
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m36s
2026-05-19 07:54:52 +02:00
RaymondVerhoef
190d1a6e0b feat: add KnowledgeGraph component for visualizing, editing, and syncing handbook content with AI-driven analysis
All checks were successful
On Push to Main / test (push) Successful in 31s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m49s
2026-05-18 22:12:19 +02:00
RaymondVerhoef
d2b067735c feat: add UploadZone component for drag-and-drop file processing
All checks were successful
On Push to Main / test (push) Successful in 31s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m34s
2026-05-18 22:03:10 +02:00
RaymondVerhoef
00541f4a08 feat: add KnowledgeGraph visualization and handbook synchronization component for admin interface 2026-05-18 22:01:02 +02:00
RaymondVerhoef
3555048308 chore: update document title to Learning Platform 2026-05-18 21:48:20 +02:00
RaymondVerhoef
590912ac04 feat: add KnowledgeGraph component for visualizing and managing knowledge base topics and relations 2026-05-18 21:42:14 +02:00
RaymondVerhoef
d71caa41f6 feat: implement interactive Knowledge Graph visualization with AI-driven content analysis and handbook synchronization tools 2026-05-18 21:30:05 +02:00
RaymondVerhoef
f68d76e3d2 feat: add giteaService for fetching files and content from GitHub repositories 2026-05-18 21:25:18 +02:00
RaymondVerhoef
7b84545dc5 feat: add KnowledgeGraph component for D3-based visualization and AI-driven graph management 2026-05-18 21:21:58 +02:00
RaymondVerhoef
f35550f270 feat: add knowledge graph component and persistent handbook sync state collection 2026-05-18 21:13:17 +02:00
RaymondVerhoef
9f3e1113a6 chore: update pocketbase command to include migrations directory and set working directory
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m32s
2026-05-18 20:46:34 +02:00
RaymondVerhoef
07a41938f1 feat: add created and updated autodate fields to sources collection via migration 2026-05-18 20:35:03 +02:00
RaymondVerhoef
3afef7785e feat: perform extensive database collection migrations and updates to PocketBase schema
All checks were successful
On Push to Main / test (push) Successful in 43s
On Push to Main / publish (push) Successful in 1m6s
On Push to Main / deploy-dev (push) Successful in 1m38s
2026-05-18 20:12:40 +02:00
RaymondVerhoef
d68164880f chore: update package-lock.json dependencies 2026-05-18 19:57:43 +02:00
RaymondVerhoef
08f5b1fe18 feat: implement 52-week annual curriculum system with admin management and automated topic progression 2026-05-18 19:49:05 +02:00
RaymondVerhoef
228d0d7a54 docs: update README and AI_AGENT.md with platform architecture, selective content generation, and deployment details
All checks were successful
On Push to Main / test (push) Successful in 28s
On Push to Main / publish (push) Successful in 54s
On Push to Main / deploy-dev (push) Successful in 1m27s
2026-05-17 20:35:12 +02:00
RaymondVerhoef
06eb974825 feat: add Anthropic API client and knowledge graph extraction pipeline
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m35s
2026-05-17 18:20:29 +02:00
RaymondVerhoef
5659ff6726 feat: implement learning module page with AI-generated content and feedback workflow
All checks were successful
On Push to Main / test (push) Successful in 29s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m34s
2026-05-17 17:53:09 +02:00
RaymondVerhoef
5b37c04588 feat: implement AI-driven learning content generation service and interactive student dashboard
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m32s
2026-05-17 17:10:40 +02:00
RaymondVerhoef
98e32d8ac0 feat: implement R42 chat infrastructure with Anthropic API integration and custom design system
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m31s
2026-05-17 16:48:40 +02:00
RaymondVerhoef
43d01dff58 docs: document global disablement of PocketBase auto-cancellation in AI_AGENT.md 2026-05-17 16:03:48 +02:00
RaymondVerhoef
b28956bbe7 feat: initialize PocketBase client with global auto-cancellation disabled and custom session expiry handling
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m33s
2026-05-17 15:27:34 +02:00
RaymondVerhoef
e2de7f0729 feat: add GitHub repository synchronization functionality with document extraction pipeline and update security policy to permit GitHub API access.
All checks were successful
On Push to Main / test (push) Successful in 29s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m32s
2026-05-17 15:15:51 +02:00
RaymondVerhoef
2374413282 feat: add anthropic-dangerous-direct-browser-access header to Anthropic proxy in Caddyfile
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m34s
2026-05-17 14:47:11 +02:00
RaymondVerhoef
77d411b77e feat: implement Anthropic API client with simulation support and add PocketBase configuration with session expiry handling
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m33s
2026-05-17 14:27:31 +02:00
RaymondVerhoef
775c3030d5 feat: implement PocketBase database schema setup script and core API service for content management
All checks were successful
On Push to Main / test (push) Successful in 29s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m33s
2026-05-17 14:18:26 +02:00
RaymondVerhoef
1bc51b3cb6 feat: initialize PocketBase backend with database structure, API helpers, and setup scripts 2026-05-17 14:16:00 +02:00
RaymondVerhoef
1ba647fecd feat: add roles documentation and database configuration files while removing deployment guide
All checks were successful
On Push to Main / test (push) Successful in 38s
On Push to Main / publish (push) Successful in 1m4s
On Push to Main / deploy-dev (push) Successful in 1m38s
2026-05-17 13:39:58 +02:00
RaymondVerhoef
271baba860 chore: add environment variable configuration for Anthropic API key
All checks were successful
On Push to Main / test (push) Successful in 40s
On Push to Main / publish (push) Successful in 1m6s
On Push to Main / deploy-dev (push) Successful in 1m41s
2026-05-17 10:39:40 +02:00
RaymondVerhoef
8b57e10754 feat: implement admin dashboard with management modules and PocketBase setup script 2026-05-17 10:29:06 +02:00
RaymondVerhoef
e68335e159 feat: implement automated Ansible deployment workflows and secure Anthropic API key management via reverse proxy injection
All checks were successful
On Push to Main / test (push) Successful in 41s
On Push to Main / publish (push) Successful in 1m8s
On Push to Main / deploy-dev (push) Successful in 1m39s
2026-05-17 10:00:14 +02:00
RaymondVerhoef
bb39a5fa71 feat: add script to initialize and configure PocketBase collections
All checks were successful
On Push to Main / test (push) Successful in 39s
On Push to Main / publish (push) Successful in 1m8s
On Push to Main / deploy-dev (push) Successful in 1m43s
2026-05-16 20:00:28 +02:00
RaymondVerhoef
267fa340a8 feat: initialize database connection module for data persistence
All checks were successful
On Push to Main / test (push) Successful in 39s
On Push to Main / publish (push) Successful in 1m6s
On Push to Main / deploy-dev (push) Successful in 1m40s
2026-05-16 19:44:30 +02:00
RaymondVerhoef
6e25cdbf25 feat: modularize deployment by adding environment-specific compose configurations and standardizing pocketbase data paths
All checks were successful
On Push to Main / test (push) Successful in 38s
On Push to Main / publish (push) Successful in 1m7s
On Push to Main / deploy-dev (push) Successful in 1m41s
2026-05-16 18:22:19 +02:00
RaymondVerhoef
4b7562749d feat: add docker compose configurations for development and production environments
All checks were successful
On Push to Main / test (push) Successful in 38s
On Push to Main / publish (push) Successful in 1m7s
On Push to Main / deploy-dev (push) Successful in 1m39s
2026-05-15 15:06:48 +02:00
RaymondVerhoef
7234074c21 chore: initialize PocketBase environment with project metadata, configuration, and data structure types
All checks were successful
On Push to Main / test (push) Successful in 42s
On Push to Main / publish (push) Successful in 1m7s
On Push to Main / deploy-dev (push) Successful in 1m40s
2026-05-15 14:55:34 +02:00
RaymondVerhoef
0a73ab3466 feat: implement content management dashboard and update proxy configurations to support AI-assisted learning material generation
All checks were successful
On Push to Main / test (push) Successful in 23s
On Push to Main / publish (push) Successful in 50s
On Push to Main / deploy-dev (push) Successful in 1m22s
2026-05-15 12:53:53 +02:00
RaymondVerhoef
aaafe413f6 feat: add docker-compose configuration for production learning platform and pocketbase services
All checks were successful
On Push to Main / test (push) Successful in 18s
On Push to Main / publish (push) Successful in 52s
On Push to Main / deploy-dev (push) Successful in 1m27s
2026-05-14 22:30:52 +02:00
RaymondVerhoef
e223836d7d feat: add Leren learning page for content generation and feedback, and create TestManager admin component
Some checks failed
On Push to Main / test (push) Has been cancelled
On Push to Main / publish (push) Has been cancelled
On Push to Main / deploy-dev (push) Has been cancelled
2026-05-14 22:21:39 +02:00
RaymondVerhoef
0673122536 feat: create TestManager component to manage and review topic-specific question banks 2026-05-14 22:15:25 +02:00
RaymondVerhoef
bb0aef92cf feat: add script to automate initialization of PocketBase collections 2026-05-14 22:08:23 +02:00
RaymondVerhoef
74ba5d3dc0 feat: implement knowledge testing system with leaderboard, quiz generation, and PocketBase integration 2026-05-14 16:53:10 +02:00
RaymondVerhoef
42d7209773 feat: add Leren page for AI-generated learning modules and update Caddyfile configuration for static routing
Some checks failed
On Push to Main / test (push) Has been cancelled
On Push to Main / publish (push) Has been cancelled
On Push to Main / deploy-dev (push) Has been cancelled
2026-05-14 16:33:07 +02:00
RaymondVerhoef
59e746a8a8 feat: add anthropic API proxy route to Caddyfile and revert to npm install in Dockerfile 2026-05-14 13:09:22 +02:00
RaymondVerhoef
e783c5f1e7 feat: implement Docker-based production deployment pipeline with automated Ansible provisioning, CI/CD workflows, and Caddy server configuration.
Some checks failed
On Push to Main / test (push) Has been cancelled
On Push to Main / publish (push) Has been cancelled
On Push to Main / deploy-dev (push) Has been cancelled
2026-05-14 10:21:42 +02:00
RaymondVerhoef
3a4f6d7c22 Merge pull request #1 from RaymondVerhoef/claude/read-codebase-iHovz
Add Docker Compose configuration for frontend service
2026-05-12 09:14:59 +02:00
622 changed files with 69873 additions and 2070 deletions

View File

@@ -0,0 +1,48 @@
# caveman
Talk like smart caveman. Same brain, fewer tokens.
## What it does
Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped.
Six intensity levels:
| Level | What change |
|-------|-------------|
| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. |
| `full` | Default. Drop articles, fragments OK, short synonyms. |
| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. |
| `wenyan-lite` | Classical Chinese register, light compression. |
| `wenyan-full` | Maximum 文言文. 80-90% character reduction. |
| `wenyan-ultra` | Extreme classical compression. |
Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part.
## How to invoke
```
/caveman # full mode (default)
/caveman lite # lighter compression
/caveman ultra # extreme compression
/caveman wenyan # classical Chinese
stop caveman # back to normal prose
```
## Example output
Question: "Why does my React component re-render?"
Normal prose:
> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue.
Caveman (full):
> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
Caveman (ultra):
> Inline obj prop → new ref → re-render. `useMemo`.
## See also
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
- [Caveman README](../../README.md) — repo overview, install, benchmarks

View File

@@ -0,0 +1,74 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
wenyan-lite, wenyan-full, wenyan-ultra.
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
## Persistence
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
Default: **full**. Switch: `/caveman lite|full|ultra`.
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
Pattern: `[thing] [action] [reason]. [next step].`
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
## Intensity
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
Example — "Why React component re-render?"
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "物出新參照致重繪。useMemo .Wrap之。"
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
Example — "Explain database connection pooling."
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
## Auto-Clarity
Drop caveman when:
- Security warnings
- Irreversible action confirmations
- Multi-step sequences where fragment order or omitted conjunctions risk misread
- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions)
- User asks to clarify or repeats question
Resume caveman after clear part done.
Example — destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
> ```sql
> DROP TABLE users;
> ```
> Caveman resume. Verify backup exist first.
## Boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.

35
.github/workflows/deploy-dev.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Deploy to Dev
on:
workflow_call:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Ansible and collections
run: |
pip install --break-system-packages ansible
ansible-galaxy collection install ansible.posix community.docker
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SRV_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
- name: Run Ansible playbook
run: |
ansible-playbook \
-i infra/development/hosts.ini \
-e "ansible_ssh_private_key_file=~/.ssh/deploy_key" \
-e "anthropic_api_key=${{ secrets.ANTHROPIC_API_KEY }}" \
-e "entra_tenant_id=${{ secrets.ENTRA_TENANT_ID }}" \
-e "entra_client_id=${{ secrets.ENTRA_CLIENT_ID }}" \
-e "entra_client_secret=${{ secrets.ENTRA_CLIENT_SECRET }}" \
-e "entra_admin_emails=${{ secrets.ENTRA_ADMIN_EMAILS }}" \
infra/development/site/deploy-playbook.yml

37
.github/workflows/deploy-prod.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Deploy to Production
on:
workflow_call:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
- name: Install Ansible and collections
run: |
pip install --break-system-packages ansible
ansible-galaxy collection install ansible.posix community.docker
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SRV_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
- name: Run Ansible playbook
run: |
ansible-playbook \
-i infra/production/hosts.ini \
-e "ansible_ssh_private_key_file=~/.ssh/deploy_key" \
-e "anthropic_api_key=${{ secrets.ANTHROPIC_API_KEY }}" \
-e "entra_tenant_id=${{ secrets.ENTRA_TENANT_ID }}" \
-e "entra_client_id=${{ secrets.ENTRA_CLIENT_ID }}" \
-e "entra_client_secret=${{ secrets.ENTRA_CLIENT_SECRET }}" \
-e "entra_admin_emails=${{ secrets.ENTRA_ADMIN_EMAILS }}" \
infra/production/site/deploy-playbook.yml

41
.github/workflows/on-create-tag.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: On Create Tag
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
publish:
uses: ./.github/workflows/publish-image.yml
with:
build_mode: production
secrets: inherit
update-compose:
needs: publish
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
token: ${{ github.token }}
- name: Update image tag in production compose.yml
run: |
VERSION=${GITHUB_REF_NAME#v}
sed -i "s|\(image:.*:\)[^[:space:]]*|\1${VERSION}|" infra/production/site/compose.yml
- name: Commit and push
run: |
git config user.name "respellion-bot"
git config user.email "no-reply.git@respellion.nl"
git add infra/production/site/compose.yml
git commit -m "chore: update prod image to ${GITHUB_REF_NAME#v}"
git push origin main
deploy-prod:
needs: update-compose
uses: ./.github/workflows/deploy-prod.yml
secrets: inherit

View File

@@ -0,0 +1,20 @@
name: On Pull Request to Main
on:
pull_request:
branches: [main]
workflow_call:
jobs:
test:
uses: ./.github/workflows/test.yml
publish:
needs: test
uses: ./.github/workflows/publish-image.yml
secrets: inherit
deploy-dev:
needs: publish
uses: ./.github/workflows/deploy-dev.yml
secrets: inherit

21
.github/workflows/on-push-main.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: On Push to Main
on:
push:
branches: [main]
jobs:
test:
uses: ./.github/workflows/test.yml
publish:
needs: test
uses: ./.github/workflows/publish-image.yml
with:
build_mode: development
secrets: inherit
deploy-dev:
needs: publish
uses: ./.github/workflows/deploy-dev.yml
secrets: inherit

56
.github/workflows/publish-image.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Publish Image
on:
workflow_call:
inputs:
build_mode:
description: "Build mode (development or production)"
type: string
default: production
workflow_dispatch:
inputs:
build_mode:
description: "Build mode (development or production)"
type: string
default: production
env:
REGISTRY: git.labs.respellion.tech
IMAGE: respellion/learning-platform/learning-platform
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: respellion-bot
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
tags: |
type=sha,format=short,prefix=
type=raw,value=latest
type=semver,pattern={{version}}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BUILD_MODE=${{ inputs.build_mode }}

64
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Test
on:
workflow_call:
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t learning-platform .
# The production Caddyfile is otherwise never exercised by CI — the test
# image below swaps in Caddyfile.test — so a parse error only surfaced at
# deploy time, after the broken container had already replaced the healthy
# one (issue #20/#26). Validate it here, inside the freshly built image
# where it lives at /etc/caddy/Caddyfile. This runs caddy against the
# exact file that ships and needs no bind mount — a `-v "$PWD":...` mount
# does not propagate to the sibling Docker daemon in the Gitea runner,
# which is why the earlier mount-based step failed with
# "open Caddyfile: no such file or directory" (#26).
# Caddyfile.test needs no separate check: the "Test homepage" step below
# runs the test image and fails if that config can't serve.
- name: Validate production Caddyfile
run: |
docker run --rm --entrypoint caddy learning-platform \
validate --adapter caddyfile --config /etc/caddy/Caddyfile
- name: Build test image
run: |
cat > Dockerfile.test <<'EOF'
FROM learning-platform
COPY Caddyfile.test /etc/caddy/Caddyfile
EOF
docker build -t learning-platform-test -f Dockerfile.test .
- name: Start container
run: |
docker run -d --network gitea --name test-container learning-platform-test
sleep 5
- name: Test homepage
run: |
status=$(curl -s -o /dev/null -w "%{http_code}" http://test-container)
if [ "$status" != "200" ]; then
echo "Homepage failed with status $status"
exit 1
fi
echo "✓ Homepage OK"
- name: Test security headers
run: |
headers=$(curl -sI http://test-container)
echo "$headers" | grep -q "X-Frame-Options" || (echo "Missing X-Frame-Options" && exit 1)
echo "$headers" | grep -q "X-Content-Type-Options" || (echo "Missing X-Content-Type-Options" && exit 1)
echo "✓ Security headers OK"
- name: Cleanup
if: always()
run: docker stop test-container && docker rm test-container

13
.gitignore vendored
View File

@@ -22,3 +22,16 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Secrets
.env
.env.*
# PocketBase
pb_data/
pocketbase.exe
pocketbase.zip
# Claude Code local agent state (settings.local.json holds per-user
# command allowlists; never share across teammates)
.claude/

View File

@@ -2,57 +2,153 @@
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-26 — Per-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**. It is completely self-contained and currently runs without a dedicated backend database.
* **Frontend:** React, React Router, Tailwind CSS.
* **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)
There is **no backend database**. All data is persisted locally in the browser using a custom wrapper around `window.localStorage` located at `src/lib/storage.js`.
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`.
**Namespacing Convention:**
You must strictly adhere to these prefixes when reading/writing to `storage.js`:
* `kb:*` - Knowledge base data (e.g., `kb:topics`, `kb:relations`, `kb:content:{topicId}`).
* `admin:*` - Admin settings (e.g., `admin:anthropic_key`, `admin:sources`, `admin:use_simulation`).
* `user:{id}:*` - Specific user progress (e.g., `user:{id}:week:{n}:learn_done`).
* `quiz:bank:{topicId}` - Cached banks of AI-generated multiple-choice questions.
* `quiz:result:{userId}:week:{n}` - The exact score and payload of a user's weekly test.
* `leaderboard:current` - The active points ledger. Array of objects: `{ userId, name, points, testsCompleted, perfectScores }`.
* `users:registry` - Array of registered users, including admins.
**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):
* `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:** 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`).
**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.
> 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.
**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.
**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 acts as a proxy to the Anthropic API (`claude-sonnet-4`).
* **Location:** `src/lib/api.js`.
* **Proxy:** In Docker, `/api/anthropic` is proxied via Nginx to bypass CORS restrictions. If the user reports CORS errors during local dev, ensure the Vite proxy in `vite.config.js` matches the Nginx spec.
* **JSON Enforcement:** Prompts *must* strictly enforce that Claude returns *only* raw JSON. Do not let the AI wrap the response in markdown blocks (`\`\`\``) unless you build a regex to strip them (which is currently implemented via `.match(/\{[\s\S]*\}/)`).
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`).
Respellion relies on a premium, modern aesthetic.
* **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. Gamification Rules
If you are extending the Gamification system (`src/pages/Leaderboard.jsx`):
* Tests grant **+2 points** per correct answer.
* A 100% score grants the **Perfectionist** badge.
* Completing 1 test grants **First Steps**, completing 5 grants **Veteran**.
* Do not reset points dynamically. Read from `leaderboard:current`, mutate, and save back immediately.
## 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` | Slides with bullets and speaker notes |
| `infographic` | `content.infographic` | Headline, tagline, stats, steps, quote |
## 6. Docker & Deployment
The app is fully containerized.
* **Build:** `docker build -t respellion-app:latest .`
* **Run:** `docker run -d -p 8080:80 --name respellion respellion-app:latest`
* **Nginx:** Ensure any new frontend routes are caught by the Nginx `try_files $uri $uri/ /index.html;` directive defined in `nginx.conf` to prevent 404s on page refresh.
`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.**
## 7. How to Add New Features
1. **Define State:** Decide where the data lives in `localStorage`.
2. **Build UI:** Create components using `Card`, `Button`, and `framer-motion` for smooth entry (`animate-in fade-in slide-in-from-bottom-4`).
3. **Wire Logic:** Connect to `src/store/AppContext.jsx` if it requires global user context, otherwise manage locally and persist to `storage.js`.
4. **Test:** Run the Docker container to ensure no build errors exist.
Article refinement uses targeted patch tools (`set_intro`, `set_section`, `add_section`, `remove_section`, `replace_takeaways`) so the model edits only what changed.
## 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) |
`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. 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. 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 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.
* `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. 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. 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. 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`.
* **PocketBase migrations & the `_migrations` ledger (issue #18).** The deployed Labs/prod databases were originally provisioned WITHOUT `--migrationsDir` (schema came from `scripts/setup-pb-collections.mjs`), so their ledger started empty. `pb_migrations/1000000000_baseline_ledger_sync.js` marks the historical files as applied on such databases — never remove it, never rename an applied migration file (the ledger is filename-based), and give new migrations a timestamp that sorts after the existing ones. A failed migration aborts `pocketbase serve` → container crash-loop → 502 on every `/api/*` call; the deploy playbooks gate on `/api/health` to catch this.
* **OIDC provider config lives in a hook, not a migration.** `pb_hooks/entra_oidc.pb.js` reconciles the Entra provider from `ENTRA_*` env on every bootstrap + cron tick. Don't move provider credentials back into a one-shot migration — an apply while the env is empty would permanently disable OAuth2.
* **JSVM hook scope.** PocketBase runs each hook callback as an isolated program: top-level functions in a `*.pb.js` file are NOT in scope inside callbacks. Shared helpers live in `pb_hooks/utils.js` and are pulled in per-callback via `require(`${__hooks}/utils.js`)`.
## 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.

1016
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

142
CLAUDE.md Normal file
View File

@@ -0,0 +1,142 @@
# CLAUDE.md
## What this is
The **Respellion Learning Platform** — an internal AI-powered learning app that
keeps employees current with the company's evolving knowledge base. Employees
follow a perpetual **26-week curriculum**, each working through weekly learning
sessions and tests. An AI assistant called **R42** is available on every screen.
Admins upload source documents that Claude extracts into a knowledge graph.
This is a **single-page React application** (Vite) backed by **PocketBase**.
There is **no separate backend** — all logic runs in the browser and talks
directly to PocketBase collections and (via a proxy) the Anthropic API.
---
## Tech stack
| Layer | Technology |
|---|---|
| Frontend | React 19 + Vite 8, React Router 7 |
| Styling | Vanilla CSS (CSS custom properties) + Tailwind v4 utilities mapped to those variables |
| Animation | Framer Motion |
| Icons | Lucide React |
| Graph viz | D3.js (admin knowledge graph only) |
| Backend / DB / auth | PocketBase (self-hosted, SQLite) |
| AI | Anthropic Claude via a reverse-proxy (Caddy in prod, Vite proxy in dev) |
| Retrieval (RAG) | Local TF-IDF over the knowledge graph — **no Qdrant, no embeddings API** |
| Validation | Zod on all AI tool output |
| Infra | Docker + Caddy; Ansible playbooks under `infra/` |
**Claude models** (`src/lib/llm.js`, by tier):
- `fast``claude-haiku-4-5-20251001` (R42 chat, quiz batches, flashcards)
- `standard``claude-sonnet-4-6` (extraction, article/slides/infographic, curriculum)
- `reasoning``claude-opus-4-7`
Admins can override any tier's model string from the Settings tab (persisted in
localStorage as `admin:model:{tier}`).
---
## Repository structure
```
repo/
├── CLAUDE.md ← you are here
├── AI_AGENT.md ← detailed architecture/patterns guide — read this
├── README.md ← quickstart
├── PROTECTED.md ← files you must not modify
├── docs/ ← spec files (describe the system as built)
├── src/ ← THE APPLICATION (React/Vite)
│ ├── pages/ ← route screens (Dashboard, Leren, Testen, Leaderboard, Login, Onboarding, Admin/)
│ ├── components/ ← ui primitives, admin panels, chat (R42), micro_learning
│ ├── lib/ ← services: llm, db, extractionPipeline, learningService,
│ │ microLearningService, testService, curriculumService, retrieval, pb
│ ├── hooks/ ← React hooks
│ └── store/ ← AppContext (global state)
├── pb_migrations/ ← PocketBase schema migrations (JS, applied by the PB binary)
├── scripts/ ← setup-pb-collections.mjs (local collection bootstrap)
├── public/ ← static assets, fonts
├── infra/ ← Ansible deploy playbooks (development / production)
├── Caddyfile, Dockerfile, docker-compose.yml ← deployment (frozen)
└── stylesheet.css ← authoritative visual reference (frozen)
```
> **Note on `/app`:** the top-level `app/` directory is **abandoned scaffolding**
> from the original Next.js + Qdrant design that was never shipped. It is not
> wired into `package.json`, Vite, or Docker. **Ignore it.** The real app is `/src`.
---
## Absolute constraints
1. Never modify any file listed in `PROTECTED.md`.
2. Never modify `stylesheet.css` — it is the authoritative visual reference. Use the
CSS variables in `src/index.css` and the Tailwind classes mapped to them.
3. Treat the deployment files as frozen unless explicitly asked: `Dockerfile`,
`Caddyfile`, `docker-compose.yml`, `infra/`, `.github/workflows/`.
4. Never delete files without explicit confirmation.
5. Never re-enable PocketBase auto-cancellation — `pb.autoCancellation(false)` in
`src/lib/pb.js` is deliberate (see AI_AGENT.md §2).
6. There is **no podcast** learning type. It was removed. Do not re-add it.
7. Ask before acting when scope is unclear.
---
## Code conventions
- All `src/lib/db.js` functions are `async` — always `await` them.
- All Claude tool output is validated through Zod (`src/lib/llmSchemas.js`) before use.
Never reach `/api/anthropic` directly — go through `callLLM` in `src/lib/llm.js`.
- No client-side API key. The Anthropic key is injected server-side by the proxy.
- Reuse the UI primitives in `src/components/ui/` (`Card`, `Button`, `Tag`, `Input`).
- Mobile-first — the employee app targets 375px width and scales up.
- No hardcoded hex colors; use the design-system CSS variables / Tailwind tokens.
---
## Running locally
```bash
npm install
./pocketbase.exe serve # or the muchobien/pocketbase Docker image
node scripts/setup-pb-collections.mjs # first run only — bootstraps collections
npm run dev # Vite dev server (proxies /api/anthropic)
```
Set `ANTHROPIC_API_KEY` in the environment so the Vite proxy can inject it
(`vite.config.js`). PocketBase migrations in `pb_migrations/` apply automatically
when the PB binary starts.
| Command | Purpose |
|---|---|
| `npm run dev` | Vite dev server |
| `npm run build` | Production build to `dist/` |
| `npm test` | Vitest unit tests |
| `npm run lint` | ESLint |
---
## Documentation map
- `AI_AGENT.md` — detailed patterns, gotchas, and subsystem guide. **Start here for any real work.**
- `docs/architecture.md` — system design and data flows
- `docs/data-model.md` — every PocketBase collection and field
- `docs/ingestion-spec.md` — upload → knowledge-graph extraction
- `docs/generation-spec.md` — learning content + micro-learning generation
- `docs/curriculum-spec.md` — 26-week per-user curriculum engine
- `docs/r42-spec.md` — the R42 chatbot
- `docs/auth-spec.md` — Azure (Entra ID) login: OIDC via PocketBase, provisioning, roles
- `docs/frontend-spec.md` — screens, routing, onboarding
- `docs/gamification-spec.md` — points, badges, leaderboard
- `micro-learning-spec.md` — micro-learning learner experience
---
## When you are uncertain
Check the spec files in `docs/` and `AI_AGENT.md` first. If the spec does not
cover it, read the actual code in `src/` — it is the source of truth. Flag gaps
explicitly rather than assuming. Do not treat the abandoned `/app` scaffolding or
`legacy`-style references in old comments as guidance.

67
Caddyfile Normal file
View File

@@ -0,0 +1,67 @@
:80 {
encode gzip zstd
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
Content-Security-Policy "default-src 'self'; connect-src 'self' https://api.github.com https://raw.githubusercontent.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
}
handle /api/anthropic/* {
uri strip_prefix /api/anthropic
reverse_proxy https://api.anthropic.com {
header_up Host api.anthropic.com
header_up x-api-key "{$ANTHROPIC_API_KEY}"
header_up anthropic-dangerous-direct-browser-access "true"
}
}
handle /api/* {
reverse_proxy pocketbase-learning:8090
}
handle /_/* {
reverse_proxy pocketbase-learning:8090
}
handle {
root * /srv
@static {
path *.css *.js *.png *.jpg *.jpeg *.gif *.webp *.svg *.woff *.woff2 *.ttf
}
header @static Cache-Control "public, max-age=31536000, immutable"
@html {
path *.html /
}
header @html Cache-Control "public, max-age=0, must-revalidate"
try_files {path} /index.html
file_server
}
handle_errors {
# Don't mask API errors with the SPA shell — return a proper
# JSON error so the frontend can display a meaningful message.
# NOTE: `respond` only accepts `body`/`close` subdirectives; the
# Content-Type must be set via a separate `header` directive. An
# invalid subdirective is a Caddyfile parse error and crash-loops
# the container at startup (issue #20).
@api path /api/*
header @api Content-Type application/json
respond @api `{"code":{err.status_code},"message":"Backend unavailable"}` {err.status_code}
rewrite * /index.html
file_server
}
log {
output stdout
format console
}
}

25
Caddyfile.test Normal file
View File

@@ -0,0 +1,25 @@
:80 {
root * /srv
file_server
encode gzip
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
try_files {path} /index.html
handle_errors {
rewrite * /index.html
file_server
}
log {
output stdout
format console
}
}

View File

@@ -1,133 +0,0 @@
# 🎯 Overdrachtsdocument Productie-deployment van **Respellion Learning Platform** met Portainer
> **Doel** Dit document geeft een *complete* handleiding zodat een ontwikkelaar of AI-agent de Respellion Learning Platform applicatie kan bouwen, configureren en in productie draaien op een Hetzner-VPS via Portainer.
---
## 1. Project-structuur
```
learning-platform/
├─ Dockerfile # Productie Dockerfile (Nginx + React/Vite)
├─ nginx.conf # Nginx configuratie voor Anthropic API proxy
├─ docker-compose.prod.yml # Productie-compose configuratie (Caddy SSL + Frontend)
└─ src/ # React frontend broncode
```
> **Opmerking**: Deze applicatie is een 100% frontend applicatie ("Single Page Application"). De interne state wordt opgeslagen in `localStorage`. Er is **geen externe backend of database (zoals Postgres)** nodig. De enige netwerk calls gaan via de interne Nginx proxy direct naar de Anthropic API.
---
## 2. Kernbestanden inhoud & rationale
### 2.1 `docker-compose.prod.yml`
```yaml
services:
frontend:
build: .
restart: always
expose:
- "80"
caddy:
image: caddy:2-alpine
restart: always
ports:
- "80:80"
- "443:443"
environment:
- DOMAIN_NAME=${DOMAIN_NAME}
entrypoint:
- /bin/sh
- -c
- |
# Build Caddyfile on container start
CLEAN_DOMAIN=$(printf '%s' "$DOMAIN_NAME" | tr -d ';')
cat > /etc/caddy/Caddyfile <<EOF
${CLEAN_DOMAIN} {
reverse_proxy /* frontend:80
}
EOF
# Run Caddy with the generated file
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
volumes:
- caddy_data:/data
- caddy_config:/config
depends_on:
- frontend
volumes:
caddy_data:
caddy_config:
```
**Belangrijkste punten**
| Item | Waarom |
|------|--------|
| **`frontend` (Nginx)** | Bevat de React applicatie en proxy-configuratie voor Anthropic. Exposeert intern poort 80. |
| **`caddy` (Reverse Proxy)** | Ontvangt alle externe requests op poort 80 en 443 en haalt volautomatisch een SSL Let's Encrypt certificaat op voor `DOMAIN_NAME`. Stuurt verkeer door naar `frontend:80`. |
| **Volumes `caddy_data/config`** | Slaan de SSL certificaten persistent op zodat Caddy niet bij elke herstart de rate-limits van Let's Encrypt overschrijdt. |
---
### 2.2 `Dockerfile` (Productie)
De applicatie wordt in 2 fasen ("multi-stage") gebouwd:
1. **Node.js Build:** `npm install` en `npm run build` worden uitgevoerd om de statische Vite applicatie te compileren naar de `dist/` map.
2. **Nginx Serve:** Een lightweight Nginx Alpine image wordt gestart. Het pakt de `dist/` bestanden en past `nginx.conf` toe om API-aanroepen (CORS blokkades) te omzeilen door als proxy te fungeren.
---
## 3. Omgevingsvariabelen (te definiëren in Portainer)
| Variabele | Voorbeeld | Toelichting |
|-----------|-----------|-------------|
| `DOMAIN_NAME` | `leren.mijndomein.nl` | De domeinnaam die Caddy zal beveiligen met HTTPS. Zorg dat je DNS A-record naar je VPS-IP wijst. |
*(De Anthropic API key wordt ingevuld via de instellingen in de applicatie (Settings menu) door een admin, en veilig opgeslagen in localStorage van de gebruiker. Hij hoeft dus niet in Portainer te staan!)*
---
## 4. Deploy-procedure (stap-voor-stap)
### Portainer stack aanmaken / bijwerken
| Stap | Actie | Details |
|------|-------|---------|
| **1** | Navigeer naar **Stacks → + Add stack** | Geef een herkenbare naam (bijv. `respellion-platform`). |
| **2** | **Repository URL** invullen | `https://github.com/<gebruiker>/learning-platform.git` |
| **3** | **Compose path** = `docker-compose.prod.yml` | Zorg dat dit exact overeenkomt met de bestandsnaam in de repo. |
| **4** | **Environment variables** toevoegen | Voeg `DOMAIN_NAME` toe. |
| **5** | **Deploy the stack** | Portainer start de applicatie en Caddy haalt direct het SSL certificaat op (max. 60 sec). |
| **6** | **Controle** | Surf naar `https://jouw-domein.nl`. Je zou de Respellion inlogscherm moeten zien. |
### Handmatige Force-Rebuild
Wanneer de repo is geüpdatet en je wil de nieuwe versie afdwingen via command-line:
```bash
docker compose -f docker-compose.prod.yml up -d --build --no-cache
```
---
## 5. Veelvoorkomende valkuilen & Fixes
| Symptom | Oorzaak | Oplossing |
|---------|---------|-----------|
| **SSL Error / ERR_CONNECTION_REFUSED** | DNS is nog niet actief of poort 443 staat dicht in je firewall (Hetzner). | Check Hetzner Cloud Console en open TCP poort 80 & 443 in de Inbound Firewall. |
| **Anthropic API Werkt Niet** | Foute key of Nginx proxy mist configuratie. | Open de app, ga naar Admin -> Settings en vul je Anthropic Key in. Controleer ook of `nginx.conf` correct wordt ingeladen in Nginx. |
| **Witte pagina na deploy** | Oude Docker image wordt gebruikt. | Doe een force-rebuild (`--build --no-cache`) in Portainer of via de console. |
---
## 6. Checklist voor een nieuwe implementatie
- [ ] Repository up-to-date en gepusht naar `main`.
- [ ] `docker-compose.prod.yml` is gedefinieerd in Portainer.
- [ ] Environment variable `DOMAIN_NAME` is correct ingesteld.
- [ ] DNS A-Record staat op de juiste server.
- [ ] Na eerste inlog: Open "Admin" -> "Settings" en voeg de Anthropic API Key toe, klik op Opslaan.

View File

@@ -1,28 +1,21 @@
# Stage 1: Build the React application
FROM node:22-alpine as build
FROM node:24-alpine AS builder
WORKDIR /app
# Copy package files and install dependencies
COPY package*.json ./
COPY package.json package-lock.json ./
RUN npm install
# Copy the rest of the application files
COPY . .
# Build the Vite project for production
RUN npm run build
ARG BUILD_MODE=development
RUN npm run build -- --mode $BUILD_MODE
# Stage 2: Serve the application with Nginx
FROM nginx:alpine
FROM caddy:2-alpine
# Copy custom Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /srv
COPY Caddyfile /etc/caddy/Caddyfile
# Copy the build output to Nginx's html directory
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80 443
# Expose port 80
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1

17
LICENSE.md Normal file
View File

@@ -0,0 +1,17 @@
The MIT License (MIT)
Copyright (c) 2022 - present, Gani Georgiev
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

30
PROTECTED.md Normal file
View File

@@ -0,0 +1,30 @@
# Protected files — do not modify
These files and directories must never be modified by Claude Code without an
explicit request. They belong to the deployment pipeline and the authoritative
visual reference, and are otherwise frozen.
## Deployment & CI/CD
.github/workflows/
Dockerfile
docker-compose.yml
Caddyfile
Caddyfile.test
## Infrastructure & provisioning
infra/ (Ansible playbooks: development / production)
## Stylesheet
stylesheet.css (repo root — authoritative visual reference, use as-is)
## Abandoned scaffolding (do not edit or rely on)
app/ (original Next.js + Qdrant design, never shipped — the real app is /src)
---
Notes:
- The active application lives entirely in `/src`. Editing the app means editing `/src`.
- PocketBase schema changes go through `pb_migrations/` (and should be mirrored in
`scripts/setup-pb-collections.mjs`) — those are NOT frozen.
- If a task genuinely requires touching a frozen file (e.g. a deliberate deployment
change), confirm with the human first.

View File

@@ -1,16 +1,91 @@
# React + Vite
# Respellion Learning Platform
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
An internal AI-powered learning platform that keeps Respellion employees up to date with the company's evolving knowledge base.
Currently, two official plugins are available:
## Features
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
- **Onboarding** — On first login each employee enrolls into the curriculum with one tap. Their 26-week cycle starts then and there.
- **Weekly Learning Station** — Each employee gets a topic for their current curriculum week, drawn from the active 26-week schedule (with a deterministic hash fallback when no curriculum is active). They engage via micro-learnings (concept explainer, scenario quiz, flashcard set) and can also generate long-form formats (Article, Slides, Infographic) on demand.
- **Weekly Test** — A 5-question AI-generated quiz for the week's topic. Results feed the leaderboard.
- **Leaderboard & Gamification** — Points for correct answers, badges for milestones (First Steps, Veteran, Perfectionist).
- **R42 Chatbot** — An always-available AI assistant (Claude) grounded in the knowledge graph via local TF-IDF retrieval. It can propose graph updates that admins approve or reject.
- **Admin Panel** — Manage the knowledge graph, upload source files, review/refine generated content, generate the curriculum, and manage the team.
## React Compiler
## Tech Stack
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
| Layer | Technology |
|---|---|
| Frontend | React 19 + Vite 8, React Router 7 |
| Styling | Vanilla CSS (custom properties) + Tailwind v4 utilities |
| Animations | Framer Motion |
| Icons | Lucide React |
| Graph viz | D3.js (admin knowledge graph only) |
| Backend / DB / auth | PocketBase (self-hosted, SQLite) |
| Retrieval | Local TF-IDF (no Qdrant, no embeddings API) |
| AI | Anthropic Claude (via Caddy reverse proxy) |
| Infra | Docker + Caddy; Ansible playbooks under `infra/` |
## Expanding the ESLint configuration
**Claude models (by tier):** `fast` = `claude-haiku-4-5-20251001`, `standard` = `claude-sonnet-4-6`, `reasoning` = `claude-opus-4-7`.
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
## Getting Started (local dev)
```bash
# 1. Install dependencies
npm install
# 2. Start PocketBase
./pocketbase.exe serve
# 3. Bootstrap collections (first run only)
node scripts/setup-pb-collections.mjs
# 4. Start the dev server
npm run dev
```
Set `ANTHROPIC_API_KEY` in your environment — the Vite dev server proxies `/api/anthropic` and injects the key server-side (see `vite.config.js`). PocketBase migrations in `pb_migrations/` apply automatically when the binary starts.
## Deployment (Docker)
```bash
docker compose up -d
```
The `Caddyfile` handles:
- SPA fallback routing
- `/api/anthropic/*` → Anthropic API (server-side API key injection)
- `/api/*` and `/_/*` → the PocketBase sidecar
## Key Files
| File | Purpose |
|---|---|
| `src/lib/llm.js` | Core Anthropic wrapper (`callLLM`): tiers, retry, schema validation, telemetry |
| `src/lib/db.js` | All PocketBase data access |
| `src/lib/extractionPipeline.js` | Uploaded file → knowledge-graph extraction |
| `src/lib/learningService.js` | On-demand content generation (article / slides / infographic) |
| `src/lib/microLearningService.js` | Micro-learning generation (3 types) |
| `src/lib/curriculumService.js` | 26-week per-user curriculum engine |
| `src/lib/retrieval.js` | TF-IDF retrieval for R42 |
| `src/store/AppContext.jsx` | Global state; derives the user's curriculum week from their start date |
| `AI_AGENT.md` | Detailed context guide for AI coding agents |
## Content Types
On-demand long-form content (the `content` collection), generated per type and merged into the cached object:
| Type | Key in DB | Description |
|---|---|---|
| Article | `content.article` | Long-form reading |
| Slides | `content.slides` | Slide deck with speaker notes |
| Infographic | `content.infographic` | Visual summary with stats and steps |
> The podcast type was removed. Do not re-add it.
Micro-learnings (the `micro_learnings` collection): `concept_explainer`, `scenario_quiz`, `flashcard_set`.
## Documentation
- **`CLAUDE.md`** — project overview and constraints for AI coding agents.
- **`AI_AGENT.md`** — full architectural guide (patterns, gotchas, decisions).
- **`docs/`** — subsystem specs (architecture, data model, ingestion, generation, curriculum, R42, frontend, gamification).

0
app/frontend/.gitkeep Normal file
View File

View File

@@ -0,0 +1,80 @@
{
"pages": {
"/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/page.js"
],
"/layout": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/css/app/layout.css",
"static/chunks/app/layout.js"
],
"/auth/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/auth/page.js"
],
"/admin/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/admin/page.js"
],
"/admin/layout": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/admin/layout.js"
],
"/admin/documents/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/admin/documents/page.js"
],
"/admin/knowledge/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/admin/knowledge/page.js"
],
"/admin/curriculum/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/admin/curriculum/page.js"
],
"/app/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/app/page.js"
],
"/app/layout": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/app/layout.js"
],
"/app/session/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/app/session/page.js"
],
"/app/library/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/app/library/page.js"
],
"/app/profile/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/app/profile/page.js"
],
"/app/library/[topicId]/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/app/library/[topicId]/page.js"
],
"/_not-found/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/_not-found/page.js"
]
}
}

View File

@@ -0,0 +1,19 @@
{
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [
"static/development/_buildManifest.js",
"static/development/_ssgManifest.js"
],
"rootMainFiles": [
"static/chunks/webpack.js",
"static/chunks/main-app.js"
],
"pages": {
"/_app": []
},
"ampFirstPages": []
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
{"type": "commonjs"}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,8 @@
{
"/_not-found/page": "app/_not-found/page.js",
"/auth/page": "app/auth/page.js",
"/admin/documents/page": "app/admin/documents/page.js",
"/app/session/page": "app/app/session/page.js",
"/admin/curriculum/page": "app/admin/curriculum/page.js",
"/app/library/page": "app/app/library/page.js"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"

View File

@@ -0,0 +1,21 @@
self.__BUILD_MANIFEST = {
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [],
"rootMainFiles": [
"static/chunks/webpack.js",
"static/chunks/main-app.js"
],
"pages": {
"/_app": []
},
"ampFirstPages": []
};
self.__BUILD_MANIFEST.lowPriorityFiles = [
"/static/" + process.env.__NEXT_BUILD_ID + "/_buildManifest.js",
,"/static/" + process.env.__NEXT_BUILD_ID + "/_ssgManifest.js",
];

View File

@@ -0,0 +1,6 @@
{
"version": 3,
"middleware": {},
"functions": {},
"sortedMiddleware": []
}

View File

@@ -0,0 +1 @@
self.__REACT_LOADABLE_MANIFEST="{}"

View File

@@ -0,0 +1 @@
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"

View File

@@ -0,0 +1 @@
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}"

View File

@@ -0,0 +1,5 @@
{
"node": {},
"edge": {},
"encryptionKey": "QnzKIJRR8GrjaHk7QhiM928LOJzfdW5Af6gmYU5xeZs="
}

View File

@@ -0,0 +1,75 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
exports.id = "vendor-chunks/@swc";
exports.ids = ["vendor-chunks/@swc"];
exports.modules = {
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js":
/*!**************************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js ***!
\**************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_base),\n/* harmony export */ _class_private_field_loose_base: () => (/* binding */ _class_private_field_loose_base)\n/* harmony export */ });\nfunction _class_private_field_loose_base(receiver, privateKey) {\n if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {\n throw new TypeError(\"attempted to use private field on non-instance\");\n }\n\n return receiver;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlLmpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQU87QUFDUDtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNnRCIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2NsYXNzX3ByaXZhdGVfZmllbGRfbG9vc2VfYmFzZS5qcz9kY2RiIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlKHJlY2VpdmVyLCBwcml2YXRlS2V5KSB7XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwocmVjZWl2ZXIsIHByaXZhdGVLZXkpKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXCJhdHRlbXB0ZWQgdG8gdXNlIHByaXZhdGUgZmllbGQgb24gbm9uLWluc3RhbmNlXCIpO1xuICAgIH1cblxuICAgIHJldHVybiByZWNlaXZlcjtcbn1cbmV4cG9ydCB7IF9jbGFzc19wcml2YXRlX2ZpZWxkX2xvb3NlX2Jhc2UgYXMgXyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js":
/*!*************************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js ***!
\*************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_key),\n/* harmony export */ _class_private_field_loose_key: () => (/* binding */ _class_private_field_loose_key)\n/* harmony export */ });\nvar id = 0;\n\nfunction _class_private_field_loose_key(name) {\n return \"__private_\" + id++ + \"_\" + name;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQTs7QUFFTztBQUNQO0FBQ0E7QUFDK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9sZWFybmluZy1wbGF0Zm9ybS1mcm9udGVuZC8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL19jbGFzc19wcml2YXRlX2ZpZWxkX2xvb3NlX2tleS5qcz9hZjBiIl0sInNvdXJjZXNDb250ZW50IjpbInZhciBpZCA9IDA7XG5cbmV4cG9ydCBmdW5jdGlvbiBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkobmFtZSkge1xuICAgIHJldHVybiBcIl9fcHJpdmF0ZV9cIiArIGlkKysgKyBcIl9cIiArIG5hbWU7XG59XG5leHBvcnQgeyBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkgYXMgXyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
/*!*******************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.js ***!
\*******************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_default),\n/* harmony export */ _interop_require_default: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQLDJDQUEyQztBQUMzQztBQUN5QyIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzPzY4ZDkiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js":
/*!********************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_wildcard.js ***!
\********************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_wildcard),\n/* harmony export */ _interop_require_wildcard: () => (/* binding */ _interop_require_wildcard)\n/* harmony export */ });\nfunction _getRequireWildcardCache(nodeInterop) {\n if (typeof WeakMap !== \"function\") return null;\n\n var cacheBabelInterop = new WeakMap();\n var cacheNodeInterop = new WeakMap();\n\n return (_getRequireWildcardCache = function(nodeInterop) {\n return nodeInterop ? cacheNodeInterop : cacheBabelInterop;\n })(nodeInterop);\n}\nfunction _interop_require_wildcard(obj, nodeInterop) {\n if (!nodeInterop && obj && obj.__esModule) return obj;\n if (obj === null || typeof obj !== \"object\" && typeof obj !== \"function\") return { default: obj };\n\n var cache = _getRequireWildcardCache(nodeInterop);\n\n if (cache && cache.has(obj)) return cache.get(obj);\n\n var newObj = { __proto__: null };\n var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;\n\n for (var key in obj) {\n if (key !== \"default\" && Object.prototype.hasOwnProperty.call(obj, key)) {\n var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;\n if (desc && (desc.get || desc.set)) Object.defineProperty(newObj, key, desc);\n else newObj[key] = obj[key];\n }\n }\n\n newObj.default = obj;\n\n if (cache) cache.set(obj, newObj);\n\n return newObj;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkLmpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxLQUFLO0FBQ0w7QUFDTztBQUNQO0FBQ0EsdUZBQXVGOztBQUV2Rjs7QUFFQTs7QUFFQSxtQkFBbUI7QUFDbkI7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7O0FBRUE7O0FBRUE7QUFDQTtBQUMwQyIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV93aWxkY2FyZC5qcz9hYjM4Il0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIF9nZXRSZXF1aXJlV2lsZGNhcmRDYWNoZShub2RlSW50ZXJvcCkge1xuICAgIGlmICh0eXBlb2YgV2Vha01hcCAhPT0gXCJmdW5jdGlvblwiKSByZXR1cm4gbnVsbDtcblxuICAgIHZhciBjYWNoZUJhYmVsSW50ZXJvcCA9IG5ldyBXZWFrTWFwKCk7XG4gICAgdmFyIGNhY2hlTm9kZUludGVyb3AgPSBuZXcgV2Vha01hcCgpO1xuXG4gICAgcmV0dXJuIChfZ2V0UmVxdWlyZVdpbGRjYXJkQ2FjaGUgPSBmdW5jdGlvbihub2RlSW50ZXJvcCkge1xuICAgICAgICByZXR1cm4gbm9kZUludGVyb3AgPyBjYWNoZU5vZGVJbnRlcm9wIDogY2FjaGVCYWJlbEludGVyb3A7XG4gICAgfSkobm9kZUludGVyb3ApO1xufVxuZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfd2lsZGNhcmQob2JqLCBub2RlSW50ZXJvcCkge1xuICAgIGlmICghbm9kZUludGVyb3AgJiYgb2JqICYmIG9iai5fX2VzTW9kdWxlKSByZXR1cm4gb2JqO1xuICAgIGlmIChvYmogPT09IG51bGwgfHwgdHlwZW9mIG9iaiAhPT0gXCJvYmplY3RcIiAmJiB0eXBlb2Ygb2JqICE9PSBcImZ1bmN0aW9uXCIpIHJldHVybiB7IGRlZmF1bHQ6IG9iaiB9O1xuXG4gICAgdmFyIGNhY2hlID0gX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlKG5vZGVJbnRlcm9wKTtcblxuICAgIGlmIChjYWNoZSAmJiBjYWNoZS5oYXMob2JqKSkgcmV0dXJuIGNhY2hlLmdldChvYmopO1xuXG4gICAgdmFyIG5ld09iaiA9IHsgX19wcm90b19fOiBudWxsIH07XG4gICAgdmFyIGhhc1Byb3BlcnR5RGVzY3JpcHRvciA9IE9iamVjdC5kZWZpbmVQcm9wZXJ0eSAmJiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xuXG4gICAgZm9yICh2YXIga2V5IGluIG9iaikge1xuICAgICAgICBpZiAoa2V5ICE9PSBcImRlZmF1bHRcIiAmJiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqLCBrZXkpKSB7XG4gICAgICAgICAgICB2YXIgZGVzYyA9IGhhc1Byb3BlcnR5RGVzY3JpcHRvciA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqLCBrZXkpIDogbnVsbDtcbiAgICAgICAgICAgIGlmIChkZXNjICYmIChkZXNjLmdldCB8fCBkZXNjLnNldCkpIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShuZXdPYmosIGtleSwgZGVzYyk7XG4gICAgICAgICAgICBlbHNlIG5ld09ialtrZXldID0gb2JqW2tleV07XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBuZXdPYmouZGVmYXVsdCA9IG9iajtcblxuICAgIGlmIChjYWNoZSkgY2FjaGUuc2V0KG9iaiwgbmV3T2JqKTtcblxuICAgIHJldHVybiBuZXdPYmo7XG59XG5leHBvcnQgeyBfaW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js":
/*!*************************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js ***!
\*************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _tagged_template_literal_loose),\n/* harmony export */ _tagged_template_literal_loose: () => (/* binding */ _tagged_template_literal_loose)\n/* harmony export */ });\nfunction _tagged_template_literal_loose(strings, raw) {\n if (!raw) raw = strings.slice(0);\n\n strings.raw = raw;\n\n return strings;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2UuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQOztBQUVBOztBQUVBO0FBQ0E7QUFDK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9sZWFybmluZy1wbGF0Zm9ybS1mcm9udGVuZC8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL190YWdnZWRfdGVtcGxhdGVfbGl0ZXJhbF9sb29zZS5qcz83YjYyIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBfdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2Uoc3RyaW5ncywgcmF3KSB7XG4gICAgaWYgKCFyYXcpIHJhdyA9IHN0cmluZ3Muc2xpY2UoMCk7XG5cbiAgICBzdHJpbmdzLnJhdyA9IHJhdztcblxuICAgIHJldHVybiBzdHJpbmdzO1xufVxuZXhwb3J0IHsgX3RhZ2dlZF90ZW1wbGF0ZV9saXRlcmFsX2xvb3NlIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js\n");
/***/ }),
/***/ "(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
/*!*******************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.js ***!
\*******************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_default),\n/* harmony export */ _interop_require_default: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHJzYykvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQLDJDQUEyQztBQUMzQztBQUN5QyIsInNvdXJjZXMiOlsid2VicGFjazovL2xlYXJuaW5nLXBsYXRmb3JtLWZyb250ZW5kLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzPzFhYjAiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
/***/ })
};
;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,215 @@
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({});
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ loaded: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ var threw = true;
/******/ try {
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ threw = false;
/******/ } finally {
/******/ if(threw) delete __webpack_module_cache__[moduleId];
/******/ }
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/create fake namespace object */
/******/ (() => {
/******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
/******/ var leafPrototypes;
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 16: return value when it's Promise-like
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = this(value);
/******/ if(mode & 8) return value;
/******/ if(typeof value === 'object' && value) {
/******/ if((mode & 4) && value.__esModule) return value;
/******/ if((mode & 16) && typeof value.then === 'function') return value;
/******/ }
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ var def = {};
/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
/******/ for(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {
/******/ Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
/******/ }
/******/ def['default'] = () => (value);
/******/ __webpack_require__.d(ns, def);
/******/ return ns;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/ensure chunk */
/******/ (() => {
/******/ __webpack_require__.f = {};
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = (chunkId) => {
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/ __webpack_require__.f[key](chunkId, promises);
/******/ return promises;
/******/ }, []));
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/get javascript chunk filename */
/******/ (() => {
/******/ // This function allow to reference async chunks and sibling chunks for the entrypoint
/******/ __webpack_require__.u = (chunkId) => {
/******/ // return url for filenames based on template
/******/ return "" + chunkId + ".js";
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("89440127ad6e3f87")
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/node module decorator */
/******/ (() => {
/******/ __webpack_require__.nmd = (module) => {
/******/ module.paths = [];
/******/ if (!module.children) module.children = [];
/******/ return module;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/startup entrypoint */
/******/ (() => {
/******/ __webpack_require__.X = (result, chunkIds, fn) => {
/******/ // arguments: chunkIds, moduleId are deprecated
/******/ var moduleId = chunkIds;
/******/ if(!fn) chunkIds = result, fn = () => (__webpack_require__(__webpack_require__.s = moduleId));
/******/ chunkIds.map(__webpack_require__.e, __webpack_require__)
/******/ var r = fn();
/******/ return r === undefined ? result : r;
/******/ }
/******/ })();
/******/
/******/ /* webpack/runtime/require chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded chunks
/******/ // "1" means "loaded", otherwise not loaded yet
/******/ var installedChunks = {
/******/ "webpack-runtime": 1
/******/ };
/******/
/******/ // no on chunks loaded
/******/
/******/ var installChunk = (chunk) => {
/******/ var moreModules = chunk.modules, chunkIds = chunk.ids, runtime = chunk.runtime;
/******/ for(var moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) runtime(__webpack_require__);
/******/ for(var i = 0; i < chunkIds.length; i++)
/******/ installedChunks[chunkIds[i]] = 1;
/******/
/******/ };
/******/
/******/ // require() chunk loading for javascript
/******/ __webpack_require__.f.require = (chunkId, promises) => {
/******/ // "1" is the signal for "already loaded"
/******/ if(!installedChunks[chunkId]) {
/******/ if("webpack-runtime" != chunkId) {
/******/ installChunk(require("./" + __webpack_require__.u(chunkId)));
/******/ } else installedChunks[chunkId] = 1;
/******/ }
/******/ };
/******/
/******/ module.exports = __webpack_require__;
/******/ __webpack_require__.C = installChunk;
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/ })();
/******/
/************************************************************************/
/******/
/******/
/******/ })()
;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More