Compare commits

..

69 Commits

Author SHA1 Message Date
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
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
163 changed files with 17675 additions and 1242 deletions

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

@@ -0,0 +1,31 @@
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 }}" \
infra/development/site/deploy-playbook.yml

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

@@ -0,0 +1,33 @@
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 }}" \
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 }}

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

@@ -0,0 +1,48 @@
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 .
- 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,146 @@
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-18 — Adds 52-week annual curriculum system (§12). Reflects selective content generation (3 types), ISO week alignment, GitHub sync folder change, and AI extraction token limits.
## 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.
This is a single-page React application built with **Vite**, backed by **PocketBase** as the database and auth layer.
* **Frontend:** React, React Router, Vanilla CSS (via CSS variables) + Tailwind utility classes mapped to those variables.
* **Backend:** PocketBase (self-hosted). All data is stored in PocketBase collections, not localStorage.
* **Animations:** Framer Motion (used extensively for page transitions, podium effects, and gamification feedback).
* **Icons:** Lucide React.
* **Visualizations:** D3.js (used strictly for the Admin Knowledge Graph).
## 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:**
* `topics` — Knowledge graph nodes (`id`, `label`, `type`, `description`).
* `relations` — Knowledge graph edges (`source`, `target`, `type`).
* `content` — AI-generated learning modules per topic (`topic_id`, `data`). The `data` field is a **merged JSON object** containing only the content types that have been generated for that topic (e.g. `{ article: {...}, slides: [...] }`). New types are shallow-merged into the existing object by `learningService.js`; nothing is ever overwritten.
* `quiz_banks` — Banks of AI-generated questions per topic (`topic_id`, `questions[]`).
* `quiz_results` — User test scores (`user_id`, `week_number`, `score`, `total`, `percentage`, etc.).
* `quiz_cache` — Cached weekly quiz for a user (`user_id`, `week_number`, `questions[]`, `meta`).
* `learn_progress` — Whether a user completed the weekly learning session (`user_id`, `week_number`, `done`).
* `leaderboard` — Points ledger (`user_id`, `name`, `points`, `tests_completed`).
* `team_members` — Registered users with PIN auth (`name`, `role`, `pin`).
* `sources` — Uploaded source documents and their extraction status (`name`, `status`, `error`).
* `curriculum` — Annual learning schedule (`year`, `week_number`, `topic_id`, `theme`, `quarter`, `is_review_week`, `sort_order`). One entry per week per year. Managed via the admin Curriculum tab.
* `settings` — Key/value store for app-wide settings (`key`, `value`).
**localStorage** is only used for **admin browser settings** (not user data):
* `respellion:admin:anthropic_key` — Anthropic API key.
* `respellion:admin:model` — Model override.
* `respellion:admin:use_simulation` — Simulation mode toggle.
* `kb:suggestions` — Pending/approved/rejected graph deltas proposed by R42. Always mutated via `kbStore` (see §8).
* `quiz:active:{userId}` — Boolean flag set while the user is mid-quiz. R42's FAB is hidden when this is true (quiz-integrity rule).
* `chat:thread:{userId}` — Persisted R42 conversation, capped at 50 messages.
**Session:** User login is PIN-based. The logged-in user's ID is stored in `sessionStorage` under `respellion_session` and resolved against `team_members` on app load (see `src/store/AppContext.jsx`).
**Week Number:** The current ISO-8601 week number is calculated dynamically on app load via `getWeekNumber(new Date())` in `src/store/AppContext.jsx`. It is **not** stored in the database. The `ADVANCE_WEEK` action still exists for admin use, but initial state always reflects the real calendar week.
**Curriculum Year:** The curriculum year is derived from `new Date().getFullYear()` via `getCurriculumYear()` in `src/lib/curriculumService.js`. It is not stored — always computed.
**Important:** All `db.js` functions are `async`. Always `await` them — omitting `await` will silently pass a Promise where data is expected.
**Auto-Cancellation:** The PocketBase JS SDK has auto-cancellation enabled by default. This causes concurrent identical requests (like `db.getTopics()` during React StrictMode renders or concurrent Promise.all) to abort with `ClientResponseError 0`. This feature is **globally disabled** in `src/lib/pb.js` via `pb.autoCancellation(false)` to prevent UI crashes during concurrent fetching.
## 3. The AI Integration (Anthropic)
The application acts as a proxy to the Anthropic API (`claude-sonnet-4`).
The application calls the Anthropic API via a proxy to avoid CORS issues.
* **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]*\}/)`).
* **Proxy:** In Docker, `/api/anthropic` is proxied via Caddy to the Anthropic API endpoint. In local dev, configure `vite.config.js` to proxy the same path. The API key is injected server-side by Caddy; there is **no client-side API key**.
* **Token limit:** `generateContent` uses `max_tokens: 8192`. Do not lower this — large knowledge-base files need the headroom.
* **JSON Enforcement:** Prompts *must* strictly enforce that Claude returns *only* raw JSON. Do not let the AI wrap the response in markdown blocks — a regex strip via `.match(/\{[\s\S]*\}/)` is already applied in all service files.
## 4. Design System & Aesthetics
Respellion relies on a premium, modern aesthetic.
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`).
## 5. Gamification Rules
## 5. Learning Content Types
`src/lib/learningService.js` supports **three** content types for selective generation:
| Type | Schema key | Description |
|---|---|---|
| `article` | `content.article` | Title, intro, sections, key takeaways |
| `slides` | `content.slides` | Array of slides with bullets and speaker notes |
| `infographic` | `content.infographic` | Headline, tagline, stats, steps, quote |
**There is no podcast type.** It was removed. Do not re-add it.
`generateLearningContent(topic, force, selectedType)` accepts one of the three types above, or `'all'` (admin regeneration). Cache-hit logic checks `content[selectedType]` directly. On generation, the new data is shallow-merged into the existing cached object so other types are preserved.
The `LearningContentViewer` tab bar reflects exactly these three modes. The empty-state for an un-generated tab shows a "Generate [Type]" button that calls `onGenerate(activeMode)` passed from the parent.
## 6. 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.
* Tests grant **+2 points** per correct answer (handled in `testService.js → saveTestResult`).
* A 100% score grants the **Perfectionist** badge (computed at render time in `Leaderboard.jsx`).
* Completing 1 test grants **First Steps**, completing 5 grants **Veteran**.
* Do not reset points dynamically. Read from `leaderboard:current`, mutate, and save back immediately.
* Points are accumulated in the `leaderboard` collection via `db.upsertLeaderboardEntry`.
## 6. Docker & Deployment
The app is fully containerized.
## 7. Docker & Deployment
The app is fully containerized. PocketBase runs as a sidecar service.
* **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.
* **Run:** `docker compose up -d` (see `docker-compose.yml`).
* **Caddy (reverse proxy):** Handles SPA fallback, injects the Anthropic API key via a `Authorization` header on `/api/anthropic/*` requests, and proxies `/pb/*` to the PocketBase service. Config lives in `Caddyfile`.
* **PocketBase URL:** Resolved from `VITE_PB_URL` env var, or falls back to `window.location.origin + '/pb'` (see `src/lib/pb.js`).
## 7. 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.
## 8. GitHub Knowledge-Base Sync
The Admin upload panel (`src/components/admin/UploadZone.jsx`) can sync markdown/text files directly from a GitHub repository.
* **Default folder:** `docs/knowledge-base/` of the `respellion/employee-handbook` repo. This is persisted in the `settings` collection under the key `github:url` so admins can change it from the UI.
* **Change detection:** Each file's SHA is stored as `github:sha:<filename>` in `settings`. Files whose SHA differs are marked *Updated*; new files are *New*. Up-to-date files are skipped.
* **Extraction pipeline (`src/lib/extractionPipeline.js`):** Calls `anthropicApi.generateContent` with a strict JSON-only system prompt. To prevent truncated responses on large files, the prompt limits extraction to **max 15 topics** and their most important relations. If the AI returns non-JSON, the file is marked `failed` in the `sources` collection.
* **Deduplication:** A file already in `sources` with `status: completed` will throw and not be re-processed. Delete the source record first to force a re-analysis.
* **Do not increase topic cap beyond 15** without also verifying the `max_tokens: 8192` budget is sufficient for the expected file size.
## 9. R42 Chatbot
The platform ships a global chatbot avatar called **R42**, rendered as the Respellion `{ r }` brand mark in three states (idle / typing / error).
* **Mark component:** `src/components/ui/Mark.jsx`. Pure SVG; renders the brand mark with `state`, `size`, `theme`, `showFrame`, `brace`, `letter` props. Requires the `BallPill` font (loaded via `@font-face` in `src/index.css`, served from `public/fonts/BallPill-light.otf`). Falls back to JetBrains Mono (already loaded).
* **Chat module:** `src/components/chat/`.
* `ChatLauncher.jsx` — global FAB; auto-hides when `quiz:active:{userId}` is set. Listens to the `respellion:quiz-state` window event for fast updates.
* `ChatWindow.jsx` — 380×480 chat panel; Esc closes; renders messages from `useChat`.
* `useChat.js` — owns the message list, persists to `chat:thread:{userId}`, calls `anthropicApi.chat()`. `buildKbContext` is async (PocketBase), so `send()` is fully async.
* `prompts.js` — system prompt, greeting, and the `propose_graph_delta` tool spec.
* `rag.js` — fetches `kb:topics` + `kb:relations` from PocketBase; lazy-loads `kb:content:{id}` only when a topic is mentioned. Returns `{ context, topics }` so `validateDelta` can reuse the fetched topics without a second round-trip.
* **Multi-turn API:** `anthropicApi.chat(systemPrompt, messages, { tools })` in `src/lib/api.js`. Returns the raw Anthropic response (`{ content: [...], stop_reason }`) so callers can read both text blocks and `tool_use` blocks. No API key header — Caddy proxy injects it server-side, matching the existing `generateContent` pattern.
* **Quiz-integrity rule:** `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on start and clears it on every non-quiz phase + unmount, plus dispatches a `respellion:quiz-state` event. Never bypass this — letting users ask R42 mid-quiz would break scoring.
* **Graph refinement:** when R42's `tool_use` block proposes a `propose_graph_delta`, `rag.js` validates (no duplicate ids, no orphan relations, caps 3 topics / 5 relations) and surfaces a confirmation chip inline.
* **Admin user clicks Ja**`kbStore.applyDelta` writes to PocketBase via `db.upsertTopic` / `db.addRelation` immediately.
* **Non-admin clicks Ja**`kbStore.appendSuggestion` queues an entry in `kb:suggestions` localStorage (status `pending`).
* **Admin approval UI:** `src/components/admin/SuggestionsQueue.jsx`, mounted at the top of the Knowledge Graph admin panel. Approve calls `kbStore.approveSuggestion(id)` which runs the same `applyDelta` merge (async, PocketBase) and flips status to `approved`. Reject flips to `rejected` for audit.
* **kbStore:** `src/lib/kbStore.js` is the single source of truth for KB mutations from the chatbot path. Topics/relations go to PocketBase; suggestions queue goes to localStorage. Dispatches `respellion:kb-updated` after any write so the D3 graph and the queue panel refresh without a reload.
## 10. How to Add New Features
1. **Define Schema:** Add a new PocketBase collection via the Admin UI or the init script (`scripts/setup-pb-collections.mjs`).
2. **Add DB Helpers:** Add async CRUD functions in `src/lib/db.js`.
3. **Build UI:** Create components using `Card`, `Button`, and `framer-motion` for smooth entry (`animate-in fade-in slide-in-from-bottom-4`).
4. **Wire Logic:** Connect to `src/store/AppContext.jsx` if it requires global user context, otherwise manage state locally.
5. **Test:** Run the Docker stack (`docker compose up`) to ensure no build errors exist.
## 11. Known Gotchas & Decisions
* **No podcast type.** The podcast learning type was deliberately removed. The three supported types are `article`, `slides`, and `infographic`.
* **Week number is computed, not stored.** Do not add a `admin:current_week` setting — the ISO week is always derived from `new Date()`.
* **Caddy, not Nginx.** Older docs/comments may reference Nginx. The reverse proxy is Caddy. Update any references you encounter.
* **AI token budget.** If you see `[Pipeline] AI returned non-JSON response` in the logs, the response was truncated. Increase the topic cap prompt constraint before raising `max_tokens`.
* **PocketBase auto-cancellation is OFF.** `pb.autoCancellation(false)` is set globally in `src/lib/pb.js`. Never re-enable it — it causes abort errors during concurrent fetches in React StrictMode.
## 12. Annual Curriculum System
The platform uses a **52-week annual curriculum** so every employee covers all knowledge-base topics in one calendar year.
* **Service:** `src/lib/curriculumService.js` — curriculum engine with topic lookup, progress tracking, and auto-generation.
* **DB functions:** `db.getCurriculum(year)`, `db.getCurriculumWeek(year, week)`, `db.setCurriculumWeek(...)`, `db.bulkSetCurriculum(year, weeks[])`.
* **Admin UI:** `src/components/admin/CurriculumManager.jsx` — accessed via the "Curriculum" tab in the admin panel. Admins can auto-generate a schedule from KB topics or manually assign topics per week.
* **Same topic for everyone:** All employees study the same topic each week — this is by design to enable team discussion and shared quizzes.
* **Quarterly structure:** Weeks 113 (Q1), 1426 (Q2), 2739 (Q3), 4052 (Q4). Review/recap weeks at 13, 26, 39, 52.
* **Fallback:** If no curriculum exists for the current year, `getAssignedTopic()` falls back to the legacy hash-based assignment for backward compatibility.
* **Progress tracking:** `getYearProgress(userId)` and `getQuarterProgress(userId, quarter)` compute completion from the `learn_progress` collection against the curriculum.
* **Auto-generate:** `autoGenerateCurriculum(year)` distributes all non-fact topics across 48 content weeks + 4 review weeks. If there are fewer topics than weeks, they cycle. If more, excess topics remain in the self-service library.
* **Do not remove the hash fallback** — it ensures the platform works even without a configured curriculum.
Good luck! You are building a platform that empowers continuous learning. Keep it fast, keep it beautiful, and keep the user engaged.

View File

@@ -0,0 +1,573 @@
# AI Pipeline Hardening — Implementation Plan
> **Audience:** an AI agent executing this plan against the Respellion Learning Platform.
> **Owner before this work:** Raymond Verhoef (rve@respellion.nl).
> **Source of truth for repo conventions:** [`AI_AGENT.md`](AI_AGENT.md). Read it before starting.
This plan upgrades the platform's interaction with the Anthropic API: how prompts are built, how responses are parsed, how the model is retried, and how outputs are validated. It is broken into six phases that can be implemented and shipped independently. Each phase ends with verifiable acceptance criteria.
> **Status (2026-05-20):** Phases 15 implemented and shipped. Phase 6 (eval harness) is intentionally **out of scope** for this initiative — the production pipeline is hardened to the level the platform needs, and a golden-set runner can be reopened later as a stand-alone task if regression risk grows. The repo no longer carries any TODO from this plan.
---
## 0. Operating principles
These rules govern every phase. Re-read them before you commit.
1. **PocketBase is the source of truth.** No persistent state in localStorage (see [`AI_AGENT.md`](AI_AGENT.md) §2). The Anthropic API is proxied via Caddy; **never** add `x-api-key` headers in frontend code.
2. **No behaviour regressions.** Existing UI flows (extraction, weekly learning, weekly quiz, R42 chat, handbook sync, analyze-graph) must keep working after every phase. Phases are additive.
3. **Schema-first.** Where the model produces structured output, define a JSON Schema (Zod) and validate every response. Reject (don't paper over) malformed output.
4. **Single LLM entry point.** After Phase 1 there is exactly one module that talks to `/api/anthropic/v1/messages`. All callers go through it.
5. **No silent truncation.** If `stop_reason === 'max_tokens'` on a structured-output call, throw — never persist a partial parse.
6. **Cache what is stable, vary what is dynamic.** Use prompt caching for system prompts and KB context. User messages are never cached.
7. **Comments and docs:** follow the repo's terse style. Don't add explanatory comments unless the *why* is non-obvious.
8. **Migrations:** when changing a PocketBase schema, add a migration in `pb_migrations/` following the existing timestamp prefix convention. Never edit shipped migrations.
9. **Stop and ask** if you encounter a decision the plan doesn't cover (e.g. a model deprecation, a missing collection, a failing test that looks pre-existing).
### Files you will touch (or create) across all phases
| Path | Purpose |
|---|---|
| `src/lib/llm.js` *(new, Phase 1)* | Single Anthropic client wrapper |
| `src/lib/llmSchemas.js` *(new, Phase 1)* | Zod schemas for every structured task |
| `src/lib/llmRetry.js` *(new, Phase 1)* | Retry + backoff + abort policy |
| `src/lib/random.js` *(new, Phase 4)* | FisherYates shuffle + RNG helpers |
| `src/lib/api.js` | Becomes a thin re-export from `llm.js` (back-compat) |
| `src/lib/extractionPipeline.js` | Migrated to `llm.js` + tool use + overlap chunking |
| `src/lib/learningService.js` | Migrated to `llm.js` + tool use + patch-refine |
| `src/lib/testService.js` | Migrated to `llm.js` + tool use + dedup + shuffle fix |
| `src/components/admin/KnowledgeGraph.jsx` | `analyzeGraph` → tool use + dry-run preview |
| `src/components/chat/rag.js` | Retrieval (TF-IDF) + `lookup_topic` tool |
| `src/components/chat/prompts.js` | Split system prompt into cacheable + dynamic |
| `src/components/chat/useChat.js` | Wire retrieval + truncation |
| `pb_migrations/*` | Schema additions for `llm_calls`, question `difficulty`, topic `relevance_locked` |
| `evals/` *(new, Phase 6)* | Golden-set eval harness |
---
## Phase 1 — Foundation (single LLM client + robust parsing)
**Goal:** every LLM call goes through one module that handles retry, timeout, abort, JSON extraction, and schema validation. No behaviour change visible to the user.
### 1.1 Create `src/lib/llmRetry.js`
Implements the retry policy used by `llm.js`.
**Behaviour:**
- Exponential backoff with full jitter, base 1000ms, cap 16000ms.
- Retries only on these HTTP statuses: `408, 425, 429, 500, 502, 503, 504, 529`.
- Honours `Retry-After` header (seconds or HTTP date). If present and ≤ 60s, use it; if > 60s, fail fast.
- Default `maxRetries = 4`.
- Does **not** retry on `AbortError`.
**Exported interface:**
```js
// withRetry: (fn: (attempt:number) => Promise<T>, opts?) => Promise<T>
// RetryableError(status, retryAfterMs)
export async function withRetry(fn, { maxRetries = 4, signal } = {}) { ... }
export class RetryableError extends Error { constructor(status, retryAfterMs) { ... } }
```
### 1.2 Create `src/lib/llmSchemas.js`
One Zod schema per structured task. Install Zod (`npm i zod`).
Required schemas (names + shape match what callers already produce — do not change field names):
- `extractionResultSchema``{ topics: Topic[], relations: Relation[] }` matching the existing `SYSTEM_PROMPT` in [`extractionPipeline.js`](src/lib/extractionPipeline.js).
- `handbookResultSchema` — same shape, but `relation.type` enum unified to `related_to | depends_on | part_of | executed_by` (see Phase 3 task 3.5 — for now the schema accepts both `executes` and `executed_by`, normalize `executes → executed_by` post-validation).
- `learningArticleSchema`, `learningSlidesSchema`, `learningInfographicSchema`, `learningAllSchema` matching [`learningService.js`](src/lib/learningService.js).
- `quizQuestionsSchema``{ questions: Question[] }` with `options.length === 4` and `correctIndex ∈ [0,3]`.
- `customTopicSchema``{ label, type: 'concept'|'role'|'process', description }`.
- `graphActionsSchema``{ merges, deletions, newRelations, relevanceUpdates }` matching [`KnowledgeGraph.jsx:329`](src/components/admin/KnowledgeGraph.jsx).
- `proposeGraphDeltaSchema` — matches `PROPOSE_GRAPH_DELTA_TOOL.input_schema` in [`prompts.js`](src/components/chat/prompts.js).
**Acceptance:** every schema has at least one happy-path Vitest test in `src/lib/__tests__/llmSchemas.test.js` (add `vitest` if not present).
### 1.3 Create `src/lib/llm.js`
The single Anthropic client. All other modules must call only this one.
**Public interface:**
```js
// Task tier — used to pick a model from settings.
// 'fast' → admin:model:fast (default: claude-haiku-4-5-20251001)
// 'standard' → admin:model:standard (default: claude-sonnet-4-6)
// 'reasoning' → admin:model:reasoning (default: claude-opus-4-7)
export async function callLLM({
task, // string, e.g. 'extract.source' — used for logging only
tier = 'standard',
system, // string OR Array<{ type:'text', text:string, cache_control?:{type:'ephemeral'} }>
messages, // [{ role, content }] OR omitted (use `user`)
user, // shorthand for [{role:'user', content: user}]
tools, // optional Anthropic tool definitions
toolChoice, // optional, e.g. { type:'tool', name:'emit_knowledge_graph' }
schema, // optional Zod schema for text→JSON path (used only when no tool)
maxTokens = 4096,
temperature = 0,
signal, // AbortSignal
}): Promise<{
text: string,
toolUses: Array<{ name, input }>,
stopReason: string,
usage: { input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens },
requestId: string | null,
model: string,
durationMs: number,
}>
```
**Key requirements:**
1. **Simulation mode** preserved: if `storage.get('admin:use_simulation') === true`, return a deterministic stub (use existing `simulateResponse` payload for backward compatibility — branch on `task` prefix to return a matching stub).
2. **Fetch** with `AbortController`; default **60-second timeout** if caller didn't pass a signal.
3. **Retry** through `withRetry` (Phase 1.1).
4. **Auth-portal detection** preserved: if response is not `application/json`, throw `Your session has expired. Please refresh the page and log in again.` exactly as today.
5. **No truncation acceptance**: if `stop_reason === 'max_tokens'` AND caller passed `schema` OR `toolChoice` requested a tool, throw `LLMTruncatedError`.
6. **Robust JSON extraction** when caller passed `schema` (and no tool was used): use `parseStructuredText(text)` that
- strips ```` ```json ```` and ```` ``` ```` fences,
- finds the outermost balanced JSON value (object **or** array) via a tiny brace-matching scan, not regex,
- throws `LLMOutputError` if no balanced JSON found,
- runs Zod `schema.parse` on the result.
7. **Tool path:** when `tools` is provided and the model emits `tool_use`, return them under `toolUses`. Validate each tool's input against the corresponding Zod schema if the caller wired one in (via `toolSchemas: { [toolName]: ZodSchema }`).
8. **Logging:** after every call, append a row to a new PocketBase collection `llm_calls` (best-effort — never block on this; catch and console.debug failures). Fields: `task, model, tier, duration_ms, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, stop_reason, ok, error_msg`. See Phase 5 task 5.6 for the migration.
9. **Custom errors**: `LLMHttpError`, `LLMTruncatedError`, `LLMOutputError`, `LLMValidationError`. All extend `Error` and set `name` for `instanceof` checks.
### 1.4 Make `src/lib/api.js` a thin shim
Replace the existing `anthropicApi.generateContent` and `anthropicApi.chat` implementations with calls into `llm.js`. Preserve the exact exported names and return shapes so no caller breaks.
```js
// api.js after Phase 1
export const anthropicApi = {
async generateContent(systemPrompt, userMessage, maxRetries = 1) {
const { text } = await callLLM({
task: 'legacy.generateContent',
tier: 'standard',
system: systemPrompt,
user: userMessage,
maxTokens: 8192,
temperature: 0,
});
return text;
},
async chat(systemPrompt, messages, opts = {}) {
const r = await callLLM({
task: 'legacy.chat',
tier: 'standard',
system: systemPrompt,
messages,
tools: opts.tools,
maxTokens: 1024,
temperature: 0.3, // chat default — see Phase 5
});
return {
content: [
...(r.text ? [{ type:'text', text: r.text }] : []),
...r.toolUses.map(tu => ({ type:'tool_use', name: tu.name, input: tu.input })),
],
stop_reason: r.stopReason,
};
},
};
```
### 1.5 Update default model + tiered settings
- Replace `DEFAULT_MODEL = 'claude-sonnet-4-20250514'` with the three tier defaults above.
- In **Admin → Settings**, add three model selects (`fast`, `standard`, `reasoning`). Read existing `admin:model` as a legacy fallback for `standard` (so existing users don't lose their override).
### Phase 1 acceptance criteria
- [ ] `npm run lint` passes; `npm run test` passes (Vitest).
- [ ] Every existing user flow (extraction, weekly content, weekly quiz, R42, handbook sync, analyze graph) still works against the live API.
- [ ] `grep -r "fetch.*anthropic" src/` returns only `src/lib/llm.js`.
- [ ] Simulation mode toggle still returns stubbed responses for all flows.
- [ ] Manually verify: kill the network mid-call → request aborts within 60s and surfaces a clear error message.
- [ ] Manually verify: rate-limit the proxy (429 + `Retry-After: 5`) → call retries once after ~5s and then succeeds.
---
## Phase 2 — Prompt caching & tool-based structured outputs
**Goal:** structured-output tasks no longer parse JSON out of prose. Large stable prompts are cached.
### 2.1 Migrate extraction to tool use
In [`extractionPipeline.js`](src/lib/extractionPipeline.js):
- Replace the "Return JSON only" instruction with a tool: `emit_knowledge_graph` whose `input_schema` mirrors `extractionResultSchema`.
- Replace `anthropicApi.generateContent(...)` with `callLLM({ ..., tools:[emitKnowledgeGraphTool], toolChoice:{ type:'tool', name:'emit_knowledge_graph' } })`.
- Read the validated object from `toolUses[0].input`.
- Same migration for `analyzeHandbookDelta` (tool `emit_handbook_delta`).
- Delete every `responseText.match(/\{[\s\S]*\}/)` site.
### 2.2 Migrate learning, quiz, custom-topic, graph-actions to tool use
Same pattern, in:
- [`learningService.js`](src/lib/learningService.js): tools `emit_learning_article`, `emit_learning_slides`, `emit_learning_infographic`, `emit_learning_all`, `emit_custom_topic`.
- [`testService.js`](src/lib/testService.js): tool `emit_quiz_questions`.
- [`KnowledgeGraph.jsx:297`](src/components/admin/KnowledgeGraph.jsx): tool `emit_graph_actions`.
### 2.3 Prompt caching
Pass `system` as an array of blocks so the stable parts can be cached:
```js
system: [
{ type:'text', text: STABLE_SYSTEM_HEADER, cache_control: { type:'ephemeral' } },
{ type:'text', text: dynamicPart }, // not cached
],
```
Apply caching to:
- Extraction `SYSTEM_PROMPT` and `HANDBOOK_SYSTEM_PROMPT` (both fully stable → cache the whole block).
- R42 system prompt — split into three blocks: stable preamble (cached), KB context (cached *only* while the graph hasn't changed; bust by appending a short hash of the topic IDs+labels — Phase 5 details), and per-turn role line (not cached).
### 2.4 Patch-based learning refinement
Refactor `refineLearningContent` ([`learningService.js:147`](src/lib/learningService.js:147)) from "return the full updated JSON" to **patch operations** via tools:
- `set_section(heading: string, body: string)` — replace one section by heading match.
- `add_section(heading: string, body: string, position: 'start'|'end')`.
- `remove_section(heading: string)`.
- `replace_takeaways(items: string[])`.
- `set_intro(intro: string)`.
Apply patches client-side to the cached object. Re-validate against `learningArticleSchema` after patching; reject the whole turn if invalid.
### Phase 2 acceptance criteria
- [ ] No regex JSON extraction left in `src/`: `grep -rn "match(/\\\\{\\[\\\\s\\\\S\\]\\*\\\\}/)" src/` returns nothing.
- [ ] Token usage telemetry shows `cache_read_input_tokens > 0` on the second extraction call within 5 minutes (cache hit).
- [ ] Re-running extraction on a known source produces the same topic count ±10% as before this phase.
- [ ] `refineLearningContent` round-trip ("make the intro shorter") produces only the changed section in the diff against the prior cached content.
---
## Phase 3 — Extraction quality
**Goal:** fewer near-duplicate topics, no silent truncation, adaptive throttling, unified vocabulary.
### 3.1 Sentence-aware chunking with overlap
Replace `chunkText` in [`extractionPipeline.js:87`](src/lib/extractionPipeline.js:87):
- Target **~2000 input tokens per chunk**. Approximate as `chars / 4`. Configurable via `MAX_CHUNK_CHARS = 8000`.
- **200-token overlap** between chunks (`OVERLAP_CHARS = 800`).
- Split on sentence boundaries (`/(?<=[.!?])\s+/`) first; fall back to paragraph boundary if a sentence is too long; never produce a chunk larger than `MAX_CHUNK_CHARS`.
- Add a guard: if a single sentence exceeds `MAX_CHUNK_CHARS`, hard-split at character boundary and log a warning.
### 3.2 Stateful extraction
Before each chunk after the first, prepend to the user message:
```text
Already-extracted topic IDs (do NOT create new IDs for these — reuse them if the same concept appears here):
- software-engineer
- onboarding-buddy
...
```
Cap the list at 200 IDs by recency to keep token cost bounded. The model will then reuse IDs instead of inventing variants like `software-developer`.
### 3.3 Adaptive throttling
Replace the hard `setTimeout(r, 12000)` in [`extractionPipeline.js:127`](src/lib/extractionPipeline.js:127) and the 15s sleep in [`KnowledgeGraph.jsx:274`](src/components/admin/KnowledgeGraph.jsx:274) with a shared token-bucket limiter in `src/lib/llmRetry.js`:
```js
export const extractionLimiter = createLimiter({ rps: 5/60, burst: 1 }); // 5 req/min
// usage: await extractionLimiter.acquire();
```
`callLLM` accepts an optional `limiter` param that it `await`s before fetch. On 429 with `Retry-After`, the limiter is paused for that duration.
### 3.4 Preserve admin-edited relevance
Add a migration introducing `relevance_locked: bool` on `topics`. Set it to `true` whenever an admin edits `learning_relevance` via the UI ([`KnowledgeGraph.jsx`](src/components/admin/KnowledgeGraph.jsx) edit handler — locate by searching for `setLearningRelevance` or the relevance form field).
In `mergeKnowledgeGraph` ([`extractionPipeline.js:167`](src/lib/extractionPipeline.js:167)), when `relevance_locked`, never overwrite `learning_relevance`.
### 3.5 Unify relation vocabulary
Pick **one** set: `related_to | depends_on | part_of | executed_by`. Migrate:
- `HANDBOOK_SYSTEM_PROMPT` ([`extractionPipeline.js:42`](src/lib/extractionPipeline.js:42)) — change `executes` to `executed_by` and swap the source/target in the prompt example.
- Write a one-shot migration script `pb_migrations/<timestamp>_normalize_relation_types.js` that rewrites any existing `executes` relation to `executed_by` and swaps `source ↔ target`.
- Verify R42's `validateDelta` ([`rag.js:108`](src/components/chat/rag.js:108)) already enforces this set (it does) — no change needed there.
### 3.6 Cancellation
Add a "Cancel" button to the source-processing UI in `ContentManager.jsx` / `UploadZone.jsx` (locate the one that displays extraction progress). Wire it to abort the in-flight `callLLM` via the `signal` it receives. On cancel, set source status to `cancelled` (add to status enum migration).
### Phase 3 acceptance criteria
- [ ] Running extraction twice on the same `sources/ROLES.md` produces zero new topics on the second run (idempotency through reused IDs).
- [ ] Locked-relevance topics survive re-extraction.
- [ ] No fixed `setTimeout` ≥ 5s anywhere in `src/` (`grep -rn "setTimeout" src/`).
- [ ] Cancelling an extraction mid-run leaves the source in `cancelled` state, not `processing`.
- [ ] `pb_migrations` includes the relation-vocabulary normalization and the `relevance_locked` column.
---
## Phase 4 — Quiz & content quality
**Goal:** quiz questions are positionally unbiased, deduped, and difficulty-tagged. Random helpers are correct.
### 4.1 Random helpers
Create `src/lib/random.js`:
```js
export function shuffle(arr) { /* FisherYates, returns NEW array */ }
export function sample(arr, n) { /* unbiased sample without replacement */ }
export function pickInt(min, maxInclusive) { /* uniform integer */ }
```
Replace every `.sort(() => 0.5 - Math.random())` with `shuffle(arr)`:
- [`testService.js:122`](src/lib/testService.js:122) and [`testService.js:163`](src/lib/testService.js:163).
- Any other site found by `grep -rn "0.5 - Math.random()" src/`.
### 4.2 Debias `correctIndex` in quiz prompt
In [`testService.js:81`](src/lib/testService.js:81):
- Change the example in the prompt to use `"correctIndex": 2` (not 0).
- Add to the prompt: *"Distribute correctIndex roughly evenly across 0, 1, 2, and 3. Do not place the correct answer at the same position more than 4 out of 10 times."*
- After parsing, run a check: if more than 50% of the batch share the same `correctIndex`, log a warning and re-roll up to 2 times.
### 4.3 Difficulty field
- Add to `quizQuestionsSchema`: `difficulty: 'easy'|'medium'|'hard'`.
- Update the prompt to require difficulty on every question (current prompt says "4 easy, 4 medium, 2 hard" but never tagged — now tag it).
- Migration: add `difficulty` to the `quiz_banks.questions[]` element. PocketBase stores `questions` as JSON, so the migration is a no-op at the column level; older records get `difficulty: 'medium'` on read (add a normalizer in `db.js`).
### 4.4 Question dedup
In `forceGenerateTopicQuestions` ([`testService.js:65`](src/lib/testService.js:65)):
- Normalize question text (lowercase, strip punctuation, collapse whitespace) → `normKey`.
- Before persisting, drop any new question whose `normKey` matches an existing bank question.
- Log dropped duplicates with `console.debug('[quiz] dropped duplicate:', text)`.
### 4.5 Quality gate
In the same function, after schema validation:
- Reject the whole batch if any question has fewer than 4 distinct options.
- Reject if any option contains `"all of the above"`, `"none of the above"`, `"both A and B"` (case-insensitive).
- Reject if `explanation.trim().length < 20`.
- Surface the rejection to the admin UI with a "Retry" button.
### 4.6 Custom topic ID hygiene
In `generateCustomTopic` ([`learningService.js:177`](src/lib/learningService.js:177)):
- Generate kebab-case ID from the polished label, not `Date.now()`.
- Collision check against existing topics (append `-2`, `-3`, … if needed).
- Default `learning_relevance: 'standard'` when the model omits it.
### Phase 4 acceptance criteria
- [ ] No `.sort(() => 0.5 - Math.random())` anywhere in `src/`.
- [ ] Sample of 50 fresh quiz questions across 5 topics: no position holds >40% of correct answers.
- [ ] Re-running quiz generation for the same topic does not grow the bank with semantic duplicates.
- [ ] Custom topics created via R42 use kebab-case IDs and pass schema validation.
---
## Phase 5 — R42 retrieval & telemetry
**Goal:** R42 stops shipping the entire KG every turn; conversations are bounded; every call is logged.
### 5.1 TF-IDF retrieval in the browser
Create `src/lib/retrieval.js`:
```js
export function buildIndex(topics) { /* TF-IDF over label + description */ }
export function retrieveTopK(index, query, k = 10) { /* returns Topic[] */ }
```
Implementation: a small dependency-free TF-IDF — tokenize on `/[a-zA-Z0-9-]+/`, lowercase, drop stopwords (Dutch + English short list). Cache the index on the `topics` array reference. About 100 lines.
### 5.2 Rewrite `buildKbContext`
In [`rag.js:11`](src/components/chat/rag.js:11):
- Use `retrieveTopK(index, userMessage, 10)` to pick which topics go into the system prompt.
- Always include any topic whose ID or label is mentioned verbatim (existing behaviour).
- Drop the full "every topic" dump.
- Return `{ context, retrievedTopics, allTopics }``validateDelta` continues to use `allTopics`.
### 5.3 `lookup_topic` tool
Add a second R42 tool in [`prompts.js`](src/components/chat/prompts.js):
```js
export const LOOKUP_TOPIC_TOOL = {
name: 'lookup_topic',
description: 'Fetch the full description and any deeper learning content for a topic. Use when the retrieved context does not contain enough to answer.',
input_schema: { type:'object', properties: { id:{type:'string'} }, required:['id'] },
};
```
In `useChat.js`, when the model emits a `lookup_topic`, fetch via `db.getTopics()` + `db.getContent(id)` and append a `tool_result` block, then call `callLLM` again with the extended messages. Cap to **3 lookup hops per turn** to avoid loops.
### 5.4 R42 prompt cache busting
KB-context block is cached as ephemeral. The cache key is automatic per text, so any change busts it. **Append a stable suffix** to the KB block: `"\n[kb_hash: <first-8-chars-of-sha256-of-sorted-topic-ids>]"`. This guarantees the block content changes the moment a topic is added/removed, even if the included topic list looks similar.
### 5.5 Conversation truncation
In `useChat.js`:
- Keep last **12 turns** verbatim in `apiMessages`.
- Older messages: if more than 12 turns exist, summarize the older ones with a cheap (`tier: 'fast'`) call and prepend a single `{ role:'system'-equivalent inside the user history is not allowed by Anthropic — instead prepend a user/assistant pair }` block, OR simply drop older turns. **Default: drop older turns and prepend a one-line `assistant` message saying "(earlier conversation truncated)".** Summarization is an optional follow-up.
### 5.6 `llm_calls` collection
Add a migration `pb_migrations/<timestamp>_created_llm_calls.js` for collection `llm_calls`:
| Field | Type | Notes |
|---|---|---|
| `task` | text | e.g. `extract.source` |
| `model` | text | resolved model ID |
| `tier` | text | `fast` / `standard` / `reasoning` |
| `duration_ms` | number | |
| `input_tokens` | number | |
| `output_tokens` | number | |
| `cache_read_tokens` | number | |
| `cache_create_tokens` | number | |
| `stop_reason` | text | |
| `ok` | bool | |
| `error_msg` | text | nullable |
Wire `callLLM` to write to it (best-effort, never throws). Add a minimal `Admin → Diagnostics` view that shows the last 100 calls and aggregate cost (using public Anthropic prices in a constant; refresh manually).
### 5.7 R42 defaults
- `temperature: 0.3` for R42 chat in `callLLM`.
- `maxTokens: 2048` for R42 (text + tool budget).
### Phase 5 acceptance criteria
- [ ] R42 system prompt is ≤ 4000 tokens regardless of KG size (verify on a graph with 200+ topics).
- [ ] Adding a topic in the admin UI causes the **next** R42 call to show `cache_read_tokens === 0` for the KB block, then subsequent calls to show non-zero.
- [ ] R42 successfully answers a question about a topic whose ID was not in the user's message by emitting `lookup_topic` (manual verification).
- [ ] `Admin → Diagnostics` shows recent LLM calls with model, tokens, duration.
---
## Phase 6 — Eval harness (NOT IMPLEMENTED — out of scope)
> Phase 6 was deliberately skipped. The acceptance criteria for Phases 15 give enough confidence in extraction, content, quiz, R42, and telemetry that a golden-set harness is not currently load-bearing. Leave this section intact as a starting point for a future, separately-scoped initiative.
**Goal:** prompt or model changes can be measured before they ship.
### 6.1 Golden sets
Create `evals/` at the repo root:
```
evals/
extraction/
cases/
roles-handbook.txt
governance-handbook.txt
expected/
roles-handbook.json # { mustContain: ['software-engineer', ...], minTopics: 20 }
governance-handbook.json
quiz/
cases/
software-engineer.json # topic to generate quiz for
rubric.md # human-readable quality rubric
chat/
scripts/
ask-about-known-topic.json
ask-about-unknown-topic.json
propose-new-role.json
```
10 extraction cases, 5 quiz topics, 10 chat scripts is enough to start.
### 6.2 Runner
`evals/run.mjs` — Node script that:
1. Loads each case.
2. Invokes the same code path the app uses (import from `src/lib/*` directly; reuse the simulation toggle off).
3. Compares against expectations:
- **Extraction:** `mustContain` IDs present? `minTopics` met? No `stop_reason: 'max_tokens'`?
- **Quiz:** distribution of `correctIndex`, mean explanation length, banned phrases.
- **Chat:** for each script, did the expected tool fire? Did the reply contain expected anchors?
4. Writes `evals/results/<ISO-timestamp>.json` and a Markdown diff against the previous baseline.
Add `npm run eval` to `package.json`.
### 6.3 Prompt versioning
In each prompt module, export `PROMPT_VERSION = '2026-05-20-001'`. Persist it on the artifact (`content.data.prompt_version`, `quiz_banks.questions[i].prompt_version`, `topics.metadata.prompt_version`). Add an admin button "Mark stale content for regeneration" that lists artifacts whose version is older than the current.
### Phase 6 acceptance criteria
- [ ] `npm run eval` runs end-to-end against the live API and produces a result file.
- [ ] CI (or a manual check) runs evals on every change to `src/lib/llm.js`, prompts, or schemas.
- [ ] Each AI-generated artifact in PocketBase carries a `prompt_version` and is filterable by it.
---
## Cross-phase verification checklist
After every phase, run this short checklist before merging:
1. **Build:** `npm run build` succeeds.
2. **Lint:** `npm run lint` clean.
3. **Tests:** `npm run test` green.
4. **Smoke flows** in dev (simulation off, real API key):
- Add a source via `Admin → Sources`, extract, verify topics appear.
- Visit `Leren` for the current week, generate article. Then slides.
- Visit `Testen`, generate weekly quiz. Submit. Score lands on leaderboard.
- Open R42, ask a known and an unknown question; propose a new topic; admin accepts it.
- Run `Admin → Knowledge Graph → Analyze & Optimize Graph`.
- Run `Admin → Knowledge Graph → Sync Handbook` (small repo or mock).
5. **Simulation toggle:** flip simulation mode on and confirm no real API calls happen (Network tab).
---
## Rollback strategy
Every phase is shippable on its own. If a phase introduces a regression:
- **Phase 12:** `git revert` the merge commit. `api.js` retains the legacy interface, so callers that haven't migrated still work.
- **Phase 3:** revert + the relation-vocabulary migration is reversible (rerun the reverse swap).
- **Phase 4:** revert; quiz schema additions are forward-compatible (older readers ignore `difficulty`).
- **Phase 5:** revert + drop the `llm_calls` collection if undesired.
- **Phase 6:** purely additive; remove the `evals/` folder if abandoned.
Never `git push --force` to `main`. PR-per-phase.
---
## Out of scope (do not do as part of this plan)
- Replacing PocketBase. Stays as-is.
- Server-side embeddings or a vector store. Phase 5 deliberately uses in-browser TF-IDF.
- Streaming responses. Mentioned as a future improvement; not in this plan.
- Multi-tenant changes. The platform serves one company.
- UI redesign of the Admin pages beyond what each phase requires.
---
## Glossary
- **Tier** — coarse model class (`fast`/`standard`/`reasoning`) mapped to a concrete Anthropic model ID via admin settings.
- **Tool use** — Anthropic's structured-output mechanism. The model emits a `tool_use` content block whose `input` is schema-valid JSON.
- **Prompt caching** — Anthropic feature where blocks marked `cache_control: { type:'ephemeral' }` are reused across requests at lower input cost. 5-minute TTL.
- **TF-IDF** — Term-frequency / inverse-document-frequency. A classic IR scoring function used here as a cheap retrieval signal.
- **KB context** — The block in R42's system prompt that lists topics and relations from the knowledge graph.
- **Delta** — A proposed addition to the knowledge graph emitted by R42 via the `propose_graph_delta` tool.

1016
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

57
Caddyfile Normal file
View File

@@ -0,0 +1,57 @@
: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 {
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.

View File

@@ -1,16 +1,80 @@
# 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/)
- **Weekly Learning Station** — Each employee is assigned a topic each week (via deterministic hash of user ID + week number). They choose their preferred format: Article, Slides, or Infographic. Content is generated on-demand by Claude and cached per topic.
- **Weekly Test** — AI-generated quiz based on the knowledge graph. Results are stored and feed the leaderboard.
- **Leaderboard & Gamification** — Points for correct answers, badges for streaks and perfect scores.
- **R42 Chatbot** — An always-available AI assistant (backed by Claude) with access to the full knowledge graph. Can propose graph updates that admins approve or reject.
- **Admin Panel** — Manage the knowledge graph, sync from GitHub, review generated content, refine it with AI, and monitor team progress.
## 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 18 + Vite |
| Styling | Vanilla CSS (custom properties) + Tailwind utility classes |
| Animations | Framer Motion |
| Icons | Lucide React |
| Graph viz | D3.js (admin knowledge graph only) |
| Backend / DB | PocketBase (self-hosted) |
| AI | Anthropic Claude (via Caddy reverse proxy) |
| Infra | Docker + Caddy |
## Expanding the ESLint configuration
## Getting Started (local dev)
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.
```bash
# 1. Install dependencies
npm install
# 2. Start PocketBase (Windows)
./pocketbase.exe serve
# 3. Start the dev server
npm run dev
```
The Vite dev server proxies `/api/anthropic` and `/pb` — see `vite.config.js`.
## Deployment (Docker)
```bash
docker compose up -d
```
The `Caddyfile` handles:
- SPA fallback routing
- `/pb/*` → PocketBase sidecar
- `/api/anthropic/*` → Anthropic API (with server-side API key injection)
## Key Files
| File | Purpose |
|---|---|
| `src/lib/learningService.js` | Selective content generation (article / slides / infographic) |
| `src/lib/extractionPipeline.js` | GitHub file → knowledge graph extraction |
| `src/lib/api.js` | Anthropic API wrapper (`generateContent` + `chat`) |
| `src/lib/db.js` | All PocketBase data access |
| `src/lib/giteaService.js` | GitHub API client (folder listing + raw file fetch) |
| `src/store/AppContext.jsx` | Global state; computes ISO week number on load |
| `src/components/admin/UploadZone.jsx` | GitHub sync UI (default folder: `docs/knowledge-base/`) |
| `AI_AGENT.md` | Detailed context guide for AI coding agents |
## Content Types
Learning content is generated **on demand 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.
## Documentation
- **`AI_AGENT.md`** — Full architectural guide for AI coding agents (patterns, gotchas, decisions).
- **`CHANGELOG.md`** — PocketBase upstream changelog (not application changelog).

View File

@@ -1,37 +0,0 @@
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:

View File

@@ -2,5 +2,20 @@ services:
frontend:
build: .
restart: unless-stopped
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
ports:
- "8080:80"
pocketbase-learning:
image: ghcr.io/muchobien/pocketbase:latest
container_name: pocketbase-learning
restart: unless-stopped
working_dir: /pb
command: ["serve", "--http=0.0.0.0:8090", "--dir=/pb/pb_data", "--migrationsDir=/pb/pb_migrations"]
volumes:
- pb_data:/pb/pb_data
- ./pb_migrations:/pb/pb_migrations
volumes:
pb_data:

View File

@@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
globalIgnores(['dist', 'pb_migrations']),
{
files: ['**/*.{js,jsx}'],
extends: [
@@ -14,8 +14,32 @@ export default defineConfig([
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
globals: {
...globals.browser,
__BUILD_SHA__: 'readonly',
__BUILD_TIME__: 'readonly',
},
parserOptions: { ecmaFeatures: { jsx: true } },
},
rules: {
// Conventional initial-load pattern across the admin/chat surface;
// refactoring every effect is out of scope and not currently
// observed to cause cascading renders in practice.
'react-hooks/set-state-in-effect': 'off',
},
},
{
files: ['src/store/AppContext.jsx'],
rules: {
'react-refresh/only-export-components': 'off',
},
},
{
files: ['vite.config.js'],
languageOptions: { globals: { ...globals.node } },
},
{
files: ['src/lib/__tests__/**/*.{js,jsx}'],
languageOptions: { globals: { ...globals.node } },
},
])

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>app</title>
<title>Learning Platform</title>
</head>
<body>
<div id="root"></div>

View File

@@ -0,0 +1,2 @@
[development]
development ansible_host=46.224.220.37 ansible_port=4484 ansible_user=root

View File

@@ -0,0 +1,28 @@
version: "3"
networks:
learning-platform:
external: true
services:
learning-platform:
image: git.labs.respellion.tech/respellion/learning-platform/learning-platform:latest
container_name: learning-platform
restart: unless-stopped
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
networks:
- learning-platform
pocketbase-learning:
image: ghcr.io/muchobien/pocketbase:latest
container_name: pocketbase-learning
restart: unless-stopped
command: ["serve", "--http=0.0.0.0:8090", "--dir=/pb/pb_data"]
volumes:
- pb_data:/pb/pb_data
networks:
- learning-platform
volumes:
pb_data:

View File

@@ -0,0 +1,58 @@
- name: Deploy Learning Platform with Ansible
hosts: all
become: yes
gather_facts: yes
vars:
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
tasks:
- name: Set destination directory
ansible.builtin.set_fact:
dest_dir: "/opt/learning-platform"
- name: Ensure dependencies are installed
ansible.builtin.package:
name:
- docker-ce
state: present
ignore_errors: yes
register: docker_valid
- name: Include geerlingguy.docker role conditionally
include_role:
name: geerlingguy.docker
when: docker_valid is failed
- name: Ensure target directory exists
ansible.builtin.file:
path: "{{ dest_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Copy compose.yml to destination
ansible.builtin.copy:
src: compose.yml
dest: "{{ dest_dir }}/compose.yml"
- name: Create .env file for secrets
ansible.builtin.copy:
dest: "{{ dest_dir }}/.env"
content: |
ANTHROPIC_API_KEY={{ anthropic_api_key | default('') }}
mode: '0600'
- name: Pull latest image
community.docker.docker_compose_v2:
project_src: "{{ dest_dir }}"
state: present
files: compose.yml
pull: always
- name: Start services with Docker Compose
community.docker.docker_compose_v2:
project_src: "{{ dest_dir }}"
state: present
files: compose.yml
recreate: always

View File

@@ -0,0 +1,2 @@
[production]
production ansible_host=91.107.213.195 ansible_port=22 ansible_user=root

View File

@@ -0,0 +1,30 @@
version: "3"
networks:
learning-platform:
external: true
volumes:
pb_data:
services:
learning-platform:
image: git.labs.respellion.tech/respellion/learning-platform/learning-platform:latest
container_name: learning-platform
restart: unless-stopped
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
networks:
- learning-platform
pocketbase-learning:
image: ghcr.io/muchobien/pocketbase:latest
container_name: pocketbase-learning
restart: unless-stopped
working_dir: /pb
command: ["serve", "--http=0.0.0.0:8090", "--dir=/pb/pb_data", "--migrationsDir=/pb/pb_migrations"]
volumes:
- pb_data:/pb/pb_data
- ./pb_migrations:/pb/pb_migrations
networks:
- learning-platform

View File

@@ -0,0 +1,64 @@
- name: Deploy Learning Platform with Ansible
hosts: all
become: yes
gather_facts: yes
vars:
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
tasks:
- name: Set destination directory
ansible.builtin.set_fact:
dest_dir: "/opt/learning-platform"
- name: Ensure dependencies are installed
ansible.builtin.package:
name:
- docker-ce
state: present
ignore_errors: yes
register: docker_valid
- name: Include geerlingguy.docker role conditionally
include_role:
name: geerlingguy.docker
when: docker_valid is failed
- name: Ensure target directory exists
ansible.builtin.file:
path: "{{ dest_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Copy compose.yml to destination
ansible.builtin.copy:
src: compose.yml
dest: "{{ dest_dir }}/compose.yml"
- name: Copy pb_migrations to destination
ansible.builtin.copy:
src: ../../../pb_migrations
dest: "{{ dest_dir }}/"
mode: "0755"
- name: Create .env file for secrets
ansible.builtin.copy:
dest: "{{ dest_dir }}/.env"
content: |
ANTHROPIC_API_KEY={{ anthropic_api_key | default('') }}
mode: '0600'
- name: Pull latest image
community.docker.docker_compose_v2:
project_src: "{{ dest_dir }}"
state: present
files: compose.yml
pull: always
- name: Start services with Docker Compose
community.docker.docker_compose_v2:
project_src: "{{ dest_dir }}"
state: present
files: compose.yml
recreate: always

408
package-lock.json generated
View File

@@ -11,9 +11,11 @@
"d3": "^7.9.0",
"framer-motion": "^12.38.0",
"lucide-react": "^1.14.0",
"pocketbase": "^0.26.9",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.15.0"
"react-router-dom": "^7.15.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -28,7 +30,8 @@
"globals": "^17.5.0",
"postcss": "^8.5.14",
"tailwindcss": "^4.3.0",
"vite": "^8.0.10"
"vite": "^8.0.10",
"vitest": "^4.1.7"
}
},
"node_modules/@babel/code-frame": {
@@ -62,7 +65,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -272,6 +274,29 @@
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -820,6 +845,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
@@ -1103,6 +1135,24 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/esrecurse": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
@@ -1130,7 +1180,6 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1171,13 +1220,125 @@
}
}
},
"node_modules/@vitest/expect": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz",
"integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.1.7",
"@vitest/utils": "4.1.7",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz",
"integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.1.7",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz",
"integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz",
"integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.7",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz",
"integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.7",
"@vitest/utils": "4.1.7",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz",
"integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz",
"integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.7",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1212,6 +1373,16 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/autoprefixer": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
@@ -1305,7 +1476,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -1341,6 +1511,16 @@
],
"license": "CC-BY-4.0"
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -1709,7 +1889,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -1859,6 +2038,13 @@
"node": ">=10.13.0"
}
},
"node_modules/es-module-lexer": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
"dev": true,
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1888,7 +2074,6 @@
"integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
@@ -2055,6 +2240,16 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -2065,6 +2260,16 @@
"node": ">=0.10.0"
}
},
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2348,7 +2553,6 @@
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -2808,6 +3012,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
"license": "MIT"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2878,6 +3093,13 @@
"node": ">=8"
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -2891,7 +3113,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -2899,6 +3120,12 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pocketbase": {
"version": "0.26.9",
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.9.tgz",
"integrity": "sha512-Tiv1/hNuUzRdvT0d8hF03dfzuefQ1WdSRp1A1q3wzFC/WYhQcbU/Qlaubl/3ZDo6xvFXBS8JAgBS/L+ms7nkVQ==",
"license": "MIT"
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
@@ -2919,7 +3146,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -2961,7 +3187,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2971,7 +3196,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3121,6 +3345,13 @@
"node": ">=8"
}
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true,
"license": "ISC"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3131,6 +3362,20 @@
"node": ">=0.10.0"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true,
"license": "MIT"
},
"node_modules/std-env": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
"dev": true,
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
@@ -3152,6 +3397,23 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
"dev": true,
"license": "MIT"
},
"node_modules/tinyexec": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
"integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
@@ -3169,6 +3431,16 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyrainbow": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -3235,7 +3507,6 @@
"integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -3308,6 +3579,96 @@
}
}
},
"node_modules/vitest": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz",
"integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.1.7",
"@vitest/mocker": "4.1.7",
"@vitest/pretty-format": "4.1.7",
"@vitest/runner": "4.1.7",
"@vitest/snapshot": "4.1.7",
"@vitest/spy": "4.1.7",
"@vitest/utils": "4.1.7",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
"obug": "^2.1.1",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"std-env": "^4.0.0-rc.1",
"tinybench": "^2.9.0",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.1.0",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.7",
"@vitest/browser-preview": "4.1.7",
"@vitest/browser-webdriverio": "4.1.7",
"@vitest/coverage-istanbul": "4.1.7",
"@vitest/coverage-v8": "4.1.7",
"@vitest/ui": "4.1.7",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser-playwright": {
"optional": true
},
"@vitest/browser-preview": {
"optional": true
},
"@vitest/browser-webdriverio": {
"optional": true
},
"@vitest/coverage-istanbul": {
"optional": true
},
"@vitest/coverage-v8": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
},
"vite": {
"optional": false
}
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3324,6 +3685,23 @@
"node": ">= 8"
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
},
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -3358,9 +3736,7 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -7,15 +7,18 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"test": "vitest run",
"preview": "vite preview"
},
"dependencies": {
"d3": "^7.9.0",
"framer-motion": "^12.38.0",
"lucide-react": "^1.14.0",
"pocketbase": "^0.26.9",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.15.0"
"react-router-dom": "^7.15.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -30,6 +33,7 @@
"globals": "^17.5.0",
"postcss": "^8.5.14",
"tailwindcss": "^4.3.0",
"vite": "^8.0.10"
"vite": "^8.0.10",
"vitest": "^4.1.7"
}
}

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1872121667",
"indexes": [],
"listRule": "",
"name": "content",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1872121667");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_2848292486",
"indexes": [],
"listRule": "",
"name": "quiz_banks",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2848292486");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1883724256",
"indexes": [],
"listRule": "",
"name": "relations",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1883724256");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1124997656",
"indexes": [],
"listRule": "",
"name": "sources",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1124997656");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_3980519374",
"indexes": [],
"listRule": "",
"name": "team_members",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3980519374");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_2800040823",
"indexes": [],
"listRule": "",
"name": "topics",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2800040823");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_3906957607",
"indexes": [],
"listRule": "",
"name": "leaderboard",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3906957607");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1945839367",
"indexes": [],
"listRule": "",
"name": "learn_progress",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1945839367");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1132733175",
"indexes": [],
"listRule": "",
"name": "quiz_cache",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1132733175");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_3663793586",
"indexes": [],
"listRule": "",
"name": "quiz_results",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3663793586");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_2769025244",
"indexes": [],
"listRule": "",
"name": "settings",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2769025244");
return app.delete(collection);
})

View File

@@ -0,0 +1,53 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1345053523",
"max": 0,
"min": 0,
"name": "test_field",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_730968720",
"indexes": [],
"listRule": null,
"name": "test_col2",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_730968720");
return app.delete(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1883724256");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1883724256",
"indexes": [],
"listRule": "",
"name": "relations",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_2800040823");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_2800040823",
"indexes": [],
"listRule": "",
"name": "topics",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,223 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("_pb_users_auth_");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"authAlert": {
"emailTemplate": {
"body": "<p>Hello,</p>\n<p>We noticed a login to your {APP_NAME} account from a new location:</p>\n<p><em>{ALERT_INFO}</em></p>\n<p><strong>If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.</strong></p>\n<p>If this was you, you may disregard this email.</p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
"subject": "Login from a new location"
},
"enabled": true
},
"authRule": "",
"authToken": {
"duration": 604800
},
"confirmEmailChangeTemplate": {
"body": "<p>Hello,</p>\n<p>Click on the button below to confirm your new email address.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Confirm new email</a>\n</p>\n<p><i>If you didn't ask to change your email address, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
"subject": "Confirm your {APP_NAME} new email address"
},
"createRule": "",
"deleteRule": "id = @request.auth.id",
"emailChangeToken": {
"duration": 1800
},
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cost": 0,
"help": "",
"hidden": true,
"id": "password901924565",
"max": 0,
"min": 8,
"name": "password",
"pattern": "",
"presentable": false,
"required": true,
"system": true,
"type": "password"
},
{
"autogeneratePattern": "[a-zA-Z0-9]{50}",
"help": "",
"hidden": true,
"id": "text2504183744",
"max": 60,
"min": 30,
"name": "tokenKey",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": true,
"type": "text"
},
{
"exceptDomains": null,
"help": "",
"hidden": false,
"id": "email3885137012",
"name": "email",
"onlyDomains": null,
"presentable": false,
"required": true,
"system": true,
"type": "email"
},
{
"help": "",
"hidden": false,
"id": "bool1547992806",
"name": "emailVisibility",
"presentable": false,
"required": false,
"system": true,
"type": "bool"
},
{
"help": "",
"hidden": false,
"id": "bool256245529",
"name": "verified",
"presentable": false,
"required": false,
"system": true,
"type": "bool"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 255,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "file376926767",
"maxSelect": 1,
"maxSize": 0,
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"name": "avatar",
"presentable": false,
"protected": false,
"required": false,
"system": false,
"thumbs": null,
"type": "file"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"fileToken": {
"duration": 180
},
"id": "_pb_users_auth_",
"indexes": [
"CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)",
"CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''"
],
"listRule": "id = @request.auth.id",
"manageRule": null,
"mfa": {
"duration": 600,
"enabled": false,
"rule": ""
},
"name": "users",
"oauth2": {
"enabled": false,
"mappedFields": {
"avatarURL": "avatar",
"id": "",
"name": "name",
"username": ""
}
},
"otp": {
"duration": 180,
"emailTemplate": {
"body": "<p>Hello,</p>\n<p>Your one-time password is: <strong>{OTP}</strong></p>\n<p><i>If you didn't ask for the one-time password, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
"subject": "OTP for {APP_NAME}"
},
"enabled": false,
"length": 8
},
"passwordAuth": {
"enabled": true,
"identityFields": [
"email"
]
},
"passwordResetToken": {
"duration": 1800
},
"resetPasswordTemplate": {
"body": "<p>Hello,</p>\n<p>Click on the button below to reset your password.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Reset password</a>\n</p>\n<p><i>If you didn't ask to reset your password, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
"subject": "Reset your {APP_NAME} password"
},
"system": false,
"type": "auth",
"updateRule": "id = @request.auth.id",
"verificationTemplate": {
"body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Click on the button below to verify your email address.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Verify</a>\n</p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
"subject": "Verify your {APP_NAME} email"
},
"verificationToken": {
"duration": 259200
},
"viewRule": "id = @request.auth.id"
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1872121667");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1872121667",
"indexes": [],
"listRule": "",
"name": "content",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3906957607");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_3906957607",
"indexes": [],
"listRule": "",
"name": "leaderboard",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1945839367");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1945839367",
"indexes": [],
"listRule": "",
"name": "learn_progress",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_2848292486");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_2848292486",
"indexes": [],
"listRule": "",
"name": "quiz_banks",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1132733175");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1132733175",
"indexes": [],
"listRule": "",
"name": "quiz_cache",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3663793586");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_3663793586",
"indexes": [],
"listRule": "",
"name": "quiz_results",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_2769025244");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_2769025244",
"indexes": [],
"listRule": "",
"name": "settings",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1124997656");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_1124997656",
"indexes": [],
"listRule": "",
"name": "sources",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3980519374");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
}
],
"id": "pbc_3980519374",
"indexes": [],
"listRule": "",
"name": "team_members",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,53 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_730968720");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1345053523",
"max": 0,
"min": 0,
"name": "test_field",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_730968720",
"indexes": [],
"listRule": null,
"name": "test_col2",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
});
return app.save(collection);
})

View File

@@ -0,0 +1,64 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text525672509",
"max": 0,
"min": 0,
"name": "topic_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "json2918445923",
"maxSize": 0,
"name": "data",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_1872121667",
"indexes": [],
"listRule": "",
"name": "content",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1872121667");
return app.delete(collection);
})

View File

@@ -0,0 +1,107 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number666537513",
"max": null,
"min": null,
"name": "points",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number2161202778",
"max": null,
"min": null,
"name": "tests_completed",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number684605397",
"max": null,
"min": null,
"name": "learnings_completed",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}
],
"id": "pbc_3906957607",
"indexes": [],
"listRule": "",
"name": "leaderboard",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3906957607");
return app.delete(collection);
})

View File

@@ -0,0 +1,76 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "bool271442091",
"name": "done",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}
],
"id": "pbc_1945839367",
"indexes": [],
"listRule": "",
"name": "learn_progress",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1945839367");
return app.delete(collection);
})

View File

@@ -0,0 +1,64 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text525672509",
"max": 0,
"min": 0,
"name": "topic_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "json2329695445",
"maxSize": 0,
"name": "questions",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_2848292486",
"indexes": [],
"listRule": "",
"name": "quiz_banks",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2848292486");
return app.delete(collection);
})

View File

@@ -0,0 +1,88 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "json2329695445",
"maxSize": 0,
"name": "questions",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"help": "",
"hidden": false,
"id": "json3622966325",
"maxSize": 0,
"name": "meta",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_1132733175",
"indexes": [],
"listRule": "",
"name": "quiz_cache",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1132733175");
return app.delete(collection);
})

View File

@@ -0,0 +1,157 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number848901969",
"max": null,
"min": null,
"name": "score",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number3257917790",
"max": null,
"min": null,
"name": "total",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number2466988094",
"max": null,
"min": null,
"name": "percentage",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number3573180981",
"max": null,
"min": null,
"name": "time_used",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1410257210",
"max": 0,
"min": 0,
"name": "completed_at",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "json3012992423",
"maxSize": 0,
"name": "breakdown",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"help": "",
"hidden": false,
"id": "number2092809343",
"max": null,
"min": null,
"name": "points_earned",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}
],
"id": "pbc_3663793586",
"indexes": [],
"listRule": "",
"name": "quiz_results",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3663793586");
return app.delete(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1602912115",
"max": 0,
"min": 0,
"name": "source",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1181691900",
"max": 0,
"min": 0,
"name": "target",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2363381545",
"max": 0,
"min": 0,
"name": "type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_1883724256",
"indexes": [],
"listRule": "",
"name": "relations",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1883724256");
return app.delete(collection);
})

View File

@@ -0,0 +1,68 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2324736937",
"max": 0,
"min": 0,
"name": "key",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text494360628",
"max": 0,
"min": 0,
"name": "value",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_2769025244",
"indexes": [],
"listRule": "",
"name": "settings",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2769025244");
return app.delete(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2063623452",
"max": 0,
"min": 0,
"name": "status",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1574812785",
"max": 0,
"min": 0,
"name": "error",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_1124997656",
"indexes": [],
"listRule": "",
"name": "sources",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1124997656");
return app.delete(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text3045404147",
"max": 0,
"min": 0,
"name": "pin",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1466534506",
"max": 0,
"min": 0,
"name": "role",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_3980519374",
"indexes": [],
"listRule": "",
"name": "team_members",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3980519374");
return app.delete(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text245846248",
"max": 0,
"min": 0,
"name": "label",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2363381545",
"max": 0,
"min": 0,
"name": "type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1843675174",
"max": 0,
"min": 0,
"name": "description",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_2800040823",
"indexes": [],
"listRule": "",
"name": "topics",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2800040823");
return app.delete(collection);
})

View File

@@ -0,0 +1,53 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 255,
"min": 1,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1345053523",
"max": 0,
"min": 0,
"name": "test_field",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_1553367558",
"indexes": [],
"listRule": null,
"name": "test_col3",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1553367558");
return app.delete(collection);
})

View File

@@ -0,0 +1,53 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 255,
"min": 1,
"name": "id",
"pattern": "^[a-zA-Z0-9_-]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1345053523",
"max": 0,
"min": 0,
"name": "test_field",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_3270645669",
"indexes": [],
"listRule": null,
"name": "test_col4",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3270645669");
return app.delete(collection);
})

View File

@@ -0,0 +1,64 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1872121667");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text525672509",
"max": 0,
"min": 0,
"name": "topic_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "json2918445923",
"maxSize": 0,
"name": "data",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_1872121667",
"indexes": [],
"listRule": "",
"name": "content",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,76 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1945839367");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "bool271442091",
"name": "done",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}
],
"id": "pbc_1945839367",
"indexes": [],
"listRule": "",
"name": "learn_progress",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,64 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_2848292486");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text525672509",
"max": 0,
"min": 0,
"name": "topic_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "json2329695445",
"maxSize": 0,
"name": "questions",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_2848292486",
"indexes": [],
"listRule": "",
"name": "quiz_banks",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,88 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1132733175");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "json2329695445",
"maxSize": 0,
"name": "questions",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"help": "",
"hidden": false,
"id": "json3622966325",
"maxSize": 0,
"name": "meta",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_1132733175",
"indexes": [],
"listRule": "",
"name": "quiz_cache",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,157 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3663793586");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number848901969",
"max": null,
"min": null,
"name": "score",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number3257917790",
"max": null,
"min": null,
"name": "total",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number2466988094",
"max": null,
"min": null,
"name": "percentage",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number3573180981",
"max": null,
"min": null,
"name": "time_used",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1410257210",
"max": 0,
"min": 0,
"name": "completed_at",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "json3012992423",
"maxSize": 0,
"name": "breakdown",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"help": "",
"hidden": false,
"id": "number2092809343",
"max": null,
"min": null,
"name": "points_earned",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}
],
"id": "pbc_3663793586",
"indexes": [],
"listRule": "",
"name": "quiz_results",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1883724256");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1602912115",
"max": 0,
"min": 0,
"name": "source",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1181691900",
"max": 0,
"min": 0,
"name": "target",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2363381545",
"max": 0,
"min": 0,
"name": "type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_1883724256",
"indexes": [],
"listRule": "",
"name": "relations",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1124997656");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2063623452",
"max": 0,
"min": 0,
"name": "status",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1574812785",
"max": 0,
"min": 0,
"name": "error",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_1124997656",
"indexes": [],
"listRule": "",
"name": "sources",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3980519374");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text3045404147",
"max": 0,
"min": 0,
"name": "pin",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1466534506",
"max": 0,
"min": 0,
"name": "role",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_3980519374",
"indexes": [],
"listRule": "",
"name": "team_members",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_2800040823");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text245846248",
"max": 0,
"min": 0,
"name": "label",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2363381545",
"max": 0,
"min": 0,
"name": "type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1843675174",
"max": 0,
"min": 0,
"name": "description",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_2800040823",
"indexes": [],
"listRule": "",
"name": "topics",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,107 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3906957607");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number666537513",
"max": null,
"min": null,
"name": "points",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number2161202778",
"max": null,
"min": null,
"name": "tests_completed",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number684605397",
"max": null,
"min": null,
"name": "learnings_completed",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}
],
"id": "pbc_3906957607",
"indexes": [],
"listRule": "",
"name": "leaderboard",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,68 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_2769025244");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2324736937",
"max": 0,
"min": 0,
"name": "key",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text494360628",
"max": 0,
"min": 0,
"name": "value",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_2769025244",
"indexes": [],
"listRule": "",
"name": "settings",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})

View File

@@ -0,0 +1,53 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1553367558");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 255,
"min": 1,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1345053523",
"max": 0,
"min": 0,
"name": "test_field",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_1553367558",
"indexes": [],
"listRule": null,
"name": "test_col3",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
});
return app.save(collection);
})

View File

@@ -0,0 +1,53 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3270645669");
return app.delete(collection);
}, (app) => {
const collection = new Collection({
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 255,
"min": 1,
"name": "id",
"pattern": "^[a-zA-Z0-9_-]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1345053523",
"max": 0,
"min": 0,
"name": "test_field",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_3270645669",
"indexes": [],
"listRule": null,
"name": "test_col4",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
});
return app.save(collection);
})

View File

@@ -0,0 +1,64 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text525672509",
"max": 0,
"min": 0,
"name": "topic_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "json2918445923",
"maxSize": 0,
"name": "data",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_1872121667",
"indexes": [],
"listRule": "",
"name": "content",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1872121667");
return app.delete(collection);
})

View File

@@ -0,0 +1,64 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text525672509",
"max": 0,
"min": 0,
"name": "topic_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "json2329695445",
"maxSize": 0,
"name": "questions",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_2848292486",
"indexes": [],
"listRule": "",
"name": "quiz_banks",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2848292486");
return app.delete(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1602912115",
"max": 0,
"min": 0,
"name": "source",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1181691900",
"max": 0,
"min": 0,
"name": "target",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2363381545",
"max": 0,
"min": 0,
"name": "type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_1883724256",
"indexes": [],
"listRule": "",
"name": "relations",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1883724256");
return app.delete(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2063623452",
"max": 0,
"min": 0,
"name": "status",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1574812785",
"max": 0,
"min": 0,
"name": "error",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_1124997656",
"indexes": [],
"listRule": "",
"name": "sources",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1124997656");
return app.delete(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 255,
"min": 1,
"name": "id",
"pattern": "^[a-zA-Z0-9_-]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text245846248",
"max": 0,
"min": 0,
"name": "label",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2363381545",
"max": 0,
"min": 0,
"name": "type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1843675174",
"max": 0,
"min": 0,
"name": "description",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_2800040823",
"indexes": [],
"listRule": "",
"name": "topics",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2800040823");
return app.delete(collection);
})

View File

@@ -0,0 +1,107 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number666537513",
"max": null,
"min": null,
"name": "points",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number2161202778",
"max": null,
"min": null,
"name": "tests_completed",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number684605397",
"max": null,
"min": null,
"name": "learnings_completed",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}
],
"id": "pbc_3906957607",
"indexes": [],
"listRule": "",
"name": "leaderboard",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3906957607");
return app.delete(collection);
})

View File

@@ -0,0 +1,76 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "bool271442091",
"name": "done",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}
],
"id": "pbc_1945839367",
"indexes": [],
"listRule": "",
"name": "learn_progress",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1945839367");
return app.delete(collection);
})

View File

@@ -0,0 +1,88 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "json2329695445",
"maxSize": 0,
"name": "questions",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"help": "",
"hidden": false,
"id": "json3622966325",
"maxSize": 0,
"name": "meta",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"id": "pbc_1132733175",
"indexes": [],
"listRule": "",
"name": "quiz_cache",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1132733175");
return app.delete(collection);
})

View File

@@ -0,0 +1,157 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2809058197",
"max": 0,
"min": 0,
"name": "user_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number848901969",
"max": null,
"min": null,
"name": "score",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number3257917790",
"max": null,
"min": null,
"name": "total",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number2466988094",
"max": null,
"min": null,
"name": "percentage",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number3573180981",
"max": null,
"min": null,
"name": "time_used",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1410257210",
"max": 0,
"min": 0,
"name": "completed_at",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "json3012992423",
"maxSize": 0,
"name": "breakdown",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"help": "",
"hidden": false,
"id": "number2092809343",
"max": null,
"min": null,
"name": "points_earned",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}
],
"id": "pbc_3663793586",
"indexes": [],
"listRule": "",
"name": "quiz_results",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3663793586");
return app.delete(collection);
})

View File

@@ -0,0 +1,68 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2324736937",
"max": 0,
"min": 0,
"name": "key",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text494360628",
"max": 0,
"min": 0,
"name": "value",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_2769025244",
"indexes": [],
"listRule": "",
"name": "settings",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2769025244");
return app.delete(collection);
})

View File

@@ -0,0 +1,83 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text3045404147",
"max": 0,
"min": 0,
"name": "pin",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text1466534506",
"max": 0,
"min": 0,
"name": "role",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"id": "pbc_3980519374",
"indexes": [],
"listRule": "",
"name": "team_members",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3980519374");
return app.delete(collection);
})

View File

@@ -0,0 +1,150 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number3145888567",
"max": null,
"min": null,
"name": "year",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number3807012359",
"max": null,
"min": null,
"name": "week_number",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text525672509",
"max": 0,
"min": 0,
"name": "topic_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text2541086472",
"max": 0,
"min": 0,
"name": "theme",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number478273799",
"max": null,
"min": null,
"name": "quarter",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "bool1753762221",
"name": "is_review_week",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
},
{
"help": "",
"hidden": false,
"id": "number1169138922",
"max": null,
"min": null,
"name": "sort_order",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_2236782419",
"indexes": [],
"listRule": "",
"name": "curriculum",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2236782419");
return app.delete(collection);
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1124997656")
if (collection) {
// Check if created already exists
let hasCreated = false;
for (let f of collection.fields) {
if (f.name === 'created') hasCreated = true;
}
if (!hasCreated) {
collection.fields.addAt(4, new Field({
"hidden": false,
"id": "autodate_sources_created",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
}))
collection.fields.addAt(5, new Field({
"hidden": false,
"id": "autodate_sources_updated",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}))
return app.save(collection)
}
}
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1124997656")
if (collection) {
collection.fields.removeById("autodate_sources_created")
collection.fields.removeById("autodate_sources_updated")
return app.save(collection)
}
})

View File

@@ -0,0 +1,90 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_path123",
"max": 0,
"min": 0,
"name": "path",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_sha123",
"max": 0,
"min": 0,
"name": "sha",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_handbooksync123",
"indexes": [
"CREATE UNIQUE INDEX `idx_handbook_path` ON `handbook_sync_state` (`path`)"
],
"listRule": "",
"name": "handbook_sync_state",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_handbooksync123");
return app.delete(collection);
})

View File

@@ -0,0 +1,30 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_2800040823")
// add field
collection.fields.addAt(4, new Field({
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_learning_relevance",
"max": 0,
"min": 0,
"name": "learning_relevance",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2800040823")
// remove field
collection.fields.removeById("text_learning_relevance")
return app.save(collection)
})

View File

@@ -0,0 +1,23 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_2800040823")
// add field — relevance_locked is set to true whenever an admin edits
// learning_relevance via the UI; mergeKnowledgeGraph must never overwrite
// learning_relevance on a locked topic during re-extraction.
collection.fields.addAt(5, new Field({
"hidden": false,
"id": "bool_relevance_locked",
"name": "relevance_locked",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_2800040823")
collection.fields.removeById("bool_relevance_locked")
return app.save(collection)
})

View File

@@ -0,0 +1,43 @@
/// <reference path="../pb_data/types.d.ts" />
// One-shot data migration: rewrite legacy "executes" relations to the
// canonical "executed_by" vocabulary by swapping source and target.
// Previously `role --executes--> process`; canonical is
// `process --executed_by--> role`.
migrate((app) => {
const records = app.findRecordsByFilter(
"pbc_1883724256", // relations collection
'type = "executes"',
'',
0,
0,
)
for (const rec of records) {
const source = rec.get("source")
const target = rec.get("target")
rec.set("type", "executed_by")
rec.set("source", target)
rec.set("target", source)
app.save(rec)
}
}, (app) => {
// Reverse: turn executed_by back into executes (best-effort — only those
// created before this migration would have been "executes"; rolling back
// will affect any newer executed_by rows too).
const records = app.findRecordsByFilter(
"pbc_1883724256",
'type = "executed_by"',
'',
0,
0,
)
for (const rec of records) {
const source = rec.get("source")
const target = rec.get("target")
rec.set("type", "executes")
rec.set("source", target)
rec.set("target", source)
app.save(rec)
}
})

View File

@@ -0,0 +1,209 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"help": "",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_llm_task",
"max": 0,
"min": 0,
"name": "task",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_llm_model",
"max": 0,
"min": 0,
"name": "model",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_llm_tier",
"max": 0,
"min": 0,
"name": "tier",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"help": "",
"hidden": false,
"id": "number_llm_duration",
"max": null,
"min": null,
"name": "duration_ms",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number_llm_input",
"max": null,
"min": null,
"name": "input_tokens",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number_llm_output",
"max": null,
"min": null,
"name": "output_tokens",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number_llm_cache_r",
"max": null,
"min": null,
"name": "cache_read_tokens",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"help": "",
"hidden": false,
"id": "number_llm_cache_c",
"max": null,
"min": null,
"name": "cache_create_tokens",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_llm_stop",
"max": 0,
"min": 0,
"name": "stop_reason",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "bool_llm_ok",
"name": "ok",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
},
{
"autogeneratePattern": "",
"help": "",
"hidden": false,
"id": "text_llm_err",
"max": 0,
"min": 0,
"name": "error_msg",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate_llm_created",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate_llm_updated",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_llm_calls_001",
"indexes": [
"CREATE INDEX `idx_llm_calls_created` ON `llm_calls` (`created`)",
"CREATE INDEX `idx_llm_calls_task` ON `llm_calls` (`task`)"
],
"listRule": "",
"name": "llm_calls",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_llm_calls_001");
return app.delete(collection);
})

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 380 B

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