feat(maestro): daemon status indicator and restart overlay on web
The sidebar footer gains a status dot beside the spawn button: green while the daemon answers, amber while reconnecting, red while it is gone. Hovering it shows the release the daemon is running — commit hash and commit date. When the events socket drops, `DaemonLink` starts polling `GET /api/info`. Past a 1200 ms grace window an overlay blocks the UI (and swallows the app's shortcuts) until the daemon answers again. If it comes back on a different commit the page reloads, so a tab never drives new server code from the bundle that was just replaced; a plain restart just unlocks. docs/RESTART.md is the contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
eaf60d9dac
commit
4d7e368d31
@@ -0,0 +1,78 @@
|
||||
# Restart and upgrade contract
|
||||
|
||||
Maestro is upgraded in place: the operator reinstalls the binary and systemd
|
||||
restarts the daemon under every browser tab and TUI that is attached to it.
|
||||
tmux keeps the sessions alive across that (see `docs/INSTALL.md`), but clients
|
||||
lose their sockets. This document is the contract for what a client does about
|
||||
it — what the daemon publishes, how a client tells a restart from a blip, and
|
||||
what the user sees while it happens.
|
||||
|
||||
## What the daemon publishes
|
||||
|
||||
`GET /api/info` carries two fields beyond identity and presets:
|
||||
|
||||
| field | meaning |
|
||||
| ---------- | -------------------------------------------------------------- |
|
||||
| `build` | `{ commit, commit_date, dirty }` — the commit the binary was compiled from |
|
||||
| `instance` | uuid regenerated on every daemon start |
|
||||
|
||||
`build` is stamped at compile time by `crates/maestro-daemon/build.rs`, which
|
||||
runs `git` in the checkout it is compiled in. The Nix build has no `.git` in the
|
||||
crane-filtered source tree, so `flake.nix` passes the flake's own revision as
|
||||
`MAESTRO_BUILD_COMMIT` / `MAESTRO_BUILD_DATE` / `MAESTRO_BUILD_DIRTY`. A build
|
||||
with neither source reports empty strings — clients render that as
|
||||
"unknown release" and fall back to `instance` alone.
|
||||
|
||||
`instance` is the one that never goes missing. It says *this process*, so two
|
||||
replies that differ on it are two runs of the daemon, even when the commit did
|
||||
not change (a plain `systemctl restart`, or a crash).
|
||||
|
||||
## How a client reads them
|
||||
|
||||
Three verdicts, from comparing the reply before an outage with the one after
|
||||
(`compareIdentity` in `web/src/daemonLink.ts`, mirrored by
|
||||
`crates/maestro-tui/src/daemon_link.rs`):
|
||||
|
||||
- **upgraded** — both commits are known and differ. New server code.
|
||||
- **restarted** — commits match (or are unknown) and `instance` differs.
|
||||
- **same** — neither changed; the socket blipped, the daemon never moved.
|
||||
|
||||
Only *upgraded* is special. A browser tab is holding a bundle that the upgraded
|
||||
daemon no longer serves, so it reloads the page rather than driving new server
|
||||
code from an old bundle. The TUI is its own binary — a reload would not replace
|
||||
it — so it stays up, reconnects, and says which release it is now talking to;
|
||||
the user restarts `maestro tui` when they want the new client.
|
||||
|
||||
## What the user sees
|
||||
|
||||
The **status indicator** lives in the sidebar footer, left of the spawn button,
|
||||
in both clients. Its colour is the link state, and it names the running release
|
||||
— commit hash and commit date — on hover (web) or in the status bar (TUI).
|
||||
|
||||
| state | colour | meaning |
|
||||
| -------------- | ------ | -------------------------------------------------- |
|
||||
| `connecting` | amber | first probe hasn't landed |
|
||||
| `online` | green | daemon answered; same process, or a resolved new one |
|
||||
| `reconnecting` | amber | socket dropped, still inside the grace window |
|
||||
| `down` | red | unreachable past the grace window — the UI blocks |
|
||||
| `updating` | red | answered on a new commit; the page is reloading |
|
||||
|
||||
The **restart overlay** is up for exactly `down` and `updating`. It is not
|
||||
dismissable and it swallows the app's keyboard shortcuts: everything behind it
|
||||
would act on a daemon that is not there. It clears itself the moment a probe
|
||||
succeeds — there is no "retry" button, because the client is already retrying.
|
||||
|
||||
## Timing
|
||||
|
||||
- **Grace window** — 1200 ms between the socket dropping and the overlay
|
||||
appearing. A wifi hiccup or a laptop waking up should not throw a modal.
|
||||
- **Poll cadence** — `GET /api/info` every 700 ms while unreachable, flat, not
|
||||
backed off: a restart takes a second or two and the point is to unlock as soon
|
||||
as it is over. The events socket keeps its own exponential backoff for the
|
||||
session stream; the two are independent, and the probe is what decides state.
|
||||
|
||||
The events socket dropping is the trigger, not a timer: the daemon cannot warn
|
||||
anyone it is about to die, so its socket closing is the earliest signal there
|
||||
is. A reconnect of that socket does **not** by itself clear the state — only a
|
||||
probe does, because the socket carries no identity and reconnecting to an
|
||||
upgraded daemon must reload rather than resume.
|
||||
Reference in New Issue
Block a user