my-homelab-configs/scripts/report-render

148 lines
3.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
MODE="compact"
ONLY="all"
TITLE="${REPORT_TITLE:-Jeannie Report}"
INPUT_FILE=""
cleanup() {
if [ -n "$INPUT_FILE" ] && [ -f "$INPUT_FILE" ]; then
rm -f "$INPUT_FILE"
fi
}
trap cleanup EXIT
usage() {
cat <<EOF
Usage: report-render [--title TITLE] [--details] [--only failures|warnings|problems|all] [--json]
Reads TSV rows from stdin:
status<TAB>area<TAB>check<TAB>summary<TAB>detail<TAB>hint
Statuses: ok, warn, fail, skip
EOF
}
while (($# > 0)); do
case "$1" in
--title)
TITLE="${2:-}"
shift 2
;;
--details)
MODE="details"
shift
;;
--only)
ONLY="${2:-all}"
shift 2
;;
--json)
MODE="json"
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 1
;;
esac
done
INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/jeannie-report.XXXXXX")"
cat >"$INPUT_FILE"
python3 - "$TITLE" "$MODE" "$ONLY" "$INPUT_FILE" <<'PY'
import json
import sys
from collections import OrderedDict
title, mode, only, input_file = sys.argv[1:5]
rows = []
with open(input_file, encoding="utf-8") as handle:
for raw in handle:
raw = raw.rstrip("\n")
if not raw:
continue
parts = raw.split("\t")
while len(parts) < 6:
parts.append("")
status, area, check, summary, detail, hint = parts[:6]
status = status.strip().lower() or "skip"
rows.append(
{
"status": status,
"area": area.strip() or "General",
"check": check.strip(),
"summary": summary.strip(),
"detail": detail.strip(),
"hint": hint.strip(),
}
)
def include(row):
status = row["status"]
if only == "all":
return True
if only == "failures":
return status == "fail"
if only == "warnings":
return status == "warn"
if only == "problems":
return status in {"fail", "warn"}
return True
rows = [row for row in rows if include(row)]
if mode == "json":
print(json.dumps({"title": title, "rows": rows}, indent=2))
raise SystemExit(0)
counts = {"fail": 0, "warn": 0, "ok": 0, "skip": 0}
for row in rows:
counts[row["status"]] = counts.get(row["status"], 0) + 1
print(title)
print("=" * len(title))
print(
f"fail={counts.get('fail', 0)} "
f"warn={counts.get('warn', 0)} "
f"ok={counts.get('ok', 0)} "
f"skip={counts.get('skip', 0)}"
)
groups = OrderedDict()
for row in rows:
groups.setdefault(row["area"], []).append(row)
status_rank = {"fail": 0, "warn": 1, "skip": 2, "ok": 3}
status_label = {"fail": "fail", "warn": "warn", "ok": "ok", "skip": "skip"}
for area, area_rows in groups.items():
print(f"\n== {area} ==")
for row in sorted(area_rows, key=lambda item: (status_rank.get(item["status"], 9), item["check"])):
status = status_label.get(row["status"], row["status"])
check = row["check"][:30]
summary = row["summary"]
print(f"{status:<5} {check:<30} {summary}")
if mode == "details" or row["status"] in {"fail", "warn"}:
if row["detail"]:
print(f" detail: {row['detail']}")
if row["hint"]:
print(f" fix: {row['hint']}")
problems = [row for row in rows if row["status"] in {"fail", "warn"}]
if problems:
first = problems[0]
print("\nNext Fix")
if first["hint"]:
print(first["hint"])
else:
print(f"Investigate {first['area']} / {first['check']}")
PY