From dfec6df2fb876fadb20d24a5bd8f4b4400a0f659 Mon Sep 17 00:00:00 2001 From: batsumaru <> Date: Wed, 1 Jul 2026 15:38:51 +0900 Subject: Split monolithic status check.sh into modular check scripts Replace the single-file prototype with independently-addable check scripts (checks/*.sh) that each emit JSONL rows, a shared helper lib, and one orchestrator/renderer (render.sh) that groups rows by section and writes the static status page. A broken check script now shows up as a down/warn row instead of crashing the whole render. Co-Authored-By: Claude Sonnet 5 --- statuspage/render.sh | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 statuspage/render.sh (limited to 'statuspage/render.sh') diff --git a/statuspage/render.sh b/statuspage/render.sh new file mode 100644 index 0000000..22df5e7 --- /dev/null +++ b/statuspage/render.sh @@ -0,0 +1,155 @@ +#!/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) + return s +} + +function sqclass(status) { + if (status == "ok") return "green" + if (status == "warn") return "yellow" + 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 "" htmlesc(host) " status" + print "" + print "" + print "" + print "" + print "

" htmlesc(host) "

" + print "

updated " htmlesc(now) "

" + + for (i = 1; i <= norder; i++) { + sect = order[i] + if (!(sect in count)) continue + print "

" htmlesc(sect) "

" + print "" + for (j = 1; j <= count[sect]; j++) { + valcell = (V[sect, j] == "") ? "" : "" + print "" valcell "" + } + print "
" htmlesc(V[sect, j]) "
" htmlesc(L[sect, j]) "
" + } + + print "" + print "" +} +AWKEOF + +awk -v host="$HOST" -v now="$NOW" -f "$AWKFILE" "$JSONL" >"$OUT" -- cgit v1.3