Files
solelog/docs/superpowers/specs/2026-06-17-phase-3b1-manual-sessions-design.md

6.8 KiB
Raw Permalink Blame History

Phase 3b·1 — Manual Session Entry/Edit + Admin Stop/Fix — Design

  • Created: 2026-06-17
  • Status: Approved (brainstorming) — ready for implementation plan
  • Tracker: Plane (workspace solelog, project SoleLog)
  • Cycle: First of three Phase 3b cycles (then reports/export, then user management)
  • Touches: packages/shared, apps/api, apps/admin, apps/worker

Goal

An admin can find any work session, create a manual one for a worker, edit/correct any session, and stop or cancel a worker's stuck active session — the "manual fallback wherever something fails" from the vision. Because the backend is the source of truth, the worker converges to admin changes (no stuck stopwatch).

Done when: an admin can list/filter all sessions, hand-create a completed session for a worker, edit any session's fields (server recomputes duration), and stop/cancel a worker's active session — and within ~15s the worker's stopwatch reflects a stop/cancel done by the admin.

Scope decisions (confirmed during brainstorming, 2026-06-17)

  1. Slicing: three sequenced 3b cycles; this one first (manual entry/edit + admin stop/fix), then reports/export, then user management.
  2. Sessions UI: a full "Sessies" admin screen (the all-sessions list deferred from 3a lands here; the reports cycle reuses it).
  3. Editable fields: start/end time, activity, insole type, pair count, paused seconds, notes, status. The worker is chosen only on create — no reassignment on edit. Duration is always derived server-side (end start paused), never typed.
  4. Worker convergence: folded into this cycle — the worker polls and reconciles to server truth (an admin stop/cancel reflects on the phone within ~15s; no stuck state).

A. Backend (under the existing admin-gated /api/admin/* guard in routes/admin.ts)

  • GET /api/admin/users{ id, name, email }[] from the user table (ordered by name), to populate the create form's worker picker. Direct DB query — no better-auth client dependency.
  • POST /api/admin/sessions — manual create. Body CreateManualSessionInput: user_id, activity_id, insole_type, pair_count, start_time, end_time, paused_seconds?, notes?. Produces a completed session, source='manual', paused_at=null, duration_seconds = max(0, round((endstart)/1000) paused_seconds).
  • PUT /api/admin/sessions/:id — edit any session. Body AdminUpdateSessionInput: activity_id, insole_type, pair_count, start_time, end_time(nullable), paused_seconds, notes, status. Recomputes duration_seconds from times paused when end_time is present; when status='active'/end_time null, duration_seconds=null. No user reassignment.
  • POST /api/admin/sessions/:id/stop — quick "stop now": fold any open pause, end=now, status='completed', recompute duration.
  • POST /api/admin/sessions/:id/discardstatus='discarded', end=now.

New @solelog/shared contracts: CreateManualSessionInput, AdminUpdateSessionInput. No DB migration — reuses existing work_sessions columns (incl. the pause fields). Responses use the existing toWorkSession mapper (so they carry user_name/activity_name where joined).

Validation

end ≥ start; pair_count ≥ 1; paused_seconds ≥ 0 and ≤ (endstart); activity must exist; user must exist (create); insole_type a valid InsoleType. Invalid → 400; missing session/user → 404. No hard delete — cancellation is status='discarded' (already excluded from exports).

B. Admin UI

  • components/Sidebar.tsx — add { to: '/sessies', label: 'Sessies' } to navItems; drop 'Handmatig' from the muted soonItems (now built → leaves ['Rapporten', 'Gebruikers']).
  • App.tsx — add <Route path="/sessies" element={<Sessions />} />.
  • screens/Sessions.tsx — lists all sessions via useAllSessions (GET /api/admin/sessions, newest first), a status filter (alle / actief / voltooid / geannuleerd), and a + Nieuwe registratie button. Each row: worker · activity · type · worked (+ pauze) · date. Active rows show [Stop] [Annuleer]; all rows show ✎ edit.
  • components/SessionForm.tsx — shared create/edit form: worker picker (create only, from useAdminUsers), activity dropdown, insole-type, pair count, start/end datetime-local, paused, status (edit only), notes, and a live "gewerkt" preview. Submits create or update.
  • api/admin-sessions.ts — add useAllSessions, useAdminUsers, useCreateManualSession, useUpdateSession, useAdminStopSession, useAdminDiscardSession (all invalidate the ['admin','sessions'] query family; keep the existing useActiveSessions).
  • screens/Live.tsx — add [Stop] [Annuleer] to each LiveCard, wired to the admin stop/discard hooks (invalidate the active query).

C. Worker convergence (apps/worker)

  • api/sessions.ts — give useActiveSessions a refetchInterval: 15000 (poll), plus the default refetch-on-window-focus.
  • screens/Stopwatch.tsx — extend the active-session effect to reconcile: if a session is running locally (sessionId set) but the latest activeSessionsQuery.data no longer contains a matching active session for it, the session was stopped/cancelled elsewhere → reset the timer and surface a brief notice "Deze sessie is door de beheerder gestopt." Also: treat a 409 from the worker's own stop/discard as already-closed → reset locally instead of erroring.

Error handling

  • Worker reconciliation notice is transient (dismissible / auto-clears on next start).
  • Admin form: inline validation errors mirror the API 400s (end before start, count < 1).
  • Stop/discard on a non-active session → 409; admin UI refetches and the row reflects truth.

Testing

  • API (admin.test.ts): create computes duration + source='manual'; edit recomputes and rejects end<start; stop/discard act on another user's session; all 401/403 gated; GET /api/admin/users returns the roster.
  • Admin: Sessions list renders + status filter; create form posts the right body; edit prefills + PUTs; Live Stop/Annuleer fire the right mutations.
  • Worker: when the active query returns without a locally-running session, the stopwatch resets + shows the notice; a 409 on stop resets instead of erroring.

Out of scope (later 3b cycles)

  • Aggregated/on-screen reporting + all-users filtered CSV (reports cycle).
  • Full user management UI — create/role/deactivate via /api/auth/admin/* (user-mgmt cycle); this cycle only reads the roster (GET /api/admin/users) for the picker.
  • Real-time push (SSE) — polling is sufficient at this scale.

Build approach

spec → writing-plans → one Workflow (~7 TDD tasks), commit per task, final verify. Tracked as a Plane epic.