| 1 |
# magicmirror: which daemons to watch. |
| 2 |
# |
| 3 |
# Install at ~/.config/magicmirror/magicmirror.toml, or pass a path as the first |
| 4 |
# argument, or set $OPS_VIEWER_CONFIG. |
| 5 |
# |
| 6 |
# Adding a service is an edit to this file and nothing else. magicmirror knows |
| 7 |
# nothing about tiers, gates, apps or targets; it renders whatever the source |
| 8 |
# emits. A new producer gets a UI by emitting the payload. |
| 9 |
# |
| 10 |
# Three tabs, and there are three however many sources are configured: |
| 11 |
# |
| 12 |
# 1 live every source at once, worst first, each source's nodes indented |
| 13 |
# under it, with a detail pane for whatever the cursor is on. |
| 14 |
# 2 logs what every source has reported lately, grouped by which one said |
| 15 |
# it and newest first within each. |
| 16 |
# 3 store the series a [[store]] below names. Empty unless one is declared. |
| 17 |
# |
| 18 |
# tab/shift-tab or 1-3 to switch, up/down to move, enter to run the selected |
| 19 |
# node's action, q to quit. |
| 20 |
# |
| 21 |
# A source sets exactly one of `url` and `path`. `url` is a daemon to poll, with |
| 22 |
# /status.json appended. `path` is a payload on disk, which is what a batch |
| 23 |
# producer leaves behind: something that runs for hours and then stops has |
| 24 |
# nothing to poll, and making it grow an HTTP listener would be paying a |
| 25 |
# daemon's cost for a producer that is not one. Either way magicmirror keeps the |
| 26 |
# last payload it read across a failed poll, so a producer that finished at 03:20 |
| 27 |
# still reads at 15:00. |
| 28 |
# |
| 29 |
# magicmirror is read-only until a source sets allow_actions = true. A source |
| 30 |
# with it off still shows the actions its nodes declare; it just refuses to |
| 31 |
# fire them. With it on, Enter on a node row in the live tab opens a picker of |
| 32 |
# that node's actions; |
| 33 |
# a plain action fires, a confirm action asks for 'y', and a danger action |
| 34 |
# (sando's rollback-b) asks you to type its key. This keeps magicmirror that is |
| 35 |
# merely pointed at a daemon from ever moving it by accident. |
| 36 |
|
| 37 |
# Age past which a source's answer stops counting as current, for any source |
| 38 |
# that does not set its own. A source that answers with something hours old is |
| 39 |
# reported as degraded no matter how green its contents: the failure this |
| 40 |
# whole surface exists to catch is a check that kept saying "ok" about a |
| 41 |
# snapshot nobody had refreshed in forty days. |
| 42 |
stale_after_secs = 60 |
| 43 |
|
| 44 |
# Which theme to render in. Unset means follow the terminal: magicmirror reads |
| 45 |
# COLORFGBG and picks a light or dark theme to match, so it does not paint a |
| 46 |
# light palette onto a night session. Set it to a theme id to pin one instead, |
| 47 |
# e.g. "makenotwork", "carbonfox", "nord". Own themes go in |
| 48 |
# ~/.config/magicmirror/themes/ and outrank the bundled set of the same id. |
| 49 |
# |
| 50 |
# theme = "system" |
| 51 |
|
| 52 |
# Each URL below is the address that daemon's own deploy example binds, which is |
| 53 |
# the file to check when a source reads as unreachable. A daemon bound to |
| 54 |
# loopback is only visible to magicmirror on the same host. |
| 55 |
# |
| 56 |
# A tailnet bind is necessary but not sufficient for magicmirror on another machine: |
| 57 |
# the tailnet ACL decides separately whether that machine may open the port. As |
| 58 |
# of 2026-07-29 the ACL is one-directional between the user-owned hosts and the |
| 59 |
# tagged ones, so magicmirror on astra cannot reach fw13's daemons at any port even |
| 60 |
# though both sit on the tailnet. Check the ACL, not just `listen`, when a source |
| 61 |
# reads unreachable from one host and fine from another. |
| 62 |
[[source]] |
| 63 |
name = "sando" |
| 64 |
# Matches `listen` in sando/deploy/sando-daemon.toml.example: fw13's tailnet |
| 65 |
# address, deliberately not 0.0.0.0, and reachable from fw13 itself. |
| 66 |
url = "http://100.103.89.95:7766" |
| 67 |
# Sando gates its reads, so a token is required. The token is NAMED here, never |
| 68 |
# pasted: this file describes topology and has every reason to be readable, |
| 69 |
# while sandod already takes the same value from its environment. |
| 70 |
token_env = "SANDO_API_TOKEN" |
| 71 |
poll_secs = 5 |
| 72 |
# Sando declares promote and rollback actions that move production. Leave this |
| 73 |
# off to watch read-only; turn it on deliberately to drive deploys from here. |
| 74 |
# allow_actions = true |
| 75 |
|
| 76 |
[[source]] |
| 77 |
name = "bento" |
| 78 |
# Matches `listen` in bento/deploy/bento-daemon.toml.example. Loopback, so this |
| 79 |
# source only works for magicmirror running on the same host as bentod. magicmirror |
| 80 |
# elsewhere needs bentod moved to its tailnet address, which turns its auth on. |
| 81 |
url = "http://127.0.0.1:8765" |
| 82 |
# Bento leaves its reads open, so no token is needed. Set token_env anyway if |
| 83 |
# that ever changes; an unset variable is treated as no token. |
| 84 |
poll_secs = 10 |
| 85 |
|
| 86 |
# PoM polls its own targets on a slower cycle, so hold it to a slower staleness |
| 87 |
# limit rather than flagging it degraded between its own checks. Each target PoM |
| 88 |
# watches is a node; the rollup shows the worst of them. PoM gates /status.json |
| 89 |
# behind the same bearer token as its /api/* reads, so name the token here — it |
| 90 |
# is the value in pom's serve.api_token, supplied to magicmirror via the |
| 91 |
# environment, never pasted into this file. |
| 92 |
[[source]] |
| 93 |
name = "pom" |
| 94 |
# PoM listens on 9100, never 9000, and there is no host named "pom" to resolve. |
| 95 |
# Point this at the instance you want: astra's tailnet address below, or |
| 96 |
# 127.0.0.1:9100 for a pom running on this machine. See pom/deploy/pom-*.toml. |
| 97 |
url = "http://100.106.221.39:9100" |
| 98 |
token_env = "POM_API_TOKEN" |
| 99 |
poll_secs = 30 |
| 100 |
stale_after_secs = 600 |
| 101 |
|
| 102 |
# witchbroom: the nightly repo x check sweep on astra. A batch producer, so this |
| 103 |
# is a file rather than a URL — it writes its payload at the end of a run and |
| 104 |
# stops. The path is witchbroom's own `status_path` default; change both or |
| 105 |
# neither. Only useful for a magicmirror on astra, since the file is local. |
| 106 |
# |
| 107 |
# A file source takes no token_env and no allow_actions: there is nothing to |
| 108 |
# authenticate to and no base to resolve an action's URL against. Both are |
| 109 |
# refused at load rather than ignored. |
| 110 |
# |
| 111 |
# stale_after_secs is a day and a half, not sixty seconds. The sweep runs |
| 112 |
# nightly, so anything shorter reports it degraded for twenty-three hours out of |
| 113 |
# every twenty-four; a day and a half is "it missed a night", which is the thing |
| 114 |
# actually worth seeing. |
| 115 |
[[source]] |
| 116 |
name = "witchbroom" |
| 117 |
path = "~/.local/state/witchbroom/status.json" |
| 118 |
poll_secs = 60 |
| 119 |
stale_after_secs = 129600 |
| 120 |
|
| 121 |
# --------------------------------------------------------------------------- |
| 122 |
# Stores: the third tab. |
| 123 |
# --------------------------------------------------------------------------- |
| 124 |
# |
| 125 |
# Optional, and normally absent. Everything above is the `ops-status` contract, |
| 126 |
# which is what lets a new daemon arrive with a UI already written. A store is |
| 127 |
# the one exception: a producer's own SQLite file, opened read-only, for exactly |
| 128 |
# the series named below. The break is deliberate and stays narrow. |
| 129 |
# |
| 130 |
# Why the meaning is here rather than in the data. witchbroom's store is one |
| 131 |
# `(series, labels, value, at)` table with an `f64`, on purpose: a new metric is |
| 132 |
# a new string rather than a migration, and the accepted cost is that the schema |
| 133 |
# cannot say what a number is or what unit it is in. So either every series gets |
| 134 |
# rendered unlabelled, which is a table browser, or the operator names the ones |
| 135 |
# worth watching and says what each is. The second was chosen. |
| 136 |
# |
| 137 |
# The consequence, and it is the point rather than a limitation: A SERIES WITH NO |
| 138 |
# ENTRY BELOW IS NOT SHOWN. Silence over noise. If a number is missing from the |
| 139 |
# tab, the fix is a [[store.series]] here, not a fallback in the code. |
| 140 |
# |
| 141 |
# A named series the store holds nothing for still gets a row, saying so. A soak |
| 142 |
# target that has never reported is exactly the thing worth noticing, and |
| 143 |
# omitting it would look identical to never having configured it. A store that |
| 144 |
# is missing or unreadable shows an `unavailable` row above whatever it last |
| 145 |
# said, so old numbers are never mistaken for current ones. |
| 146 |
# |
| 147 |
# [[store]] |
| 148 |
# name = "witchbroom" |
| 149 |
# # witchbroom's own observations.db default. Opened read-only, never created: |
| 150 |
# # a typo here stays a visible error rather than becoming an empty database |
| 151 |
# # that reads as a producer which has never recorded anything. |
| 152 |
# path = "~/.local/state/witchbroom/observations.db" |
| 153 |
# # Slower than a source poll by an order of magnitude, because a batch producer |
| 154 |
# # writes once at the end of a run that took hours. Defaults to 60. |
| 155 |
# poll_secs = 300 |
| 156 |
# |
| 157 |
# [[store.series]] |
| 158 |
# series = "soak.coverage_edges" # the store's own series name, verbatim |
| 159 |
# label = "Coverage reached" # what goes on screen |
| 160 |
# unit = "edges" # optional; some numbers count nothing in particular |
| 161 |
# |
| 162 |
# [[store.series]] |
| 163 |
# series = "cache.size_bytes" |
| 164 |
# label = "Compiler cache" |
| 165 |
# unit = "bytes" |
| 166 |
|