MaxyDocs
View as markdown →

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.md at 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

MountPurposeKey methods
/sessionAdmin cookie session: PIN-gated mint, validate, rotate.GET /, POST /
/sessionsLegacy 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-sessionsSole 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-deletePOST { 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-spawnPOST { 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-sessionsSpawn 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.adminModelCLAUDE_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

MountPurpose
/graph-searchFiltered node search backing the /graph page filter chips.
/graph-subgraphNeighbourhood expansion around a focal node.
/graph-deleteSoft-trash a node (sets _trashed:true).
/graph-restoreUndo trash.
/graph-labels-in-graphDistinct label list for the filter dropdown.
/graph-default-viewAccount-scoped saved view (zoom, focal id, filters).

Artefacts and files

MountPurpose
/sidebar-artefactsLists every editable artefact for the sidebar Artefacts view (KnowledgeDocuments + this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates).
/sidebar-artefact-contentReads a single artefact's bytes for the artefact pane.
/sidebar-artefact-savePersists an artefact edit.
/attachmentPer-attachment binary fetch (images, PDFs, etc.).
/filesFile 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

MountPurpose
/browserProgrammatic Chromium launcher used by personal-assistant browser-automation flows.
/browser-iframeBrowser-iframe event ingest from the in-app preview surface.
/device-browserDrives the device's own browser tab (VNC Chromium on Pi).

Diagnostics

MountPurpose
/logsServer log tail with type=stream|error|sse and sessionId filters; powers the in-chat View logs popover (see internals.md "Conversations modal — View logs").
/eventsGeneric SSE event ingest from the UI client.
/log-ingestLoopback-only structured log ingest for MCP and PTY-spawn observability lines (see admin-session.md "Memory MCP write-path outcome lines").
/claude-infoReturns Claude CLI binary path, version, and OAuth status.
/claude-capabilitiesReturns the resolved capability matrix the UI uses to gate features.
/agentsLists every installed agent template; supports DELETE /:slug for user-created public agents and POST /:slug/project for project assignment.
/cloudflareTunnel setup surface — see cloudflare.md for the OAuth flow.
/linkedin-ingestLinkedIn Basic Data Export ingest entry point (see linkedin-extension.md).
/health-brandBrand-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-statsHost 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.

ClassTriggerVisual
.system-stats--warncpuPct >= 0.9 or memUsedPct >= 0.9Widget 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--critcpuPct >= 0.98 or memUsedPct >= 0.98Adds a 1.2 s pulsing background animation (@keyframes system-stats-crit-pulse) on top of the warn colour.
.system-stats__fig--warnPer-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).

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.

SurfaceWhat it doesBacked by
+ New session buttonOpens NewSessionModal, which POSTs {channel, permissionMode, model, initialMessage} to /api/admin/claude-sessions.claude-sessions.ts
Mode triggerPer-(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 actionsInline 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 listLists every KnowledgeDocument plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (downloadPathGET /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 widgetCPU / RAM widget at the foot of the sidebar.system-stats.ts (see above)
FooterOperator 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. processStartedAt resets on every brand-service restart; Neo4j probe is bounded to 1 s and reports conversationDb: '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 on maxy-edge.service so the Software Update modal can read it while the brand service is mid-restart.
  • 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/setup drives.
  • deployment.md — Pi setup; the brand-service / edge- service split that the health-vs-version table above relies on.