docs: spec + plan for pause accounting, reorder, login-tab fix
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user