The admin layout guard rendered only a "Redirecting to login..." placeholder for the /admin/login child route, trapping every unauthenticated visitor. Exempt the login route from the auth gate so the form renders correctly. Also wire the new POST /api/auth/refresh endpoint (from the dual-token migration) into both auth.init() and the api request() 401 handler, so sessions survive the 15-minute access-token lifetime without a hard logout. Adds a Playwright regression test asserting the login form is visible in a clean (no-cookie) browser context.
8.2 KiB
Implementation Plan: Room Editor Refactor (Unified Visualization)
Objective: Replace the broken hardcoded SeatMap.svelte with a unified, dynamic room renderer that works across Admin Live View and Student Check-in.
Pre-flight: Existing Bugs This Work Fixes
Acknowledge these before starting — do not assume current behaviour is correct.
- Student check-in seat selection is silently broken in production.
frontend/src/lib/components/SeatMap.svelte:33–58uses hardcoded seat IDs (T1-1…T4-5). These IDs do not exist in any room'slayout_json, soPOST /api/checkinis rejected bybackend/src/routes/checkin.rs:200–207("invalid seat") for every seat click. Migrating to the dynamic renderer is the fix. - Admin live view shows no occupancy data.
frontend/src/routes/admin/live/[slotId]/+page.svelte:161calls<SeatMap variant="tutor" scale={0.78} />with noassignments/studentsprops — so even thoughattendancesis loaded, nothing renders in the seat map. - Check-in response is mistyped on the frontend.
s/[code]/+page.svelte:82treats the response ofPOST /api/checkinas anAttendanceobject, but the backend returns{"ok": true}(checkin.rs:159–281). Currently masked becauseloadInfo()is re-called immediately after. Fix this type error while migrating.
Note on other consumers of SeatMap: A grep of frontend/src/ found SeatMap is used only in the two routes described below. There are no dashboard, print-view, or mobile-only consumers.
1. Extend RoomCanvas (Decision: Don't Fork)
Task 1: Add Read-Only / Interactive Modes to RoomCanvas.svelte
Files to Modify:
frontend/src/lib/RoomCanvas.svelte
Decision rationale: RoomCanvas.svelte:14–24 already accepts occupiedSeatIds, mySeatId, studentNames, selectedId, onElementClick, and editable. Creating a separate DynamicRoomView.svelte would duplicate the SVG/element rendering logic and risk divergence. Extend RoomCanvas instead.
Changes:
- Add a
clickable: booleanprop (defaultfalse) — enablesonElementClickin read-only mode without enabling edit handles. This maps to thecheckinandnotesuse cases. - Remove the hardcoded
width="800" height="600"(line 65). Replace with aviewBoxcomputed from element extents (or a configured canvas size),preserveAspectRatio="xMidYMid meet", and a CSSwidth: 100%; height: autoso the SVG scales responsively to its container. Verify this does not break the editor (pass a fixedstyle="width:800px"wrapper in the editor route). - Add high-fidelity styling per the design handoff: rounded tables (
rx/ryon rect), specific seat styling (circle or rounded-rect), label positioning (centred on table, below seat icon), seat-state colours (vacant / occupied / mine).
Prop summary after changes:
| Prop | Type | Purpose |
|---|---|---|
elements |
LayoutElement[] |
Layout data |
editable |
boolean |
Enables drag, resize, add, delete |
clickable |
boolean |
Enables onElementClick in read-only mode |
occupiedSeatIds |
string[] |
Seats to style as occupied |
mySeatId |
string | null |
Seat to style as "mine" |
studentNames |
Record<string,string> |
Labels overlaid on occupied seats |
selectedId |
string | null |
Currently selected element (editor) |
onElementClick |
(id: string) => void |
Click callback |
2. Integration & Cleanup
Task 2: Replace SeatMap in Admin Live View
Files to Modify:
frontend/src/routes/admin/live/[slotId]/+page.sveltebackend/src/routes/attendance.rs(extend API response)
Pre-step — extend the backend API response (required):
attendance.rs:62–105 (get_session_attendance) returns SessionAttendance but does not include room layouts. The page has no way to know the layout. Options:
- (Recommended) Extend
SessionAttendanceinbackend/src/models.rsandattendance.rsto include each slot'slayout: Option<Vec<LayoutElement>>keyed per slot. This avoids an N+1 fetch. - Alternative: have the page call
api.admin.rooms.get(slot.room_id)after loading the slot. Simpler but adds a round-trip.
Frontend changes:
- Replace
<SeatMap variant="tutor" scale={0.78} />at line 161 with<RoomCanvas elements={slot.layout ?? []} clickable={true} occupiedSeatIds={...} studentNames={...} onElementClick={handleSeatClick} />. occupiedSeatIds: derive fromattendances.map(a => a.seat_id).filter(Boolean).studentNames: derive fromattendancesas{ [seat_id]: student.name }.- Seat → student mapping (new logic required): The existing note-editor is driven by
selectedStudentIdandtoggleAttendancetakesstudentId. The newonElementClick(seatId)must look upattendances.find(a => a.seat_id === seatId)?.student_idto populateselectedStudentId. Add this mapping inhandleSeatClick.
Task 3: Replace SeatMap in Student Check-in
Files to Modify:
frontend/src/routes/s/[code]/+page.svelte
There are 4 call sites, not 1. SeatMap is called at lines 210, 248, 316, and 368 (phone + desktop × seat-pick step + confirmed step). All four need to be replaced.
Layout data is already on the wire. GET /api/checkin/:code returns CheckinInfo.layout: LayoutElement[] | null (populated by checkin.rs:53–68 and typed in types.ts:84–88), but s/[code]/+page.svelte:43–58 discards res.layout. Read and store it: let layout = $state<LayoutElement[]>([]) and assign layout = res.layout ?? [].
Per call site:
- Lines 210, 316 (seat-pick step, phone and desktop): Replace with
<RoomCanvas elements={layout} clickable={true} occupiedSeatIds={occupiedSeatIds} mySeatId={null} onElementClick={selectSeat} />. - Lines 248, 368 (confirmed step, phone and desktop): Replace with
<RoomCanvas elements={layout} clickable={false} occupiedSeatIds={occupiedSeatIds} mySeatId={myAttendance?.seat_id ?? null} />.
Derived state to add:
const occupiedSeatIds = $derived(attendances.map(a => a.seat_id).filter(Boolean) as string[]);
Fix the response-typing bug (Pre-flight item 3): api.ts types checkin.post as Promise<Attendance> but the backend returns {ok: true}. Change the return type to Promise<{ok: boolean}> and update s/[code]/+page.svelte:82 accordingly.
Task 4: Deprecate SeatMap.svelte
Files to Delete:
frontend/src/lib/components/SeatMap.svelte
Hard ordering — do not delete until all of the following are true:
- All 6 call sites are migrated (1 in admin live view + 4 in
s/[code]). grep -rn "SeatMap" frontend/src/returns zero results.- All Playwright tests pass (see Task 5).
3. Verification
Recommended Ordering
- Extend the backend API (Task 2 pre-step) — unblocks frontend.
- Extend
RoomCanvaswithclickable, responsiveviewBox, and high-fidelity styling (Task 1). - Migrate
s/[code](Task 3) — backend already returns layout; this is the quickest win and immediately unblocks the broken check-in. - Migrate admin live view (Task 2) — needs new backend data and the seat→student mapping.
- Run Playwright tests.
- Delete
SeatMap.svelte(Task 4).
Automated Tests
frontend/tests/checkin-dynamic.spec.ts(new): E2E test — create a custom (non-square) room layout via the API, create a session + slot using it, open the student check-in link, verify the layout renders (not a blank grid), click a seat, verifyPOST /api/checkinsucceeds and the seat turns green. Mirror the seat IDs already used inbackend/src/routes/checkin.rs:290–653(s1,s2).frontend/tests/admin-live-dynamic.spec.ts(new): E2E test — using the same custom room, manually add attendance for a student on seats1, open the tutor live view, verify the student's name appears on the correct seat.
Manual Verification
- Create a U-shaped room layout in
Admin → Rooms. - Create a session and slot using this room.
- Open the student check-in link on a mobile viewport. Verify the U-shape is rendered and scaled to fit.
- Check in as a student by clicking a seat. Verify the seat turns green.
- Open the tutor Live View on desktop. Verify the same student appears on the correct seat in the U-shape.
- Click the occupied seat. Verify the note-editor opens for that student.