Skip to content

Logs, audit, doctor, pairing & skins pages

Live log stream, audit trail with chain verify, diagnostics, device pairing, and the UI skin/theme system.

This page covers the dashboard’s inspection and operations surfaces — the screens you reach for when something looks off or when you want to brand and pair a deployment. The Inspection section holds the live Logs stream, the tamper-evident Audit trail, and the on-demand Doctor diagnostics. The Operations section holds Device Pairing and UI Skins. Two cross-page systems also live here: the approval toaster (fed by an app-wide agent-events SSE stream) and the theme / skin runtime with its language switcher.

Use Logs to watch the daemon in real time, Audit to prove nothing has been tampered with, Doctor to triage a misconfiguration, Pairing to authorize a new browser or phone, and Skins to re-theme the dashboard. Everything here lives behind the dashboard’s bearer-token auth — see Run the dashboard to pair a browser first.

The Logs page is a live, real-time event stream from the daemon over Server-Sent Events. As the agent runs, coloured event cards appear at the bottom and auto-scroll into view. The buffer is capped at the most recent 500 entries — older cards are dropped as new ones arrive.

It subscribes to one SSE endpoint:

GET /api/daemon/logs
Authorization: Bearer rk_<token>
Accept: text/event-stream

This is a live tail: historical logs are not replayed when you connect, so you only see events that occur after the page opens. The underlying SSEClient reconnects automatically if the stream drops.

Each event renders as a colour-coded card by type. The page recognises:

Event typeMeaning
errorAn error emitted by the runtime
warn / warningA warning
tool_call / tool_call_start / tool_resultAgent tool activity
llm_requestAn outbound model request
agent_start / agent_endAgent turn boundaries
message / chatChat traffic
logGeneric log line
log_unavailableThe gateway could not read its log file

A log_unavailable event means the daemon is up but cannot tail its own log file — check file permissions and the log path rather than assuming the agent is idle.

  • Pause / Resume — freeze the stream so you can read a card without it scrolling away. New events keep arriving in the background; resume to catch up.
  • Auto-scroll — the panel follows the newest entry by default. Clicking anywhere inside the panel disables auto-scroll so you can inspect older cards; it re-engages when you scroll back to the bottom.
  • Type filters — toggle the event-type chips to hide or show whole categories (for example, mute tool_call noise to focus on error and warn).

The Audit page — the tamper-evident Merkle hash-chain trail with chain verification.

The Audit page is the viewer for Revka’s tamper-evident Merkle hash-chain audit log. Every security-relevant event is appended to an append-only JSONL log where each entry’s entry_hash is SHA-256(prev_hash || canonical_content) and carries a monotonically increasing sequence. Modifying or deleting any entry breaks every hash after it — which is exactly what the chain verifier detects.

It is backed by two endpoints:

ActionMethod + pathAuth
List eventsGET /api/audit?limit=100&event_type=<type>Bearer
Verify chain integrityGET /api/audit/verifyBearer

GET /api/audit accepts limit (default 50, max 500), an optional event_type filter, and an optional since RFC 3339 timestamp. Each event has this shape:

{
"timestamp": "2026-06-18T10:00:00Z",
"event_id": "<uuid>",
"event_type": "command_execution",
"actor": { "channel": "telegram", "user_id": "123", "username": "@alice" },
"action": { "command": "ls -la", "risk_level": "low", "approved": false, "allowed": true },
"result": { "success": true, "exit_code": 0, "duration_ms": 15 },
"security": { "policy_violation": false, "rate_limit_remaining": 19, "sandbox_backend": "firejail" },
"sequence": 42,
"prev_hash": "<64 hex>",
"entry_hash": "<64 hex>",
"signature": "<64 hex>"
}

Filter the stream with the event-type dropdown. The supported types are:

command_execution, file_access, config_change, auth_success, auth_failure, policy_violation, and security_event.

Any event whose security.policy_violation is true is flagged with a policy-violation badge so blocked or risky actions stand out at a glance.

Click Verify Chain to run the integrity check. The result renders inline with a shield icon: green when verified, red when invalid, alongside the total entry_count and an error message if the chain is broken.

GET /api/audit/verify
Authorization: Bearer rk_<token>
{ "verified": true, "entry_count": 1284 }

The same check powers the Risk Rail audit badge on the Dashboard overview, which re-polls /api/audit/verify every 60 seconds and on window focus. A failed verification means the on-disk log has been altered — the most security-significant alarm the dashboard can raise.

The Doctor page runs Revka’s built-in diagnostics suite on demand and reports a pass/warn/fail summary plus a result per check, grouped by subsystem category. It mirrors the revka doctor CLI.

Click Run Diagnostics to execute. The page posts an empty body and receives a list of results:

POST /api/doctor
Authorization: Bearer rk_<token>
Content-Type: application/json
{}
{
"results": [
{ "category": "network", "message": "Provider reachable", "severity": "ok" },
{ "category": "memory", "message": "Kumiho round-trip slow", "severity": "warn" },
{ "category": "agent", "message": "MCP server not connected", "severity": "error" }
],
"summary": { "ok": 8, "warnings": 1, "errors": 1 }
}

Each result carries a category, a human-readable message, and a severity of ok, warn, or error. The summary panel tallies the counts. Checks typically span subsystems such as API-key/provider connectivity, memory backend, MCP connections, network, and filesystem.

The Pairing page manages the devices (browsers, phones, sidecars) authorized to call the gateway. Every /api/* request carries a Authorization: Bearer rk_<token> token obtained through pairing; this page is where you mint codes for new devices, show a QR code, rotate the code, and revoke devices you no longer trust.

It uses these endpoints:

ActionMethod + pathAuth
List paired devicesGET /api/devicesBearer
Generate a new pairing codePOST /api/pairing/initiateBearer
Revoke a deviceDELETE /api/devices/{id}Bearer
Read the current startup codeGET /pair/codeNone

POST /api/pairing/initiate returns a fresh code that a new device exchanges for its own token:

{ "pairing_code": "123456" }

The device list (GET /api/devices) returns a DeviceInfo per entry — id, name, device_type, hardware, paired_at, last_seen, and ip_address — so you can tell paired clients apart and revoke the right one. Revoking with DELETE /api/devices/{id} invalidates that device’s bearer token immediately.

  1. Click Pair New Device to call /api/pairing/initiate and display a 6-digit code and a QR code.
  2. On the new device, scan the QR (or type the code manually into the dashboard login screen).
  3. The new device posts the code to POST /api/pair with optional device metadata and receives its own persistent token.
  4. The new device appears in the list. Revoke any device later with the trash icon.

The QR code encodes a JSON string so a mobile client can auto-fill the host and code:

{ "v": 1, "type": "revka-pair", "host": "<origin><basePath>", "code": "<6-digit>" }

host is the gateway origin plus any reverse-proxy base path, so a scanning client knows exactly where to send POST /api/pair. Rotating the code generates a fresh value immediately and invalidates the previous one.

Two app-wide systems run on every dashboard page, independent of which route you are viewing.

On startup the dashboard opens an application-level SSE subscription to the gateway’s broadcast event stream and shares it with every component:

GET /api/events
Authorization: Bearer rk_<token>
Accept: text/event-stream

Events use a { "type": "...", "payload": { ... }, "timestamp": "..." } envelope and include channel_event, human_approval_request, and observability metrics. This stream is the backbone for real-time updates such as the approval toaster below — components subscribe to the shared context rather than each opening their own connection.

When a workflow run step enters the approval_required state, the gateway broadcasts a human_approval_request on /api/events. The dashboard’s pending-approvals context collects those and pops a toast with Approve / Reject buttons — on whatever page you happen to be on. Acting on the toast resolves the gate inline:

POST /api/workflows/runs/{run_id}/approve
Authorization: Bearer rk_<token>
Content-Type: application/json
{ "approved": true, "feedback": "looks good" }

Send "approved": false to reject; the optional feedback string is passed back to the run. Because the toaster is cross-page, an approval raised by a background workflow surfaces even if you are on Logs, Doctor, or Skins. A badge count also shows the number of pending approvals. The same approve/reject flow is available from the Workflows, editor & runs page; the gate lifecycle is described in Runs, approvals & checkpoints.

The Skins page browses, activates, uploads, and deletes UI skin packages that re-theme the dashboard. A skin is a ZIP containing a revka-skin.json manifest with CSS-variable token overrides and named image asset slots, defined per light/dark mode. Two built-in skins — revka (the default neutral-graphite enterprise palette) and matrix (the green console look) — are always present and cannot be deleted.

It is backed by these routes:

ActionMethod + pathAuth
List installed skinsGET /api/skinsBearer
Import a skin ZIPPOST /api/skins/importBearer
Delete a skinDELETE /api/skins/{id}Bearer
Serve a skin assetGET /api/skins/{id}/assets/{path}Bearer

GET /api/skins returns skin summaries (id, name, version, builtin, and the parsed manifest); built-in skins carry "builtin": true and treat deletion as a no-op. Each skin card shows light/dark mode previews (a swatch with up to four colour chips). Click Activate to apply a skin; drag-and-drop a ZIP onto the page or use the upload button to install one; delete with the trash icon.

Import (POST /api/skins/import) takes the raw ZIP body and enforces strict limits:

LimitValue
ZIP body25 MiB
Request timeout120 seconds
Extracted total size50 MiB
File count128
Allowed extensions.json, .png, .jpg, .jpeg, .webp

The importer rejects a package that has a missing or duplicate root revka-skin.json, absolute paths, .. traversal, separators that escape the skin root, symlinks, duplicate entries, unsupported extensions, unknown modes or asset slots, the reserved IDs revka/matrix, an invalid ID, manifest asset references missing from the ZIP, or any JavaScript/CSS/HTML/SVG/remote-URL content. Imported skins are stored under {workspace_dir}/skins/{skin_id}/.

The manifest declares an id, display name, version, schemaVersion (must be 1), and a modes object with light, dark, or both. Each mode may define tokens (CSS-variable overrides), assets (slot → relative path), and an optional preview image.

revka-skin.json ← manifest at ZIP root
assets/
logo.png
avatar.webp
hero.jpg
{
"schemaVersion": 1,
"id": "rabbit_garden",
"name": "Rabbit Garden",
"version": "1.0.0",
"modes": {
"light": {
"tokens": {
"--revka-bg-base": "#f7fbf2",
"--revka-bg-surface": "#ffffff",
"--revka-text-primary": "#142018",
"--revka-signal-live": "#4e9f5b",
"--revka-radius-md": "14px"
},
"assets": {
"brandLogo": "assets/logo.png",
"operatorAvatar": "assets/avatar.webp",
"dashboardHero": "assets/hero.jpg"
},
"preview": "assets/hero.jpg"
},
"dark": {
"tokens": {
"--revka-bg-base": "#090607",
"--revka-text-primary": "#f6ecef"
}
}
}
}
FieldPurpose
schemaVersionMust be 1
idStorage ID; lowercase letters, numbers, -, and _ only
nameDisplay name
versionSkin package version
modesObject containing light, dark, or both

Authored tokens must use the canonical --revka-* namespace. The --pc-* compatibility tokens are generated by the frontend bridge and must not be authored directly. Accepted values are colours (hex, rgb(), rgba(), hsl(), hsla()) and bounded CSS lengths (px, rem, em, %) for radius/spacing/font-size-like tokens. Rejected in v1: url(...), var(...), expression(...), JavaScript-like URLs, shadow tokens, background-image tokens, and arbitrary CSS.

TokenUse
--revka-bg-basepage background
--revka-bg-shellsidebar / shell surface
--revka-bg-surfacecards and contained surfaces
--revka-bg-paneltranslucent panel background
--revka-border-soft / --revka-border-stronglow-emphasis / active borders
--revka-text-primary / --revka-text-secondary / --revka-text-mutedtext emphasis levels
--revka-signal-live / --revka-signal-networkprimary / secondary accent
--revka-radius-sm / --revka-radius-md / --revka-radius-lgcorner radii

Asset values are relative paths under assets/ (remote URLs, absolute paths, traversal, and SVG are rejected). The full slot set:

SlotUI use
brandLogosidebar / header brand image
operatorAvataroperator / agent avatar surfaces
dashboardHerodashboard banner image
dashboardShowcaselarge dashboard visual panel (character / scene / product art)
dashboardAccentsmall dashboard showcase accent prop or emblem
shellTexturesubtle shell background texture
panelDecorationsubtle panel corner decoration
pageBackdropfull application backdrop behind the shell
sidebarBackdropsidebar background art or texture
headerBackdroptop header command-band background art
graphBackdropworkflow / team graph canvas background
metricDecorationoverview metric card decoration
runCardDecorationselected-run / run-summary decoration
stepCardDecorationselected-step / selected-node decoration
timelineDecorationpriority timeline decoration
riskRailDecorationrisk rail card decoration
agentRailDecorationagent rail card decoration
commandBandDecorationcommand band card decoration
recentRunsDecorationrecent runs rail decoration
statusRunningBadgerunning status badge art
statusSuccessBadgecompleted / success status badge art
statusFailedBadgefailed status badge art
statusPendingBadgepending / blocked status badge art
statusSkippedBadgeskipped status badge art

Underneath the Skins page is a CSS-variable-based theme runtime exposed through a ThemeContext. It provides a dark/light mode toggle plus skin management to every component:

  • resolvedTheme — the effective light/dark mode currently applied.
  • toggleMode() — switch between light and dark.
  • setSkin(id) — activate an installed skin (built-in or imported).
  • deleteSkin(id) — remove an imported skin.
  • getSkinAsset(slotName) — resolve a slot (for example brandLogo) to its served URL for the active skin and mode, falling back to the built-in asset when a slot is unset.

Both the active skin and the mode persist in localStorage and are re-applied on the next load, so a branded deployment stays branded across reloads. The built-in light/dark themes always exist independently of any uploaded skin package; uploaded skins layer their token and asset overrides on top.

The dashboard is fully internationalised. Every UI string flows through a useT() hook — t(key) for plain strings and tpl(key, vars) for interpolated ones — with the supported locales defined in the i18n module. A language switcher in the layout header changes the active locale at runtime, and the choice is persisted in localStorage so it survives page reloads. Switching locale does not affect agent output or stored data; it only re-renders the dashboard chrome.