From 1807f2b6d6ec64795b60d05a40595af2bacd3282 Mon Sep 17 00:00:00 2001 From: Bas van Rossem Date: Wed, 17 Jun 2026 21:24:16 +0200 Subject: [PATCH] docs: pause-accounting + reorder session log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finalize the pause-accounting + reorderable-handelingen + login-tab-fix feature: session log (goal/work/verification/outcome), a one-line roadmap status note, and an oxfmt pass over the changed files that strips a stray trailing comma after the last call argument in the worker Stopwatch (es5 trailing-comma style) — pure formatting, tests stay green. --- apps/worker/src/screens/Stopwatch.test.tsx | 24 +++-- apps/worker/src/screens/Stopwatch.tsx | 2 +- docs/roadmap.md | 2 +- .../2026-06-17-pause-reorder-loginfix.md | 99 +++++++++++++++++++ 4 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 docs/sessions/2026-06-17-pause-reorder-loginfix.md diff --git a/apps/worker/src/screens/Stopwatch.test.tsx b/apps/worker/src/screens/Stopwatch.test.tsx index d2f6813..108aacf 100644 --- a/apps/worker/src/screens/Stopwatch.test.tsx +++ b/apps/worker/src/screens/Stopwatch.test.tsx @@ -91,7 +91,7 @@ function renderStopwatch() { return render( - , + ); } @@ -104,14 +104,18 @@ describe('Stopwatch', () => { resumeMutate = vi.fn(); mockedUseActivities.mockReturnValue(query>([FREZEN, PRINTEN])); mockedUseActiveSessions.mockReturnValue(query>([])); - mockedUseStartSession.mockReturnValue(mutation>(startMutate)); + mockedUseStartSession.mockReturnValue( + mutation>(startMutate) + ); mockedUseStopSession.mockReturnValue(mutation>(stopMutate)); mockedUseDiscardSession.mockReturnValue( - mutation>(discardMutate), + mutation>(discardMutate) + ); + mockedUsePauseSession.mockReturnValue( + mutation>(pauseMutate) ); - mockedUsePauseSession.mockReturnValue(mutation>(pauseMutate)); mockedUseResumeSession.mockReturnValue( - mutation>(resumeMutate), + mutation>(resumeMutate) ); }); @@ -185,7 +189,7 @@ describe('Stopwatch', () => { const user = userEvent.setup(); // Recover an active session so the screen renders the running UI. mockedUseActiveSessions.mockReturnValue( - query>([activeSession()]), + query>([activeSession()]) ); renderStopwatch(); @@ -193,7 +197,7 @@ describe('Stopwatch', () => { await user.click(cancel); expect( - screen.getByRole('button', { name: 'Nogmaals tikken ter bevestiging' }), + screen.getByRole('button', { name: 'Nogmaals tikken ter bevestiging' }) ).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'Nogmaals tikken ter bevestiging' })); @@ -204,7 +208,7 @@ describe('Stopwatch', () => { it('pauses via the server when the display is tapped while running', async () => { const user = userEvent.setup(); mockedUseActiveSessions.mockReturnValue( - query>([activeSession()]), + query>([activeSession()]) ); renderStopwatch(); @@ -221,7 +225,7 @@ describe('Stopwatch', () => { mockedUseActiveSessions.mockReturnValue( query>([ activeSession({ paused_at: new Date().toISOString(), paused_seconds: 30 }), - ]), + ]) ); renderStopwatch(); @@ -237,7 +241,7 @@ describe('Stopwatch', () => { mockedUseActiveSessions.mockReturnValue( query>([ activeSession({ paused_at: new Date().toISOString(), paused_seconds: 30 }), - ]), + ]) ); renderStopwatch(); diff --git a/apps/worker/src/screens/Stopwatch.tsx b/apps/worker/src/screens/Stopwatch.tsx index dd1c606..108c453 100644 --- a/apps/worker/src/screens/Stopwatch.tsx +++ b/apps/worker/src/screens/Stopwatch.tsx @@ -118,7 +118,7 @@ export default function Stopwatch() { setPauseStartedMs(null); setNowMs(Date.now()); }, - }, + } ); } diff --git a/docs/roadmap.md b/docs/roadmap.md index 77994ba..0aa6720 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,7 +1,7 @@ # Insole Production Time Tracker — Rebuild Roadmap & Project Overview - **Created:** 2026-06-17 -- **Status:** Approved — living project doc; Phases 0–2 implemented + Phase **3a** implemented (`docs/superpowers/plans/2026-06-17-phase-3a-admin-panel.md`) +- **Status:** Approved — living project doc; Phases 0–2 implemented + Phase **3a** implemented (`docs/superpowers/plans/2026-06-17-phase-3a-admin-panel.md`) + server-authoritative **pause accounting** (worked-vs-paused duration), **reorderable handelingen** (admin ↑/↓), and the **login-tab fix** landed (`docs/superpowers/plans/2026-06-17-pause-reorder-loginfix.md`) - **Type:** Greenfield rebuild of an inherited app - **Tracked in git** under `docs/` (the project's documentation source of truth). diff --git a/docs/sessions/2026-06-17-pause-reorder-loginfix.md b/docs/sessions/2026-06-17-pause-reorder-loginfix.md new file mode 100644 index 0000000..420244e --- /dev/null +++ b/docs/sessions/2026-06-17-pause-reorder-loginfix.md @@ -0,0 +1,99 @@ +# Session: 2026-06-17 — Pause accounting + reorderable handelingen + login-tab fix + +## Goal + +Ship four maintainer-reported items grouped around one root change (server-authoritative +pause): + +1. **Admin shows paused sessions as running** — pause was client-only, so the admin live + view (filters `status='active'`) could not tell paused from running. +2. **Reorderable handelingen** — admin sets the order with ↑/↓ arrows; the worker picker + follows. +3. **Saved duration ignored pause** — stop stored wall-clock `(end − start)`, but the + stopwatch displayed *worked* time. Now save worked time and store paused time too. +4. **Wrong default tab after re-login** — logout on the Account tab left the URL at + `/account`, so the next login re-mounted there instead of the Stopwatch. + +Spec: `docs/superpowers/specs/2026-06-17-pause-reorder-loginfix-design.md`; +plan: `docs/superpowers/plans/2026-06-17-pause-reorder-loginfix.md`. + +## Work done + +Implemented task-by-task per the plan (TDD throughout), one commit per task: + +- **Task 1 — Contracts + schema + migration + mappers** (`0d82b6e`). `@solelog/shared`: + `WorkSession` gains `paused_seconds: number` + `paused_at: string | null`; `Activity` + gains `sort_order: number`; new `ReorderActivitiesInput`. Schema: `work_sessions` + `paused_seconds` (int NOT NULL default 0) + `paused_at` (timestamp_ms nullable); + `activities.sort_order` (int NOT NULL default 0). Migration `0003` generated via + `db:generate` and applied. `toWorkSession`/`toActivity` map the new fields. +- **Task 2 — Pause/resume endpoints + stop math + CSV column** (`974ecb1`). + `POST /api/sessions/:id/pause` (active + not paused → set `paused_at`, else 409), + `POST /api/sessions/:id/resume` (paused → accumulate `paused_seconds`, clear `paused_at`, + else 409). `stop` folds any open pause span into `paused_seconds`, then + `duration_seconds = max(0, wall − paused_seconds)`. `/api/export` gains a + `Paused Duration` column (after `Total Duration`). +- **Task 3 — Orderable activities + reorder endpoint** (`56e0162`). `GET /api/activities` + orders by `(sort_order, name)`; `POST` appends with `max(sort_order)+1`; + `PUT /api/activities/reorder` (admin-gated, registered *before* `:id` routes) assigns + `sort_order = index`, validates the id set (unknown/missing → 400), and returns the + reordered list. +- **Task 4 — Worker Stopwatch server pause + recovery** (`ce396ec`). `usePauseSession` / + `useResumeSession` hooks (invalidate `['sessions']`); tapping the display calls + pause/resume; the local clock stays for snappy feel but the server is source of truth. + Recovery-on-load seeds `isPaused`/`pausedMs`/`pauseStartedMs` from `paused_at` / + `paused_seconds`. +- **Task 5 — Worker History paused line + login-tab fix** (`1765f40`). History card shows + a grey "Pauze H:MM:SS" pill when `paused_seconds > 0`. `AuthContext.signOut` does + `window.history.replaceState(null, '', '/')` before clearing auth so the next login lands + on the Stopwatch. +- **Task 6 — Admin Live paused state + login-tab fix** (`0b0a6bd`). A "Gepauzeerd" badge + when `paused_at` is set; the elapsed timer freezes (worked = + `(paused_at − start) − paused_seconds`) and a paused total is shown. Admin + `AuthContext.signOut` gets the same path reset (lands on Live). +- **Task 7 — Admin Activities ↑/↓ reorder** (`e48df48`). `useReorderActivities()` → + `PUT /api/activities/reorder`. Each non-editing row gets ↑/↓ buttons (aria-labels + "Verplaats omhoog/omlaag"), disabled at the ends, swapping with the neighbour and + firing the mutation. +- **Task 8 — Docs, lint, verification** (this task). Lint/format on the feature files, full + green matrix, an in-process live smoke, and this session log + roadmap note. + +## Verification (Task 8) + +- `npx oxlint` — clean (exit 0). +- `npx oxfmt` on the feature-changed files only — reformatted two Task 4 files + (`apps/worker/src/screens/Stopwatch.tsx` + `.test.tsx`) that carried a stray trailing + comma after the last call argument (es5 trailing-comma style strips it); pure formatting, + worker tests + typecheck stay green afterward. All other feature files were already clean. +- `yarn workspace @solelog/api typecheck` — pass; `test` — **60 passed** (12 files). +- `yarn workspace @solelog/worker typecheck` — pass; `test` — **28 passed** (8 files); + `build` — pass (vite, 91 modules). +- `yarn workspace @solelog/admin typecheck` — pass; `test` — **21 passed** (5 files); + `build` — pass (vite, 89 modules). +- **Live smoke** — driven **in-process** (`createApp()` + `app.request`) against a real + on-disk SQLite file freshly migrated to `0003`; no server started, so port 3000 was never + bound (avoids the Windows libsql lock trap). Worker: start → pause (`paused_at` set, still + active) → resume (`paused_seconds = 2`, `paused_at` cleared) → stop + (`duration_seconds = 2`, `paused_seconds = 2`, wall ≈ 4; `duration + paused ≈ wall` and + `duration < wall`). Admin: `PUT /api/activities/reorder` → `GET /api/activities` reflects + the new order; a worker reorder → 403. The smoke script + temp DB were deleted afterward; + port 3000 confirmed free. + +## Outcome + +The feature is implemented and green across all three workspaces. Pause is now +server-authoritative: the admin live view shows a "Gepauzeerd" badge with a frozen timer, +the stored `duration_seconds` is worked time (paused time stored separately and surfaced in +the worker History and the CSV `Paused Duration` column), admins reorder handelingen with +↑/↓ arrows (the worker picker inherits the order), and logging out resets the route to `/` +in both clients so the next login lands on Stopwatch (worker) / Live (admin). + +The two unrelated working-tree edits to `.env.prod.example` / `docker-compose.prod.yml` +(deploy SQLite bind-mount config) were left untouched — out of this feature's scope. + +## Next + +- Phase 3b inherits the paused fields through `/api/admin/sessions` (via `toWorkSession`): + the all-sessions list / reports view should show worked + paused per the design. +- Admin pause/resume/stop of *another worker's* session remains Phase 3b (manual-entry / + admin-control work); pause stayed a worker action here.