18-Web-Admin-Panel
18 — Laravel Web Admin Panel & Control Center Spec
Purpose. The Laravel application is not a headless API. It hosts a first-class web UI — the Control Center — that is the system's primary operator surface. The iPhone app is a thin remote control over the same HTTP/SSE contract. This document specifies the web client.
Parity rule. Every operator action the iPhone app can perform, the Control Center can perform — and vice-versa. The HTTP/SSE API in 14 — API Endpoints Reference is the single contract.
1. Architecture & stack
- Framework. Laravel 11 monolith. Server-rendered, hypermedia-first.
- Admin CRUD layer. Filament 3 for resource management — projects, server params, agent providers, users, API tokens, audit log, rate-limit overrides. Filament gives us tables, forms, validation, soft-deletes, and roles for free.
- Operator layer. Livewire 3 + Alpine.js 3 + Tailwind CSS 3 for the high-interaction surfaces: run console, file diff viewer, snapshot compare, RAG inspector, plan/approve flow. These are not Filament resources because they are stream-driven and stateful.
- Realtime. Laravel Reverb (native WebSockets) for the web; the same backend also emits SSE for the iPhone. One broadcaster, two transports.
- Build. Vite, TypeScript for Alpine plugins, PostCSS, Tailwind JIT. No SPA framework, no Inertia — keep it boring.
- Auth (web). Session cookies + CSRF + 2FA (Fortify). Same
Usermodel as the API.
- Auth (API). Sanctum personal-access tokens with abilities. Issued from the Control Center → Profile → API Tokens.
- Authorization. Spatie laravel-permission. Roles:
owner, admin, operator, reviewer, viewer. Abilities mirror Sanctum abilities for the API.
- Layout. Filament shell for admin pages; a custom Tailwind shell (
resources/views/layouts/console.blade.php) for operator pages so Livewire components can take full bleed.
2. Why Laravel hosts the web UI (decision record)
| Option | Verdict | Reason |
|---|---|---|
| Separate Next.js / Nuxt SPA | Rejected | Doubles auth, doubles deploy, drifts from API contract, slower to ship. |
| Inertia.js + Vue | Rejected | Adds a JS tier without buying realtime; Livewire covers the interactive needs. |
| Filament + Livewire | Chosen | One codebase, one auth, server is the source of truth, realtime via Reverb, fastest path to parity with iPhone. |
| Pure Blade + htmx | Rejected | Possible, but Livewire 3 is closer to the team's Laravel idiom and ships morphdom + wire-stream. |
3. Navigation map
/ → redirect to /dashboard
/dashboard Operator home — last runs, active runs, alerts
/projects Project list (Filament resource)
/projects/{project} Project detail (Livewire)
/projects/{project}/files Project file browser (read-only)
/projects/{project}/map PROJECT_MAP viewer
/projects/{project}/schema DATABASE_SCHEMA viewer
/projects/{project}/diagrams ARCHITECTURE_DIAGRAMS viewer
/projects/{project}/rag RAG inspector (chunks, search, reindex)
/runs Run list with filters
/runs/new New run wizard
/runs/{run} Run detail — live console (Livewire + Reverb)
/runs/{run}/files File changes + diff viewer
/runs/{run}/snapshots Snapshots + restore
/runs/{run}/approve Plan/approve gate
/snapshots Cross-project snapshots
/docs/{project}/{path} Documentation viewer
/admin/server-params Filament — server params (masked secrets)
/admin/providers Filament — agent providers and models
/admin/users Filament — users, roles, abilities
/admin/tokens Filament — API tokens (web + iPhone)
/admin/audit Filament — audit log viewer
/admin/rate-limits Filament — per-endpoint overrides
/profile User profile, password, 2FA, sessions
/profile/tokens User's own API tokens for iPhone pairingFilament owns everything under /admin/* and /projects list. Everything else is Livewire pages mounted on the custom console layout.
4. Web ↔ iPhone parity matrix
Every row must be reachable from both clients and call the same API endpoints.
| Capability | Web (Control Center) | iPhone | API |
| Login | Session + 2FA | Token (paired from web) | POST /api/auth/token |
| List projects | /projects | Projects tab | GET /api/projects |
| Create project + clone | Filament form | New Project sheet | POST /api/projects • /clone |
| Index RAG | Project page button | Project detail button | POST /api/projects/{p}/index-rag |
| New run | /runs/new wizard | New Task sheet | POST /api/runs |
| Start / pause / resume / cancel | Run detail buttons | Run detail buttons | POST /api/runs/{r}/{action} |
| Live console | Livewire + Reverb WS | SSE via URLSession | GET /api/runs/{r}/events/stream |
| File diff viewer | Monaco diff component | Native diff view | GET /api/runs/{r}/diff |
| Approve / reject plan | Approve modal | Approve sheet | POST /api/runs/{r}/approve |
| Commit / push | Gated buttons | Gated buttons | POST /api/runs/{r}/commit • /push |
| Restore snapshot | Snapshot list action | Snapshot list action | POST /api/snapshots/{s}/restore |
| Docs viewer | Markdown renderer | Native MD renderer | GET /api/projects/{p}/docs |
| Server params | Filament resource (masked) | Read-only viewer | GET/PUT /api/settings/server-params |
A contract test (tests/Feature/ParityTest.php) asserts every API route used by the iPhone client is also exercised by at least one web Livewire/Filament action. Drift fails CI.
5. Operator screens — detailed
5.1 Dashboard (/dashboard)
- Header: workspace name, active run count, queue depth, Horizon status pill.
- Cards: "Active runs" (live), "Queued runs", "Failed in last 24h", "Stale RAG indexes".
- Table: Recent 20 runs with project, agent, status, duration, last event.
- Realtime: subscribes to
runs.dashboardReverb channel; updates rows in place.
5.2 Run detail (/runs/{run})
The centerpiece. Three-pane layout:
- Left (240px): run metadata, agent, model, branch, status pill, action buttons (pause/resume/cancel/approve/commit/push).
- Center (flex): Live console Livewire component.
- Each event renders as a row with timestamp, severity pill, event-type tag, title, expandable JSON payload.
- Stream source: Reverb channel
private-runs.{run_id}carrying the same event payloads the SSE endpoint emits.
- Filters: severity multi-select, event-type multi-select, free-text search, "only show errors" toggle.
- Sticky bottom: input box for
waiting_for_userstate — submitsPOST /api/runs/{run}/resume.
- Buttons: copy log, download
.ndjson, jump to latest, pin event.
- Right (360px): tabs — Files, Plan, RAG, Git, Commands, Snapshots.
5.3 File diff viewer (/runs/{run}/files)
- File tree on left with action badges (
+,~,-).
- Right pane: Monaco Editor in diff mode, side-by-side and inline toggle.
- Per-file actions: approve, reject, revert from snapshot, open in editor URL (vscode://, cursor://).
- Bulk "approve all" gated by role
revieweror higher.
5.4 Plan / approve gate (/runs/{run}/approve)
- Renders the AI plan as a checklist of intended steps.
- Shows pre-run snapshot id, affected files preview, commands that will run.
- Buttons: Approve & continue, Approve & auto-commit, Reject with feedback.
5.5 RAG inspector (/projects/{project}/rag)
- Top: index stats (chunk count, last indexed, stale count, embedding model).
- Search box → calls retrieval contract from page 04, shows topK with source, score, chunk_title, expandable text.
- Actions: Reindex changed, Full reindex, Purge.
5.6 Snapshots (/runs/{run}/snapshots, /snapshots)
- Table: type, created_at, git_commit_hash, archive_path, size.
- Row actions: Restore, Compare with current (opens diff viewer in compare mode), Download.
5.7 Documentation viewer (/docs/{project}/{path})
- Renders Markdown with Mermaid (mermaid.js), syntax highlighting (Shiki), anchor links.
- Sidebar TOC, breadcrumbs, "open in repo" link, last-updated badge.
- Read-only — editing happens in the codebase via runs.
6. Admin screens — Filament resources
All under /admin/*. Implemented as standard Filament v3 resources.
| Resource | Pages | Notes |
|---|---|---|
ProjectResource | List, Create, Edit, View | Soft-delete; "Clone now" action; "Reindex RAG" action. |
ServerParamResource | List, Edit | Secret fields rendered as • ••••••• with reveal-on-click + audit entry. Validation per param. |
AgentProviderResource | List, Edit | Toggle provider on/off, set default model, test connection. |
UserResource | List, Create, Edit | Role assignment, ability matrix, force-logout. |
ApiTokenResource | List, Create, Revoke | Display token once on create; QR code for iPhone pairing. |
AuditLogResource | List, View | Read-only; filters by actor, action, target, time. |
RateLimitResource | List, Edit | Per-endpoint overrides. |
Filament policies map 1:1 to Sanctum abilities.
7. Realtime transport
- Broadcaster:
BROADCAST_DRIVER=reverb.
- Channels:
private-runs.{run_id}— per-run event stream (mirrors SSE).
private-projects.{project_id}— project-level signals (RAG progress, file scan).
private-dashboard— workspace-wide counters.
- Client: Laravel Echo +
pusher-js(Reverb-compatible). Wired into Livewire components via#[On('echo-private:...')].
- Auth: standard
/broadcasting/authwith session for web, Sanctum token for any future web-token use.
- Fallback: if WS connection drops, Livewire polls
/api/runs/{run}/events?since=last_idevery 3s until it reconnects.
- Backpressure: server batches events into groups of up to 50 per WS frame or every 250 ms, whichever first.
The SSE endpoint for iPhone reads the same agent_events rows; the broadcaster and the SSE responder are two sinks for the same ConsoleEventService::publish() call.
8. Component inventory (Livewire + Blade)
app/Livewire/
├── Console/
│ ├── RunConsole.php Live event stream + filters
│ ├── EventRow.php Single event with payload expand
│ ├── ResumeInput.php Sticky input for waiting_for_user
│ └── ConsoleFilters.php
├── Files/
│ ├── FileTree.php
│ ├── DiffViewer.php Wraps Monaco via Alpine
│ └── ApproveBar.php
├── Run/
│ ├── RunHeader.php Status pill + action buttons
│ ├── PlanPanel.php
│ ├── GitPanel.php
│ ├── RagPanel.php
│ └── SnapshotPanel.php
├── Project/
│ ├── ProjectHeader.php
│ ├── RagInspector.php
│ └── DocsViewer.php
└── Dashboard/
├── ActiveRunsCard.php
└── RecentRunsTable.phpresources/views/
├── layouts/
│ ├── console.blade.php Operator layout (full bleed)
│ └── app.blade.php Filament wrapper
├── components/
│ ├── severity-pill.blade.php
│ ├── status-pill.blade.php
│ ├── kbd.blade.php
│ └── empty-state.blade.php
└── livewire/...9. Styling system
- Tailwind 3 with custom tokens in
tailwind.config.js:- Severity colors:
info=sky-500,success=emerald-500,warning=amber-500,error=rose-500,debug=zinc-400.
- Status colors mirror page 12 console spec.
- Severity colors:
- Dark mode default; light mode toggle in profile menu.
proseclass for docs viewer with@tailwindcss/typography.
- Monospace stack:
JetBrains Mono, ui-monospace, SFMono-Regular, Menlo.
- Icons: Heroicons (Filament default) + Lucide via Blade Icons.
10. Performance budgets
| Surface | Target | Mechanism |
|---|---|---|
| Dashboard TTFB | < 200 ms p95 | Cached counters, no N+1. |
| Run detail first paint | < 400 ms p95 | Skeleton, then hydrate from Reverb. |
| Console event lag | < 500 ms wire-to-render p95 | Reverb direct push, no polling. |
| Diff viewer for 1 MB file | < 800 ms | Monaco lazy-loaded, server pre-computes diff. |
| Filament table page | < 250 ms p95 | Indexed queries, eager loads. |
11. Accessibility
- WCAG 2.1 AA target.
- Keyboard shortcuts in console:
j/knavigate events,ffocus filter,/search,rresume,ppause,g dgo to dashboard.
- All action buttons reachable via Tab; focus rings preserved.
- Severity not encoded by color alone — every pill has text + icon.
- Screen-reader live region for new console events (polite).
12. Telemetry & audit
- Every Control Center action that mutates state writes an
audit_logrow: actor, ip, ua, route, target, before/after hash.
- Filament audit viewer reads from the same table.
- Web client emits
client_perfbeacons for first-paint and console-lag percentiles.
13. iPhone pairing flow
- Operator opens
/profile/tokensin the Control Center.
- Clicks Create token for iPhone → Sanctum personal-access token with abilities checklist.
- Server displays the plaintext token once + a QR code encoding
agentws://pair?base=https://...&token=....
- iPhone app scans QR → stores token in Keychain → first call
GET /api/mevalidates.
- Token row in DB stores
device_name,last_used_at,ip,ua; revocable from the same page.
14. Testing
- Filament resources: Pest feature tests per resource (list/create/edit/delete + policy).
- Livewire components:
Livewire::test()for state transitions; snapshot tests for rendered HTML.
- Realtime: integration test asserts that emitting a
ConsoleEventpublishes to both Reverb channel and SSE stream and that a LivewireRunConsoleinstance receives it.
- Parity test:
tests/Feature/ParityTest.phpwalksroutes/api.php, collects routes tagged#[IphoneClient], and asserts each is exercised by at least one web action route in a manifest.
- Browser: Playwright smoke tests for login → new run → see first event → pause → resume → approve → commit.
15. Deployment
- Single Laravel app, one container image, behind nginx.
- Sidecar: Reverb on its own port (
8080internal), proxied at/app/{appId}and/apps/{appId}/events.
- Horizon supervisor inside the app container or as separate worker container in prod.
- Vite build runs in CI; assets served by nginx with long cache +
Vite::manifest().
- Health endpoints:
/up(Laravel),/healthz/reverb,/healthz/horizon.
16. Open questions
- Should the Control Center embed Claude Code's own UI when present, or remain the canonical surface? Default: canonical surface.
- Multi-tenant teamspaces in v1, or single workspace? Default: single workspace, design schema-ready for tenancy.
- Inline AI chat inside the docs viewer? Out of scope for v1.
- Self-hosted Monaco vs CDN? Default: self-hosted via Vite.
17. Acceptance criteria
- An operator can complete the full flow — create project → clone → index RAG → new run → watch live console → approve plan → review diff → commit — entirely in the Control Center.
- The exact same flow is reachable on the iPhone app using the same API.
- A new API route added without a corresponding web action fails the parity CI check.
- Secrets are never rendered in plain text in any view.
- Reverb downtime degrades gracefully to polling without losing events.