Skip to main content

max / makenotwork

Rename ops-viewer to magicmirror Crate, binary, directory, config path and theme search path all move together. The config path has no fallback, so astra's deployed panel keeps running on the old binary and the old ~/.config/ops-viewer/ until it is redeployed; migrating the unit, the binary and the config directory is one deliberate step, not a side effect of this commit. Also updates the sando manifest comment, the site credits line, and the wiki marker in main.rs, which now points at magicmirror-overview. 91 tests pass under the new name.
Author: Max Johnson <me@maxj.phd> · 2026-08-12 17:49 UTC
Signed with PGP, not checked
Commit: f85a30168c5fb65d0a887e759db8f8415fe2a252
Parent: 437cc10
14 files changed, +95 insertions, -95 deletions
@@ -1,7 +1,7 @@
1 1 # Workspace root so `cargo build`/`cargo test` at sando/ covers the daemon.
2 2 # Kept as a workspace (rather than a flat crate) so future sando-side crates can
3 3 # join without a restructure. The sando-tui member was retired 2026-07-22 when
4 - # the shared ops-viewer replaced it; see wiki [[release-viewer-overview]].
4 + # the shared magicmirror replaced it; see wiki [[magicmirror-overview]].
5 5 [workspace]
6 6 resolver = "3"
7 7 members = ["daemon"]
@@ -82,7 +82,7 @@
82 82
83 83 **Errors**: `anyhow`, `thiserror`.
84 84
85 - **CLI and TUI** (mnw-cli, ops-viewer, wam): `clap`, `ratatui`, `crossterm`, `russh`, `russh-sftp`, `dirs`.
85 + **CLI and TUI** (mnw-cli, magicmirror, wam): `clap`, `ratatui`, `crossterm`, `russh`, `russh-sftp`, `dirs`.
86 86
87 87 **MCP**: `rmcp`.
88 88
@@ -1,16 +1,16 @@
1 - //! Which theme the viewer renders in, and where it loads from.
1 + //! Which theme magicmirror renders in, and where it loads from.
2 2 //!
3 3 //! No hex value is written anywhere in this crate. There is deliberately no
4 4 //! built-in fallback palette: a missing or malformed theme is an error the
5 5 //! operator sees, not something papered over by rendering in colors that exist
6 6 //! nowhere in the theme files.
7 7 //!
8 - //! The shared app convention is wiki `makeover-app-convention`. The viewer is
8 + //! The shared app convention is wiki `makeover-app-convention`. magicmirror is
9 9 //! one of the store-less apps that note describes, with one difference worth
10 - //! knowing: it already owns a config file, so `viewer.toml` *is* the
10 + //! knowing: it already owns a config file, so `magicmirror.toml` *is* the
11 11 //! file-backed store and the convention's unprefixed `theme` key lives there.
12 12 //! There is no picker and nothing writes the key back, which is why this
13 - //! module has no `remember`: the viewer displays, and choosing a theme is an
13 + //! module has no `remember`: magicmirror displays, and choosing a theme is an
14 14 //! edit to the same file that says what to display.
15 15
16 16 use std::path::PathBuf;
@@ -19,34 +19,34 @@
19 19 use makeover::{ThemeDefaults, ThemeDirs, ThemeMeta, ThemeSelection, Variant};
20 20 use makeover_tui::{Fidelity, Theme};
21 21
22 - /// The viewer's own light theme: the platform's titular skin.
22 + /// magicmirror's own light theme: the platform's titular skin.
23 23 const DEFAULT_LIGHT: &str = "makenotwork";
24 24
25 - /// The viewer's own dark theme.
25 + /// magicmirror's own dark theme.
26 26 ///
27 27 /// MNW authors no dark skin of its own, so this is the nearest thing in the
28 28 /// bundled set: near-neutral greys on the same axis as `makenotwork`'s
29 29 /// parchment, rather than a theme with a hue of its own to bring. It is a
30 30 /// fallback and not a pin — `ThemeSelection::resolve` reaches any installed
31 31 /// dark theme when this one is missing, and the operator names the theme they
32 - /// want in `viewer.toml`.
32 + /// want in `magicmirror.toml`.
33 33 const DEFAULT_DARK: &str = "carbonfox";
34 34
35 35 /// Theme search path: the operator's own themes win, then makeover's bundled
36 36 /// set.
37 37 ///
38 38 /// Two tiers and not three. There is no packaged `/usr/share` tier because
39 - /// nothing packages the viewer — it is an operator binary run out of a build,
39 + /// nothing packages magicmirror — it is an operator binary run out of a build,
40 40 /// so a system tier would be a directory that never exists.
41 41 fn search_path() -> Vec<(PathBuf, bool)> {
42 42 ThemeDirs::new()
43 43 .bundled(makeover::bundled_themes_dir())
44 - .custom(config_home().map(|config| config.join("ops-viewer").join("themes")))
44 + .custom(config_home().map(|config| config.join("magicmirror").join("themes")))
45 45 .build()
46 46 }
47 47
48 - /// `$XDG_CONFIG_HOME`, else `~/.config`, matching where the viewer already
49 - /// looks for `viewer.toml`.
48 + /// `$XDG_CONFIG_HOME`, else `~/.config`, matching where magicmirror already
49 + /// looks for `magicmirror.toml`.
50 50 fn config_home() -> Option<PathBuf> {
51 51 if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME").filter(|v| !v.is_empty()) {
52 52 return Some(PathBuf::from(xdg));
@@ -54,12 +54,12 @@
54 54 std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config"))
55 55 }
56 56
57 - /// The themes the viewer falls back to when nothing has been chosen.
57 + /// The themes magicmirror falls back to when nothing has been chosen.
58 58 fn defaults() -> ThemeDefaults {
59 59 ThemeDefaults::new(DEFAULT_LIGHT, DEFAULT_DARK)
60 60 }
61 61
62 - /// Every theme the viewer can render, from the same search path it loads from.
62 + /// Every theme magicmirror can render, from the same search path it loads from.
63 63 ///
64 64 /// makeover's own scan rather than a second one: a list here that disagreed
65 65 /// with [`load`] would resolve to an id that then failed to load.
@@ -67,7 +67,7 @@
67 67 makeover::list_themes_from_dirs(&search_path())
68 68 }
69 69
70 - /// What the viewer is being drawn on, as makeover's vocabulary.
70 + /// What magicmirror is being drawn on, as makeover's vocabulary.
71 71 ///
72 72 /// The terminal's answer to a `prefers-color-scheme` media query, and the only
73 73 /// one available without writing an OSC query and waiting for a reply before
@@ -133,7 +133,7 @@
133 133 pub(crate) fn fixed() -> Theme {
134 134 let (_, source) = makeover::embedded_themes()
135 135 .find(|(id, _)| *id == DEFAULT_LIGHT)
136 - .expect("makeover embeds the viewer's default theme");
136 + .expect("makeover embeds magicmirror's default theme");
137 137 let colors = makeover::parse_theme_str(DEFAULT_LIGHT, source, false)
138 138 .expect("the embedded default theme parses");
139 139 Theme::from_theme(&colors).expect("the embedded default theme is complete")
@@ -160,7 +160,7 @@
160 160 }
161 161 }
162 162
163 - // Nothing in `viewer.toml` is Follow, not a pin on whatever the first run
163 + // Nothing in `magicmirror.toml` is Follow, not a pin on whatever the first run
164 164 // guessed.
165 165 #[test]
166 166 fn an_unset_key_follows_the_terminal() {
@@ -187,7 +187,7 @@
187 187 }
188 188
189 189 // Following resolves to the theme matching the terminal rather than to a
190 - // fixed default, and both ids the viewer names are ones makeover ships.
190 + // fixed default, and both ids magicmirror names are ones makeover ships.
191 191 #[test]
192 192 fn following_resolves_to_the_theme_matching_the_terminal() {
193 193 let available = available();
@@ -1054,6 +1054,25 @@
1054 1054 "winapi",
1055 1055 ]
1056 1056
1057 + [[package]]
1058 + name = "magicmirror"
1059 + version = "0.1.0"
1060 + dependencies = [
1061 + "anyhow",
1062 + "chrono",
1063 + "makeover",
1064 + "makeover-tui",
1065 + "ops-status",
1066 + "ratatui",
1067 + "reqwest",
1068 + "rustls",
1069 + "serde",
1070 + "serde_json",
1071 + "tempfile",
1072 + "tokio",
1073 + "toml",
1074 + ]
1075 +
1057 1076 [[package]]
1058 1077 name = "makeover"
1059 1078 version = "2.5.0"
@@ -1200,25 +1219,6 @@
1200 1219 "serde_json",
1201 1220 ]
1202 1221
1203 - [[package]]
1204 - name = "ops-viewer"
1205 - version = "0.1.0"
1206 - dependencies = [
1207 - "anyhow",
1208 - "chrono",
1209 - "makeover",
1210 - "makeover-tui",
1211 - "ops-status",
1212 - "ratatui",
1213 - "reqwest",
1214 - "rustls",
1215 - "serde",
1216 - "serde_json",
1217 - "tempfile",
1218 - "tokio",
1219 - "toml",
1220 - ]
1221 -
1222 1222 [[package]]
1223 1223 name = "ordered-float"
1224 1224 version = "4.6.0"
@@ -2903,20 +2903,12 @@
2903 2903 checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
2904 2904
2905 2905 [[patch.unused]]
2906 - name = "kberg"
2907 - version = "0.1.0"
2906 + name = "synckit-client"
2907 + version = "0.8.0"
2908 2908
2909 2909 [[patch.unused]]
2910 - name = "painhours"
2911 - version = "0.1.0"
2912 -
2913 - [[patch.unused]]
2914 - name = "tagtree"
2915 - version = "0.4.0"
2916 -
2917 - [[patch.unused]]
2918 - name = "docengine"
2919 - version = "0.7.0"
2910 + name = "synckit-config"
2911 + version = "0.2.0"
2920 2912
2921 2913 [[patch.unused]]
2922 2914 name = "quasi-axum"
@@ -2943,9 +2935,17 @@
2943 2935 version = "0.1.0"
2944 2936
2945 2937 [[patch.unused]]
2946 - name = "synckit-client"
2947 - version = "0.8.0"
2938 + name = "docengine"
2939 + version = "0.7.0"
2948 2940
2949 2941 [[patch.unused]]
2950 - name = "synckit-config"
2951 - version = "0.2.0"
2942 + name = "kberg"
2943 + version = "0.1.0"
2944 +
2945 + [[patch.unused]]
2946 + name = "painhours"
2947 + version = "0.1.0"
2948 +
2949 + [[patch.unused]]
2950 + name = "tagtree"
2951 + version = "0.4.0"
@@ -1,4 +1,4 @@
1 - //! What the viewer knows, and the rollup derived from it.
1 + //! What magicmirror knows, and the rollup derived from it.
2 2 //!
3 3 //! The model holds one [`SourceState`] per configured source and nothing about
4 4 //! what any of them mean. Everything the rollup shows is computed from the
@@ -270,8 +270,8 @@
270 270 /// Enter on a source node: open the action picker, or explain why not.
271 271 ///
272 272 /// A node with no actions does nothing. A source with actions disabled says
273 - /// so rather than silently ignoring the key, because a viewer that looks
274 - /// like it should be able to act and does not is worse than one that says it
273 + /// so rather than silently ignoring the key, because a panel that looks like
274 + /// it should be able to act and does not is worse than one that says it
275 275 /// cannot.
276 276 pub(crate) fn open_actions(&mut self) {
277 277 let Tab::Source(i) = self.tab else { return };
@@ -1,12 +1,12 @@
1 1 [package]
2 - name = "ops-viewer"
2 + name = "magicmirror"
3 3 version = "0.1.0"
4 4 edition = "2024"
5 5 license = "MIT"
6 6 description = "One terminal surface over every operator daemon that emits an ops-status payload. Tabs per source plus a worst-first rollup."
7 7
8 8 [[bin]]
9 - name = "ops-viewer"
9 + name = "magicmirror"
10 10 path = "src/main.rs"
11 11
12 12 [dependencies]
@@ -1,17 +1,17 @@
1 - # ops-viewer: which daemons to watch.
1 + # magicmirror: which daemons to watch.
2 2 #
3 - # Install at ~/.config/ops-viewer/viewer.toml, or pass a path as the first
3 + # Install at ~/.config/magicmirror/magicmirror.toml, or pass a path as the first
4 4 # argument, or set $OPS_VIEWER_CONFIG.
5 5 #
6 - # Adding a service is an edit to this file and nothing else. The viewer knows
6 + # Adding a service is an edit to this file and nothing else. magicmirror knows
7 7 # nothing about tiers, gates, apps or targets; it renders whatever the source
8 8 # emits at GET /status.json. A new daemon gets a UI by emitting the payload.
9 9 #
10 - # The viewer is read-only until a source sets allow_actions = true. A source
10 + # magicmirror is read-only until a source sets allow_actions = true. A source
11 11 # with it off still shows the actions its nodes declare; it just refuses to
12 12 # fire them. With it on, Enter on a node opens a picker of that node's actions;
13 13 # a plain action fires, a confirm action asks for 'y', and a danger action
14 - # (sando's rollback-b) asks you to type its key. This keeps a viewer that is
14 + # (sando's rollback-b) asks you to type its key. This keeps magicmirror that is
15 15 # merely pointed at a daemon from ever moving it by accident.
16 16
17 17 # Age past which a source's answer stops counting as current, for any source
@@ -21,22 +21,22 @@
21 21 # snapshot nobody had refreshed in forty days.
22 22 stale_after_secs = 60
23 23
24 - # Which theme to render in. Unset means follow the terminal: the viewer reads
24 + # Which theme to render in. Unset means follow the terminal: magicmirror reads
25 25 # COLORFGBG and picks a light or dark theme to match, so it does not paint a
26 26 # light palette onto a night session. Set it to a theme id to pin one instead,
27 27 # e.g. "makenotwork", "carbonfox", "nord". Own themes go in
28 - # ~/.config/ops-viewer/themes/ and outrank the bundled set of the same id.
28 + # ~/.config/magicmirror/themes/ and outrank the bundled set of the same id.
29 29 #
30 30 # theme = "system"
31 31
32 32 # Each URL below is the address that daemon's own deploy example binds, which is
33 33 # the file to check when a source reads as unreachable. A daemon bound to
34 - # loopback is only visible to a viewer on the same host.
34 + # loopback is only visible to magicmirror on the same host.
35 35 #
36 - # A tailnet bind is necessary but not sufficient for a viewer on another machine:
36 + # A tailnet bind is necessary but not sufficient for magicmirror on another machine:
37 37 # the tailnet ACL decides separately whether that machine may open the port. As
38 38 # of 2026-07-29 the ACL is one-directional between the user-owned hosts and the
39 - # tagged ones, so a viewer on astra cannot reach fw13's daemons at any port even
39 + # tagged ones, so magicmirror on astra cannot reach fw13's daemons at any port even
40 40 # though both sit on the tailnet. Check the ACL, not just `listen`, when a source
41 41 # reads unreachable from one host and fine from another.
42 42 [[source]]
@@ -56,7 +56,7 @@
56 56 [[source]]
57 57 name = "bento"
58 58 # Matches `listen` in bento/deploy/bento-daemon.toml.example. Loopback, so this
59 - # source only works for a viewer running on the same host as bentod. A viewer
59 + # source only works for magicmirror running on the same host as bentod. magicmirror
60 60 # elsewhere needs bentod moved to its tailnet address, which turns its auth on.
61 61 url = "http://127.0.0.1:8765"
62 62 # Bento leaves its reads open, so no token is needed. Set token_env anyway if
@@ -67,7 +67,7 @@
67 67 # limit rather than flagging it degraded between its own checks. Each target PoM
68 68 # watches is a node; the rollup shows the worst of them. PoM gates /status.json
69 69 # behind the same bearer token as its /api/* reads, so name the token here — it
70 - # is the value in pom's serve.api_token, supplied to the viewer via the
70 + # is the value in pom's serve.api_token, supplied to magicmirror via the
71 71 # environment, never pasted into this file.
72 72 [[source]]
73 73 name = "pom"
@@ -1,7 +1,7 @@
1 - //! What the viewer is pointed at.
1 + //! What magicmirror is pointed at.
2 2 //!
3 3 //! Adding a service is a config edit and nothing else — no code change, no new
4 - //! tab type, no widget. That property is what makes the viewer modular rather
4 + //! tab type, no widget. That property is what makes magicmirror modular rather
5 5 //! than merely tabbed, and it is worth defending: the moment a source needs a
6 6 //! special case here, the shell has started learning domain vocabulary.
7 7
@@ -30,8 +30,8 @@
30 30 /// id to pin.
31 31 ///
32 32 /// Unprefixed `theme`, which is the family convention's key name (wiki
33 - /// `makeover-app-convention`) — this file is already scoped to the viewer,
34 - /// so a `viewer-theme` would be saying it twice. Absent reads as "follow
33 + /// `makeover-app-convention`) — this file is already scoped to magicmirror,
34 + /// so a `magicmirror-theme` would be saying it twice. Absent reads as "follow
35 35 /// the terminal" rather than as a pin on whatever the first run guessed.
36 36 #[serde(default)]
37 37 pub theme: Option<String>,
@@ -58,11 +58,11 @@
58 58 pub poll_secs: u64,
59 59 #[serde(default)]
60 60 pub stale_after_secs: Option<u64>,
61 - /// Whether this source's declared actions may be fired from the viewer.
61 + /// Whether this source's declared actions may be fired from magicmirror.
62 62 ///
63 - /// Defaults off. A viewer is a display first, and some declared actions
63 + /// Defaults off. magicmirror is a display first, and some declared actions
64 64 /// (sando's `promote-b`, `rollback-b`) move production. Firing is opt-in per
65 - /// source so that pointing the viewer at a daemon can never move it by
65 + /// source so that pointing magicmirror at a daemon can never move it by
66 66 /// accident: the operator turns a source's actions on deliberately, the same
67 67 /// place they already name its token. A source with this off still renders
68 68 /// its actions; it just refuses to issue them.
@@ -91,7 +91,7 @@
91 91 ///
92 92 /// An action carries a path (`/rollback/b`); a producer that instead gives a
93 93 /// full URL is honored as-is, so a daemon can point an action at somewhere
94 - /// other than itself without the viewer second-guessing it.
94 + /// other than itself without magicmirror second-guessing it.
95 95 pub(crate) fn action_url(&self, path: &str) -> String {
96 96 if path.starts_with("http://") || path.starts_with("https://") {
97 97 return path.to_string();
@@ -115,9 +115,9 @@
115 115 impl Config {
116 116 pub(crate) fn load(path: &Path) -> Result<Self> {
117 117 let raw = std::fs::read_to_string(path)
118 - .with_context(|| format!("reading viewer config at {}", path.display()))?;
118 + .with_context(|| format!("reading magicmirror config at {}", path.display()))?;
119 119 let cfg: Config = toml::from_str(&raw)
120 - .with_context(|| format!("parsing viewer config at {}", path.display()))?;
120 + .with_context(|| format!("parsing magicmirror config at {}", path.display()))?;
121 121 cfg.validate()?;
122 122 Ok(cfg)
123 123 }
@@ -125,7 +125,7 @@
125 125 fn validate(&self) -> Result<()> {
126 126 anyhow::ensure!(
127 127 !self.sources.is_empty(),
128 - "no [[source]] entries: the viewer would have nothing to show"
128 + "no [[source]] entries: magicmirror would have nothing to show"
129 129 );
130 130 let mut seen = std::collections::HashSet::new();
131 131 for source in &self.sources {
@@ -163,7 +163,7 @@
163 163
164 164 fn write(body: &str) -> (tempfile::TempDir, std::path::PathBuf) {
165 165 let dir = tempfile::tempdir().unwrap();
166 - let path = dir.path().join("viewer.toml");
166 + let path = dir.path().join("magicmirror.toml");
167 167 std::fs::write(&path, body).unwrap();
168 168 (dir, path)
169 169 }
@@ -181,7 +181,7 @@
181 181
182 182 /// Every source at once, worst first.
183 183 ///
184 - /// Without this the viewer is N tabs you still have to visit one at a time,
184 + /// Without this magicmirror is N tabs you still have to visit one at a time,
185 185 /// which is the situation it replaces, with extra steps.
186 186 fn render_rollup(model: &Model, theme: &Theme, now: DateTime<Utc>, frame: &mut Frame, area: Rect) {
187 187 let order = model.rollup_order(now);
@@ -23,7 +23,7 @@
23 23 /// data with no inherent meaning (a project's color, a series on a chart), and
24 24 /// a theme is free to author them as the status hues restated — `makenotwork`
25 25 /// gives `category.six` and `status.info` the same value, which collapsed
26 - /// `undistributed` onto `pending` in exactly the theme the viewer ships with.
26 + /// `undistributed` onto `pending` in exactly the theme magicmirror ships with.
27 27 ///
28 28 /// So the two extra states are carried by a modifier on the nearest intent,
29 29 /// which is the renderer's own doctrine: a terminal's real constraint is
@@ -45,7 +45,7 @@
45 45 Status::Pending => Style::default().fg(theme.status_info),
46 46 // Muted, and inverted so it is anything but quiet. An unreachable
47 47 // source is a real absence — no status intent is honest about it — but
48 - // it is the loudest thing a viewer can fail to tell you, so it is drawn
48 + // it is the loudest thing magicmirror can fail to tell you, so it is drawn
49 49 // as a filled block rather than as gray text.
50 50 Status::Unknown => Style::default()
51 51 .fg(theme.content_muted)
@@ -288,7 +288,7 @@
288 288
289 289 #[test]
290 290 fn progress_survives_nonsense_without_panicking() {
291 - // A producer bug must not take the viewer down.
291 + // A producer bug must not take magicmirror down.
292 292 assert!(progress(5.0, 0.0, None, 40).contains("5/0"));
293 293 assert!(progress(-1.0, 10.0, None, 40).starts_with('['));
294 294 assert!(progress(99.0, 10.0, None, 40).starts_with('['));
@@ -396,8 +396,8 @@
396 396 // The failure this guards is silent: a theme free to author its intents
397 397 // however it likes can hand two statuses the same hue, and the one
398 398 // screen whose whole job is telling them apart stops doing it. Checked
399 - // over every theme makeover ships, not only the viewer's own default,
400 - // since an operator can name any of them in `viewer.toml`.
399 + // over every theme makeover ships, not only magicmirror's own default,
400 + // since an operator can name any of them in `magicmirror.toml`.
401 401 for (id, source) in makeover::embedded_themes() {
402 402 let colors = makeover::parse_theme_str(id, source, false)
403 403 .unwrap_or_else(|e| panic!("bundled theme `{id}` does not parse: {e}"));
@@ -2,14 +2,14 @@
2 2 //! payload.
3 3 //!
4 4 //! Design + rationale: maintainer wiki.
5 - //! <!-- wiki: release-viewer-overview -->
5 + //! <!-- wiki: magicmirror-overview -->
6 6 //!
7 7 //! Tabs, one per hooked-in source, plus a rollup that shows every source at
8 8 //! once, worst first. The rollup is the reason the product exists: without it
9 9 //! this is N tabs you still have to visit one at a time, which is the situation
10 10 //! it replaces.
11 11 //!
12 - //! The viewer knows nothing about tiers, gates, apps, or targets. It renders
12 + //! magicmirror knows nothing about tiers, gates, apps, or targets. It renders
13 13 //! the shared contract in `ops-status` and nothing else, which is what lets a
14 14 //! new daemon arrive with a UI already written.
15 15 //!
@@ -97,7 +97,7 @@
97 97 return Ok(PathBuf::from(path));
98 98 }
99 99 let home = std::env::var("HOME").context("HOME is unset and no config path was given")?;
100 - Ok(PathBuf::from(home).join(".config/ops-viewer/viewer.toml"))
100 + Ok(PathBuf::from(home).join(".config/magicmirror/magicmirror.toml"))
101 101 }
102 102
103 103 // The TUI run loop owns model/sources/channels for the app's lifetime.
@@ -63,7 +63,7 @@
63 63 ticker.tick().await;
64 64 let result = fetch(&client, &url, source.token().as_deref()).await;
65 65 // The token is read per request rather than captured once, so rotating
66 - // it does not require restarting the viewer.
66 + // it does not require restarting magicmirror.
67 67 if tx
68 68 .send(Update {
69 69 index,
@@ -6,7 +6,7 @@
6 6 //! lives at the edge, off the render loop, so a slow or hung daemon cannot
7 7 //! freeze the surface you are trying to read.
8 8 //!
9 - //! The viewer never learns what an action *means*. It has a method, a URL, an
9 + //! magicmirror never learns what an action *means*. It has a method, a URL, an
10 10 //! optional body, and a bearer token, all declared by the producer, and it
11 11 //! issues exactly that. `promote` and `rollback` are opaque strings here.
12 12
@@ -33,7 +33,7 @@
33 33 /// early would abort work the server is mid-way through. Still bounded, so a
34 34 /// daemon that never answers does not leak a task forever. A synchronous
35 35 /// operation that genuinely outlasts this is a daemon-shape problem, not a
36 - /// viewer one; the next poll still shows the true resulting state.
36 + /// magicmirror one; the next poll still shows the true resulting state.
37 37 const ACTION_TIMEOUT: Duration = Duration::from_mins(2);
38 38
39 39 /// Body text kept from a failed response, past which it is noise in a one-line