81 Commits

Author SHA1 Message Date
CC Worker
49f84655f7 merge: exam-marker FastAPI backend (S4-5/6/7)
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
Brings in the full exam-marker HTTP API on /api/exam (as-user RLS, E1/E2 fixes):
- S4-5 template CRUD (hybrid PUT + PATCH)
- S4-6 batches/scans/marks/results/CSV (A7), roster-from-class_students
- S4-7 Neo4j projection on save + neo4j-sync
Also fixes pre-existing E7: storage.py brace-doubling crash (all uploads).

Verified: 35 unit tests; live as-user RLS smoke .94 (templates 17/17, batches
20/20); live graph smoke .94+.209 (projection 17/17). Reviewed; data-loss guard
added (409 on destructive template PUT once marks recorded).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 19:16:52 +00:00
CC Worker
e269e67f27 fix(exam): block destructive template PUT once marks recorded (review #1)
PUT full-replace deletes exam_questions, and mark_entries.question_id cascades
ON DELETE — so re-saving the setup canvas after marking began would silently
wipe recorded marks. Guard: 409 if any mark_entry exists for the template's
batches. Mark-scheme edits (PATCH /questions/{id}) are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 19:15:35 +00:00
CC Worker
77bb0766ff feat(exam): Neo4j projection on template save + neo4j-sync (S4-7)
modules/database/services/exam_projection.py projects a saved template into
cc.public.exams: ExamPaper -> Question/Part -> Region + Part-[:ASSESSES]->
SpecPoint, joined by shared UUIDs (exam_questions.id, exam_response_areas.id,
exam_code, spec_code). Full re-sync per exam_code (idempotent). Reads via
service role + writes via system Neo4j driver (R3.5.1 documented graph-writer).

Wiring (R3.5.4/R5.3):
- PUT /templates/{id} enqueues project_template_safe via BackgroundTasks
  (swallows failures so a graph hiccup never fails the canvas save).
- POST /templates/{id}/neo4j-sync — manual trigger, as-user auth + owner check,
  runs synchronously and returns projection counts.

Unit tests: projection scheduled on PUT; neo4j-sync owner/403/404.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 19:02:18 +00:00
CC Worker
98be55ab57 fix(storage): de-double braces in storage.py (set-of-dict crash)
Pre-existing bug (E7): the whole file had doubled braces, so every dict literal
was a set containing a dict -> 'unhashable type: dict' at runtime, and log
f-strings printed literal {braces}. This broke StorageManager.upload_file /
list_bucket_contents / create_bucket / bucket-init for ALL callers (incl.
files.py uploads), not just exam scans. Mechanical de-double; no logic change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:46:50 +00:00
CC Worker
62234dbbcb fix(exam): blank total only for absent AND unmarked; flip status on mark
A roster student starts 'absent' and a direct mark would otherwise still show a
blank total. Now total is blank only when absent with no marks; recording a mark
advances the submission out of absent/unmatched to 'marking'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:43:09 +00:00
CC Worker
a1d297ac30 test(exam): replace StorageAdmin with fake class in scan tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:41:23 +00:00
CC Worker
5ad9c01cde feat(exam): batches, scans, marks, results, CSV (S4-6)
Adds routers/exam/batches.py (mounted alongside templates under /api/exam):
- POST/GET /batches — batch creation seeds the cohort from class_students AS
  THE USER (cs_read requires caller teaches/admins the class); each active
  enrollee becomes a student_submissions row (status='absent') so no student
  is ever dropped from results (A7). Display names denormalised via a
  documented service-role profiles read (deny-all as-user, E4).
- GET /batches/{id}/queue — submissions + per-submission mark counts + progress.
- GET /batches/{id}/results + /csv — every roster student incl. absent (blank
  marks/total); CSV row always present (A7 baked into the contract).
- PUT /marks/{id} — upsert; batch_id derived server-side from the submission
  (client never supplies the RLS scoping key).
- POST /batches/{id}/scans — E3 guards: MIME check, hard size ceiling (chunked
  read), %PDF magic-byte sniff; owner-only; stores via service-role storage;
  manual/ordered matching (QR-decode is a follow-on, no QR fixtures yet).

Unit tests cover batch/roster-seed/list, queue, results+CSV A7, mark upsert
round-trip, and all scan guards + owner check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:40:10 +00:00
CC Worker
96f9fb2446 test(exam): accept 401 or 403 for unauthenticated request
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:50:55 +00:00
CC Worker
f52c3267ca feat(exam): /api/exam template CRUD router (as-user RLS, E1 fix)
S4-5: new routers/exam/ package mounted at /api/exam (R5.1/E5, not under
/database/). Template CRUD with hybrid persistence (R5.2):

- POST/GET/GET{id}/PUT{id}/DELETE{id} /templates + PATCH /questions/{qid}
- Calls Supabase AS THE USER via SupabaseAnonClient.for_user (E1 fix), so the
  RLS in 72-exam-marker.sql is enforced; no service-role for user-facing ops.
- Institute resolved/validated via the user_institute_ids() SECURITY DEFINER
  RPC (institute_memberships is deny-all as-user per E4); client-supplied
  institute_id is validated, never trusted (R5.5).
- Ownership pre-checked before writes (E2); out-of-scope ids read back as 404
  under RLS (IDOR-safe). Soft-delete archives, never hard-deletes.
- PUT full-replace preserves client UUIDs as Neo4j join keys (spec §2).
- eb_exams.exam_code denormalised via a documented service-role catalogue
  lookup (eb_exams is shared reference data, deny-all as-user per E4).

Unit tests cover auth, CRUD, ownership/IDOR, institute validation, soft-delete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:49:58 +00:00
CC Worker
6ce6272a1e merge: feat/exam-marker-exam-paper-seed (exam-marker foundation)
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
2026-06-06 17:01:49 +00:00
CC Worker
b8cb9083ec merge: feat/exam-marker-cohort-seed (exam-marker foundation) 2026-06-06 17:01:49 +00:00
CC Worker
8427063bd1 merge: feat/exam-marker-neo4j-graph (exam-marker foundation) 2026-06-06 17:01:49 +00:00
CC Worker
5f822eaf87 feat(seed): AQA-PHYS-8463 spec + paper for exam-marker test
Adds the real AQA GCSE Physics 8463 specification and the AQA-PHYS-8463-1H-22-JUN
exam paper (Paper 1, Higher, June 2022, QP) to seed_curriculum.py, with storage_loc
pointing at the uploaded PDF in the cc.examboards bucket. spec_code AQA-PHYS-8463
matches the cc.public.exams Specification node (S4-1).

Applied + verified on dev .94: eb_specifications + eb_exams rows present; the real PDF
(3,963,384 bytes) is uploaded to cc.examboards/aqa/physics/8463/AQA-PHYS-8463-1H-22-JUN.pdf
and retrievable (HTTP 200, exact byte match). seed run populated the empty catalogue
(7 specs / 16 exams / 42 Neo4j topics).

NOTE: the PDF upload is a one-time ops step (curl from the host to the Storage API) —
the container can't reach the host file. A reproducible fixture-upload step is a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:19:10 +00:00
CC Worker
c690caa26d feat(exams): cc.public.exams Neo4j graph init + node schemas
- modules/database/schemas/nodes/exams/exam_nodes.py: neontology node classes for
  ExamBoard/Specification/SpecPoint/ExamPaper/Question/Part/Region (uuid_string joins
  to Supabase exam_questions.id / exam_response_areas.id / eb_exams.exam_code).
- run/initialization/init_exam_graph.py: idempotent init — creates the shared public
  cc.public.exams database, 10 uniqueness constraints, and seeds AQA + AQA-PHYS-8463
  (GCSE Physics) with its 8 top-level topic SpecPoints.

Applied + verified on dev Neo4j (192.168.0.209, enterprise): db online, 10 constraints,
AQA-[:PUBLISHES]->AQA-PHYS-8463-[:HAS_SPEC_POINT]->8 points. Full sub-point catalogue is
a later data task. spec_code AQA-PHYS-8463 must match the eb_exams seed (S4-3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:14:56 +00:00
CC Worker
0ce654c6c6 feat(seed): markable cohort for exam-marker (9P/Ph1)
Adds seed_cohort_9p_ph1.py — creates N student accounts (default 10) and enrols
them all into one class (Greenfield Year 9 Physics 9P/Ph1) so there is a real
cohort to mark. The canonical timetable seeds enrol one student per year-band,
leaving every class with <=1 student.

Uses the same paths as the canonical seeds (auth admin create user, profiles +
institute_memberships upsert, POST /database/timetable/classes/{id}/students as
school admin). Idempotent. Self-contained HTTP (runs inside the ccapi container).

Verified on dev .94: 10 created + enrolled, 0 errors; 9P/Ph1 roster = 11;
physics teacher sees all 11 under as-user RLS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 15:00:00 +00:00
CC Worker
4b296cff74 fix: include school_id in bootstrap profile select query
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
The _get_profile select list omitted school_id, causing
/me/bootstrap to always return null for that field even after
the column was populated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 07:26:30 +00:00
CC Worker
3711b52ea4 Merge fix/r6-timetable-endpoint: R6-D timetable endpoint + R6-E classes fix
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
- GET /database/timetable/timetables with optional filters (R6-D)
- Return empty collections instead of 400 when user has no school (R6-E)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 01:15:00 +00:00
CC Worker
d3465eca7b R6-D: add GET /database/timetable/timetables endpoint
- New router at routers/database/timetable/timetables.py
- Accepts optional class_id, type, active query params
- Returns {"timetables": [...]} scoped to caller's school
- Fixed broken import path in run/routers.py (tools → timetable module)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 01:14:55 +00:00
CC Worker
9de949d212 R6-E2: return empty collections when user has no school
- _require_institute returns Optional[str] instead of raising 400
- list_classes / my_teaching_classes / my_student_classes / list_school_students
  now return empty arrays when school_id is missing
2026-06-02 23:36:05 +00:00
f203f376e9 fix(supabase): remove duplicate apikey header in _create_base_client
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
supabase-py injects apikey internally via create_client(url, key). Manually
setting headers['apikey'] caused PostgREST to log "Duplicate API key found /
JSON could not be generated" on every bootstrap request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 21:37:10 +00:00
52f5ef4ca2 feat: add init service to dev compose, remove redundant build sections
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
- Add init service behind --profile init with cc-api-dev:latest image
- Remove build sections from backend-dev and backend-test (pre-built images)
- Add env var comments referencing .env.example
- Standardize INIT_MODE=infra default, RUN_INIT=true for init service
2026-05-29 21:15:52 +01:00
ead4452277 feat: add P0 seed scripts for timetable, planned lessons, file cabinets, and curriculum
- seed_kevlarai_timetable.py: Mirror Greenfield timetable structure for KevlarAI
  (8 classes, 2 teachers, 2 students, full slot/materialize/sync pipeline)
- seed_planned_lessons.py: 2-3 planned lessons per teacher across both schools
  (6 plans total, idempotent via title+subject check)
- seed_file_cabinets.py: One file cabinet per class with sample documents
  (14 cabinets, ~28 files, document_artefacts, cabinet_memberships)
- seed_curriculum.py: Exam board specifications and exams (AQA, Edexcel, OCR)
  (6 specs, 12 exam papers, Neo4j curriculum topics per school)
2026-05-29 21:15:05 +01:00
e66c8ec291 t4: consolidate seed scripts, remove demo modes, standardize passwords
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
2026-05-29 19:51:32 +01:00
abc90fa1b6 test: align Supabase user client header expectation
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
2026-05-28 19:19:05 +01:00
39ad1818ae Merge branch agent/p0-correctness-security
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
# Conflicts:
#	modules/database/supabase/utils/client.py
2026-05-28 19:17:22 +01:00
1738af0e3d Merge branch agent/tlsync-token-t_a69128a1 (TLSync JWT token endpoint)
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
2026-05-28 18:04:25 +01:00
7808a0ae56 Add TLSync token endpoint 2026-05-28 17:55:37 +01:00
47409c499e Merge branch agent/me-bootstrap
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
2026-05-28 17:52:27 +01:00
88a3193e01 Keep platform bootstrap permissions additive 2026-05-28 15:10:54 +01:00
4f6634e088 Implement Supabase-first me bootstrap 2026-05-28 14:14:35 +01:00
54760083b5 fix: tighten API P0 auth and route handling 2026-05-28 12:42:42 +01:00
550d405935 Merge branch agent/phase-a-dev-runtime
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
2026-05-28 12:37:09 +01:00
df40ddc286 fix: keep health errors non-sensitive 2026-05-28 11:43:59 +01:00
310e273aa5 feat: expose API runtime identity in health 2026-05-28 11:32:04 +01:00
7fede4d082 fix: run API dev stack in dev mode 2026-05-28 10:15:33 +01:00
b452c9f593 test: add dev stack integration checks
Some checks failed
api-ci-deploy / test-build-deploy (push) Has been cancelled
2026-05-27 23:24:28 +01:00
647f41e421 chore: consolidate api dev compose 2026-05-27 22:55:01 +01:00
9b49e92722 chore: add .gitignore for Python API project 2026-05-27 21:56:21 +01:00
3beb8069d3 docs: add .env.example with all API environment variables 2026-05-27 21:55:26 +01:00
d5bda761d6 fix: enable per-user RLS via SupabaseAnonClient.for_user() and StorageUser(access_token=) 2026-05-27 21:51:58 +01:00
ef75f08392 fix(redis): connect during health checks 2026-05-27 16:50:25 +01:00
0d828315bb fix(graph-tree): class_code column, SubjectClass expansion handler
- _query_teacher_classes: fix code -> class_code (Supabase column name), add section_id param
- _build_timetable_section: tag class nodes with section_id=timetable
- _build_classes_section: tag class nodes with section_id=classes
- SubjectClass handler: section=timetable -> taught_lessons for class; section=classes -> enrolled students

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 13:30:53 +01:00
b71995f4fb fix(graph-tree): switch class/timetable data source to Supabase
- _query_teacher_classes: now queries class_teachers + class_students tables instead of non-existent Neo4j TEACHER_HAS_CLASS relationship
- _build_classes_section: updated signature to (user_id, institute_id, institute_db)
- _build_timetable_section: updated signature; loads classes from Supabase, not Neo4j TIMETABLE_HAS_CLASS
- TeacherTimetable lazy handler: simplified (classes pre-loaded in section builder)
- AcademicWeek timetable-term: Supabase taught_lessons query by date range instead of Neo4j
- expose supabase_institute_id from _resolve_institute call

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 12:58:47 +01:00
bf3df05632 fix(nav): fix AcademicWeek Cypher bug, add TeacherTimetable tree handlers, timetable-term view
- Fix uuid_string: $id Cypher bug in AcademicWeek handler (days were never loading)
- Pre-load SubjectClass children in _build_timetable_section (By Class view)
- Add TeacherTimetable handler: By Class (TIMETABLE_HAS_CLASS) + By Term (ACADEMIC_TIMETABLE_HAS_ACADEMIC_YEAR chain)
- Add timetable-term context propagation through AcademicTerm -> AcademicWeek -> TaughtLesson
- AcademicWeek in timetable-term context returns TaughtLessons filtered by teacher email
- Pass user_email from credentials to _get_children_for_node
- Propagate section_id on AcademicWeek nodes so week expansion stays in timetable context

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 12:12:46 +01:00
b42b409bb2 fix(nav): AcademicWeek children + days support in graph tree API
- AcademicWeek now returns has_children=true in both /calendar/academic
  endpoint and _get_children_for_node for AcademicTerm case
- Added AcademicWeek case to _get_children_for_node: queries
  ACADEMIC_WEEK_HAS_ACADEMIC_DAY relationships to return AcademicDay children
- Academic calendar can now expand weeks to show individual days

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 10:54:23 +01:00
caeee6c9e4 fix: correct profiles.user_type constraint and admin_profiles column names in reset/seed
- reset_environment: profiles PATCH now sets only school_id=null (removing invalid
  user_type='platform_admin' that violated profiles_user_type_check constraint)
- seed_environment: same profiles PATCH fix; admin_profiles upsert now uses correct
  column names (admin_role, is_super_admin, display_name) matching 002_schema.sql
- Platform admin status is correctly tracked via admin_profiles.is_super_admin=true
  and JWT user_metadata.user_type='platform_admin', not profiles.user_type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 06:27:23 +01:00
0596ee5e2c feat(seed): Greenfield full timetable seed with classes and student enrollment
seed_greenfield_timetable.py creates the complete school data set:
- POST /timetable/setup: school_timetables, academic_years, terms, weeks, days
- POST /timetable/materialize-periods: 1624 academic_periods (203 days x 8 periods)
- 17 classes (Physics/Maths/English/History/Science, Yr7-12) with correct metadata
- class_teachers links (primary teacher per class)
- teacher timetable init + slot assignments (class_id FK patched onto slots)
- class_students enrollment: student1->Yr9 (5 classes), student2->Yr10 (4), student3->Yr11 (2)
- POST /timetable/materialize: 1462 taught_lessons all with class_id populated
- POST /admin/seed-timetable endpoint wired in platform_admin_router

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 05:59:06 +01:00
9c32887407 fix: use PUT (not PATCH) for Supabase admin user_metadata update
Supabase Auth admin API requires PUT /auth/v1/admin/users/{id} to update
a user record — PATCH returns 405. Corrects the seed step that sets
kcar's user_type to 'platform_admin'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:23:59 +01:00
035ea17844 fix: prevent platform admin from being auto-enrolled in default school
Two root causes fixed:

1. seed_environment.py: KevlarAI website was 'https://kevlarai.com' (real
   domain) instead of 'https://kevlarai.test'. Also, seed step 8 now patches
   kcar's auth user_metadata to set user_type='platform_admin' on every
   reset+seed, so the fix is self-healing and doesn't require manual DB edits.

2. provisioning_service.py: user_type_map now maps 'platform_admin' to
   ('superadmin', 'superadmin'), so _ensure_membership() is never called for
   platform admin accounts and they are never silently enrolled in the
   default institute.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:16:22 +01:00
52532ce00f feat(phase-c): lesson plans library backend — CRUD, delivery linking, AI suggest
Adds lesson_plans_router.py with 10 endpoints under /lessons/plans:
GET/POST /plans, GET/PATCH/DELETE /plans/{id}, POST /plans/{id}/deliver,
GET /plans/{id}/deliveries, POST/DELETE /plans/{id}/collaborators,
POST /plans/{id}/suggest (Ollama-backed per-field AI suggestions).

objectives and activities stored as JSONB arrays with Bloom taxonomy support.
Registers router in run/routers.py. Adds seed_test_environment.py for
platform-admin triggered reset + seed of demo users and Neo4j.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 03:59:26 +01:00