6.8 KiB
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)
- Slicing: three sequenced 3b cycles; this one first (manual entry/edit + admin stop/fix), then reports/export, then user management.
- Sessions UI: a full "Sessies" admin screen (the all-sessions list deferred from 3a lands here; the reports cycle reuses it).
- 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. - 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 theusertable (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. BodyCreateManualSessionInput: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. BodyAdminUpdateSessionInput:activity_id, insole_type, pair_count, start_time, end_time(nullable), paused_seconds, notes, status. Recomputesduration_secondsfrom times − paused whenend_timeis present; whenstatus='active'/end_timenull,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' }tonavItems; drop'Handmatig'from the mutedsoonItems(now built → leaves['Rapporten', 'Gebruikers']).App.tsx— add<Route path="/sessies" element={<Sessions />} />.screens/Sessions.tsx— lists all sessions viauseAllSessions(GET /api/admin/sessions, newest first), a status filter (alle / actief / voltooid / geannuleerd), and a+ Nieuwe registratiebutton. 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, fromuseAdminUsers), 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— adduseAllSessions,useAdminUsers,useCreateManualSession,useUpdateSession,useAdminStopSession,useAdminDiscardSession(all invalidate the['admin','sessions']query family; keep the existinguseActiveSessions).screens/Live.tsx— add[Stop][Annuleer]to eachLiveCard, wired to the admin stop/discard hooks (invalidate the active query).
C. Worker convergence (apps/worker)
api/sessions.ts— giveuseActiveSessionsarefetchInterval: 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 (sessionIdset) but the latestactiveSessionsQuery.datano 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 rejectsend<start; stop/discard act on another user's session; all 401/403 gated;GET /api/admin/usersreturns the roster. - Admin: Sessions list renders + status filter; create form posts the right body; edit
prefills + PUTs; Live
Stop/Annuleerfire 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.