Files
learning-platform/pb_migrations/1781200000_created_onboarding.js
RaymondVerhoef 5214c9db3b
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
feat: 5-day theme-level onboarding track from the "New here?" card (#30)
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

190 lines
5.0 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
//
// Issue #30 — Onboarding track (5-day, theme-level introduction).
//
// Two collections:
// onboarding_overviews — cached per-theme overview content (one row per theme),
// keyed by the theme name, with a topics_fingerprint for
// staleness detection / regeneration.
// onboarding_completions — per-user, per-theme completion marker.
//
// Design notes (consistent with issues #18/#27):
// * team_member_id is a plain TEXT field, NOT a relation — admins can delete
// team_members, and a relation with cascadeDelete would otherwise block that
// delete / drag the collection into the migration graph. Completions are keyed
// by (team_member_id, theme); orphan rows after a member delete are harmless.
// * API rules are set EXPLICITLY to authenticated-only. The tighten-rules
// migration (1781000001) already ran against the collections that existed then,
// so new collections must lock themselves down or they default to public.
const AUTHED = '@request.auth.id != ""';
migrate((app) => {
const overviews = new Collection({
"id": "pbc_onboarding_overviews0",
"name": "onboarding_overviews",
"type": "base",
"system": false,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text_id_oov",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"system": false,
"id": "text_theme_oov",
"name": "theme",
"type": "text",
"required": true,
"presentable": false,
"max": 0,
"min": 0,
"pattern": ""
},
{
"system": false,
"id": "json_content_oov",
"name": "content",
"type": "json",
"required": true,
"presentable": false
},
{
"system": false,
"id": "text_fingerprint_oov",
"name": "topics_fingerprint",
"type": "text",
"required": true,
"presentable": false,
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "autodate_created_oov",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": true,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate_updated_oov",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": true,
"type": "autodate"
}
],
"indexes": [
"CREATE UNIQUE INDEX `idx_onboarding_overviews_theme` ON `onboarding_overviews` (`theme`)"
],
"listRule": AUTHED,
"viewRule": AUTHED,
"createRule": AUTHED,
"updateRule": AUTHED,
"deleteRule": AUTHED
});
app.save(overviews);
const completions = new Collection({
"id": "pbc_onboarding_completions0",
"name": "onboarding_completions",
"type": "base",
"system": false,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text_id_ocp",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"system": false,
"id": "text_ocp_team_member",
"name": "team_member_id",
"type": "text",
"required": true,
"presentable": false,
"max": 0,
"min": 0,
"pattern": ""
},
{
"system": false,
"id": "text_ocp_theme",
"name": "theme",
"type": "text",
"required": true,
"presentable": false,
"max": 0,
"min": 0,
"pattern": ""
},
{
"hidden": false,
"id": "autodate_created_ocp",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": true,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate_updated_ocp",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": true,
"type": "autodate"
}
],
"indexes": [
"CREATE UNIQUE INDEX `idx_onboarding_completions_user_theme` ON `onboarding_completions` (`team_member_id`, `theme`)"
],
"listRule": AUTHED,
"viewRule": AUTHED,
"createRule": AUTHED,
"updateRule": AUTHED,
"deleteRule": AUTHED
});
app.save(completions);
}, (app) => {
const completions = app.findCollectionByNameOrId("onboarding_completions");
if (completions) {
app.delete(completions);
}
const overviews = app.findCollectionByNameOrId("onboarding_overviews");
if (overviews) {
app.delete(overviews);
}
})