Commit Graph

91 Commits

Author SHA1 Message Date
Bas van Rossem
75c5a9f350 test(admin): Activities + app/nav — fill gaps + lifecycle test
Fill coverage gaps and lock in behavior for the Activities screen and
app/nav (Sidebar) domain (plan task B-D6). All four domain src files
(screens/Activities.tsx, api/activities.ts, App.tsx, components/Sidebar.tsx)
now at 100% stmts/branch/funcs/lines.

- Activities.test.tsx: insole-type toggle select/deselect (both branches),
  reduced-subset create, add disabled when no types, edit insole types,
  cancel-without-save, empty-types-defaults-all editor, empty state,
  error-falls-back-to-empty, and the create->edit types->reorder->delete
  lifecycle test.
- Sidebar.test.tsx (new): brand + nav links + email, active-route highlight,
  end-match keeps Live inactive off-root, nav click switches screen + moves
  highlight, Uitloggen calls signOut, email hidden without useMe data.
- Activities.tsx: three /* c8 ignore next */ annotations on the defensive
  early-return guards in handleAdd/handleSave/handleMove (each mirrored by a
  disabled button, so unreachable via the UI). Comment-only, no behavior change.

No bugs found — the domain code was already correct; the tests lock it in.
2026-07-23 15:18:59 +02:00
Bas van Rossem
8a6bc9ea7d test(admin): Users — fill gaps + lifecycle test 2026-07-23 15:07:12 +02:00
Bas van Rossem
5aa6febd65 test(admin): Reports + date-range — fill gaps + lifecycle test 2026-07-23 14:53:38 +02:00
Bas van Rossem
e7eefe8f62 test(admin): Sessions + SessionForm — fill gaps + lifecycle test
Drives Sessions.tsx and SessionForm.tsx to 100% stmts/branch/funcs/lines.

Added gap tests: sessions error state, in-filter empty message, row
placeholders (missing worker/activity/type/date), frozen worked-time for a
paused active session, edit-form open, and form cancel; SessionForm create/edit
validation paths, non-400 error surfacing, null serialization (empty zooltype +
notitie, status=active clears end_time), pair/pause floors, and the defensive
no-session edit guard.

Lifecycle test (Sessions.test.tsx): open create form -> fill a valid manual
session -> it appears in the list -> edit the row -> the change persists, driven
by a stateful apiFetch mock.

Annotated one unreachable branch: localToIso's Number.isNaN guard is c8-ignored
(a datetime-local input only yields a valid datetime or empty).

No bugs found; no behavior changes.
2026-07-23 14:44:32 +02:00
Bas van Rossem
77db308e41 test(admin): Live view — fill gaps + lifecycle test
Drove Live.tsx from 93.24% to 100% (stmts/branch/funcs/lines); elapsed.ts already 100%. Added tests for the error state, non-array payload fallback, placeholder labels (missing name/activity/type), the busy/disabled action buttons, the loading state, and a combined running+paused lifecycle test. No production behavior changed; no bugs found.
2026-07-23 14:30:26 +02:00
Bas van Rossem
64911a0052 test(admin): Auth (admin-only gate) — fill gaps + lifecycle test 2026-07-23 14:25:04 +02:00
Bas van Rossem
e174c29a77 test(worker): lib/api + app/nav — fill gaps + lifecycle test 2026-07-23 14:15:48 +02:00
Bas van Rossem
32bdaa3e31 test(worker): Account — fill gaps + lifecycle test 2026-07-23 14:07:14 +02:00
Bas van Rossem
13cbc50c4f test(worker): History — fill gaps + lifecycle test 2026-07-23 14:03:04 +02:00
Bas van Rossem
36d4754722 test(worker): Auth (context/storage/login) — fill gaps + lifecycle test
Drive the worker Auth domain to 100% coverage:
- AuthContext.tsx: 91.3/80 -> 100/100 (signIn body, mount-restore branches, useAuth guard)
- Login.tsx: 78/50 -> 100/100 (handleSubmit success/error/pending, submit disable)
- auth-storage.ts: already 100 (no change)

New Login.test.tsx adds the domain lifecycle test: from the unauthenticated app,
a successful login stores the token and flips the context to the authenticated
tabbed shell. Extended AuthContext.test.tsx covers signIn delegation, mount
restore (token present/absent), and the out-of-provider throw.

No bugs found; code was already correct. Note: the design checklist's
'clears on 401' worker behavior is not implemented (no global 401 interceptor)
and is left as-is (missing feature, not a defect).
2026-07-23 13:50:33 +02:00
Bas van Rossem
9c99d56f99 test(api): Admin users — fill gaps + lifecycle test
Fill the remaining coverage gaps in src/routes/admin-users.ts and add the
domain lifecycle test, bringing the file to 100% stmts/branch/funcs/lines.

New tests (apps/api/test/admin-users.test.ts):
- password route: 404 unknown id; 400 when better-auth setUserPassword throws
  (driven via a spy so the catch path is deterministic).
- toListItem: a null DB role lists as 'worker' (nullish-coalescing fallback).
- role route: 400 on an invalid role body; 404 on an unknown id.
- reactivate route: 404 on an unknown id.

Lifecycle (apps/api/test/integration/user-lifecycle.test.ts): admin creates a
worker -> worker signs in and the bearer authorizes /api/me -> admin deactivates
(session rows deleted, so the pre-issued token 401s and fresh sign-in is blocked)
-> admin reactivates -> sign-in works again.

The two last-admin guards (role + deactivate routes) are unreachable over HTTP
(the self-demote / self-deactivate guards above already handle caller===target,
so any caller reaching them is a distinct active admin and
activeAdminsExcluding(target) >= 1). Annotated with /* c8 ignore */ + reasons
rather than left as silent gaps; the guard logic is unit-tested via
activeAdminsExcluding.

No production behavior changed; no bugs found.
2026-07-23 13:43:59 +02:00
Bas van Rossem
31d13a8381 test(api): Reports + admin export — fill gaps + lifecycle test
Drives the report/export parts of src/routes/admin.ts to 100%
stmts/branch/funcs/lines. Adds one lifecycle test plus gap tests for the
previously-uncovered branches; no production behavior changed.

Tests added (apps/api/test):
- report.test.ts: invalid insole_type / non-numeric activity_id → 400;
  activity filter narrows; null insole_type bucketed "Onbekend"; null
  duration counts as 0 worked; export 400 on missing/inverted range.
  Migrated the report + export describes to resetDb(beforeEach) so exact
  totals are order-independent (removed the activity_id isolation
  workaround in the all-users export test).
- integration/report-aggregation.test.ts: lifecycle — 2 workers × 2
  activities × 2 types → exact totals + every by_worker/by_activity/
  by_type row (values and worked-desc order) → user_id filter narrows.

Defensive annotations in admin.ts report loop (behavior unchanged):
- pausedSeconds/pairCount ?? 0 — NOT NULL columns, DB-impossible nulls.
- user_name/activity_name ?? 'Onbekend' — unreachable; userId FK cascades
  on delete and activityId FK is enforced (libsql enforces FKs), so every
  session always joins to a real user and activity.

No bugs found; the code was already correct.
2026-07-22 13:03:04 +02:00
Bas van Rossem
bdf65ae685 test(api): Admin sessions — fill gaps + lifecycle test
Fills the session-part coverage gaps in src/routes/admin.ts (cross-user list,
active list, manual create/edit, admin stop/discard) and adds the admin↔worker
lifecycle test.

New route-level tests (apps/api/test/admin.test.ts):
- manual create: 400 on schema-invalid body; 400 when paused_seconds > span.
- edit: 404 non-numeric id; 400 schema-invalid body; 400 unparseable start_time;
  400 paused_seconds > span; 404 unknown activity.
- stop: 404 non-numeric/unknown id; folds an open pause into paused_seconds.
- discard: 404 non-numeric/unknown id; 409 when already closed.

Lifecycle (apps/api/test/integration/admin-worker-session.test.ts): worker
starts → admin stops via the admin endpoint → session lands in /api/admin/report
and /api/admin/export with matching totals (uses resetDb for exact counts).

admin.ts session parts now at 100% stmts/lines; every session-part branch
covered. The one remaining session-part branch (row.pausedSeconds ?? 0 in the
stop route) is c8-ignored — paused_seconds is NOT NULL DEFAULT 0, so the null
branch is DB-impossible. The remaining uncovered branches in admin.ts are the
report/export parts, owned by the Reports domain (B-A6).

No behavior-changing bugs found; all session-part code was already correct.
2026-07-22 12:41:52 +02:00
Bas van Rossem
b72076ee15 test(api): Worker export/CSV — fill gaps + lifecycle test
Drove src/lib/csv.ts to 100% stmts/branch/funcs/lines (was 97.95%/55.55%):
- added a null-fallback row test covering every ?? default (activityName,
  userName, insoleType→Kurk, pairCount→2, duration/paused→0) and the null
  end-time branch (line 65).
- added CSV-escaping edge tests: commas, embedded quotes, and newlines inside
  a field stay inside the quoted cell.
- added formatDuration edge (hours >99 → 100:00:00).
- added a /api/export ordering-by-start-time behavior test and adopted
  resetDb per-test isolation in the export suite.

No bugs found; CSV code behaves correctly. sessions.ts /api/export route
already at 100%. Full api suite green (150 tests).
2026-07-22 12:24:04 +02:00
Bas van Rossem
9ecbf952d3 test(api): Work-sessions — fill gaps + lifecycle test
Drives src/routes/sessions.ts and src/lib/work-session.ts to 100%
stmts/branch/funcs/lines (branch was 84.12% / 83.33%).

- sessions.test.ts: added error/edge branch tests — malformed-JSON start
  (400), non-numeric id on pause/resume/stop/discard (404), pause/resume of a
  no-longer-active session (409), discard missing (404) + already-closed (409),
  stop missing (404), and discard ownership scoping (user B → 404).
- work-session.test.ts: mapper unit tests — null insoleType, nullish
  pausedSeconds → 0, null durationSeconds/notes, full-field shape with
  activity/user opts, and opts defaulting to undefined.
- test/integration/worker-session-lifecycle.test.ts: the anchor lifecycle test
  (start→pause→resume→stop→history→export).

No bugs found — the code was correct. The two remaining uncovered branches were
the defensive `row.pausedSeconds ?? 0` guards in resume/stop; pausedSeconds is
NOT NULL DEFAULT 0 in the schema, so the null branch is unreachable via any
route. Annotated both with `/* c8 ignore next -- reason */` (comment only, no
behavior change).
2026-07-22 12:14:32 +02:00
Bas van Rossem
af7251398c test(api): Activities — fill gaps + lifecycle test 2026-07-22 12:01:03 +02:00
Bas van Rossem
e6e4ca0a1e test(api): Auth & /me — fill gaps + lifecycle test 2026-07-22 11:50:32 +02:00
Bas van Rossem
2fa8aa9154 test(api): Infra (health/db/schema/seed/env/migrate) — fill gaps + lifecycle test
Drives all infra src files to 100% (env.ts, db/client.ts, db/ensure-dir.ts,
db/migrate.ts, db/seed.ts, routes/health.ts).

- env.test.ts: dev defaults when unset, overrides when set (PORT coerced),
  CORS_ORIGINS parsing (trim + drop empties) and empty-fallback.
- db.test.ts: runMigrations idempotency; ensureDbDir no-op for non-file URLs
  and bare filenames (branch coverage).
- seed.test.ts: production guard — no dev users seeded when NODE_ENV=production.
- c8-ignore the migrate.ts/seed.ts CLI direct-run blocks (unreachable under vitest).

No lifecycle test (infra domain, per plan). No bugs found — infra code correct;
only coverage gaps. All 112 API tests green; typecheck + oxlint clean.
2026-07-22 11:39:34 +02:00
Bas van Rossem
f872daf117 test(api): add resetDb helper + per-test isolation (proof: sessions suite) 2026-07-22 11:28:01 +02:00
Bas van Rossem
3c79e51e99 test: add @vitest/coverage-v8 + test:coverage to all workspaces 2026-07-22 11:28:01 +02:00
Bas van Rossem
b9aa24c5ae harden(api): adminGuard rejects banned admins; fix mixed-case create + real last-admin guard coverage 2026-06-24 23:12:40 +02:00
Bas van Rossem
0f42960f00 feat(admin): Gebruikers screen (list, create, role, password, deactivate) 2026-06-24 23:03:34 +02:00
Bas van Rossem
4b80898b0e feat(api): admin reset-password endpoint 2026-06-24 22:57:35 +02:00
Bas van Rossem
40977f8abd feat(api): deactivate/reactivate users (ban + revoke sessions) with guards 2026-06-24 22:54:02 +02:00
Bas van Rossem
88b7773317 feat(api): set user role with self-demote + last-admin guards 2026-06-24 22:50:52 +02:00
Bas van Rossem
2b4b633961 feat(api): admin-users router with enriched roster + user creation 2026-06-24 22:46:59 +02:00
Bas van Rossem
a32406de69 refactor(api): extract adminGuard + add user-management contracts 2026-06-24 22:42:35 +02:00
Bas van Rossem
8d75be0462 feat(admin): Rapporten screen with filters, totals, breakdowns + CSV export 2026-06-24 16:40:25 +02:00
Bas van Rossem
8ad2e69ec3 feat(admin): reports API client (useReport + downloadExport) 2026-06-24 16:33:28 +02:00
Bas van Rossem
d33fcb7cce feat(api): admin all-users filtered CSV export 2026-06-24 16:30:17 +02:00
Bas van Rossem
4b213e25a8 feat(api): admin report endpoint + ReportResponse contracts 2026-06-24 16:25:36 +02:00
Bas van Rossem
01fa18f401 refactor(api): extract buildSessionsCsv shared by worker + admin exports 2026-06-24 16:22:06 +02:00
Bas van Rossem
7d3daaa760 feat(worker): poll + reconcile stopwatch to server (admin stop/cancel) 2026-06-17 22:50:31 +02:00
Bas van Rossem
f1ec249ee7 feat(admin): stop/cancel an active session from the live view 2026-06-17 22:47:05 +02:00
Bas van Rossem
69b46bee04 feat(admin): manual session create/edit form
Add the shared SessionForm component (create/edit) used by the Sessies screen.
- Create mode: worker picker (useAdminUsers), activity (useActivities), zooltype,
  pair count, start/end datetime-local, pauze, notitie; posts CreateManualSessionInput.
- Edit mode: hides the worker picker, prefills from the row session, adds a status
  select; PUTs AdminUpdateSessionInput (end_time nulled when status=active).
- Builds ISO start_time/end_time from the datetime-local values; live "gewerkt"
  preview = end - start - paused; inline error on a 400.
- Wires the form into Sessions.tsx ("+ Nieuwe registratie" -> create, pencil -> edit).

Products affected: SoleLog admin client
2026-06-17 22:44:06 +02:00
Bas van Rossem
b5366413b6 feat(admin): sessions management screen (list + filter + actions) 2026-06-17 22:38:28 +02:00
Bas van Rossem
53c66ee144 feat(api): admin manual-session create/edit/stop/discard + users roster
Sub
Phase 3b.1 needs admin-side write paths so an admin can hand-create a
session for a worker, correct any session, and stop/cancel a worker's
stuck active session — duration always derived server-side.

Adds under the existing /api/admin/* admin guard:
- GET /api/admin/users — {id,name,email} roster ordered by name (worker picker)
- POST /api/admin/sessions — manual create -> completed, source=manual,
  duration = max(0, round((end-start)/1000) - paused)
- PUT /api/admin/sessions/:id — edit any session (no user reassignment);
  recomputes duration when end_time present, else duration null
- POST /api/admin/sessions/:id/stop — fold open pause, end=now, completed
- POST /api/admin/sessions/:id/discard — status=discarded, end=now

Validates end>=start, pair_count>=1, paused<=span, activity/user exist
(400/404); stop/discard on an already-closed session -> 409. Responses
reuse toWorkSession.

Products affected: SoleLog backend (apps/api)
2026-06-17 22:32:58 +02:00
Bas van Rossem
70ac27ec8e style: align oxfmt to trailing-comma 'all' and normalize code
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s
The repo was authored prettier-style (trailing-comma 'all') but .oxfmtrc.json
was set to 'es5', so every formatted file diverged. Switch the config to 'all'
to match the existing code, ignore docs/** and **/drizzle/** (prose + generated
snapshots the formatter should not own), and reformat the source tree once for
consistency. No behavioural change; all suites green (api 60, worker 28, admin 21).
2026-06-17 21:36:18 +02:00
Bas van Rossem
1807f2b6d6 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.
2026-06-17 21:24:16 +02:00
Bas van Rossem
e48df48376 feat(admin): reorder handelingen with up/down arrows 2026-06-17 21:18:07 +02:00
Bas van Rossem
0b0a6bd073 feat(admin): show paused sessions in live view; reset to live on logout
- Live cards freeze the worked timer at paused_at and show an amber
  "Gepauzeerd" badge plus a "Pauze H:MM:SS" total when paused.
- AuthContext.signOut resets the path to / so the next admin login lands
  on Live rather than the tab it logged out from.
2026-06-17 21:14:15 +02:00
Bas van Rossem
a7c8925b3c feat(deploy): build + serve worker and admin as static nginx images
All checks were successful
Build and Push Docker Image / build (push) Successful in 53s
- per-app Dockerfiles (vite build → nginx) + SPA nginx.conf
- Gitea workflow pushes 3 images; frontends bake VITE_API_URL
- docker-compose.prod.yml (registry images, solelog_network) + .env.prod.example
- docker-compose.yml runs the full stack locally; add .dockerignore
2026-06-17 21:11:32 +02:00
Bas van Rossem
1765f4036c fix(worker): show paused time in history; reset to stopwatch on logout 2026-06-17 21:10:01 +02:00
Bas van Rossem
ce396ecf2d feat(worker): server-authoritative pause/resume on the stopwatch 2026-06-17 21:06:10 +02:00
Bas van Rossem
56e0162230 feat(api): orderable activities + admin reorder endpoint 2026-06-17 20:58:06 +02:00
Bas van Rossem
974ecb120d feat(api): server-authoritative pause/resume + worked-time stop + CSV paused
Add user-scoped POST /api/sessions/:id/pause and /resume endpoints,
mirroring the stop handler's ownership/lookup and 401/404/409 guards.
Pause sets paused_at (status stays active); resume folds the open span
into paused_seconds and clears paused_at.

Change stop to fold any open pause span into paused_seconds, then set
duration_seconds = max(0, round((end-start)/1000) - paused_seconds) so
saved duration is worked time, and clear paused_at.

Add a "Paused Duration" column to /api/export (after "Total Duration")
using formatDuration(paused_seconds).

Products affected: SoleLog backend (apps/api)
2026-06-17 20:54:42 +02:00
Bas van Rossem
0d82b6efbc feat(shared,api): add pause + sort_order columns and contracts
Adds server-side pause accounting and activity ordering primitives.
- WorkSession contract gains paused_seconds (number) and paused_at (ISO string | null).
- Activity contract gains sort_order (number); new ReorderActivitiesInput zod.
- work_sessions += paused_seconds (int NOT NULL DEFAULT 0) + paused_at (timestamp_ms nullable).
- activities += sort_order (int NOT NULL DEFAULT 0).
- toWorkSession / toActivity map the new fields; generated migration 0003.

The new fields are additive; existing api/worker/admin tests stay green.

Products affected: SoleLog backend (apps/api), shared contracts (packages/shared)
2026-06-17 20:49:56 +02:00
Bas van Rossem
eae9a53a26 fix(docker): drop COPY .yarn/ — dir is gitignored, absent in CI builds
All checks were successful
Build and Push Docker Image / build (push) Successful in 37s
2026-06-17 20:45:53 +02:00
Bas van Rossem
76ad235c9f docs(admin): phase 3a session log + roadmap status 2026-06-17 19:17:20 +02:00
Bas van Rossem
c0d9d21991 feat(admin): activity management (handelingen CRUD) 2026-06-17 19:11:50 +02:00