Add reusable report renderer
This commit is contained in:
parent
dcfab54e06
commit
a7156663d9
|
|
@ -322,6 +322,10 @@ edge/public checks. Use `recover-power --dry-run` to preview the sequence.
|
|||
health, edge health, capacity pressure, security posture, public certificates,
|
||||
inventory, and docs freshness into a compact pass/warn/fail report.
|
||||
|
||||
Report-oriented commands can share the reusable renderer in
|
||||
`scripts/report-render`. It reads status TSV rows and prints compact grouped
|
||||
summaries by default, with details or JSON available for future TUI/web output.
|
||||
|
||||
`gitops-status` prints a focused Argo CD view: application sync and health,
|
||||
out-of-sync or degraded apps, repository secret presence, recent Argo CD events,
|
||||
and recent repo-server/application-controller errors.
|
||||
|
|
|
|||
|
|
@ -322,6 +322,10 @@ edge/public checks. Use `recover-power --dry-run` to preview the sequence.
|
|||
health, edge health, capacity pressure, security posture, public certificates,
|
||||
inventory, and docs freshness into a compact pass/warn/fail report.
|
||||
|
||||
Report-oriented commands can share the reusable renderer in
|
||||
`scripts/report-render`. It reads status TSV rows and prints compact grouped
|
||||
summaries by default, with details or JSON available for future TUI/web output.
|
||||
|
||||
`gitops-status` prints a focused Argo CD view: application sync and health,
|
||||
out-of-sync or degraded apps, repository secret presence, recent Argo CD events,
|
||||
and recent repo-server/application-controller errors.
|
||||
|
|
|
|||
|
|
@ -71,6 +71,10 @@ Gitea, RPi DNS, Pimox workers, Kubernetes, GitOps/apps, and edge/public checks.
|
|||
health, capacity pressure, security posture, public certificates, inventory, and
|
||||
docs freshness into a compact pass/warn/fail report.
|
||||
|
||||
Report renderer
|
||||
: Report-oriented commands can share `scripts/report-render`, which reads status
|
||||
TSV rows and prints compact grouped summaries, detailed output, or JSON.
|
||||
|
||||
`gitops-status`
|
||||
: Print a focused Argo CD status view with application sync/health, out-of-sync
|
||||
or degraded apps, repository secret presence, recent events, and recent
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
#!/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
|
||||
|
|
@ -26,7 +26,7 @@ if ! kubectl --kubeconfig "$KUBECONFIG_PATH" get --raw=/readyz >/dev/null 2>&1;
|
|||
fi
|
||||
|
||||
section "Ingress Routes"
|
||||
INGRESS_JSON="$(mktemp "${TMPDIR:-/tmp}/homelab-ingress.XXXXXX.json")"
|
||||
INGRESS_JSON="$(mktemp "${TMPDIR:-/tmp}/homelab-ingress.XXXXXX")"
|
||||
kubectl --kubeconfig "$KUBECONFIG_PATH" get ingress -A -o json >"$INGRESS_JSON"
|
||||
python3 - "$INGRESS_JSON" "$KUMA_MONITORS_FILE" <<'PY'
|
||||
import json
|
||||
|
|
|
|||
Loading…
Reference in New Issue