6.5 KiB
Session: 2026-06-17 — Phase 3b·1 (Manual session entry/edit + admin stop/fix + worker convergence)
Goal
Let an admin take full control of sessions: list/filter every session, manually create a
completed session for a worker, edit any session (server recomputes duration), and
stop/cancel a worker's active session — and make the worker's stopwatch converge to those
admin changes within ~15 s with no stuck state. No DB migration (reuses the existing
work_sessions columns, incl. the pause fields from the prior cycle).
Spec: docs/superpowers/specs/2026-06-17-phase-3b1-manual-sessions-design.md;
plan: docs/superpowers/plans/2026-06-17-phase-3b1-manual-sessions.md.
Work done
Implemented task-by-task per the plan (TDD throughout), one commit per task:
- Task 1 — Shared contracts (
00993c6).@solelog/sharedgainsCreateManualSessionInput(worker id + activity + insole type + pair count +start_time/end_time+paused_secondsdefault 0 + optionalnotes) andAdminUpdateSessionInput(editable fields incl. nullableend_time/insole_typeand astatus), with their inferred types. Duration is never accepted from the client. - Task 2 — Backend admin session write endpoints + users roster (
53c66ee). Inapps/api/src/routes/admin.ts(behind the existing/api/admin/*admin guard):GET /api/admin/users(id/name/email, ordered by name),POST /api/admin/sessions(validates the worker + activity, rejectsend < startandpaused > span, storessource='manual',status='completed', server-computedduration_seconds),PUT /api/admin/sessions/:id(recomputes duration; nullableend_timekeeps an active row open), andPOST …/:id/stop(folds any open pause span,end=now, completes) /…/:id/discard(status='discarded',end=now). All session responses go throughtoWorkSession. Workers hitting any of these get 403; stop/discard on an already-closed session → 409. - Task 3 — Admin api hooks + Sessions screen (
b536641).api/admin-sessions.ts:useAllSessions,useAdminUsers,useCreateManualSession,useUpdateSession,useAdminStopSession,useAdminDiscardSession(mutations invalidate['admin','sessions']). Newscreens/Sessions.tsx(title "Sessies", status<select>filter alle/actief/voltooid/geannuleerd,+ Nieuwe registratie, a row per session with worked time + aPauze …line when paused, ✎ on all rows, Stop/Annuleer on active rows). Sidebar gains a "Sessies" nav item and drops "Handmatig" from the soon group;App.tsxgets the/sessiesroute; table/form/action styles added tostyles.css. - Task 4 — Admin create/edit session form (
69b46be).components/SessionForm.tsx(mode: 'create' | 'edit'): create mode shows a worker picker (useAdminUsers) and postsCreateManualSessionInput; edit mode prefills from the session, hides the worker picker, and PUTs the changed fields. Activity<select>, insole-type toggles, pair-count stepper, start/enddatetime-local(built into ISO), paused input, status<select>(edit only), notes, and a derived "gewerkt" line (end − start − paused). Wired into Sessions:+ Nieuwe registratieopens create, ✎ opens edit. - Task 5 — Stop/Annuleer on the Live view (
f1ec249). The activeLiveCardgains Stop (POST …/:id/stop) and Annuleer (…/discard) buttons via the admin hooks; on success the active query invalidates and the card drops off. - Task 6 — Worker poll + reconcile to server truth (
7d3daaa).useActiveSessionsgetsrefetchInterval: 15000. The stopwatch's active-session effect now resets to idle and shows "Deze sessie is door de beheerder gestopt." when the worker's local session id is absent from the server's active list (covers admin stop, discard, and edit-to-completed alike); a 409 from stop/discard resets locally with no error surfaced. The existing "adopt an active session when idle" recovery is kept. - Task 7 — Docs, lint, verification (this task). Lint/format clean, full green matrix, in-process endpoint coverage, and this session log + roadmap note.
Verification (Task 7)
npx oxlint— clean (exit 0).npx oxfmt --checkover the 16 cycle-changed source files — "All matched files use the correct format" (earlier tasks formatted as they went; nothing to reformat).yarn workspace @solelog/api typecheck— pass;test— 75 passed (12 files).yarn workspace @solelog/admin typecheck— pass;test— 36 passed (7 files);build— pass (vite, 91 modules).yarn workspace @solelog/worker typecheck— pass;test— 30 passed (8 files);build— pass (vite, 91 modules).- Smoke — the plan's live-server smoke was deliberately driven in-process instead of
by starting the API (
createApp()+app.requestinapps/api/test/admin.test.ts), so port 3000 was never bound — this avoids the Windows libsql lock trap. The in-process tests cover every smoke scenario end-to-end:GET /api/admin/users(admin 200 / worker 403);POST /api/admin/sessionswithpaused_seconds: 600over a 1 h span →source='manual',status='completed',duration_seconds === 3000(3600 − 600);end < start→ 400; unknown user/activity → 404;PUTedit recomputes duration;…/:id/stopon another worker's active session → completed with computed duration;…/:id/discard→status='discarded'; stop after discard → 409; worker token on any write → 403.
Outcome
Phase 3b·1 is implemented and green across all three workspaces. An admin can list and
filter every session, manually log a completed session for a worker (duration always
derived server-side and excluding paused time, source='manual'), edit any session,
and stop or cancel a worker's running session from either the Sessies screen or the
Live cards. The worker stopwatch polls every 15 s and reconciles to server truth — when
an admin stops/cancels/edits a worker's active session, the worker's clock resets and shows
"Deze sessie is door de beheerder gestopt." within ~15 s, with no stuck state (a racing 409
also resets cleanly). Roadmap Phase 3 status updated: 3b·1 done; reports/export and user
management remain.
The two unrelated working-tree edits to .env.prod.example / docker-compose.prod.yml
(maintainer's deploy work) were left untouched — out of this cycle's scope.
Next (Phase 3b remainder)
- Reports + all-users filtered CSV export (current
/api/exportis self-scoped). - User management via better-auth
/api/auth/admin/*.