# Fleet alert bridge — monitoring tools into the althing inbox `althing-alert-bridge.service` runs on nh3-dev as lkraven, listening at `10.100.10.50:8096`. It turns an HTTP alert into a `postbox send` to the **infra-ops** inbox, using the established automation identity. | route | sender | payload | |---|---|---| | `POST /beszel` | Beszel (Shoutrrr generic JSON) | `{title, message}` | | `POST /kuma` | Uptime Kuma (webhook, JSON) | `{heartbeat, monitor, msg}` | ```sh scripts/elway infra-ops@10.100.10.50 --playbook playbooks/althing-alert-bridge.yaml systemctl status althing-alert-bridge curl -fsS http://10.100.10.50:8096/healthz # lists the routes it serves ``` ## Why one bridge with a route registry It was `beszel-althing` until 2026-09-21, with the `[Beszel]` subject prefix and the Beszel hub footer hardcoded. When Uptime Kuma was rebuilt as the fleet's service layer it needed the same path — and routing it through unchanged would have delivered Kuma outages labelled `[Beszel]`, pointing the reader at the wrong dashboard. **An alert that lies about its own source is worse than no alert.** Generalising cost a dict. A sibling service would have cost a second unit, a second port, and a second thing to notice had died. The name went with it: a service called `beszel-althing` that also carries Kuma alerts is exactly the invisible coupling that makes a future session look in the wrong place. ⚠ **`/beszel` is frozen.** Its prefix, footer and default title must stay byte-identical — that path was verified end-to-end in production (2026-09-10, thread `01M25Z0WFDJM92GPTJQF769HJ7`) and a refactor is not allowed to quietly change what it emits. `deliver()` still defaults to the Beszel route so the original three tests exercise it unchanged, and a test asserts the Kuma footer never leaks into a Beszel body or vice versa. ## Payload shapes are handled here, not in the sending tool Uptime Kuma can render a custom webhook body, which would have let the bridge stay dumb. It is done here instead, because Kuma's notification config lives in its own database — and that database was destroyed and rebuilt from scratch on 2026-09-21. Anything that lives only in a tool's DB is lost on the next rebuild. Format knowledge belongs in git, next to a test. `parse_kuma` also handles the **monitorless** case: `testNotification` and certificate-expiry alerts arrive with no monitor and no heartbeat, and fabricating `unknown monitor is ?` from those makes a real alert read like a bug — observed live, then fixed and pinned by a test. ## Operational notes - Source-allowlisted by IP (`ALERT_ALLOWED_SOURCES`). `10.250.50.70` is ana-docker, which now runs **both** the Beszel hub and Uptime Kuma, so one entry covers both senders. - Delivery reports success only after `postbox` returns a receipt. Failures return HTTP 502 and land in the journal; **there is no retry queue**, so a post-office outage can lose an alert. - `/healthz` checks the bridge process, not the downstream inbox. End-to-end verification means sending a real POST and reading the thread back — both routes were verified that way on 2026-09-21 (`postbox thread` does not consume the inbox). - Legacy `BESZEL_*` env names still resolve, so a half-finished deploy starts instead of crash-looping. New deployments use `ALERT_*`. - To reroute, change `ALERT_RECIPIENT` in the unit and redeploy. Leave `ALTHING_HANDLE` as infra-ops so the sender stays identifiable as automation. ```sh python3 -m unittest discover -s services/althing-alert-bridge -p 'test_*.py' # 11 tests ```