# 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 | `updating` is web-only, because reloading is web-only. The TUI goes straight back to `online` after an upgrade and puts `updated: (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/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.