The status bar gains the TUI's half of the same contract the web client got: a coloured dot plus the release the daemon is running, pinned right so a narrow terminal truncates the key hints rather than the release. `DaemonLink` mirrors web/src/daemonLink.ts — the events loop reports its connects and disconnects, a probe task polls `GET /api/info` while the daemon is unreachable, and past the grace window a modal covers the frame and locks the keyboard (quit excepted) until it answers. A daemon that comes back on a new commit cannot be reloaded into a running TUI binary, so the status bar says `updated: <hash> (restart tui)` instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.5 KiB
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
instancediffers. - 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 |
updating is web-only, because reloading is web-only. The TUI goes straight
back to online after an upgrade and puts updated: <hash> (restart tui) on
its status bar instead, until the user restarts it.
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. In the TUI the quit binding still
resolves, so a blocked client is never a trapped one. 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/infoevery 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.