docs(admin): phase 3a spec + implementation plan

This commit is contained in:
Bas van Rossem
2026-06-17 18:46:11 +02:00
parent 6fce7a7197
commit 7cdc88e824
2 changed files with 436 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
# Phase 3a — Admin Panel (MVP) Design
- **Created:** 2026-06-17
- **Status:** Approved (brainstorming) — ready for implementation plan
- **Phase:** 3a (first of two slices of roadmap Phase 3; see `docs/roadmap.md` §9)
- **Tracker:** Plane (workspace `solelog`, project SoleLog)
## Goal
A working `apps/admin` desktop web app where an admin logs in, watches who is working
**live**, and manages **handelingen** (activities). Everything consumes existing backend
endpoints plus one tiny backend touch (`role` on `/api/me`). Reports/export, user
management, and manual session entry/edit are explicitly deferred to **Phase 3b**.
_Done when:_ an admin can sign in to the admin app, see who is working right now (auto-
refreshing), and add/edit/delete activities. A worker who signs in with valid credentials
is rejected with "Geen toegang".
## Scope decisions (confirmed during brainstorming, 2026-06-17)
1. **Slice** → MVP-first, two cycles. 3a = shell + admin login + live view + activity
management (existing endpoints). 3b = reports/export + user management + manual
entry/edit.
2. **Auth** → reuse the worker's bearer-token flow (`POST /api/auth/sign-in/email`
capture `set-auth-token` → localStorage → `Authorization: Bearer`). Gate admin access
by reading `role` from `/api/me`; non-admins are signed out with "Geen toegang".
3. **Layout** → fixed left sidebar + content area (scales as 3b adds sections).
4. **Live refresh** → react-query `refetchInterval: 5000` (5s); elapsed time ticks
client-side every second.
5. **Live view is read-only in 3a** — no stop/fix button (admin-stopping another worker's
session needs a new backend endpoint → 3b).
## Architecture
```
apps/admin/ (new Vite + React 18 + TS SPA, dev port 5174)
src/
lib/ api.ts (apiFetch + signIn), auth-storage.ts ← copied from worker
auth/ AuthContext.tsx (signIn + admin-role gate via /api/me)
api/ admin-sessions.ts (useActiveSessions), activities.ts (CRUD hooks)
screens/ Login.tsx, Live.tsx, Activities.tsx
components/ Sidebar.tsx
lib/ elapsed.ts (HH:MM:SS formatter, ported from worker stopwatch)
App.tsx, main.tsx, styles.css
```
The admin app is a **client only** — it talks to the existing backend over HTTP with a
bearer token. No DB access. It mirrors `apps/worker`'s toolchain and conventions exactly
so the build can copy proven patterns.
### Backend change (the only one in 3a)
- `packages/shared/src/index.ts`: add `role: Role` to `PublicUser` (so `MeResponse.user`
carries it).
- `apps/api/src/routes/me.ts`: include `role` in the response (read from the session user,
default `'worker'`). The worker app ignores the extra field — no worker change needed.
## Components
- **`lib/api.ts` / `lib/auth-storage.ts`** — copied verbatim from worker; bearer token in
localStorage, `apiFetch<T>` adds the `Authorization` header.
- **`auth/AuthContext.tsx`** — `signIn(email,password)` calls the worker `signIn`, then
fetches `/api/me`; if `role !== 'admin'` it clears the token and throws so Login shows
"Geen toegang — alleen beheerders." `isAuthed` reflects a present token; an admin check
on mount (re-fetch `/api/me`) guards against a stale worker token.
- **`screens/Login.tsx`** — Dutch email+password form (no self-signup), error line.
- **`components/Sidebar.tsx`** — app name + signed-in email + logout (⏻); nav items **Live**
and **Handelingen**; a muted/disabled "binnenkort" group hints 3b (Rapporten /
Gebruikers / Handmatig).
- **`screens/Live.tsx`** — consumes `GET /api/admin/sessions/active` (`useActiveSessions`,
`refetchInterval: 5000`). One card per session: worker name, activity name, insole-type
pill, pair count, ticking elapsed timer (from `start_time`). Empty state "Niemand is nu
aan het werk."; header "Actief nu (N)".
- **`screens/Activities.tsx`** — port of the legacy worker `Settings.tsx` (git `decb158`):
add form (name + insole-type toggles), list with inline edit, delete-with-confirm —
desktop-styled. Uses `api/activities.ts` hooks against `/api/activities`.
## Data flow
1. Admin enters credentials → `signIn` → token stored → `/api/me` confirms `role==='admin'`.
2. Live screen polls `/api/admin/sessions/active` every 5s; each card computes elapsed from
`start_time` with a 1s client tick.
3. Activity CRUD hits `/api/activities` (GET open to any authed user; POST/PUT/DELETE are
admin-gated server-side — the admin bearer satisfies them).
## Error handling
- Login: 401/no-token → "Inloggen mislukt"; authenticated non-admin → "Geen toegang —
alleen beheerders" (token cleared).
- Live/Activities: react-query error → inline "Kon gegevens niet laden." with the data
hidden; loading → "Laden…".
- Delete activity: `window.confirm` warns that the activity's sessions are also deleted
(matches backend cascade in `activities.ts`).
## Testing
Mirror the worker's vitest + testing-library setup, mocking `apiFetch`:
- **Backend:** `/api/me` includes `role` for a worker and an admin (extend `me.test.ts`).
- **Auth/login:** admin signs in; worker credentials are rejected with "Geen toegang".
- **Shell:** sidebar renders nav; logout clears the token.
- **Live:** renders a card per active session (name/activity/type); empty state when none.
- **Activities:** add/edit/delete invoke the correct endpoints (mocked).
## Out of scope (→ Phase 3b)
- All-users filtered CSV export (current `/api/export` is self-scoped).
- User management via better-auth `/api/auth/admin/*`.
- Manual session entry/edit + admin stop/fix of another worker's session (needs new
backend endpoints).
## Build approach
spec → `writing-plans`**one Workflow** that executes the plan task-by-task (TDD, commit
per task, final lint/typecheck/test/build + live verify), matching the Phase 2 pattern and
keeping this session's context clean.