Dashboard¶
kenzy-server can serve an opt-in web dashboard — a fleet manager for your
Kenzy deployment. It is off by default and adds zero overhead when disabled
(nothing is mounted, no node-side cost). When enabled it gives you one place to see
every room node and backend service, configure nodes, control them, send
announcements, and read logs.
Keep it on the LAN
Login runs over plaintext HTTP and defaults to admin / password. Bind the
dashboard to localhost or your LAN, change the password (below), and never
port-forward the dashboard port to the public internet.
Enabling it¶
In configs/server.yaml:
dashboard:
enabled: true
bind: "127.0.0.1" # or a LAN address; never the public internet
port: 8770
controls: true # allow edits/actions (false = read-only)
logs: true # enable the log viewer
Restart kenzy-server and open http://<bind>:<port>/dashboard (e.g.
http://127.0.0.1:8770/dashboard). See the full key reference in
Server Configuration.
Logging in¶
The default login is admin / password. Change it on the server host with the
kenzy-passwd CLI (it rewrites dashboard.auth in server.yaml):
kenzy-passwd # prompts for username + new password
You can also change the password from the dashboard's Settings page. A password change takes effect immediately and signs out other sessions.
Fleet view¶
The landing page lists:
- Room nodes — one card per connected node, showing its room name, a short node id, IP address, installed Kenzy version, and live status (idle / streaming). Cards flagged ⚑ unconfigured have no saved per-node config yet. A Configure button opens the node editor.
- Backend services — STT, TTS, LLM, and Speaker health (from each service's
/health), with a few details (version, model, voice, provider).
The status pill (top right) shows whether the live channel is connected and keeps a running "last update" time. State is pushed live over a WebSocket, falling back to polling if that drops.
When controls is on, an announce composer lets you type a message and speak it
aloud on every connected node at once (synthesised once via kenzy-tts, streamed to all
rooms — a one-way public-address broadcast). You can also trigger this by voice
("tell everyone dinner's ready"); for a live two-way call between rooms, see the
intercom skill in Built-in Skills.
Configuring a node¶
Open a node's Configure page to:
- Rename its room — the room name is the node's friendly label everywhere and is
sent to the assistant as context. It is server-owned: stored in
configs/nodes/<node_id>.yaml, applied live if the node is connected and otherwise pulled on its next connect (so you can name a node before it's ever booted). Identity is the stablenode_id, so renaming a room never orphans its config. - Set up / calibrate audio — the Set up / calibrate audio… button opens a guided
wizard (device → silence → wake word). See Calibrating a node's audio
below. The raw audio keys (
audio_device, sample rates) also remain in the settings list for direct editing / pre-seeding an offline node. - Edit per-node settings — audio device + sample rates, wake-word threshold/VAD,
silence/VAD timing, wake-word models, sound files, volume (a slider), and the node's
log_level/log_capture_level. Saved values are written toconfigs/nodes/<node_id>.yamland live-re-pushed to the connected node. Each key shows a live or restart badge: live keys apply immediately on save; hardware keys are applied on the node's next boot or via the Restart button. Options with a fixed set of values (log levels, on/off, etc.) are dropdown choosers; numeric fields are number inputs. - Control the node — Trigger (start a session), Stop, or Restart (the node re-execs itself, with or without systemd).
Secrets (API keys) are never served to a node and never editable here.
Calibrating a node's audio¶
The right audio_device, silence_rms_threshold, wakeword_threshold, and
wakeword_vad_threshold depend on each room's hardware and noise level, so the defaults
are rarely ideal. The Set up / calibrate audio… button on a node's Configure page
opens a guided wizard that measures live audio and applies suggested values — no
trial-and-error YAML edits. (Requires the node connected and dashboard.controls: true.)
The wizard opens on an overview showing current values; from there run the full setup or jump straight to one step to recalibrate it:
- Audio device — choose the room's mic/speaker from the list the node reported (no
need to run
kenzy-deviceson the box). Because the device is a hardware key, the wizard saves it, restarts the node, and waits for it to reconnect before continuing — so the next steps measure the right device. (Click Keep current to skip if the device is already right.) - Silence threshold — Start, keep the room quiet (auto-stops after ~30 s), then
Apply to set
silence_rms_thresholdjust above the measured noise floor. Applies live. Too low → it keeps listening into silence; too high → it cuts you off. - Wake word — Start and say your wake word ("Hey Kenzy") a few times. Two meters
show the wake-word and voice-activity (VAD) scores, with your utterances as peaks.
Apply wake sets
wakeword_thresholdjust below your utterances (live). Queue VAD stageswakeword_vad_threshold(which suppresses near-silence false fires); it's applied — and the node restarted — when you click Finish, since the VAD gate is baked into the wake-word model at load.
The suggestions assume you follow each step's prompt (quiet for silence, speaking for wake word); all measured values are shown, and you can still fine-tune the numbers directly in the settings grid afterward.
Headless calibration (no dashboard)¶
On a node with no dashboard, run the same measurement locally:
kenzy-node --calibrate
It walks through the two phases and prints the suggested thresholds. Because node config
is server-owned (pulled on connect), the values aren't written locally — apply them
on the server, either from the dashboard's Calibration panel or by adding them to
configs/nodes/<node_id>.yaml (this node) or node_defaults in server.yaml (all
nodes).
Configuring backend services¶
The Services tab lists the configured backend services (STT, TTS, LLM, Speaker)
with live health. Open one to edit its effective config in a generic editor —
each field is the packaged default or your stored override. Saving writes
configs/services/<service>.yaml on the server and restarts the service so the new
config takes effect (the service re-pulls on boot); a separate Restart button
restarts without editing. Secrets (API keys) are read from the service host's
environment and are never shown or stored here. Requires dashboard.controls: true.
Skills¶
The Skills tab lists the skills and deterministic fast intents loaded by
kenzy-llm, each with a one-line description and an invocation count (how often it
has run since the service started). With dashboard.controls: true, each skill has an
Enable / Disable toggle: disabling one takes effect immediately, without restarting
the service (the skill stays loaded but is gated out of the tool list, execute, and
the fast path), and is persisted to configs/services/llm.yaml (skills.disabled)
so it survives a restart. Disabling a skill also disables any same-named fast intent.
Without controls, the tab is read-only.
Speakers¶
The Speakers tab manages the enrolled voice profiles held by kenzy-speaker. It
lists each enrolled voice with its sample count and the service's current
identify threshold. With dashboard.controls: true you can rename or delete
a profile. Deleting is permanent; renaming just relabels the stored embeddings.
The dashboard does not record audio in the browser. To add a voice, either run
kenzy-enroll on the server host, or use Enroll from a room: pick a connected room
node and a name, and Kenzy prompts the person at that room to say a few sentences and
enrolls them through the room's mic (enrolling an existing name adds more samples to it).
Because this is an authenticated, controls-gated operator action, it works regardless
of the speaker service's allow_voice_enroll setting (which only governs the hands-free
"Hey Kenzy, enroll me as…" voice command). Requires dashboard.controls: true.
Activity¶
With dashboard.logs: true, the Activity tab shows the recent voice interactions
the server has handled, so you can see what Kenzy heard, how it answered, and where the
time went. Each entry shows:
- the transcript (what was heard), the identified speaker and room, and the spoken response;
- a fast / LLM tag — whether the deterministic fast path handled it or it went to the language model;
- a latency breakdown (capture = STT + speaker ID in parallel, then LLM, then TTS) and the total response time.
The header summarises the fast-path hit rate and average response time across the
recent window. It's a bounded in-memory ring (no disk, ~200 entries) that updates live;
because entries include transcripts it's gated by the same dashboard.logs flag as the
log viewer, and nothing is recorded when that's off.
Logs¶
With dashboard.logs: true, the Logs tab pulls a bounded in-memory buffer from a
source you pick: the server, any backend service, or any connected node. Filter by
level (down to TRACE). Logs are pull-based — a node only keeps a buffer when the
dashboard asks it to, so a dashboard-less server adds no node overhead.
Each source captures down to its log_capture_level (default debug),
independently of what it prints to its own console (log_level). So a node logging
INFO to its console can still surface DEBUG in the viewer. Levels below a source's
capture level aren't kept — raise that source's log_capture_level (e.g. to trace,
which includes the node's per-frame audio logs) from its config to see deeper.
Temporary TRACE capture (nodes). The node's most detailed logs (per-frame
RMS/VAD) are at TRACE, off by default to avoid flooding. When a node is the selected
source, a Capture TRACE button (with a duration picker) boosts that node to TRACE
capture live for the chosen window and then auto-reverts — no restart, nothing
persisted. Refresh during/after the window to view the captured detail. Requires
dashboard.controls.
Settings¶
The Settings page shows system info (Kenzy version, server and dashboard binds, mDNS discovery), the node join token, and lets you change the dashboard password and edit a scoped subset of the server's own configuration.
Under Node provisioning the page displays the discovery.token (the shared secret a
node presents to join, also the service-to-service bearer) with a copy button, so you can
paste it into a node install — kenzy-init --profile node --token … or the installer's
--token. This is the one secret the dashboard surfaces, deliberately: it's a
provisioning value an admin needs, shown only over the authenticated Settings page (not
an upstream API key). If no token is set, the page warns that any device on the network
can register as a node.
The Server configuration editor exposes the safe-to-change keys: the dashboard
sub-flags (logs, controls), each backend service's url/timeout, the unknown-speaker
label, and mDNS discovery.enabled/instance. Saving writes a server.local.yaml
override layered over your hand-edited server.yaml (so comments are preserved) and
restarts the server to apply it — the dashboard briefly disconnects and reconnects.
For safety, lockout/secret-sensitive keys (server host/port, the dashboard bind/port,
the login credentials, and the discovery.token) are not editable here and stay
file- or CLI-managed. Because this editor is the way to turn controls on in the first
place, it requires login but not controls.
Permissions & security¶
- Both reads and mutations require login. All
/api/*endpoints (fleet state, node config, logs, transcripts) need a valid session; only the login/logout/me endpoints and the static assets are public. Mutations (config edits, rename, controls, announce) additionally requiredashboard.controls. dashboard.auth_tokenis an optional bearer for API/CLI clients; browsers use a signed, HttpOnly session cookie from the login form.- Change the default password promptly — the dashboard warns (startup log + a Settings
banner) while it's still on
admin/password. - The
discovery.token(orKENZY_SERVICE_TOKEN) doubles as a service-to-service bearer the server uses for its backend calls and log proxying. - The
/wschannel (which carries all mutations) rejects cross-site handshakes (the browserOriginmust match theHost). For extra DNS-rebinding protection when you serve the dashboard under a fixed name, setdashboard.allowed_hosts.
HTTPS (optional)¶
Login and traffic are plaintext by default — fine on a trusted wired LAN, weaker on
Wi-Fi. To encrypt it, put the dashboard behind a reverse proxy that terminates TLS
(Caddy gets you an automatic cert for a routable name; nginx/Traefik work with your own
cert). Have the proxy forward X-Forwarded-Proto: https — the dashboard then marks its
session cookie Secure automatically. Kenzy deliberately does not generate
self-signed certs (the browser warnings train people to click through security prompts).
Whatever you do, keep the dashboard off the public internet.