6.1 KiB
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):
- 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. - Reorderable handelingen. Admin sets the order; the worker picker follows it.
- 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"). - 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 (pausedMsaccumulator); the server session staysstatus='active',pausedunknown server-side.apps/api/src/routes/sessions.tsstop: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_sessionshas no pause columns;activitieshas nosort_order.- Worker logout sits on the Account tab →
BrowserRouterre-mounts at/account(#4).
A. Server-authoritative pause (#1 + #3)
Data model (migration 0003, via db:generate):
work_sessions.paused_seconds—integer NOT NULL DEFAULT 0(accumulated paused secs).work_sessions.paused_at—integer timestamp_ms NULL(set while paused; null = running).- Status stays
active | completed | discarded; a paused session is still active withpaused_atset, 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 → setpaused_at = now; else 409. Returns the updatedWorkSession.POST /api/sessions/:id/resume— paused →paused_seconds += round((now − paused_at)/1000), clearpaused_at; else 409.POST /api/sessions/:id/stop— changed: ifpaused_atset, fold the open span intopaused_secondsfirst; thenduration_seconds = round((end − start)/1000) − paused_seconds(clamp ≥ 0); setpaused_at = null, statuscompleted. 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_atset; 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_order — integer 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/activitiesorders by(sort_order, name).PUT /api/activities/reorder(admin-gated): body{ ids: number[] }(the full ordered id list) → assignssort_order = index. Validates the ids; returns the reordered list.POST /api/activitiessets new rows tosort_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 assignssort_orderby index andGETreturns ordered; create appends. - Worker: Stopwatch calls pause/resume; recovery restores paused state; History renders the
pauze line when
paused_seconds > 0; aftersignOutthe 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-plans → one Workflow (per the maintainer's standing preference),
~8 TDD tasks, commit per task, final verify pass. Tracked as a Plane epic.