Nik Afiq 39b7d6d4b4
All checks were successful
CI / changes (push) Successful in 19s
CI / test (push) Successful in 31s
CI / build-ai-gateway (push) Successful in 1m8s
CI / build-ha-gateway (push) Successful in 1m19s
CI / build-discord-bot (push) Successful in 59s
CI / build-alexa-bridge (push) Successful in 1m8s
CI / build-alert-bridge (push) Successful in 1m17s
CI / build-tts-gateway (push) Successful in 1m10s
CI / build-tts-sidecar (push) Has been skipped
CI / build-tts-model (push) Has been skipped
feat: add alert-bridge service for external cronjob -> Discord alerts
New standalone service that receives authenticated HTTP POSTs (e.g. from a
cronjob elsewhere on the home network) and posts them to a Discord channel
via an Incoming Webhook. Fire-and-forget, no dependency on discord-bot's bot
session at all - mirrors alexa-bridge's precedent of a small bridge service
with its own HTTP listener and auth rather than adding an inbound edge to an
existing internal service.

Wires alert-bridge into go.work, CI (changes filter, test job, new
build-alert-bridge job), and the other five Dockerfiles' COPY lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 22:45:18 +09:00

4.4 KiB

alert-bridge

alert-bridge turns an authenticated HTTP POST from an external caller (e.g. a cronjob running elsewhere on the home network) into a message posted to a Discord channel. It is fire-and-forget: each request becomes one new Discord message, there is no editing or reacting to prior messages.

Unlike ha-gateway/ai-gateway/discord-bot/tts-gateway, it has no dependency on any other service in this repo — it posts directly to a Discord Incoming Webhook, not through discord-bot's bot session. That keeps the whole flow to one hop and means discord-bot needed zero changes to support this feature.

Runtime Flow

  1. The process loads .env, configures logging/telemetry, and starts serving HTTP on HTTP_PORT.
  2. A caller POSTs to /alerts with a bearer token and a JSON body.
  3. The handler checks the bearer token (constant-time compare against API_KEY), validates the body, and defaults level to info if omitted.
  4. The alert is formatted as a Discord embed (color-coded by level) and POSTed to DISCORD_WEBHOOK_URL. level=error alerts additionally prefix the message with a mention of MENTION_USER_ID, since embeds alone never trigger a Discord ping.

HTTP API

  • POST /alerts — requires Authorization: Bearer <API_KEY>.

    {"source": "ba-cronjob", "message": "Finished with 0 errors", "level": "info"}
    
    • source, message: required.
    • level: optional, one of info | warn | error, defaults to info.
    • Responses: 202 delivered, 400 bad request, 401 bad/missing token, 502 the Discord webhook call itself failed.
  • GET /healthz: unauthenticated liveness/readiness check, always 200 OK.

Configuration

Environment variables:

Variable Default Description
HTTP_PORT 8080 HTTP listen port
API_KEY required Shared secret callers must present as Authorization: Bearer <API_KEY>
DISCORD_WEBHOOK_URL required Target channel's Discord Incoming Webhook URL
MENTION_USER_ID empty Discord user ID mentioned on level=error alerts; leave empty to never mention
OTEL_ENDPOINT empty OTLP gRPC collector endpoint; empty disables telemetry
LOG_LEVEL info debug, info, warn, or error
LOG_FORMAT json json or text

Example env file: .env.example

Local Run

cd alert-bridge
cp .env.example .env
# edit .env: set API_KEY and DISCORD_WEBHOOK_URL
go run ./cmd/bridge
curl -X POST http://localhost:8080/alerts \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source":"ba-cronjob","message":"Started","level":"info"}'

Test And Build

go test ./...
go build ./...

Build the container image from the workspace root:

docker build -f alert-bridge/Dockerfile -t alert-bridge:dev .

Package Map

cmd/bridge/                                   # process entrypoint and wiring
internal/adapters/primary/http/               # POST /alerts handler: auth, decode, validate
internal/adapters/secondary/discordwebhook/   # builds the embed payload, posts to the Discord webhook
internal/app/                                 # thin orchestration between the HTTP handler and the Notifier
internal/core/domain/                         # Alert{Source, Message, Level}
internal/core/ports/driven/                   # Notifier interface
internal/config/                              # environment loading
internal/logger/                              # slog setup
internal/telemetry/                           # OpenTelemetry setup

Deployment

Kubernetes manifests (Deployment/Service, and an internal-CA IngressRoute at a *.home.arpa hostname so LAN callers outside the cluster can reach it) live in the separate homelab repo, not here — not yet added as of this writing.

Limitations

  • Single channel/webhook only — no per-source routing to multiple channels.
  • No rate limiting of its own; relies on Discord's own webhook rate limits (5 requests/2s per webhook), which is more than enough for cronjob-scale notification volume.
  • Fire-and-forget only: no message editing, threading, or reaction handling. That would require routing through discord-bot's live bot session instead of a plain webhook — not needed for the current use case.