Operator console
A single-page, flag-gated dashboard for the box owner: pending approvals (approve/reject), a pending-count badge, memory search, an audit tail, and corpus stats. It is a static HTML/JS/CSS shell served by the existing facade — no separate process, no build step, no framework.
The console shell itself carries zero data and zero secrets. Every panel is populated by the
page's own JavaScript calling the facade's existing /v1/approvals, /v1/memory/search,
/v1/memory/audit, and /v1/memory/stats routes with the credentials you paste into the page.
Enable it
Off by default. Set the flag and (re)start the facade:
PANELLA_CONSOLE_ENABLED=1
In compose, put that in .env (or export it before docker compose up) — see the passthrough
line in docker-compose.yml.
Open it
http://127.0.0.1:8001/console
8001 is the facade's published port per docker-compose.yml (ports: - "127.0.0.1:8001:8001").
Loopback-only by design (see "Security notes" below).
Connect
The page asks for two secrets. Paste them into the two password fields — nothing is submitted until you click Connect, and nothing is saved anywhere: reload the page and you paste them again.
- Owner bearer token — the same bearer your MCP client uses.
panella up/panella initprint it once and save it to.panella/owner-bearerin the box home; or mint a separate labelled bearer for the console — see the team recipe's on-ramp (docs/recipes/claude-code-team-memory.md§6) for the in-containertokens mintform. - Approval token — the operator-only
local_clicredential at.panella/approval-tokenin the box home, minted bypanella init(and bypanella up, which runs init) with mode0600. Required only for the pending list and approve/reject actions; search, audit, and stats work with the bearer alone.
Security notes
- Loopback only by default. The facade publishes on
127.0.0.1, so the console is reachable only from the box itself unless you deliberately change the bind. If you re-bind beyond loopback, put a reverse proxy with its own authentication in front of it — the console's own auth model (paste-a-bearer-into-a-password-field) assumes a trusted local operator at the keyboard, not an internet-facing login page. - Both secrets live in the page's JavaScript memory only for the current tab — never
localStorage, never a cookie, never the URL. Closing the tab or reloading forgets both. - Every response the console serves carries a strict Content-Security-Policy
(
default-src 'none'; script-src 'self'; ...) and all rendering of stored content (approval previews, search hits, audit rows) usestextContent/createElementonly — neverinnerHTML— because that content can be attacker-influenced (anything that reached the approval queue).