Skip to content

Add a browser setup wizard, env-var config and a redesigned admin dashboard - #176

Open
virtitnerd wants to merge 28 commits into
Python-roborock:mainfrom
virtitnerd:feat/admin-dashboard-clean
Open

virtitnerd wants to merge 28 commits into
Python-roborock:mainfrom
virtitnerd:feat/admin-dashboard-clean

Conversation

@virtitnerd

Copy link
Copy Markdown

Problem

Setting up the server meant running the configure CLI and hand-editing config.toml, which doesn't fit Docker or Home Assistant users who configure everything through the environment. The /admin page was a bare status view with no setup flow, no activity view and no multi-vacuum layout.

Changes

These are one design: the dashboard needs the wizard, and the wizard needs the env-var path to be useful in a container.

  • Env-var config: ROBOROCK_SERVER_* variables generate config.toml in the container entrypoint. The [admin] group is optional, and a partial config can be completed later in the wizard.
  • Setup wizard: with no valid config.toml, the server falls into a browser wizard at /admin instead of exiting. It pre-fills from any env vars already set and can carry an external broker host.
  • Dashboard redesign: a shared Materialize-based theme (CDN, no build step), light and dark, for both the wizard and the dashboard. It shows one vacuum at a time.
  • Activity panel: a redacted reader over the HTTP and MQTT jsonl logs. mitm_redirect.py --activity-sync can also push MITM-captured traffic into it.
  • Docs: installation, the env-var bootstrap and the wizard.

Out of scope

  • Schedules, map backup probing and capability probing, which follow as separate PRs.
  • The wizard can't generate listener_mode, advertised_*_port or trusted_proxies yet. That's called out in reverse_proxy.md.

Tested

  • pytest: 487 passed. The 2 failures are CRLF fixture assertions in test_activity_log.py on Windows.
  • Live in Docker: the env-var bootstrap, the setup wizard and the dashboard against a real vacuum.
  • Not tested: the reverse-proxy modes. My proxy is SWAG, which can't proxy MQTT, so I ran the server in local_tls mode with SWAG's certificate mapped into the container.

New tests cover env-var parsing, the entrypoint, config diagnosis, the management app and wizard, the activity log, the device view and the MITM sync.

virtitnerd and others added 28 commits October 15, 2026 11:31
Lets Docker Compose deployments generate config.toml from environment
variables alone, covering the network/broker/storage/tls sections plus
the admin credentials needed for a bootable config. Mirrors the existing
Home Assistant add-on options.json -> config.toml flow in ha_addon.py,
but supports external broker mode and external_tls reverse-proxy setups
that the add-on intentionally doesn't need.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 2c64eef)
Falls back to ROBOROCK_SERVER_* env vars only when no config.toml
(compose-mounted or previously generated) and no HA add-on options.json
are present, so an existing hand-edited config is never clobbered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 19acdea)
Explains the required/optional ROBOROCK_SERVER_* variables and the
config.toml bind-mount gotcha (Docker creates an empty file for a
nonexistent host path, which the entrypoint then treats as an existing
but invalid config instead of falling through to env vars).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 94262cc)
The CLI wizard has always left broker.host blank for external brokers,
requiring a manual config.toml edit afterward. Add an optional
ConfigureAnswers.broker_host so callers that already have the host (the
upcoming web setup wizard) can write a ready-to-serve config in one
step; the CLI wizard's own behavior is unchanged since it still passes
broker_host="".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 017e9e0)
New always-reachable /admin: when config.toml doesn't exist yet, it
serves a browser-based setup wizard (the same fields as `configure`,
reusing write_config_setup so both paths produce identical output)
over plain HTTP on the same https_port the real stack uses once
configured. Once ReleaseSupervisor's own TLS'd /admin takes over that
port after a restart, this is out of the picture entirely - there is
never a second, separate admin surface running at the same time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 3db3e96)
run_server() now catches load_config() failures (missing file, bad
TOML, failed validation) and serves only the bootstrap /admin wizard
instead of crashing, exiting once the wizard writes a config so
Docker's restart policy brings up the full stack. The container
entrypoint no longer errors out when no config source is found at all
- it always execs into serve, which now handles that case gracefully.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 3146e37)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit e0e3dd0)
Neither the CLI configure wizard nor the browser Setup Wizard can
generate listener_mode, advertised_*_port, or trusted_proxies - they
only produce the simple local_tls case. Those are exactly the
deployment-topology settings the ROBOROCK_SERVER_* env vars already
cover (external_tls, advertised ports, trusted proxies), so a
SWAG/Caddy/Traefik reverse-proxy setup needs no manual config.toml
edits: just set the equivalent env vars instead of running either
wizard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit e7cb420)
STACK_FQDN plus network/broker/tls env vars alone now produce a valid
partial config.toml with no [admin] section, so a deployment can set
just the infra/TLS side (including external_tls reverse-proxy setups)
and finish credentials in the Setup Wizard - rather than requiring all
of ADMIN_PASSWORD/PROTOCOL_LOGIN_EMAIL/PROTOCOL_LOGIN_PIN up front or
not being able to use env vars at all. Setting some but not all three
is rejected with a clear error, since a half-set credential is never
what was intended.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 15bddb9)
Extracts network/broker/storage/tls parsing+validation into its own
function so it can succeed or fail on its own, separately from [admin].
Adds diagnose_config(), which classifies a config.toml as "ok",
"missing_admin" (everything but [admin] validates - e.g. a partial
env-var-generated config still waiting on the Setup Wizard for
credentials), or "invalid" (something else is wrong, or the file is
missing). load_config()'s own behavior for existing valid/invalid
configs is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit e39dd21)
Uses diagnose_config() to tell three states apart at /admin: "ok"
(status page), "missing_admin" (a trimmed wizard asking only for admin
credentials, appending just [admin] to the existing config.toml rather
than regenerating it), and anything else (the full wizard, as before -
this also covers a corrupt hand-edited file, which is correctly
refused rather than silently overwritten).

This is what makes the env-var path from feat/env-var-config and this
wizard actually compose: set only FQDN/TLS/network vars and finish
credentials in the browser, instead of an all-or-nothing choice
between the two.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit c93a28b)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 7f83688)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 1a2e247)
decompiled_http.jsonl / decompiled_mqtt.jsonl have no rotation and can
carry sensitive data (auth headers, onboarding localKey/passwords in
HTTP bodies, fully-decrypted device RPC payloads in MQTT entries). This
tails both files (they grow unbounded, so a full read is out) and
returns metadata-only summaries by default - method/path/topic/RPC
method name, sizes, timestamps, never raw headers/bodies/payloads.

ROBOROCK_SERVER_ACTIVITY_RAW=1 switches to complete, unredacted
entries - an explicit deployment-time opt-in, matching how rethink
exposes its own traffic view, rather than a per-view UI toggle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 9c9943d)
Adds ReleaseSupervisor._activity_payload() and GET /admin/api/activity
(auth-gated like every other admin route, ?limit= clamped to 1-500),
plus an Activity section on the dashboard following the existing
2s-polling pattern - there's no real-time push mechanism in this app
today (websockets/SSE), so this matches how every other panel already
refreshes.

Note: a full-suite run flagged test_admin_onboarding_start_is_blocked_when_new_connections_disabled
as failing once (256 passed/1 failed), but it passes reliably alone,
with just this file plus test_activity_log.py, and on a clean rerun of
the entire suite (257/257). AdminSessionManager (security.py) is
stateless and per-instance with no shared/global auth state for this
change to touch, so this looks like a pre-existing flaky/timing-
sensitive test that more total tests nudged into failing once, not a
regression from this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit dd825d2)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 470bf14)
New /internal/protocol/mitm-activity, gated by the same
admin.session_secret-based auth as the existing /internal/protocol/user-data
sync - no new trust mechanism. Appends posted entries to a new
mitm_activity.jsonl (AppPaths.mitm_activity_jsonl_path). The sync
request's own body is now also redacted in the regular HTTP activity
log, the same way the existing user-data sync already is, since it can
carry real Roborock account tokens.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 5181022)
read_recent_activity() gains an optional mitm_jsonl_path, tailed and
merged alongside the existing HTTP/MQTT sources with the same
redact-by-default / ROBOROCK_SERVER_ACTIVITY_RAW behavior. Dashboard
gets a distinct color/label for [MITM] entries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit c9647f4)
Off by default. When enabled (and a sync secret is configured), each
redirected Roborock-domain request/response is posted to the server's
new mitm-activity endpoint, so the official app's traffic is visible
in the admin dashboard's Activity panel without a separate viewer.
Sync failures are caught and logged, never allowed to interrupt the
actual MITM redirect/rewrite - this is telemetry, not part of the
redirect's own correctness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 6318cc1)
Same recipe as rethink's own management panel: Materialize CSS/JS and
Google Fonts loaded from CDN (no build step - this project has none
and isn't taking one on for this), plus one small theme stylesheet on
top for branding, dark mode (prefers-color-scheme), and a couple of
Materialize defaults that need overriding (fixed-height nav, focus
rings suppressed by its own box-shadow reset, etc).

Served from one route (register_theme_routes) so both the Setup
Wizard and the real dashboard pull the same theme, not two.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 47f9a54)
Real Materialize form fields/radios/select/buttons instead of hand-
built fieldset/input markup, using the same nav/theme as the real
dashboard. All required page text and element IDs the wizard's own
JS/tests depend on are unchanged; only markup and styling moved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 6b1b00b)
Login page and dashboard now use the same Materialize theme as the
Setup Wizard - real form fields, buttons, nav, cards - instead of raw
inline-styled markup. Every element ID the existing JS (refresh(),
renderAuth(), renderActivity(), etc.) depends on is unchanged; only
markup/styling moved. Dynamically-created cards (vacuum summary,
session list, activity feed) now use theme CSS variables instead of
hardcoded light-mode hex colors, so they stay readable in dark mode.

Also fixes a real XSS-shaped bug introduced partway through this
redesign and caught before commit: the activity feed's per-entry badge
was briefly built via innerHTML with server-reported traffic fields
(method/path/topic/host) interpolated directly in - the same bug class
already found and fixed in rethink's own device table. Rewritten with
DOM APIs (createElement/textContent) only; no interpolated HTML
anywhere in this file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit c045dca)
Every conditional section (external broker host, Cloudflare fields vs
provided-cert hint, Actalis fields) relies on toggling a "hidden" class
via JS. When the per-page <style> blocks were consolidated into the
shared stylesheet, this utility class didn't make the move - so toggled
sections stayed visible regardless of which radio/select option was
actually chosen. Caught by live testing in a real browser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit b3c7577)
- Nav bar (brand logo, status, sign-out) was breaking onto two lines:
  .nav-wrapper never had display:flex, so the brand-logo anchor (itself
  display:flex, making it block-level) pushed everything after it onto
  the next line, and the flex:1 spacer span was a no-op without a flex
  container. Fixing that turned the sign-out button into a flex item,
  which broke its icon's Materialize .right float (floats inside flex
  containers are unreliable across browsers) - replaced with flex
  ordering (inline-flex + margin-left:auto on the icon) on all buttons
  that use this same "icon at the end" pattern, and normalized
  .btn-flat's text-transform/font-weight to match the other buttons.
- Section headers used the class "orange-text" but our own CSS forces
  them to the blue accent color - the class name and rendered color
  didn't match. Renamed to "rls-heading" everywhere it's used.
- The "New Connections" checkbox: Materialize draws both the unchecked
  box and the checked checkmark tick using the same :before element,
  with :checked repositioning/rotating it into an L-shape (transparent
  top/left border, colored right/bottom). An initial fix themed all
  four border sides identically, which lost the transparent sides and
  rendered a solid diamond instead of a checkmark - retint each
  state's own colors instead of a blanket border-color.
- The MITM sync-secret hint showed a literal "..." placeholder instead
  of a real, copy-pasteable example command.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 20fb477)
Anything that does not match a real Roborock protocol route -
favicon requests, health checks, stray scans - falls through to the
catchall handler and got logged to the Activity feed identically to
real onboarding/protocol traffic, drowning it out.

resolve_route already labels genuinely unmatched requests with the
literal route name "catchall" as its final fallback rule. Skip
appending those to the persisted HTTP activity log the dashboard
reads, while every other route - named protocol routes and the
explicit blocked/auth-failure labels used elsewhere in this
function - logs exactly as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 0ecc331)
STACK_FQDN alone is enough to trigger has_env_config(), but the
default tls_mode (cloudflare_acme) then requires more vars that may
not be set - previously this ValueError propagated raw out of
write_config_from_env, uncaught, crashing the container outright and
crash-looping it forever under restart:unless-stopped. This only shows
up once both env-var config generation (feat/env-var-config) and the
Setup Wizard (feat/setup-wizard) exist together, so it's fixed here on
their combined branch rather than either alone.

Now the failure is caught and falls through into the Setup Wizard at
/admin, same as if no env vars had been set at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 6a6760d)
… set

Directly motivated by the fallback above: if env-var generation fails
partway through, whatever the operator *did* set (stack_fqdn, ports,
broker mode/host, tls_mode, base domain, email, acme_server) shouldn't
have to be retyped in the browser. Reveals the matching conditional
sections (external broker host, provided-cert vs Cloudflare fields,
Actalis fields) based on the prefilled values too, rather than always
defaulting to embedded/cloudflare_acme/zerossl regardless of intent.

Deliberately never pre-fills secrets (cloudflare_token, admin_password,
protocol_login_pin) - those are exactly what the wizard exists to
collect fresh in the browser rather than a compose file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit eb7659d)
With several vacuums the page stacked every card for every device into
one long feed. It now has a row of device chips plus a Server chip.
Picking a vacuum shows only its facts, onboarding state and traffic;
the global cards (Cloud Import, New Connections, Health) live under
Server. Selection is kept in the URL hash.

/admin/api/activity takes ?device=<duid or did> and keeps entries that
name the vacuum's did, duid or MQTT user as a whole path or topic
segment, reading a deeper tail so a quiet vacuum isn't pushed out by a
noisy one. The raw view no longer prints the local key.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit a4328d7)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant