docs: pause-accounting + reorder session log
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.
This commit is contained in:
@@ -91,7 +91,7 @@ function renderStopwatch() {
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Stopwatch />
|
||||
</QueryClientProvider>,
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,14 +104,18 @@ describe('Stopwatch', () => {
|
||||
resumeMutate = vi.fn();
|
||||
mockedUseActivities.mockReturnValue(query<ReturnType<typeof useActivities>>([FREZEN, PRINTEN]));
|
||||
mockedUseActiveSessions.mockReturnValue(query<ReturnType<typeof useActiveSessions>>([]));
|
||||
mockedUseStartSession.mockReturnValue(mutation<ReturnType<typeof useStartSession>>(startMutate));
|
||||
mockedUseStartSession.mockReturnValue(
|
||||
mutation<ReturnType<typeof useStartSession>>(startMutate)
|
||||
);
|
||||
mockedUseStopSession.mockReturnValue(mutation<ReturnType<typeof useStopSession>>(stopMutate));
|
||||
mockedUseDiscardSession.mockReturnValue(
|
||||
mutation<ReturnType<typeof useDiscardSession>>(discardMutate),
|
||||
mutation<ReturnType<typeof useDiscardSession>>(discardMutate)
|
||||
);
|
||||
mockedUsePauseSession.mockReturnValue(
|
||||
mutation<ReturnType<typeof usePauseSession>>(pauseMutate)
|
||||
);
|
||||
mockedUsePauseSession.mockReturnValue(mutation<ReturnType<typeof usePauseSession>>(pauseMutate));
|
||||
mockedUseResumeSession.mockReturnValue(
|
||||
mutation<ReturnType<typeof useResumeSession>>(resumeMutate),
|
||||
mutation<ReturnType<typeof useResumeSession>>(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<ReturnType<typeof useActiveSessions>>([activeSession()]),
|
||||
query<ReturnType<typeof useActiveSessions>>([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<ReturnType<typeof useActiveSessions>>([activeSession()]),
|
||||
query<ReturnType<typeof useActiveSessions>>([activeSession()])
|
||||
);
|
||||
renderStopwatch();
|
||||
|
||||
@@ -221,7 +225,7 @@ describe('Stopwatch', () => {
|
||||
mockedUseActiveSessions.mockReturnValue(
|
||||
query<ReturnType<typeof useActiveSessions>>([
|
||||
activeSession({ paused_at: new Date().toISOString(), paused_seconds: 30 }),
|
||||
]),
|
||||
])
|
||||
);
|
||||
renderStopwatch();
|
||||
|
||||
@@ -237,7 +241,7 @@ describe('Stopwatch', () => {
|
||||
mockedUseActiveSessions.mockReturnValue(
|
||||
query<ReturnType<typeof useActiveSessions>>([
|
||||
activeSession({ paused_at: new Date().toISOString(), paused_seconds: 30 }),
|
||||
]),
|
||||
])
|
||||
);
|
||||
renderStopwatch();
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ export default function Stopwatch() {
|
||||
setPauseStartedMs(null);
|
||||
setNowMs(Date.now());
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
99
docs/sessions/2026-06-17-pause-reorder-loginfix.md
Normal file
99
docs/sessions/2026-06-17-pause-reorder-loginfix.md
Normal file
@@ -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 <naam> 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.
|
||||
Reference in New Issue
Block a user