# dandokmang status console Static status page for the dandokmang homelab server. A cron job runs `render.sh`, which runs every check script in `checks/`, collects their output, and writes a static `index.html` served by Caddy on the WireGuard interface only. ## Layout ``` statuspage/ checks/ one script per metric or group of related metrics lib/ common.sh shared helpers (svc_status, port_check, jail_status, json_line) render.sh orchestrator + HTML renderer README.md ``` ## The check-script contract This is the part to remember when you come back in six months to add a metric: **every script in `checks/` is independent. `render.sh` never changes to support a new check.** A check script must: - Be executable (`chmod +x`). - Emit **one JSON object per line** (JSONL) to stdout, and nothing else. - Emit no output to stdout on failure (send diagnostics to stderr if you want them, but they're discarded by the renderer). Each JSON object has exactly these fields: | field | meaning | |-----------|-------------------------------------------------------------------| | `section` | groups rows under a heading: `hardware`, `host services`, `jails` (or a new section name - unrecognized sections are rendered after the known ones, in the order first seen) | | `label` | display name for the row | | `value` | display value; use `""` for a plain up/down check with no value | | `status` | one of `ok`, `warn`, `down` - maps to a green/yellow/red square | Example (`checks/wireguard.sh`): ```json {"section": "host services", "label": "wireguard (wg0)", "value": "", "status": "ok"} ``` A script can emit multiple lines - `hw.sh` emits one line each for load avg, uptime, free mem, zroot usage, and zpool health. Use `json_line SECTION LABEL VALUE STATUS` from `lib/common.sh` instead of hand-rolling the JSON - it escapes backslashes, quotes, and embedded newlines (e.g. a multi-line `zpool status -x` when a pool is degraded) so the JSONL stays one object per line no matter what a command prints. ### Adding a new check 1. Create `checks/
-.sh` (or reuse an existing section script if the metric belongs with it). 2. `. "$(dirname "$0")/../lib/common.sh"` and call `json_line` for each row. 3. `chmod +x` it. That's it - no changes to `render.sh`, no template edits. It picks up any executable `checks/*.sh` file automatically and renders whatever sections it finds. If you're on a checkout where `core.fileMode` is `false` (Windows - see below), `git add` alone won't mark a new script executable. Run `git config core.hooksPath .githooks` once per clone and a pre-commit hook will fix the bit on any staged `*.sh` file automatically; otherwise use `git update-index --chmod=+x ` by hand before committing. ### Jails are auto-discovered, not one script per jail `checks/jails.sh` is the exception to "one script per metric" - instead of a `jail-.sh` per jail, it calls `jls -n name` once and reports every *currently running* jail it finds, resolving each one's IPv4 address (via the `ip4.addr` jail parameter for classic jails, falling back to `jexec ifconfig` for VNET jails, which manage their own network stack and usually don't have `ip4.addr` set) and probing port 80 on it. Roll a new jail and it shows up on the next render automatically - no file to add. The trade-off: a jail that's *supposed* to exist but isn't running won't show up as a `down` row, it just won't appear at all, since `jls` only lists running jails. If you need "this jail should exist and doesn't" alerting, that needs an explicit expected-jails list, which isn't implemented here. ## Failure isolation `render.sh` runs each check script and handles three failure modes without aborting the whole render: - **Non-zero exit** -> a `down` row labeled with the script's filename under a synthetic `checks` section. - **No output** -> a `warn` row, same treatment. - **Output that doesn't look like our JSONL** -> a `warn` row, same treatment. So a broken check shows up as a visible red/yellow row on the page instead of silently disappearing or crashing the render. ## Dependencies Parsing is done with plain `awk`/`sed`/`grep` against the constrained JSONL shape our own `json_line` helper produces - no `jq` dependency. If checks ever need to emit richer JSON (nested objects, arrays), the awk parser in `render.sh` (`jsonval()`) will need to grow or be swapped for `jq`. ## Deployment `/usr/local/etc/statuspage` is a symlink to this checkout (`~/projects/console/statuspage` on the server). Deploying an update is just `git pull` there - cron picks up the change on its next run. Cron (as root): ``` * * * * * /usr/local/etc/statuspage/render.sh ``` Caddyfile - served over a real, browser-trusted cert via DNS-01 (Porkbun), but bound only to the WireGuard interface so it's unreachable from the public internet even though the hostname resolves via public DNS: ``` status.dandokmang.com { bind 172.16.0.1 tls { dns porkbun { api_key {env.PORKBUN_API_KEY} api_secret_key {env.PORKBUN_SECRET_KEY} } } root * /usr/local/www/status file_server } ``` This requires: - A Caddy build with the `github.com/caddy-dns/porkbun` module (the pkg-installed FreeBSD binary doesn't include it - get one from [caddyserver.com/download](https://caddyserver.com/download)). - `status.dandokmang.com` set as an explicit A record -> `172.16.0.1` in Porkbun DNS, overriding the `*.dandokmang.com` wildcard that otherwise points to the public IP. - `PORKBUN_API_KEY` / `PORKBUN_SECRET_KEY` set in the environment Caddy's service reads (Porkbun account -> API Access, enabled per-domain). Reload Caddy after editing: `service caddy reload`. Access from a WireGuard peer: `https://status.dandokmang.com`. `render.sh` writes to `/usr/local/www/status/index.html` by default; override with the `STATUSPAGE_OUT` environment variable (useful for local testing without root).