Skip to content

2026-07-31 — XP-150 session surfaces: one mode vocabulary, searchable queue, 8h duration ceiling

Three contained units over the sessions vertical, merge 0e5dec8. (1) One mode vocabulary: apps/web/src/lib/session-mode.ts owns SESSION_MODE_LABELS + SESSION_MODE_OPTIONS; before this the app spoke three dialects (list said Daring/Luring, both forms said Online/Offline, and both detail pages leaked the raw ONLINE enum to the screen). (2) Findable approval queue: search on the list contract → an or(ilike(student.name), ilike(tutor.name)) predicate inside the service’s whereParts, with a debounced 3-char input, plus a new Mode column on the queue. (3) MAX_SESSION_DURATION_MS = 8h in packages/contract/src/service/sessions/duration.ts, applied to the request / edit / reschedule / complete schemas and mirrored inline on the create + edit forms.

Daring/Luring over Online/Offline — the ticket demanded one Indonesian word per session_mode and daring/luring is the Indonesian education-sector standard. Enum comparisons (session_mode === "OFFLINE") were left alone; they compare values, not words. 8 hours is the outer edge of one sitting (full-day intensive / tryout with breaks), chosen because it still catches the realistic fat-finger — an extra zero on a 90-minute sesi is 15h. The queue’s default order stayed created_at asc (oldest first) so the SLA badge keeps meaning what it means; the sort-toggle route was out of scope.

The queue’s loading branch used to early-return a full-page skeleton, which unmounted the search box the operator was typing into on every refetch — only the table region swaps now. A contract-level zod .max() message never reaches the UI: oRPC surfaces Input validation failed and buries the text in data.issues, so any bound an operator can type needs the constant mirrored on the input too (open follow-up: validate-completion-modal.tsx still lacks that mirror). Playwright wipes outputDir at the start of every run, so the visual spec has to run last or the review has nothing to look at. Adjacent XP-99 fixture breakage (students created without default_segmentasi_id/default_tingkat_id produced no REQUESTED sesi) had to be repaired to drive the screens at all.

New rules .claude/rules/list-search-server-side.md and .claude/rules/frontend-list-controls-stay-mounted.md, both indexed in CLAUDE.md. Updated .claude/rules/frontend-error-handling.md (mirror contract bounds on the input), .claude/rules/frontend-visual-test.md (run the visual spec last; mobile column-hiding is the expected pattern; keep fixture strings realistic in length) and the playwright-local-e2e skill (seeding more than one page of rows). No locked doc (plans/ARCHITECTURE.md, plans/scope/*.md) changed.