New /exam-marker/bank page: browse leaf questions across your papers, filter by
spec point (the spec-planning surface) and subject via facet chips, select
questions into a basket, and "Create paper" → POST /custom-papers → opens the new
template in setup. Dashboard gets a "Question bank" entry point.
examRepository.getBank + createCustomPaper; BankQuestion/BankResponse/custom-paper
types. tsc clean. Pairs with the mode-3 API endpoints (already merged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The marking flow read student_submissions but there was no way to create them —
print→scan→mark could not be completed in-app. Add an "Upload scans" control to
the marking queue that posts PDFs to the existing POST /batches/{id}/scans and
refreshes the queue.
- examRepository.uploadScan(batchId, file, {studentId?, matchingMethod?}): multipart
upload (axios sets the boundary), returns the created/updated StudentSubmission.
- ExamMarkingPage: "Upload scans" button + hidden multi-file PDF input; uploads each
with matching_method='ordered' (fills the roster in order; unmatched scans become
reconcilable submissions), then reloads the queue with a success/failure summary.
No API or schema change (endpoint already existed). tsc clean; behaviour is covered
by the cc-browser-tester card (FX-V).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
serializeCanvasShapes recomputed each region's owner purely from geometry,
ignoring the persisted question_id: a region with no enclosing part (or any
region when a template had no parts) was silently dropped, and one overflowing
its part by a pixel re-parented to parts[0].
Resolve the owner in order: geometric containment (a drag into a part re-attaches)
-> persisted question_id if it still points at a saved question (survives drift /
no-container) -> nearest part on the page -> first part -> first question of any
kind. exam_response_areas.question_id is NOT NULL, so a region is never dropped
while any question exists.
Tests: never drops a region with no enclosing part; persisted question wins over
geometric nearest. Full model.test.ts suite (6) passes; tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The S5-7 review layer could surface unconfirmed AI shapes but had no way to
resolve them (confirmed was never set true), so the only "dismiss" was deletion
and the unconfirmed count never dropped. Add the resolution loop:
- A floating review-actions panel (shown while unconfirmed > 0): Accept selected,
Reject (delete) selected, and Accept all AI on the page. Accept flips
props.confirmed = true.
- Human edit = implicit confirm: nudging/resizing an AI ghost marks it confirmed
(R3.3 — a teacher who adjusts a suggestion has reviewed it). Geometry-only
trigger, so the confirmed-only update can't recurse.
- Confidence-driven opacity: unconfirmed ghosts fade by confidence instead of a
constant 0.72; confirmed AI renders solid with its kind icon (like manual).
- Live review counts read straight off the canvas so the unconfirmed chip/panel
update immediately on accept/reject (the old count reflected only saved state).
Persists through the existing PUT (confirmed already round-trips). No schema or
API change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
seedGuide() ran in onMount via the 'else' branch while template was still null during the async
fetch, creating 5 example shapes (Q1 start/end, part, response, context) that flashed on screen
until the real template + PDF loaded. Only seed the guide for a genuinely-empty template after load.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
app-dev built with --mode production was baking the PROD Supabase URL (.env)
into the bundle, so browser auth went cross-origin to supa.classroomcopilot.ai
and was CORS-blocked (and hit the wrong user store). Mirror the /__ccapi fix:
- Dockerfile: nginx /__supabase/ -> dev Supabase .94:8000 (+WS upgrade for realtime)
- supabaseClient.ts: resolve a leading-slash VITE_SUPABASE_URL against
window.location.origin so supabase-js gets an absolute same-origin URL
- docker-compose.dev.yml: bake VITE_SUPABASE_URL=/__supabase (like VITE_API_BASE)
Browser now talks only to the app host (Tailscale or LAN), no CORS, dev .94 store.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Top bar reduced to single-line height: Back becomes an icon button, caption
line removed (detail lives in the guide), Save button size=small. Guide
panel defaults collapsed and toggles via a ? icon button at bottom-right
so it doesn't occupy permanent canvas real estate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces infinite-canvas free-pan with a constrained vertical-scroll doc
view: tldraw setCameraOptions with behavior='contain', fit-x-100, and top
origin so the PDF never drifts side-to-side. Layout restructured from
floating overlays to flex column (top bar + sidebar/canvas row). Tool
panel is now a left sidebar, guide panel overlays canvas bottom-right.
PAGE_START_X changed from 260 → 0 so pages are flush left; camera
applyDocViewConstraints() called incrementally as pages stream in and
with resetZoom() once all pages are loaded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three z-order fixes in ExamTemplateSetupPage:
1. loadShapes: early-return before deletion when models is empty, so
seed guide shapes aren't wiped for a fresh template that has no
saved regions yet.
2. Incremental PDF loading (onPageReady): replace per-page sendToBack
(unreliable when all shapes are moving — reorderToBack no-ops) with
bringToFront on existing domain shapes after each page is added.
3. Final load sequence: call bringDomainShapesToFront after syncPdfPages
+ loadShapes to guarantee correct z-order regardless of how tldraw's
fractional indexer placed newly created shapes. Also called from
onMount for the same reason.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add /exam-marker/:templateId/marks route with MarkSchemePage
- Per-Part mark scheme editor: points/levels/parts/checklist/free forms
- SpecPoint picker via GET /api/exam/specs/{spec_code}/points (falls back to manual spec_ref when endpoint 404s)
- Manual neo4j-sync button; ASSESSES edge verified in cc.public.exams
- Edit marks button on each template card in dashboard
- Merge-resolved: AppRoutes, index.ts, exam.types.ts, ExamDashboardPage (kept grouped UI + added Edit marks button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rendering a 36-page AQA exam PDF sequentially created 36 large canvas
elements, causing memory pressure and keeping pdfStatus='loading' for
60–120s in headless Chrome. Two changes:
1. pdfLoader.ts: reuse a single canvas (reduces peak memory from ~120MB
to ~4MB) and fire onPageReady callback after each page so callers can
stream pages to the canvas as they render.
2. ExamTemplateSetupPage.tsx: use the callback to add each PDF page
shape to the tldraw canvas the moment it renders, making the first
page visible within a few seconds rather than after all pages load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nginx:alpine mime.types only covers .js, not .mjs. The pdfjs-dist v4
worker is output as pdf.worker-*.mjs; without the correct MIME type the
browser refuses to execute it as a module worker and pdfjs throws
'Network Error', blocking the PDF backdrop from rendering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renders template source PDFs as locked image shapes behind the exam
setup regions. Adds page geometry abstraction so shape coordinates
map to real PDF page dimensions rather than fixed PAGE_HEIGHT math.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tldraw 3.6.1 Tldraw component already registers defaultBindingUtils
internally; passing them explicitly via the bindingUtils prop caused
"Binding type 'arrow' is defined more than once" runtime crash.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Select + Furniture tool buttons used color='default', which MUI v5 resolves to
theme.palette.default (undefined) -> TypeError reading 'main'/'dark' at render,
tripping the ErrorBoundary ('Template setup canvas crashed'). Use 'inherit'.
Caught by live browser-verify t_11f5a049; model-level tests never rendered the toolbar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New ExamDashboardPage + examRepository seam; dashboard nav entry; removes the old
CCExamMarker viewer + dead CCExamMarkerPanel wiring (R1.1). Build green; route tests pass.
- New examRepository (R2.1 seam): the only module talking to /api/exam (Supabase
JWT → Bearer, axios to API_BASE). list/get/create/archive templates.
- ExamDashboardPage (/exam-marker): lists institute templates, create dialog,
archive; cards link to setup (S4-9). Wrapped in ErrorBoundary (R6.4).
- exam.types.ts mirrors the API contract.
- Dashboard 'Exam Marker' quick action (top-level discovery, R1.3/R6.1).
Note: the in-canvas TeacherNavigation is unsuitable for a nav section (it's
the worker prev/next tab bar) — flagged; used the dashboard entry instead.
- R1.1 removal: deleted src/pages/tldraw/CCExamMarker/ (old 3-PDF viewer) and
the now-dead CCExamMarkerPanel + its wiring in CCPanel/BasePanel (examMarkerProps
was only ever passed by the deleted page).
- Fixed pre-existing broken AppRoutes test (missing TimetableListPage mock export).
Build green (vite); AppRoutes route tests pass; typecheck clean for new files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-fetch classes when NeoInstitute context changes. Catch fetchMyClasses rejection with
logger.warn so unhandled promise rejections don't break the component boundary silently.
Use CircularProgress (MUI) for loading skeleton.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /database/timetable/timetables returns 404 (endpoint not implemented). Treat any
fetch error as empty state so the page renders gracefully rather than showing error UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
timetableOnlyService.getMyTimetables is referenced but never defined; calling it throws
'is not a function' at runtime. fetchTimetables calls the implemented listTimetables endpoint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bare /timetable routed to TimetablePage which called useParams<{timetableId}>() and
immediately hit !currentTimetable → "Error Loading Timetable". New TimetableListPage
uses fetchMyTimetables() from timetableStore. AppRoutes now: /timetable →
TimetableListPage, /timetable/:timetableId → TimetablePage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: disposing store while React's async state update (storeReady=false) hadn't
unmounted Tldraw yet caused tldraw's reactive signals to read currentPageId on a disposed store.
Fix: Effect 1 creates store once per user.id (never recreated on node navigation).
Effect 2 loads/reloads snapshots when context.node changes, store stays alive.
Store is only disposed when user changes or component unmounts — after setStoreReady(false)
has had time to unmount Tldraw cleanly via React's synchronous cleanup.