Admin portal
Single-admin (hard-coded grasl.t@centrum.cz) cross-tenant ops surface at /admin/*. Pixel-perfect implementation of docs/design/final/project/admin*.{jsx,css}. Non-admin users get 404 (not 403) — the portal should look like it doesn't exist.
What the admin can / cannot do
See ADR 0010 — privacy invariant. TL;DR: metadata, counts, names, timestamps, audit. Never tasks.description, knowledge_entries.body, task_comments.body.
State machine
organizations.status:
active ─┬──▶ suspended ──▶ active (reversible, audit-tracked)
└──▶ deleted ──▶ (30-day grace) ──▶ hard-delete (task 49)
│
└──▶ active (restored within grace)
users.status:
active ◀─▶ suspended (toggle; active sessions invalidated on suspend)Migration: 0016_admin_portal.sql. Service: @drobek/core admin module. Gate: apps/web/app/lib/admin.server.ts.
Suspended semantics
- `organizations.status='suspended'`:
- MCP auth (verifyAgentToken) returns null → tools fail with auth.invalid_token. - Web UI requireOrgRole blocks any non-GET request with 423 Locked. GET still works so members can see the suspension banner and export their data. - All members still log in normally; the org workspace renders a banner.
- `users.status='suspended'`: all sessions revoked immediately (in the
same DB transaction as the status flip). Next sign-in is blocked by the session lookup returning none.
Routes
| Route | Purpose |
|---|---|
/admin | Dashboard (cross-tenant metric cards + recent audit feed) |
/admin/orgs | List + status/search filter |
/admin/orgs/:id | Detail + suspend / resume / delete / restore |
/admin/users | List + email/status filter |
/admin/users/:id | Detail + suspend / unsuspend |
/admin/agents | Cross-tenant agent tokens + revoke |
/admin/queues | BullMQ waiting/active/delayed/failed |
/admin/audit | Combined admin + agent audit feed |
/admin/system | Env flag check + DB / Redis ping |
All /admin/* routes go through requireAdmin(request) which throws 404 on a non-admin session.
2FA
Out of scope. There is no 2FA in the admin portal — the old requiresTwoFactorEnrollment stub (and its always-on banner) has been removed. Admin access is gated by a single hard-coded verified email + session; the portal 404s for everyone else. Revisit only if there's a concrete need.
Email notifications
Suspend / resume / delete actions write the audit row; the welcome / status emails ship as part of task 45 (transactional emails). The action handlers in @drobek/core/admin are intentionally email-free — they record the intent in admin_audit and let the worker pattern from 45 fan out the notifications. Until then, the audit row is the source of truth.
Testing
- Unit + service integration:
packages/core/src/admin/admin.integration.test.ts
- E2E (gate + read paths):