feat(webhooks): add BetterStack status-page webhook route#14
Conversation
Extends the existing ClickStack webhook receiver with a /betterstack path instead of standing up a second daemon: the reverse proxy forwards the whole host to one port, so a second listener would be unreachable without an out-of-repo proxy change. Independently gated by its own config/betterstack-webhook.env, with an unguessable URL-query token (BetterStack subscriptions cannot send custom headers) generated and persisted at arm time. Events surface as betterstack-alert check: wakes through the existing durable wake queue, handled by a new betterstack-alert-response skill mirroring the ClickStack one.
There was a problem hiding this comment.
💡 Codex Review
firstmate/bin/fm-clickstack-arm.sh
Lines 76 to 78 in f0c4a0a
Captain, after this change the arm command considers a receiver healthy when either integration has started it, so enabling config/clickstack-webhook.env while a BetterStack-only daemon is already running makes fm-clickstack-arm.sh print healthy and exit without restarting. That existing process was launched with CSHOOK_ENABLED=0, so ClickStack POSTs continue to 404 until someone manually uses --restart; the normal arm path should restart or otherwise verify that the live daemon loaded the ClickStack route.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if [ "$had_token" = 1 ] && shared_daemon_healthy; then | ||
| "$ARM" | ||
| else | ||
| "$ARM" --restart |
There was a problem hiding this comment.
Restart when enabling BetterStack on an existing receiver
Captain, if a healthy ClickStack-only receiver is already running and the BetterStack gate is added with a pre-filled BETTERSTACK_WEBHOOK_TOKEN (or the token is supplied via env), this branch treats the receiver as healthy and delegates a plain arm, which exits without restarting the process. Because fm-clickstack-recv.sh only passes BSHOOK_ENABLED/BSHOOK_TOKEN at process start, /betterstack keeps returning 404 even though fm-betterstack-arm.sh reports ready; force a restart when the running shared daemon may not have loaded the BetterStack route.
Useful? React with 👍 / 👎.
Intent
status.teslemetry.comstatus-page updates via a webhook, mirroring the existing ClickStack alert-webhook receiver.betterstack-alertcheck:wakes through the existing durable wake queue, exactly likeclickstack-alert, so normal intake routes them to whichever secondmate scope covers uptime/observability./betterstackpath instead of standing up a second daemon.fm.ba.id.au) forwards the whole host to one port (8111 in production). A second listener on a different port would be unreachable from the public internet without an out-of-repo proxy change this task cannot make or verify, so a literal parallel daemon was rejected as unshippable.bin/fm-clickstack-recv.sh/bin/fm-clickstack-arm.shnow start the shared daemon when either gate file is present (config/clickstack-webhook.envor the newconfig/betterstack-webhook.env), and the listener dispatches by path, 404-ing each route independently unless its own gate is present.tests/fm-clickstack.test.shsuite still passes unmodified.bin/fm-betterstack-lib.sh(config + token generation),bin/fm-betterstack-poll.sh(watcher check shim),bin/fm-betterstack-arm.sh(token generation + delegates daemon lifecycle tofm-clickstack-arm.sh),docs/betterstack-webhook.md,.agents/skills/betterstack-alert-response/SKILL.md,tests/fm-betterstack.test.sh.What Changed
bin/fm-clickstack-listener.py: addedCSHOOK_ENABLED/BSHOOK_ENABLED/BSHOOK_TOKEN/BSHOOK_INBOXenv, path-based dispatch (/betterstackvs everything else), BetterStackevent_type-aware id extraction/dedup, a shared_read_bodyhelper. ClickStack's own request path is behaviorally unchanged.bin/fm-clickstack-recv.sh/bin/fm-clickstack-arm.sh: gate check nowcshook_enabled || bshook_enabled; recv passes both integrations' config through to the listener.bin/fm-bootstrap.sh: newbetterstack_webhook_setup()(mirrorsclickstack_webhook_setup()), wiresstate/betterstack-watch.check.sh, independentBETTERSTACK:status lines.bin/fm-betterstack-lib.sh,bin/fm-betterstack-poll.sh,bin/fm-betterstack-arm.sh,docs/betterstack-webhook.md,docs/examples/betterstack-webhook.env,.agents/skills/betterstack-alert-response/SKILL.md,tests/fm-betterstack.test.sh.AGENTS.mdsection 13: one-line trigger for the new skill.docs/configuration.md: one env-var reference line..gitignore:config/betterstack-webhook.env.Risk Assessment
Low-medium. The riskiest change is the shared daemon's enablement gate and env passthrough in
fm-clickstack-recv.sh/fm-clickstack-arm.sh, since that file is already live in production serving real ClickStack traffic on port 8111. Mitigated by: the full pre-existing ClickStack test suite passing unmodified, an explicitCSHOOK_ENABLEDflag added to the listener so a BetterStack-only daemon cannot accidentally open the ClickStack ingestion path with no secret, and the change being purely additive (no removed code paths).Testing
tests/fm-betterstack.test.sh(new, 14 cases): presence-gate inertness, poll surfacing, dual-gate independence in both directions, both-gates-together, token rejection (missing/wrong/unconfigured), idempotent redelivery including the "in-progress incident update overwrites" semantics, token generation + idempotence, arm's no-restart-when-already-healthy path, the real watcher e2e wake path, bootstrap activate/opt-out. All pass.tests/fm-clickstack.test.sh(pre-existing, unmodified): still fully green, confirming no ClickStack regression.shellcheck bin/*.sh bin/backends/*.sh tests/*.sh(the CI command): clean, exit 0./healthz- recorded with real output indocs/betterstack-webhook.md"Verification".Ready-to-paste URL form for the captain (the real token lives only in
config/betterstack-webhook.envon the live instance, generated at arm time, never committed):Get the real value with
bin/fm-betterstack-arm.sh(first run, generates the token) thenbin/fm-betterstack-arm.sh --show-url.Full narrative / original brief
Epic E5.1: build the BetterStack status-page webhook receiver so the captain can subscribe firstmate to status.teslemetry.com updates, modeled on the existing ClickStack webhook receiver (bin/fm-clickstack-recv.sh + fm-clickstack-arm.sh + fm-clickstack-poll.sh + config/clickstack-webhook.env + docs/clickstack-webhook.md + state/clickstack-inbox/): same architecture - opt-in via a local gitignored config file, listener stashes payloads to a state inbox, a generated check shim surfaces them as check: wakes.
Preference was to add a /betterstack route to the existing receiver process/port (8111; public path via the fm.ba.id.au reverse proxy which forwards the whole host, so a new path needs no proxy change) if that's clean, or a parallel receiver if not, justified in the PR - see "Intent" above for why the shared-process route was the only viable option given the proxy constraint.
Auth: BetterStack status-page webhook subscriptions do not support custom headers, per their own docs, so an unguessable token in the URL query is used and validated server-side.
Wake contract: stashed payloads surface as a check: wake naming betterstack-alert (mirroring clickstack-alert) so firstmate routes them to the oncall secondmate scope. docs/betterstack-webhook.md covers setup, payload notes, and a verification record; a colocated test follows the clickstack test pattern.
Delivered fork-only, never upstreamed.