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

6.5 KiB
Raw Permalink Blame History

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/shared gains CreateManualSessionInput (worker id + activity + insole type + pair count + start_time/end_time + paused_seconds default 0 + optional notes) and AdminUpdateSessionInput (editable fields incl. nullable end_time/insole_type and a status), with their inferred types. Duration is never accepted from the client.
  • Task 2 — Backend admin session write endpoints + users roster (53c66ee). In apps/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, rejects end < start and paused > span, stores source='manual', status='completed', server-computed duration_seconds), PUT /api/admin/sessions/:id (recomputes duration; nullable end_time keeps an active row open), and POST …/:id/stop (folds any open pause span, end=now, completes) / …/:id/discard (status='discarded', end=now). All session responses go through toWorkSession. 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']). New screens/Sessions.tsx (title "Sessies", status <select> filter alle/actief/voltooid/geannuleerd, + Nieuwe registratie, a row per session with worked time + a Pauze … 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.tsx gets the /sessies route; table/form/action styles added to styles.css.
  • Task 4 — Admin create/edit session form (69b46be). components/SessionForm.tsx (mode: 'create' | 'edit'): create mode shows a worker picker (useAdminUsers) and posts CreateManualSessionInput; edit mode prefills from the session, hides the worker picker, and PUTs the changed fields. Activity <select>, insole-type toggles, pair-count stepper, start/end datetime-local (built into ISO), paused input, status <select> (edit only), notes, and a derived "gewerkt" line (end start paused). Wired into Sessions: + Nieuwe registratie opens create, ✎ opens edit.
  • Task 5 — Stop/Annuleer on the Live view (f1ec249). The active LiveCard gains 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). useActiveSessions gets refetchInterval: 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 --check over 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; test75 passed (12 files).
  • yarn workspace @solelog/admin typecheck — pass; test36 passed (7 files); build — pass (vite, 91 modules).
  • yarn workspace @solelog/worker typecheck — pass; test30 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.request in apps/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/sessions with paused_seconds: 600 over a 1 h span → source='manual', status='completed', duration_seconds === 3000 (3600 600); end < start → 400; unknown user/activity → 404; PUT edit recomputes duration; …/:id/stop on another worker's active session → completed with computed duration; …/:id/discardstatus='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/export is self-scoped).
  • User management via better-auth /api/auth/admin/*.