summaryrefslogtreecommitdiff
path: root/statuspage/render.sh
diff options
context:
space:
mode:
Diffstat (limited to 'statuspage/render.sh')
-rw-r--r--statuspage/render.sh155
1 files changed, 155 insertions, 0 deletions
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(/&/, "\\&amp;", s)
+ gsub(/</, "\\&lt;", s)
+ gsub(/>/, "\\&gt;", 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 "<!DOCTYPE html>"
+ print "<html>"
+ print "<head>"
+ print "<title>" htmlesc(host) " status</title>"
+ print "<meta http-equiv=\"refresh\" content=\"30\">"
+ print "<style>"
+ print "body { background:#111; color:#ddd; font-family: monospace; padding: 2em; }"
+ print "h1 { color: #6ff; margin-bottom: 0; }"
+ print "h2 { color: #9cf; border-bottom: 1px solid #333; padding-bottom: 4px; }"
+ print "table { border-collapse: collapse; width: 100%; max-width: 600px; margin-bottom: 1.5em; }"
+ print "td { padding: 6px 10px; border-bottom: 1px solid #222; }"
+ print ".sq { display:inline-block; width:12px; height:12px; border-radius:2px; margin-right:6px; vertical-align:middle; }"
+ print ".green { background:#3c3; }"
+ print ".yellow { background:#cc3; }"
+ print ".red { background:#c33; }"
+ print ".dim { color:#888; font-size: 0.85em; }"
+ print "</style>"
+ print "</head>"
+ print "<body>"
+ print "<h1>" htmlesc(host) "</h1>"
+ print "<p class=\"dim\">updated " htmlesc(now) "</p>"
+
+ for (i = 1; i <= norder; i++) {
+ sect = order[i]
+ if (!(sect in count)) continue
+ print "<h2>" htmlesc(sect) "</h2>"
+ print "<table>"
+ for (j = 1; j <= count[sect]; j++) {
+ valcell = (V[sect, j] == "") ? "" : "<td>" htmlesc(V[sect, j]) "</td>"
+ print "<tr><td><span class=\"sq " sqclass(S[sect, j]) "\"></span>" htmlesc(L[sect, j]) "</td>" valcell "</tr>"
+ }
+ print "</table>"
+ }
+
+ print "</body>"
+ print "</html>"
+}
+AWKEOF
+
+awk -v host="$HOST" -v now="$NOW" -f "$AWKFILE" "$JSONL" >"$OUT"