#!/bin/sh # statuspage/render.sh # Orchestrator + renderer for the dandokmang status console. # # Runs every executable script in checks/, collects their JSONL rows, groups # them by section, and writes a static HTML status page. A check script # that fails or emits garbage is reported as a down/warn row instead of # aborting the whole render. See README.md for the JSONL contract that # check scripts must follow. set -u SELF_DIR=$(cd "$(dirname "$0")" && pwd) CHECKS_DIR="$SELF_DIR/checks" OUT=${STATUSPAGE_OUT:-/usr/local/www/status/index.html} HOST=$(hostname) NOW=$(date "+%Y-%m-%d %H:%M:%S %Z") . "$SELF_DIR/lib/common.sh" JSONL=$(mktemp) AWKFILE=$(mktemp) trap 'rm -f "$JSONL" "$AWKFILE"' EXIT INT TERM # valid_jsonl OUTPUT # Cheap structural check: every non-empty line must look like one of our # flat {"section":.., "label":.., "value":.., "status":..} objects. This # isn't a general JSON validator - it doesn't need to be, since the only # producer of this format is json_line() in lib/common.sh. valid_jsonl() { bad=$(printf '%s\n' "$1" | grep -v '^$' | \ grep -Evc '^\{.*"section"[[:space:]]*:.*"label"[[:space:]]*:.*"value"[[:space:]]*:.*"status"[[:space:]]*:.*\}$') [ "$bad" -eq 0 ] } for script in "$CHECKS_DIR"/*.sh; do [ -e "$script" ] || continue [ -x "$script" ] || continue name=$(basename "$script") output=$("$script" 2>/dev/null) rc=$? if [ "$rc" -ne 0 ]; then json_line "checks" "$name" "check exited with status $rc" "down" elif [ -z "$output" ]; then json_line "checks" "$name" "no output" "warn" elif ! valid_jsonl "$output"; then json_line "checks" "$name" "malformed JSONL output" "warn" else printf '%s\n' "$output" fi done >"$JSONL" # Render with awk: group rows by section (fixed order hardware/host # services/jails, then any other section in first-seen order - e.g. # "checks" for broken-script rows), preserving row order within a section. cat >"$AWKFILE" <<'AWKEOF' function jsonval(line, key, pat, start, i, c, nc, res) { pat = "\"" key "\"[ \t]*:[ \t]*\"" if (!match(line, pat)) return "" start = RSTART + RLENGTH res = "" for (i = start; i <= length(line); i++) { c = substr(line, i, 1) if (c == "\\") { nc = substr(line, i + 1, 1) if (nc == "n") res = res "\n" else res = res nc i++ continue } if (c == "\"") break res = res c } return res } function htmlesc(s) { gsub(/&/, "\\&", s) gsub(/, "\\<", s) gsub(/>/, "\\>", s) return s } function sqclass(status) { if (status == "ok") return "green" if (status == "warn") return "yellow" if (status == "info") return "gray" return "red" } BEGIN { order[1] = "hardware"; order[2] = "host services"; order[3] = "jails" norder = 3 for (i = 1; i <= norder; i++) known[order[i]] = 1 } { if ($0 == "") next section = jsonval($0, "section") label = jsonval($0, "label") value = jsonval($0, "value") status = jsonval($0, "status") if (section == "") next if (!(section in seen)) { seen[section] = 1 if (!(section in known)) { norder++; order[norder] = section } } n = ++count[section] L[section, n] = label V[section, n] = value S[section, n] = status } END { print "" print "" print "
" print "updated " htmlesc(now) "
" for (i = 1; i <= norder; i++) { sect = order[i] if (!(sect in count)) continue print "| " htmlesc(V[sect, j]) " | " print "
| " htmlesc(L[sect, j]) " | " valcell "