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.
- Pre-load: if session.currentPageId not in snapshot pages, reset to first available page
- Post-load: if store instance.currentPageId still invalid, clear store for fresh start
- On loadSnapshot error: clear store instead of silently failing with corrupt state
- Root cause: teacher1 had a saved snapshot with currentPageId pointing to a deleted page