# 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