Repository navigation
dashboard: auto-update toggle, "check for updates", dated stale-aware "updated" stamp - #1334
Merged
Merged
Conversation
An operator on tl-dell wanted scheduled deploys OFF while hestia moves daily, but still wanted one-click updates, and wanted to switch the schedule back on without a shell: "There should be a UI element that enables the time [timer] to be turned on/off easily without any coding required." The update button already starts hestia-deploy.service directly, so it does not need the timer. This adds a header toggle next to it: - DeploymentHealth.auto_update: on | off | absent | unavailable, read from the unit files (hestia-deploy.timer + timers.target.wants link) rather than by spawning systemctl, so the dashboard poll stays process-free (#1183). - POST /api/operator/deployment/auto-update {"enabled": bool}: a fixed `systemctl --user enable|disable --now hestia-deploy.timer`, behind the operator gate, witnessed before the side effect and again with the outcome. - The toggle shows only when the timer unit is installed. Turning it on asks for confirmation, because Persistent=true can start a deploy at once. Linux (user systemd) only. On macOS the deploy agent is one launchd plist and the toggle reports `unavailable` and stays hidden. surface: operator_deployment_auto_update act: arm/disarm scheduled deploys of this box S: med/reversible [construct: auto_update_trigger_for; the opposite toggle undoes it] R: pass [construct: operator_gate route_layer on operator_surface] W: pass [construct: current_operator_context + attach_operator_provenance] O: pass [construct: deployment_auto_update_intent append_chain before deployment_trigger_output] A: pass [construct: intent record (before/requested/trigger) + deployment_auto_update_changed (after/outcome)] V: n/a (reversible; turning on can trigger a deploy, which keeps hestia-deploy's own preflight + rollback) [construct: hestia-deploy.sh preflight_gate] verdict: PASS Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while answering "is there an auto-update toggle in the UI?": with the timer off, the update button never appears for a new hestia main. The dashboard compares the running build with the LOCAL manifest only, and the daemon never fetches, so a box behind origin/main reads "deployment current" and hides the button. The only way to pick up a new main, including the merge that ships the auto-update toggle, was to turn the schedule back on. - operator_deployment_update no longer short-circuits with "already current". The request goes to the supervisor, which syncs origin/main, logs CURRENT without building when nothing moved, and deploys when something did. The intent record now carries `locally_current`. - deployment_update_status reports requested/held/running/failed for a current box too (fresh records only), so a check shows "checking…" and a failed one (e.g. main no longer builds) shows "retry check". A "succeeded" record on a current box is plain idle. - The button shows whenever a supervisor is registered and the state is current or stale: "update" (warning style) when stale, "check for updates" (quiet style) when current. surface: operator_deployment_update act: ask the registered supervisor to deploy origin/main S: med/reversible [construct: hestia-deploy.sh rollback to hestia.prev] (unchanged; the act was already reachable whenever stale) R: pass [construct: operator_gate route_layer on operator_surface] W: pass [construct: current_operator_context + attach_operator_provenance] O: pass [construct: deployment_update_request_intent append_chain before create_deployment_update_request] A: pass [construct: intent record now includes locally_current] V: present [construct: hestia-deploy.sh preflight_gate refuses a gate that cannot answer; deploy.hold] verdict: PASS Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit made the update button visible on every Linux/macOS box whose state is current or stale. A normal install does not set up the deploy supervisor (deploy/from-main/README.md is a separate once-per-box step), so on a fresh box the new "check for updates" button could only fail with "Unit hestia-deploy.service not found". That is #791 episode 1, made more likely. - deploy_supervisor_installed(): hestia-deploy.service in the user systemd unit dir (Linux) or the com.web4.hestia.deploy LaunchAgent plist (macOS), read from disk so the poll spawns nothing. - deployment_health: without it, update_capable=false, state "unavailable", and update_note names the remedy (shown in the banner's tooltip). - operator_deployment_update refuses with 409 and the same remedy text before witnessing or publishing anything. #791 ask 1. S/R/W/A unchanged from the previous commit. O: the new refusal precedes the intent record and the request file, so a refused act leaves state untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…un in the tooltip The operator asked whether the footer's "updated" time shows a date "if there was an issue and multiple days had gone by". It did not: it was toLocaleTimeString() only, so data three days old read like this afternoon's. - fmt.stamp: time alone when today, "Oct 14, 3:26 PM" otherwise. - The footer stamp is re-rendered on its own 30 s timer, so it keeps ageing while every poll fails. Older than 120 s it adds "(2d ago)" in the warning colour, with a tooltip saying data has stopped arriving. That also covers the case the header misses: daemon up but no longer refreshing, header still "live". - DeploymentHealth.last_cycle: the last terminal line hestia-deploy wrote to $HESTIA_HOME/deploy.log (CURRENT, DEPLOYED, HALF-DEPLOYED, FAIL, SKIP, STALE, PREFLIGHT, BUILD-ONLY), read from a 32 KiB tail, no process spawned. It covers timer runs too, which deploy-status.tsv does not. The deployment banner tooltip now ends with "last deploy run Oct 16 1:24 PM: CURRENT …". Read-only display; no new act, so no review-gate block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI's plugin tests failed on the first commit's new route: tools/operator_surfaces_spec_test.py and operator_surfaces_ratchet_test.py require every operator route and every route the dashboard calls to belong to a capability. Adds `deployment-auto-update` beside `deployment-update`: signed-in-only, high risk, dashboard required, app planned (the same sequencing note as deployment-update), with the two display obligations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
|
NOVA review follow-up (2026-10-17): HOLD: auto-update timer action can succeed while deployment_auto_update_changed outcome-witness append fails silently. Also ensure requested state equals observed timer state and serialize opposing toggles. Report post-effect witness failure explicitly and add fault-injection/concurrency/state regressions. |
…r, serialize toggles Nova's HOLD on #1334 (2026-10-17): - The outcome record (deployment_auto_update_changed) was appended with `let _ =`, so a toggle could take effect with no record and the operator was told plain success. The reply now carries `witnessed`, and a change that took but was not recorded is a 500 whose error says the change is in force (the dashboard shows that, not "failed", so it is not clicked again). - Success was systemctl's exit code. It is now the unit files afterwards: exit 0 with the timer still reading the old state is outcome `not-applied`, a 500, recorded as such. - Two opposing toggles could interleave their read, intent record and systemctl call. A process-wide lock now covers the whole sequence, from the first read to the outcome record. The toggle body moved into `toggle_auto_update` (unit dir and runner passed in) so these can be tested without a real user systemd: - auto_update_reply_reports_what_happened_not_what_was_asked: the reply matrix, including the witness-failure fault; - auto_update_toggle_reports_a_change_systemctl_did_not_make; - opposing_auto_update_toggles_are_serialized: fails with the lock removed (checked). -- cbp-claude Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
Owner
|
Addressed Nova's HOLD in 911c223 (pushed onto this branch, no force):
Not changed here: the existing — cbp-claude |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
On tl-dell (WSL2, member seat) the operator wanted scheduled deploys off while hestia moves daily, but still wanted one-click updates, and the ability to switch the schedule back on without a shell. In their words: "There should be a UI element that enables the time [timer] to be turned on/off easily without any coding required."
The update button already starts
hestia-deploy.servicedirectly, so it doesn't need the timer. What was missing was a way to switch the timer itself. Context: forumtristan-work-update-should-be-one-step-2026-10-16.md, #791.What
DeploymentHealth.auto_update:on | off | absent | unavailable. It is read from the unit files (hestia-deploy.timerand thetimers.target.wants/link) instead of by spawningsystemctl, so the dashboard poll stays process-free (A visible dashboard tab stalls every request ~1.2 s every 20 s: /api/agents runs the inventory with a blocking Command::output() inside an async handler, and the 300 s gate-watch pass runs it under the state lock #1183).POST /api/operator/deployment/auto-update {"enabled": bool}: runs a fixedsystemctl --user enable|disable --now hestia-deploy.timer. It sits behind the operator gate and is witnessed twice: an intent record before the side effect, and an outcome record (before, requested, after) afterwards. The browser supplies only the boolean.Persistent=truecan start a deploy at once.Linux (user systemd) only. On macOS the deploy agent is a single launchd plist, so the toggle reports
unavailableand stays hidden. Follow-up if wanted.Tested
cargo test --lib(first commit): 1247 passed, 0 failed (local, againstweb4.pin31990db7), including newauto_update_state_reads_the_timer_unit_filesandauto_update_toggle_is_fixed_on_linux_and_absent_elsewhere.tools/dashboard_deployment_update_contract_test.py: PASS. Dashboard inline JS passesnode --check.disable --now→disabled/inactive), and the unit-file layout the state reader relies on matches what systemd printed (Removed …/timers.target.wants/hestia-deploy.timer).Review gate
Second commit: "check for updates" when the box looks current
Found while checking whether the toggle was live: with the timer off, the update button never appears for a new hestia main. The dashboard's stale/current compares the running build with the local manifest only, and the daemon never fetches. So a box that is behind origin/main reads "deployment current" and hides the button. The only way to pick up a new main, including the merge that ships this toggle, was to turn the schedule back on.
operator_deployment_updateno longer short-circuits with "already current". The supervisor syncs origin/main, logsCURRENTwithout building when nothing moved, and deploys when something did. The intent record now carrieslocally_current.deployment_update_statusreports fresh requested/held/running/failed records for a current box too, so the button reads "checking…" during a check and "retry check" after a failed one (e.g. main no longer builds). Asucceededrecord on a current box is plain idle.a_current_box_still_shows_an_operator_update_check_in_flight.Re-tested on the second commit:
cargo test --lib1248 passed, 0 failed.tools/dashboard_no_undef.mjsreports clean (run with the CI command). The deployment update contract test passes.Review gate (second commit; the act itself is unchanged, it was already reachable whenever stale):
Third commit: no update button where no deploy supervisor is installed
The second commit made the button visible on every Linux/macOS box. A normal install doesn't set up the deploy supervisor, so on a fresh box "check for updates" could only fail with
Unit hestia-deploy.service not found(#791 episode 1).deploy_supervisor_installed()reads it from disk, so the poll still spawns no process:hestia-deploy.servicein the user systemd unit directory on Linux,com.web4.hestia.deploy.plistin~/Library/LaunchAgentson macOS.update_capable=falseand the state isunavailable. The update note names the remedy, which the deployment banner's tooltip shows.operator_deployment_updatereturns 409 with the same remedy before writing the intent record or the request file. That covers dashboard update button: failure states don't name their remedy (unit-not-found, half-deployed manifest) #791 ask 1.#1335 is the companion: it makes
deploy/fleet/install.shinstall the supervisor (timer off) on checkout installs, so a fresh box has a working button.Re-tested on the third commit:
cargo test --lib, 1248 passed, 0 failed. The deployment update contract test passes.Fourth commit: a dated "updated" stamp that flags stale data, and the last deploy run in the tooltip
The operator asked whether the footer's "updated" time shows a date "if there was an issue and multiple days had gone by". It didn't: it used
toLocaleTimeString()only, so data three days old read like this afternoon's.fmt.stamp: time alone when it's today,Oct 14, 3:26 PMotherwise.(2d ago)in the warning colour, with a tooltip saying data has stopped arriving. This also covers daemon up but no longer refreshing, where the header still reads "live".DeploymentHealth.last_cycle: the last terminal line hestia-deploy wrote to$HESTIA_HOME/deploy.log(CURRENT / DEPLOYED / HALF-DEPLOYED / FAIL / SKIP / STALE / PREFLIGHT / BUILD-ONLY). It's read from a 32 KiB tail, with no process spawned. It covers timer runs too, whichdeploy-status.tsvdoesn't. The deployment banner's tooltip now ends withlast deploy run Oct 16 1:24 PM: CURRENT ….This is a read-only display with no new act.
Re-tested on the fourth commit:
cargo test --lib: 1249 passed, 0 failed. That includes the newlast_deploy_cycle_is_the_last_result_line_not_the_last_line, which is built from real deploy.log output, inventory noise lines included.dashboard_no_undef.mjsreports clean, and the contract test passes.Fifth commit: operator-surfaces spec
CI's plugin tests caught that the new
POST /api/operator/deployment/auto-updateroute was in no capability (operator_surfaces_spec_test.py,operator_surfaces_ratchet_test.py). This commit addsdeployment-auto-updatenext todeployment-updateindocs/operator-surfaces/spec.json: signed-in-only, high risk, dashboard required, and app planned (the same note as deployment-update). Both tests pass locally.🤖 Generated with Claude Code