No description
  • Go 77.5%
  • JavaScript 10.4%
  • HTML 4.6%
  • CSS 4%
  • Shell 2.7%
  • Other 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ivan Pavlina 3c18ce977a
All checks were successful
ci / check (push) Successful in 3m38s
ci / docker (push) Successful in 1m14s
ci / notify (push) Successful in 2s
web: add a read-only MCP endpoint
POST /mcp lets an assistant read nodes, states, alerts, one device's logs,
metric history and the dependency graph. Stateless JSON-RPC, authenticated by
a bearer token made in Settings -> Account (only its SHA-256 is stored, in
settings, so no migration). No token means 401; a request with an Origin
header is refused.

RAM is bounded per call: two concurrent calls at most, a ceiling on rows and
points per tool, log messages cut at 400 characters unless max_chars asks for
more (marked truncated; get_log_line reads one whole), and a reply over 64 KiB
is refused with a hint to narrow the query. Collector config is never returned.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 13:21:32 +01:00
.forgejo/workflows ci: drop the repull job, the router repulls on its own 2026-10-20 11:07:03 +02:00
cmd/beacon Answer the About tab's metric counts from a covering index 2026-10-14 19:09:58 +02:00
deploy Record BEACON_TRUSTED_PROXY in the hub's container parameters 2026-10-12 10:40:53 +02:00
internal web: add a read-only MCP endpoint 2026-11-08 13:21:32 +01:00
.dockerignore Keep the two device facts and drop the file that held them 2026-09-11 09:02:00 +00:00
.gitignore Keep the two device facts and drop the file that held them 2026-09-11 09:02:00 +00:00
CLAUDE.md web: add a read-only MCP endpoint 2026-11-08 13:21:32 +01:00
Dockerfile Run the agent on Windows, for gaming-vm 2026-09-24 08:10:01 +02:00
FABLE_AUDIT.md Record what became of each audit finding 2026-10-12 09:36:50 +02:00
go.mod Run the agent on Windows, for gaming-vm 2026-09-24 08:10:01 +02:00
go.sh Implement phase 1: inventory, probe collector and the alert engine 2026-09-08 15:53:18 +02:00
go.sum Implement phase 1: inventory, probe collector and the alert engine 2026-09-08 15:53:18 +02:00
README.md ci: drop the repull job, the router repulls on its own 2026-10-20 11:07:03 +02:00

Beacon

Monitoring for one homelab, in one Go binary.

Beacon runs as a container on a MikroTik RB5009 and replaces LibreNMS, Gatus and Beszel: device and service inventory with an explicit dependency graph, polling over each device's own native protocol, syslog collection, and deduplicated notifications to Gotify.

The dependency graph is the point. Every other tool here reported a host outage as ten unrelated alerts; Beacon reports it as one, naming the cause and the services it took with it.

What it does

Inventory and graph. One list of devices and services, edited in the UI, with an explicit dependency graph over it: what runs on what, what the network path goes through, what the UPS powers, what Caddy publishes. Everything else is derived from it.

Collectors. probe for ICMP, TCP and HTTP with TLS expiry. agent for the hosts that push. routeros over the REST API, esphome over the native protocol, nut for the UPS, proxmox for guest and node state, pbs for the two Proxmox Backup Servers, starlink for the dish, homeassistant for the one machine no agent can run on, and http, which reads numbers out of a device's own web page for gear with neither an API nor SNMP.

Ingestors. Syslog on UDP and TCP :514, routed to a node by source address, with filter rules applied at the front door and editable in the UI. Agent pushes over HTTPS with a per-agent token.

Alerting. A state machine with failure and success thresholds, correlation across the dependency graph so one outage is one notification naming the cause, and delivery to a single Gotify application. Plus a dead-man's switch: a ping to something outside the house on a timer, sent only while Beacon can still read its own database, so a hub that has died quietly stops saying it is fine.

Storage. beacon.db for the inventory and alert state, metrics.db with a chunk encoding behind the charts, logs.db with an FTS5 index behind the Logs page. beacon import-rrd migrates LibreNMS's RRD history into metrics.db.

The UI. Server-rendered and it keeps itself current, so a page left open on a wall shows what is true now rather than what was true when someone last pressed a key. It is one board — tiles, alerts, the dependency graph on a canvas with a force-directed layout written for it, and the inventory by tag — and a device opens as a panel over it, carrying its state, its charts, its logs, its dependencies and its own definition. Framed by the dashboard named in BEACON_FRAME_ANCESTOR, every page posts it the current number of warnings (nodes degraded) and errors (nodes down) for the badge on Beacon's button.

Running it

The toolchain runs in a container, so no Go install is needed (a host Go 1.26+ works too):

./go.sh test ./...
./go.sh build -o beacon ./cmd/beacon
BEACON_DATA=./data BEACON_SYSLOG_ADDR= ./beacon   # empty address: no privileged :514

Then open http://localhost:8080, set the password, add a node and draw its dependencies in the node's own panel. Nothing is served until that password exists. There is one account and it has no username: this is a homelab watched by one person, and a user table with one row in it was a login field to get wrong and a name to print in a corner nobody read.

The agent

Hosts that cannot be polled from outside run the same binary as an agent. Give the node an agent collector in the inventory, set a token on it, and hand the host that token:

BEACON_URL=https://beacon.exithub.net \
BEACON_TOKEN=the-token-from-the-node \
BEACON_SPOOL=/var/lib/beacon/spool \
  ./beacon agent

It pushes every 15 s and spools to disk when the hub is unreachable, oldest dropped first. Where smartctl, zpool, systemctl or nvidia-smi are present it reports what they know, it reads /proc/mdstat where the kernel has md arrays and /sys/class/drm where there is an Intel GPU, and where there is a Docker socket it also reports container state; that is read-only, and Beacon never starts, stops or updates anything. Nothing connects to the agent: it takes no commands and opens no listener. deploy/beacon-agent.service is the systemd unit.

Build and deploy

Forgejo CI vets, tests and lints the code, builds one multi-arch image (linux/amd64, linux/arm64) for both roles, pushes it to git.exithub.net/ivan.pavlina/beacon and the router's repull-tick scheduler repulls it. The agents are rolled out by hand with deploy/agent-deploy.sh. Details are in CLAUDE.md and deploy/README.md.

Migrating LibreNMS history

./beacon import-rrd --dir /data/librenms/app/rrd --dry-run

Each subdirectory of --dir is a device, matched to a node of the same name. The dry run reports what maps, what does not, and which directories have no node; run it first and read its output. The RRD reader is pure Go — no rrdtool, no cgo — and reads the finest AVERAGE archive of each file. What has no mapping is imported under librenms.* rather than dropped, so throwing away a year of some sensor's history stays a decision someone makes while looking at it.