2026-07-31 — XP-150 session surfaces: one mode vocabulary, searchable queue, 8h duration ceiling
What shipped
Section titled “What shipped”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.
Key decisions
Section titled “Key decisions”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.
Gotchas / lessons
Section titled “Gotchas / lessons”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.
Reference changes
Section titled “Reference changes”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.