6.1 KiB
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)
- Slice → MVP-first, two cycles. 3a = shell + admin login + live view + activity management (existing endpoints). 3b = reports/export + user management + manual entry/edit.
- Auth → reuse the worker's bearer-token flow (
POST /api/auth/sign-in/email→ captureset-auth-token→ localStorage →Authorization: Bearer). Gate admin access by readingrolefrom/api/me; non-admins are signed out with "Geen toegang". - Layout → fixed left sidebar + content area (scales as 3b adds sections).
- Live refresh → react-query
refetchInterval: 5000(5s); elapsed time ticks client-side every second. - 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 changes (minimal, in 3a)
packages/shared/src/index.ts: addrole: RoletoPublicUser(soMeResponse.usercarries it).apps/api/src/routes/me.ts: includerolein the response (read from the session user, default'worker'). The worker app ignores the extra field — no worker change needed.apps/api/src/env.ts+.env.example: addhttp://localhost:5174(the admin dev origin) to the defaultWEB_ORIGINS/CORS_ORIGINS. Required becauseWEB_ORIGINSdrives bothhono/corsand better-authtrustedOrigins; the admin app at :5174 calls the API at :3000 cross-origin and would otherwise be blocked.
Components
lib/api.ts/lib/auth-storage.ts— copied verbatim from worker; bearer token in localStorage,apiFetch<T>adds theAuthorizationheader.auth/AuthContext.tsx—signIn(email,password)calls the workersignIn, then fetches/api/me; ifrole !== 'admin'it clears the token and throws so Login shows "Geen toegang — alleen beheerders."isAuthedreflects 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— consumesGET /api/admin/sessions/active(useActiveSessions,refetchInterval: 5000). One card per session: worker name, activity name, insole-type pill, pair count, ticking elapsed timer (fromstart_time). Empty state "Niemand is nu aan het werk."; header "Actief nu (N)".screens/Activities.tsx— port of the legacy workerSettings.tsx(gitdecb158): add form (name + insole-type toggles), list with inline edit, delete-with-confirm — desktop-styled. Usesapi/activities.tshooks against/api/activities.
Data flow
- Admin enters credentials →
signIn→ token stored →/api/meconfirmsrole==='admin'. - Live screen polls
/api/admin/sessions/activeevery 5s; each card computes elapsed fromstart_timewith a 1s client tick. - 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.confirmwarns that the activity's sessions are also deleted (matches backend cascade inactivities.ts).
Testing
Mirror the worker's vitest + testing-library setup, mocking apiFetch:
- Backend:
/api/meincludesrolefor a worker and an admin (extendme.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/exportis 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.