Repository navigation
Conversation
|
Thanks for this. Before it gets reviewed I want to flag an overlap with #176. #214 replaces most of The two changes complement each other: #176 says schedules are out of scope, and #214 adds the schedule editor and the per-vacuum MQTT credential reset that #176 does not have. Could the editor and the reset be rebased onto #176's dashboard, using its theme and layout, so there is one dashboard? The schedule API, the credential reset logic and their tests should carry over unchanged. I'll bring #176 up to date with main first, since it conflicts now, so there is a stable base to build on. This one also depends on #213 and #179, so it makes sense for it to wait for those anyway. |
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)
Split _run_scene so a scheduled job can run the same step executor without syncing scene IDs to the robot, and add start_job(), which refuses to interrupt a routine that is already running on the device. get_routine_runner() gives other callers the per-context instance. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> (cherry picked from commit c657648)
Roborock's cloud fires server_scheduled_start jobs at nextFireTime. With the vacuum pointed at this server nothing did, so imported schedules never ran. ScheduleRunner evaluates each enabled job's cron in its own timezone and hands due jobs to the routine runner, carrying fan_power, water_box_mode and mop_mode so vac-only and vac+mop schedules behave as configured. One-shot jobs disable themselves after firing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> (cherry picked from commit b5203b2)
Start the scheduler alongside the other supervisor tasks and stop it on
shutdown. GET /user/devices/{id}/jobs now computes nextFireTime from the
cron instead of returning whatever value was captured at import time.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 14ddb5f)
schedule_store holds create/update/delete for the per-device jobs kept in the inventory, so the timer RPC handler, HTTP routes and dashboard can share one writer. Crons without their own timeZoneId now evaluate in the zone the robot is told by /location (America/New_York unless configured) rather than UTC, so a 11:00 schedule fires at 11:00 local. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> (cherry picked from commit 0751a7b)
Timer RPCs carry each schedule's vac and mop settings, and a schedule keeps the official app's link to its server_timer entry (the robot-side id in its name) so writing it back goes to the right robot entry. The four get/set/upd/del_server_timer methods are left to the robot: a robot that speaks server_timer stores those schedules itself, and answering them from this store hid the robot's real schedules and, tried once, caused data loss (the official app deletes server_timer entries it does not recognize when its schedule screen opens). A parametrized test over all four methods keeps the interceptor's method list from regressing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
POST /user/devices/{duid}/jobs creates a job (the call python-roborock
makes), and PUT/PATCH and DELETE on /jobs/{id} edit or remove one. The
GET matcher is now method-aware so a write no longer falls through to
the list route. Update and delete paths are not confirmed against the
official app; they follow the scene routes' conventions.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 5b83853)
A schedule's job (POST /user/devices/{duid}/jobs) carries a settings entry whose
"name" is the id of the matching timer the official app also saved on the robot.
The cloud runs such a job by sending server_scheduled_start with that entry, which
is the method name inside the job itself. The robot accepts it, runs the clean
(rooms or whole home) and records it as "Schedules" in its history; without the
entry it answers -10005 "Invalid timer", and with a bare id "Param should be name
+ fan + segment". python-roborock does not list the command, so it is sent through
a small SchedulerCommand enum.
A schedule with no robot-side id (one saved through the legacy set_timer), or one
the robot refuses, falls back to the plainer ways, since a refused command changes
nothing: a whole-home clean is one app_start carrying the schedule's fields plus
source 3, which the robot also records as "Schedules"; a room schedule is the
settings commands followed by a segment clean. Scenes and routines are untouched.
The new steps are produced only by schedules.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d setup flow Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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)
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)
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)
|
Agreed, @virtitnerd. I’ll hold this PR while you rebase #176. Once that provides a stable base, I’ll integrate the schedule editor and per-vacuum MQTT credential reset into your dashboard, preserving its setup wizard, environment configuration, shared theme/layout and activity panel. The underlying schedule API, credential-reset logic and tests will carry over. That will give us one dashboard with the combined functionality. This work will also wait for the scheduler dependencies in #179, including the hardening contributed from #213. |
|
Thanks. #176 is rebased onto current main now (23 commits, no conflicts, tests passing), so it is a stable base to build on. The branch is I'll keep #176 as it is until you have had a look, so you are not building on a moving branch. If you need something changed on it, say so here. |
Add schedule editing and credential recovery to the selected vacuum's dashboard sections. Preserve the shared theme, setup wizard, environment configuration, activity view, schedule API and reset backend. Keep unsaved schedule drafts across refresh and navigation, retain pending credential resets, and reject stale dashboard snapshots. Cover the combined page with API, packaging and browser regression checks.
ead65ce to
32e0631
Compare
|
Thanks for rebasing, @virtitnerd. I've updated this PR with the schedule editor and MQTT credential reset integrated into your dashboard as per-vacuum sections using the shared theme. The setup wizard, environment configuration and activity panel are preserved. The schedule API, credential-reset logic and their backend tests carried over. This now builds on #176 and the current #179 branch, including the hardening accepted from #213. Until those dependencies merge, the overall PR diff includes their changes. The dashboard additions are isolated in [this commit](pponce@32e0631). [Combined validation passed](https://github.com/pponce/local_roborock_server/actions/runs/40411911457): 730 tests on each of Python 3.11 and 3.13, 35 browser tests, wheel packaging and Docker. Live testing of this combined build remains pending. I'll keep this PR in draft and prepare #214 for merging once #176 and #179 are merged. |
Problem and result
The standalone dashboard needs a readable schedule view and a way to recover a previously paired vacuum whose old MQTT credentials block onboarding. This adds a responsive dashboard, an editor for existing jobs and a scoped credential-reset action.
Dependencies, companion work and review order
Depends on #213, which builds on the scheduler introduced in #179. Merge the server schedule support and hardening first.
This branch is stacked on #213. Until that work merges, the comparison against upstream main includes the prerequisite server changes. The isolated admin diff is:
pponce/local_roborock_server@upstream/schedule-support...upstream/admin-schedules
Please coordinate the dashboard changes with the existing #176 proposal. This PR does not replace its broader setup-wizard and environment-configuration work.
The companion LocalRock schedule proposal is:
DonSidro/LocalRock#60
LocalRock and this dashboard both use the server scheduling functionality, but neither depends on the other's UI. Once the server work is merged, they can proceed independently.
Changes
Why MQTT reset was needed
After returning an S7 to the Roborock cloud, I could not add that previously paired vacuum back to the local server while its previous MQTT login remained stored. Clearing that vacuum's credentials allowed re-pairing. The action preserves inventory, local/recovered keys, schedules, certificates, other vacuums and app logins.
Validation
I used LocalRock and the OEM Roborock app for schedule control on an S7 (a15, firmware 02.16.12) and observed the local server start it at its scheduled time. This does not establish every dashboard control or failure case on hardware. Python 3.11/3.13, browser, packaging and Docker checks passed at c22b4d9.
https://github.com/pponce/local_roborock_server/actions/runs/40214168860
https://github.com/pponce/local_roborock_server/actions/runs/40214168864
The dashboard uses the server scheduling engine; it does not add a second scheduler. LocalRock changes are separate.

