# 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((end−start)/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/discard` — `status='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 `≤ (end−start)`; 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 `} />`. - **`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