From a7156663d9c97c969bad20172a526eda4952e709 Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Mon, 29 Jun 2026 18:20:17 -0600 Subject: [PATCH] Add reusable report renderer --- README.md | 4 ++ README.md.tmpl | 4 ++ docs/jeannie.1.md | 4 ++ scripts/report-render | 147 ++++++++++++++++++++++++++++++++++++++++ scripts/route-inventory | 2 +- 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100755 scripts/report-render diff --git a/README.md b/README.md index 4400732..d7e89ce 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/README.md.tmpl b/README.md.tmpl index b1dd343..34cb091 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -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. diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index 4cae8e3..e37fb79 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -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 diff --git a/scripts/report-render b/scripts/report-render new file mode 100755 index 0000000..fd93444 --- /dev/null +++ b/scripts/report-render @@ -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 <areachecksummarydetailhint + +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 diff --git a/scripts/route-inventory b/scripts/route-inventory index eae3f22..be27220 100755 --- a/scripts/route-inventory +++ b/scripts/route-inventory @@ -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