Admin UI reference
A compact map of the admin web app: every /api/admin/* mount, every
sidebar surface, and the operator-facing widgets that read host or session
state. The deep architecture lives in platform.md (UI
layout, session reconcile, route lifecycle) and
admin-session.md (the session-cookie / PIN-rebind /
SDK-resume contract). This file is the index that points at them.
Scope and tree decision
The maxy-code/ tree does not ship a .docs/platform.md developer
doc. The legacy root tree (getmaxy/) carries one at
.docs/platform.md for the original Maxy installer; the Maxy Code tree
keeps its architecture surface in two places:
maxy-code-prd.mdat the repo root — product requirements and the source of truth for every task in.tasks/.platform/plugins/docs/references/platform.md— operator-facing architecture, loaded by the docs plugin at session start.
Anything that would have gone into maxy-code/.docs/platform.md belongs
in one of those two files instead. maxy-code/.docs/ itself is
reserved for vertical / integration notes (LinkedIn extension,
PropertyData, Real Agent standalone, MCP server inventory) — not for
core-platform docs.
Admin Hono routes
Every admin sub-app is mounted by
platform/ui/server/routes/admin/index.ts
under a per-area prefix. The outer requireAdminSession middleware
runs in server/index.ts before the aggregator; individual handlers
re-apply requireAdminSession where they need a resolved senderId.
/actions and /version are not mounted here — they live on
maxy-edge.service via server/edge-admin.ts so the upgrade view
survives the mid-run restart of the brand service. Double-mounting
either is a regression.
Sessions and chat
| Mount | Purpose | Key methods |
|---|---|---|
/session | Admin cookie session: PIN-gated mint, validate, rotate. | GET /, POST / |
/sessions | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
/sidebar-sessions | Sole data path for the sidebar Sessions list (Tasks 538 + 543). One JSONL on disk equals one row. The row's delete button (Task 543) is the only way a row disappears. Each row carries sessionId, title, startedAt, live, isSubagent, pid: number | null (basename of the matched sessions/<pid>.json), and projectDir (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level accountId so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy rcUrl field is gone (Task 543) — the row's external-link affordance now POSTs /session-rc-spawn to start a fresh local claude --remote-control <name> --session-id <sid> PTY on every click. | GET / |
/session-delete | POST { sessionId, projectDir } (Task 543). Best-effort SIGTERM of the live PID (resolved from sessions/<pid>.json body match) then unlink the JSONL + <sid>.meta.json sidecar. Absent PID file is not an error. Containment: projectDir must live under <CLAUDE_CONFIG_DIR>/projects/. | POST / |
/session-rc-spawn | POST { sessionId?, name? } (Task 543). Fire-and-forget claude --remote-control [name] [--session-id <sid>]. Present sessionId resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's /rc-spawn. The new process registers itself as its own Remote Control entry in claude.ai/code. | POST / |
/claude-sessions | Spawn surface only (Task 500). The single POST / is shared by three callers: the public/visitor bridge, linkedin-ingest, and the turn-completed-graph-write Stop-hook recorder. The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, /:id/meta, /:id/input, /:id/log) were removed — the maxy dashboard no longer manages or displays sessions. | POST / |
Task 500 — admin session management moved entirely to claude's own interfaces (claude.ai/code, claude desktop). A manager-owned per-account claude rc --spawn same-dir daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is account.json.adminModel → CLAUDE_CONFIG_DIR/settings.json "model", written by the daemon supervisor at boot. The maxy admin UI keeps a single "New session" link (https://claude.ai/code, opens in a new tab) and no session list, viewer, controls, or model/mode picker. The daemon supervisor lives at platform/services/claude-session-manager/src/rc-daemon.ts. The /session-defaults route and SpawnPreference node were deleted with the picker. /new-session-failure, /new-session-submit, and /claude-capabilities are now orphaned (consumed only by the deleted NewSessionModal) — see .tasks/501 for their removal.
Graph
| Mount | Purpose |
|---|---|
/graph-search | Filtered node search backing the /graph page filter chips. |
/graph-subgraph | Neighbourhood expansion around a focal node. |
/graph-delete | Soft-trash a node (sets _trashed:true). |
/graph-restore | Undo trash. |
/graph-labels-in-graph | Distinct label list for the filter dropdown. |
/graph-default-view | Account-scoped saved view (zoom, focal id, filters). |
Artefacts and files
| Mount | Purpose |
|---|---|
/sidebar-artefacts | Lists every editable artefact for the sidebar Artefacts view (KnowledgeDocuments + this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates). |
/sidebar-artefact-content | Reads a single artefact's bytes for the artefact pane. |
/sidebar-artefact-save | Persists an artefact edit. |
/attachment | Per-attachment binary fetch (images, PDFs, etc.). |
/files | File browser CRUD (list, download, upload, delete). Listings put directories first, then files newest-first by mtime (name tie-break) so a just-changed file leads the panel. |
Artefact download resolution (Task 524). Clicking a sidebar Artefacts row
streams the KnowledgeDocument's real backing file, which can live in one of
three on-disk classes: the admin-UI upload store (<DATA_ROOT>/uploads/<acc>/ <attachmentId>/), the agent-authored output dir (<DATA_ROOT>/accounts/<acc>/ output/<name>), and the Claude-agent upload store
(<CLAUDE_CONFIG_DIR>/uploads/<attachmentId>/, which is outside DATA_ROOT).
memory-ingest persists the real path on the node as KnowledgeDocument.sourcePath
(skipped for web docs, whose temp file is unlinked at ingest), so new ingests
resolve deterministically; pre-existing rows fall back across the three classes.
The download route (/files/download) accepts root=data (default) or
root=claude-uploads; the config-dir root carries no accountId path segment, so
it is account-scoped by a graph-ownership check (the attachmentId must map to a
KnowledgeDocument carrying the caller's accountId) rather than by path
partition. Resolution emits [admin/sidebar-artefacts] download-resolved via=…;
a web/transient doc with no persisted file is not-downloadable reason=no-persisted-file.
/data File panel — refresh and reload-survival. The panel listing is a
snapshot from its last fetch, so a file an agent writes (or an upload/delete
elsewhere) leaves stale rows and timestamps until something re-fetches. A
Refresh button beside Upload re-fetches the current folder in place (fresh
mtimes, no browser reload). The current directory is mirrored into the URL
hash (/data#path=<rel>), so a browser reload restores that folder instead of
snapping back to the data root; the hash never reaches the server and /data
has no client router, so the channel is isolated. path= is cleared at the
root for a clean URL.
Browser / device-browser
| Mount | Purpose |
|---|---|
/browser | Programmatic Chromium launcher used by personal-assistant browser-automation flows. |
/browser-iframe | Browser-iframe event ingest from the in-app preview surface. |
/device-browser | Drives the device's own browser tab (VNC Chromium on Pi). |
Diagnostics
| Mount | Purpose |
|---|---|
/logs | Server log tail with type=stream|error|sse and sessionId filters; powers the in-chat View logs popover (see internals.md "Conversations modal — View logs"). |
/events | Generic SSE event ingest from the UI client. |
/log-ingest | Loopback-only structured log ingest for MCP and PTY-spawn observability lines (see admin-session.md "Memory MCP write-path outcome lines"). |
/claude-info | Returns Claude CLI binary path, version, and OAuth status. |
/claude-capabilities | Returns the resolved capability matrix the UI uses to gate features. |
/agents | Lists every installed agent template; supports DELETE /:slug for user-created public agents and POST /:slug/project for project assignment. |
/cloudflare | Tunnel setup surface — see cloudflare.md for the OAuth flow. |
/linkedin-ingest | LinkedIn Basic Data Export ingest entry point (see linkedin-extension.md). |
/health-brand | Brand-process liveness + 1 s Neo4j probe. Returns {ok, processStartedAt, version, conversationDb: 'ok'|'error', uptimeMs}. The processStartedAt is captured at module load, so a stale value after an armed restart means the brand process never came back. |
/system-stats | Host CPU / RAM / load probe. See next section. |
The companion /api/admin/version and /api/admin/actions routes are
served by maxy-edge.service, not this aggregator. The edge process
keeps the upgrade view alive while the brand service restarts.
System-stats widget
Source:
server/routes/admin/system-stats.ts
(route) and SystemStatsWidget in
platform/ui/app/Sidebar.tsx
(consumer). CSS lives under .system-stats* in
platform/ui/app/globals.css.
What the widget shows. A compact block at the foot of the sidebar
with one row per metric: CPU 73% and RAM 71%, each followed by its
own 4 px saturation bar. Bar fill colour is a smooth green → amber →
red gradient computed as hsl(140·(1−pct), 65%, 45%) so the colour
reflects each metric's own load. Hidden when the sidebar is collapsed
to its 56 px icon rail.
Where the numbers come from. GET /api/admin/system-stats returns a
snapshot for the host. On Linux the route reads /proc/stat twice 100 ms
apart and computes cpuPct = 1 - idleDelta / totalDelta; memUsedPct
comes from (MemTotal - MemAvailable) / MemTotal in /proc/meminfo; the
three load averages come from /proc/loadavg. A single-flight module
cache means concurrent callers share one in-flight pair of /proc/stat
reads. On darwin and other non-Linux platforms the route falls back to
os.totalmem() / os.freemem() / os.loadavg() and returns cpuPct: null
— the widget renders a dash rather than fake a value.
Refresh cadence. The widget polls every 5 s
(SYSTEM_STATS_POLL_MS). Polling pauses while document.hidden is true
(tab in background) and resumes on visibilitychange. On unmount the
interval is torn down and the visibility listener removed.
Thresholds.
| Class | Trigger | Visual |
|---|---|---|
.system-stats--warn | cpuPct >= 0.9 or memUsedPct >= 0.9 | Widget text turns --danger red. Bar fill colour is independent (set by the per-bar hue gradient), so the figure text is what carries the warn signal at this band. |
.system-stats--crit | cpuPct >= 0.98 or memUsedPct >= 0.98 | Adds a 1.2 s pulsing background animation (@keyframes system-stats-crit-pulse) on top of the warn colour. |
.system-stats__fig--warn | Per-figure: applied to the specific CPU or RAM span that breached 0.9. | Same red colour, lets the operator see which of the two figures crossed first. |
The warn threshold matches the threshold the operator most commonly cares about (a fully-loaded 4-core Pi at 16 GiB RAM crosses 0.9 long before anything else on the host notices). The crit threshold pulses because it is the band where a swap-thrash episode becomes likely.
Failure handling. Any read or parse error inside the route returns
HTTP 200 with {degraded: true, reason, sampledAtMs} so the widget
keeps showing its last-known value instead of flashing zeros. Failed
fetches log [admin-ui] system-stats-fetch-failed status=<code> (or
reason=<message>) to the browser console; successful polls are silent
client-side. Server-side every poll logs one
[system-stats] poll cpuPct=<f3> memUsedPct=<f3> loadAvg1=<f2> swapUsedPct=<f3> platform=<linux|darwin|other> ms=<n>
line; errors emit
[system-stats] error file=<path|parse> reason=<message>.
Diagnostic grep.
grep '\[system-stats\] poll' ~/.${brand}/logs/server.log | tail -20
grep '\[system-stats\] error' ~/.${brand}/logs/server.log | tail
A 0.000 reading that persists across many polls while top shows load
is the delta-cache regression signature (the single-flight promise was
not released).
Sidebar surfaces
The sidebar is the entire left column of the admin UI. Its full layout,
responsive breakpoints, drag-resize behaviour, and the
new-session strip / nav rows / sessions list / footer ordering are
documented in
platform.md "The Web Interface" — that paragraph is
authoritative. This section names the surfaces and what backs each.
| Surface | What it does | Backed by |
|---|---|---|
+ New session button | Opens NewSessionModal, which POSTs {channel, permissionMode, model, initialMessage} to /api/admin/claude-sessions. | claude-sessions.ts |
| Mode trigger | Per-(accountId, userId) SpawnPreference for permissionMode and model; persists across reload, tab, device. | session-defaults.ts |
| Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | graph-search.ts, graph-subgraph.ts, sidebar-artefacts.ts |
| Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. | /claude-sessions/events, /claude-sessions |
| Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical .conversations-modal CSS block exists in globals.css but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | claude-sessions.ts |
| Artefacts list | Lists every KnowledgeDocument plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (downloadPath → GET /api/admin/files/download) so the operator opens it in their local app; rows whose file is outside DATA_ROOT (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal (Task 518). | sidebar-artefacts.ts, files.ts |
| System-stats widget | CPU / RAM widget at the foot of the sidebar. | system-stats.ts (see above) |
| Footer | Operator avatar, name, role, and the actions popover. | session.ts |
Artefact pane
The right-hand pane that opens when the operator selects an artefact, clicks a project (Graph view), or opens Browser / Data / Graph from the menu. It holds the surface side-by-side with the conversation so the chat stays live.
- Editable artefacts (KnowledgeDocuments + the account's own IDENTITY /
SOUL / KNOWLEDGE) auto-save on type via
POST /api/admin/sidebar-artefact-save. - Read-only artefacts (specialist agent templates) cannot be edited because they ship with Maxy and would be overwritten on the next install.
- PDF artefacts render inline; non-PDF binaries fall back to a Download button when the browser has no native viewer.
- Orphan rows (no readable file on disk, unsupported content type) show a one-line banner explaining the skip instead of opening to a blank pane.
Below 1080 px the pane hides and Browser / Data / Graph open as full-window pages instead. The chat-and-pane divider is drag-resizable on every admin page; double-click resets to half the available width (viewport minus sidebar), clamped to the per-pane min-width floors. The chosen width is remembered across reloads.
Health vs version
Two endpoints, two surfaces, two restart-survival roles:
GET /api/admin/health-brand(this aggregator) — brand-process liveness.processStartedAtresets on every brand-service restart; Neo4j probe is bounded to 1 s and reportsconversationDb: 'ok' | 'error'. Use this to confirm the brand process came back after a Cloudflare-setup armed restart.GET /api/admin/version(maxy-edge) — installer / brand version string. Hosted onmaxy-edge.serviceso the Software Update modal can read it while the brand service is mid-restart.
Related references
platform.md— UI layout, session reconcile model, artefact pane behaviour in full detail, breakpoints.admin-session.md— admin session token, PIN- rebind, SDK-resume, turn-recorder lifecycle, structured log lines.internals.md— retrieval pipeline, recorder auto-archive, graph-prune-denylist surface, conversation logs.cloudflare.md— tunnel setup OAuth flow that/api/admin/cloudflare/setupdrives.deployment.md— Pi setup; the brand-service / edge- service split that the health-vs-version table above relies on.