Dashboard¶
The dashboard is the web page where you run your Kenzy home: see every room and
service at a glance, name rooms, pick and tune microphones, enroll voices, send
announcements, read logs, and install updates — all from a browser, no terminal
needed after install. It's served by kenzy-server and is on by default,
reachable from any machine on your network at http://<server>:8770. (Set
dashboard.enabled: false to turn it off entirely — when disabled, nothing is
mounted and it adds zero overhead.)
Keep it on the LAN
Login runs over plaintext HTTP by default 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. To encrypt it,
enable TLS.
Enabling it¶
The shipped config already has it on. The relevant block in configs/server.yaml:
dashboard:
enabled: true
bind: "0.0.0.0" # reachable on your LAN (the default); "127.0.0.1" = this machine only
port: 8770
controls: true # allow edits/actions (false = read-only)
logs: true # enable the log viewer + Activity tab
Open http://<server>:8770 in a browser (the same machine can use
http://localhost:8770). After changing this block, restart kenzy-server. 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 writes dashboard.auth into the server.local.yaml
override layer, so the login survives upgrades and deploy config syncs):
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, live status (idle / streaming), and a
system row with the node's CPU, RAM, and disk usage plus its temperature
(refreshed every ~30 seconds; the temperature turns red at 80°C — Raspberry Pi
throttling territory, usually a sign the node wants a heatsink or better airflow).
Cards flagged ⚑ unconfigured have no saved per-node config yet; rooms with
hardware_aec: falsecarry a no AEC badge. 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 picks the
audio device, then launches the guided calibration session (the same one
"Hey Kenzy, calibrate" runs). 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, Restart (the
node re-execs itself, with or without systemd), or Upgrade (the node pip-upgrades
kenzy[node], honoringconstraints.txt, and reconnects on the new version — watch the version on its fleet card to confirm).
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,
wakeword_vad_threshold, and hardware_aec depend on each room's hardware and noise
level, so the defaults are rarely ideal. Calibration is one guided session with two
ways in — and it needs a person standing where they'd normally talk either way:
- Say "Hey Kenzy, calibrate" in the room: she speaks the instructions.
- Click Set up / calibrate audio… on the node's Configure page: the same session
runs with the instructions shown in the dashboard (the node beeps once instead of
speaking — no TTS needed). Requires the node connected and
dashboard.controls: true.
The dashboard flow starts with the one step that needs a human at a screen:
- 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. (Click Keep current to skip if the device is already right.) - Calibration — one automated pass, watched live in the dashboard:
- Echo check — the node plays a known signal through its own speaker and
measures how much leaks back into the mic. This sets
hardware_aecautomatically (present ⇒ you can interrupt her mid-sentence; absent ⇒ half-duplex mode, and intercom/alarms are disabled on that node). An ambiguous reading — or a muted/very quiet speaker — changes nothing. - Quiet phase — a few seconds of room silence (a loud noise restarts it once).
- Wake phase — say "Hey Kenzy" four times from where you'd normally speak. The
repetitions double as a sample of your voice level, and
silence_rms_thresholdis anchored to your voice (not the quiet floor), so an appliance starting up later stays below it.wakeword_thresholdandwakeword_vad_thresholdcome from the same pass. - Apply + verify — values apply automatically (the node restarts once if the VAD gate changed), then she really listens: say "Hey Kenzy" (then "never mind") to prove the wake word works — if she misses it, the threshold is nudged down, at most twice.
The result panel shows every applied value, the echo-cancellation verdict, a noise-to-speech separation rating (good / marginal / poor), and whether the wake word verified live. Anything that couldn't be measured cleanly keeps its previous value — and the panel says so. You can still fine-tune the numbers directly in the settings grid afterward.
Headless calibration (no dashboard)¶
On a node with no server flow available, run the measurement locally:
It walks through the same two phases (quiet, then speak) with the same voice-anchored
math and prints the suggested thresholds — no echo check, since that needs the
server-driven playback path. Because node config is server-owned (pulled on
connect), the values aren't written locally — apply them on the server, either from
the dashboard 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, and an Upgrade button pip-upgrades that service to the
latest release (honoring constraints.txt) and restarts it — the install runs in the
background and reports the result. 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 shows everything loaded by kenzy-llm, grouped into
collapsible modules — one accordion group per skill file (home assistant,
lists, schedule, …), because a "feature" is usually several skills plus
their fast intents. Each group header shows a skill/fast-intent count and a
Disable all / Enable all toggle for the whole module; expand a group to see
its member skills, each with a one-line description, an invocation count,
and its own individual toggle.
All toggles take effect immediately, without restarting the service (a
disabled skill stays loaded but is gated out of the tool list and the fast
path) and persist to configs/services/llm.yaml (skills.disabled, which
accepts function names and module names). Group headers also carry honesty
badges: a module that depends on another (lists requires home_assistant —
they're stored in HA's to-do entities) is marked inactive while its
dependency is off, and modules with wider blast radius say what else they power
(disabling home_assistant also idles the Home Assistant screen and lists).
Without controls, the tab is read-only.
Home Assistant¶
The Home Assistant tab edits the device curation layer for the Home Assistant
skill — the small set of things HA can't store. The device inventory itself is pulled
live from HA (via kenzy-llm) and shown as a tree (floor → area → domain → entity);
you don't list devices here. Each entity row has:
- aliases — extra spoken names ("the lamp", "black light")
- note — free-form context handed to the resolver ("the light by the chair")
- default — include in this room's bare "turn on the lights" set
- in groups — uncheck to keep it addressable by name but out of group commands (a bare "turn off all the lights" still includes it; only exclude removes it from voice entirely)
- exclude — remove it from voice control entirely
plus bulk exclusions (patterns/domains/areas) for things like smart-plug status LEDs
that show up as controllable lights, and a Lists section for the shopping/to-do
voice layer — pick which HA to-do list a bare "the list" means and give lists spoken
aliases ("the groceries"); see Built-in Skills → Lists.
Saving writes curation.yaml and refreshes the topology immediately. The tab needs kenzy-llm reachable and dashboard.controls: true
to edit (read-only otherwise). See Home Assistant.
Two states the tab is honest about: with the home_assistant module disabled
(Skills tab), a warning banner says nothing here takes effect until it's
re-enabled — but the screen stays editable, so you can stage curation before
flipping the feature on. And with no HA_API_KEY configured at all, the tab
shows step-by-step connection guidance instead of an error.
People¶
The People tab is the one surface for who Kenzy knows and what she knows about them. The model is person-first: a person can exist without a voice, but every enrolled voice belongs to a person — and memory belongs to people too, so it lives here rather than as a separate screen.
The list. Each household member appears as a card with their voice status
and memory count (voice · 5 samples · 3 memories). Open a person to
manage them. Below the list sit the things that belong to no single person: a
search across every remembered fact, Household memory (shared-tier
facts the whole house can ask for), voices without a person (from a
pre-people setup or the kenzy-enroll CLI — link them to a person, or delete
the profile), and facts without a person (memories whose person record was
deleted — forget them, or recreate the person with the same id to reclaim
them).
The person page (open a card) manages everything about one member:
- Identity — rename them (their voice profile is keyed by a stable id, so renaming never touches it) and choose which enrolled voices are theirs.
- Enroll voice — pick the room whose microphone should record, and Kenzy
prompts the person there to say a few sentences; the dashboard never records
audio in the browser. The profile is linked automatically, and enrolling
again later adds more samples, which makes recognition more reliable.
As an authenticated,
controls-gated operator action it works regardless of the speaker service'sallow_voice_enrollsetting (which only governs the hands-free "Hey Kenzy, enroll me as…" command — itself person-first: it finds or creates the person record for the name it hears). - Home Assistant person — links this member to their HA login, so asking Kenzy from the companion app arrives as them (their memory, their personalization). When Kenzy can reach your HA it's a dropdown of your actual HA people; the field (and the whole Home Assistant tab) appears only for households where HA is actually in the picture — a no-HA install shows no HA surfaces at all.
- Memories — what Kenzy holds for this person ("Hey Kenzy, remember that…"), with tier, age, and a Forget button per fact. Facts they've shared with the house are deliberately not listed here — they live only under Household memory on the People page, so tidying up one person's memories can never accidentally delete a fact the whole household relies on.
- Delete person — removes the record only; their voice profile stays relinkable, their personal facts move to the "facts without a person" bucket, and anything they shared stays in Household memory (it's the house's now).
- Privacy & data — the section that answers "what does Kenzy know about me" and "make her forget me". Export their data downloads one file: person record, voice-profile info, and every remembered fact. Don't remember… is a per-person opt-out (turning it on offers to erase what's already stored for them) — no memory writes or reads while they stay a recognized voice for everything else. Remove completely is the guest-departure case: one typed-confirm action erases their facts, deletes their voice, and removes the record (household-shared facts they contributed stay with the house). Unlike Delete person, Remove is total.
A note on scope: this tab sees all memory tiers, because the dashboard is a credentialed admin surface — tiers gate voices, not the login. By voice, a private fact is only ever spoken back to its owner, an about them fact is readable by anyone Kenzy recognizes, and a shared fact belongs to the whole house. An unrecognized voice gets no memory at all.
Person records live in data/people.yaml and the memory ledger in
data/memory/ on the server — both plain text, both riding
backups automatically (see
Speaker configuration for the people schema).
Editing and re-tiering facts from this screen, retention windows, and the
consolidation log arrive with the memory hardening phase.
Scheduled¶
The Scheduled tab lists the active timers, alarms, and reminders held by the
server — kind, name/text, target room, when it fires (live countdown for timers), and
any recurrence — with a per-entry Cancel button (requires dashboard.controls).
Entries are set by voice (see Timers, Alarms & Reminders)
and persist across server restarts. The view is available to any logged-in user —
deliberately not gated behind dashboard.logs, since these are future announcements
the operator needs to see to manage.
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.
The latency bars share one time scale across all shown interactions (the legend notes what full width represents), so segment widths are directly comparable run-to-run — a slow LLM call is visibly wider than a fast one, and fast-path replies show as slivers.
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), an update check, a backup download, the node join token, and lets you change the dashboard password and edit a scoped subset of the server's own configuration.
The Backup section downloads a .tar.gz of the deployment's state — node/service
settings, voice profiles (fetched from the speaker host when remote), HA curation,
custom skills — restorable with kenzy-init --restore. Two opt-in toggles widen the
scope: include secrets (.env — the archive then carries live API keys) and
include everything (models/). See Backup & Restore.
The API keys section is a write-only secret editor: pick a key
(OPENAI_API_KEY, HA_API_KEY, HF_TOKEN, or a custom name), paste a value, and it's
written to the server host's .env — values are never displayed or logged; the page
only shows which names are set. Restart the affected services (Services tab) to apply.
As of 3.12 this is fleet-wide: with TLS enabled, the server hands each backend
service the keys it needs over the authenticated config channel, so a key set here
takes effect on every host — you no longer maintain a separate .env per machine.
(Without TLS, secrets aren't served, and remote hosts fall back to their own local
.env.) The server's value wins, so rotating a key here actually rotates it everywhere.
Requires dashboard.controls. Unless you've enabled TLS, the
dashboard runs over plain HTTP — enter keys from a machine on your own network.
The Updates section compares the installed version against the latest kenzy release
on PyPI and flags when one is available. It's checked lazily (only when you open Settings,
cached ~1 hour) and degrades gracefully on an offline/air-gapped host. When an update is
available and dashboard.controls is on, an Upgrade server button runs
pip install -U "kenzy[server]" in the server's venv (honoring your constraints.txt
pins, pinned to the target version) and then restarts the server. The install runs in the
background — the dashboard disconnects while it works (a few minutes) and reconnects when
the server is back on the new version; a failed install is reported and leaves the server
running as-is. This upgrades the server host only; backend services and room nodes are
upgraded separately.
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. Two ways to encrypt it:
- Built-in TLS — set
tls: {cert, key}inserver.yamland the dashboard serves https (and the node WebSocket port wss) directly. A self-signed cert works; your browser shows a one-time warning (or install the cert). See Server Configuration → TLS for the one-lineopensslcommand and how Kenzy's own clients handle it. - Reverse proxy terminating 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.
Either way the dashboard marks its session cookie Secure automatically. Kenzy never
generates certs for you — TLS is a deliberate opt-in with a cert you supply. Whatever
you do, keep the dashboard off the public internet.