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

117 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_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** 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/stop`**changed**: 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_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/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-plans`**one Workflow** (per the maintainer's standing preference),
~8 TDD tasks, commit per task, final verify pass. Tracked as a Plane epic.