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

113 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/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 `≤ (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.