# 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` 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.