Files
solelog/docs/superpowers/specs/2026-06-17-pause-reorder-loginfix-design.md

6.1 KiB
Raw Blame History

Pause Accounting + Reorderable Handelingen + Login-Tab Fix — Design

  • Created: 2026-06-17
  • Status: Approved (brainstorming) — ready for implementation plan
  • Tracker: Plane (workspace solelog, project SoleLog)
  • Touches: packages/shared, apps/api, apps/worker, apps/admin

Goal

Four maintainer-reported items, grouped because three of them share one root change (server-authoritative pause):

  1. Admin shows paused sessions as running. Pause is client-only today, so the admin live view (filters status='active') can't tell paused from running.
  2. Reorderable handelingen. Admin sets the order; the worker picker follows it.
  3. Saved duration ignores pause. Stop stores wall-clock (end start), but the stopwatch displays worked time. Save worked time, and store paused time too ("gewerkt 1:00 · pauze 0:20").
  4. Wrong default tab after re-login. Logout happens on the Account tab, leaving the URL at /account; the next login re-mounts there instead of Stopwatch.

Root-cause findings (from current code)

  • apps/worker/src/screens/Stopwatch.tsx: pause is purely client-side (pausedMs accumulator); the server session stays status='active', paused unknown server-side.
  • apps/api/src/routes/sessions.ts stop: durationSeconds = round((end start)/1000) — wall-clock, includes paused time. The worker's displayed elapsed already excludes pause, hence the mismatch (#3).
  • apps/api/src/db/schema.ts: work_sessions has no pause columns; activities has no sort_order.
  • Worker logout sits on the Account tab → BrowserRouter re-mounts at /account (#4).

A. Server-authoritative pause (#1 + #3)

Data model (migration 0003, via db:generate):

  • work_sessions.paused_secondsinteger NOT NULL DEFAULT 0 (accumulated paused secs).
  • work_sessions.paused_atinteger timestamp_ms NULL (set while paused; null = running).
  • Status stays active | completed | discarded; a paused session is still active with paused_at set, so no existing status filter changes.

Shared contract WorkSession: add paused_seconds: number and paused_at: string | null (ISO). toWorkSession maps both.

Endpoints (sessions.ts, user-scoped exactly like stop/discard):

  • POST /api/sessions/:id/pause — active + not already paused → set paused_at = now; else 409. Returns the updated WorkSession.
  • POST /api/sessions/:id/resume — paused → paused_seconds += round((now paused_at)/1000), clear paused_at; else 409.
  • POST /api/sessions/:id/stopchanged: if paused_at set, fold the open span into paused_seconds first; then duration_seconds = round((end start)/1000) paused_seconds (clamp ≥ 0); set paused_at = null, status completed. Stores worked + paused.

Worker Stopwatch.tsx: pause/resume call the new endpoints (usePauseSession / useResumeSession); keep the local clock for snappy feel, server is source of truth. Recovery-on-load restores paused_at/paused_seconds (today it forces running). Displayed elapsed remains worked time; isPaused derives from paused_at.

B. Paused-time display (#3 display)

  • Worker History card: "Gewerkt H:MM:SS" + (if paused_seconds > 0) a grey "Pauze H:MM:SS".
  • Admin Live + admin sessions views: a "Gepauzeerd" badge when paused_at set; the elapsed timer freezes while paused (worked = (paused_at start) paused_seconds), and paused total shown.
  • CSV export (/api/export): new "Paused Duration" column (formatDuration(paused_seconds)); the existing "Total Duration" stays = worked.

C. Reorderable handelingen (#2) — arrow buttons

Data model (same 0003): activities.sort_orderinteger NOT NULL DEFAULT 0. Existing rows get 0; GET orders by (sort_order ASC, name ASC) so current alphabetical order is preserved until an admin reorders. Activity contract gains sort_order: number.

Endpoints (activities.ts):

  • GET /api/activities orders by (sort_order, name).
  • PUT /api/activities/reorder (admin-gated): body { ids: number[] } (the full ordered id list) → assigns sort_order = index. Validates the ids; returns the reordered list.
  • POST /api/activities sets new rows to sort_order = max(sort_order)+1 (append).

Admin Activities screen: each row gets ↑/↓ buttons (disabled at the ends) that swap with the neighbour and fire the reorder mutation (invalidate ['activities']). No new dependency. Worker picker: inherits the order from GET — no worker UI change.

D. Login-tab fix (#4)

Worker AuthContext.signOut resets the path to / (window.history.replaceState(null, '', '/')) before clearing auth, so the next authed mount starts on Stopwatch. The admin AuthContext.signOut gets the same one-liner (lands on Live) for consistency.

Error handling

  • pause/resume/stop on a non-owned or wrong-state session → 404/409 as today; client surfaces nothing intrusive (the active-session query reconciles).
  • reorder with unknown/missing ids → 400.
  • Clamp negative worked durations to 0 (guards clock skew / odd pause data).

Testing

  • API: pause sets paused_at; resume accumulates; stop excludes paused and folds an open pause span; reorder assigns sort_order by index and GET returns ordered; create appends.
  • Worker: Stopwatch calls pause/resume; recovery restores paused state; History renders the pauze line when paused_seconds > 0; after signOut the path is / so login shows Stopwatch.
  • Admin: Live shows "Gepauzeerd" + frozen timer; Activities ↑/↓ fire the reorder mutation with the swapped order; paused total shown in session views.

Out of scope

  • Admin pausing/resuming/stopping another worker's session (that's the Phase 3b manual-entry/admin-control work). Pause remains a worker action here.
  • Drag-and-drop reordering (arrows chosen; DnD would add dnd-kit against the dependency-light goal).

Build approach

One spec → writing-plansone Workflow (per the maintainer's standing preference), ~8 TDD tasks, commit per task, final verify pass. Tracked as a Plane epic.