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.