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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
- 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.
- 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.
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>
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>
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>