Add reusable report UI for status doctors

This commit is contained in:
juvdiaz 2026-06-29 23:28:32 -06:00
parent 00cfff3ae7
commit 19964cfc20
6 changed files with 462 additions and 60 deletions

View File

@ -341,11 +341,13 @@ Run a read-only health snapshot from the Debian server with:
and bootstrap through cluster lifecycle, reports, observability, recovery,
access, apps, security, AI, and destructive commands.
It reports host memory/disk, systemd services, Docker Compose stacks,
Kubernetes health when the API is reachable, Pimox worker VM status, RPi
services, Tailscale, key local/public HTTP endpoints, and "what broke" signals
such as deployment readiness, pod restarts, node pressure, disk pressure,
Traefik 5xx/404 log evidence, and recent Gitea errors.
`status` uses the reusable report UI and defaults to showing only failures and
warnings so the terminal stays readable. Use `./jeannie status --all`
to show every check, `--details` for row details, `--json` for structured
output, or `--verbose` for the older full host/Docker/Kubernetes/Pimox/RPi/
Tailscale/HTTP dump. The cascade includes "what broke" signals such as
deployment readiness, pod restarts, node pressure, disk pressure, Traefik
5xx/404 log evidence, and recent Gitea errors.
High-signal Prometheus alerts live in `apps/homelab-alerts`. They cover node
readiness, pod restart storms, unavailable deployments, node/PVC storage
@ -400,12 +402,12 @@ output. It can run safe report commands itself, such as
finding means, diagnostic commands, whether a Jeannie fix command already
exists, and risk/auto-fix safety.
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.
Rows may include separate `fix`, `graph`, and `query` fields so a warning can
point to both the next `jeannie` command and the matching Grafana panel or
Explore query.
Report-oriented commands share the reusable UI module in `scripts/report-ui`
and renderer in `scripts/report-render`. Commands emit status TSV rows, and the
renderer prints compact grouped summaries by default, with details or JSON
available for future TUI/web output. Rows may include separate `fix`, `graph`,
and `query` fields so a warning can point to both the next `jeannie` command and
the matching Grafana panel or Explore query.
`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,
@ -485,8 +487,9 @@ dirty file count, and command details.
entry, GitOps automation, cluster foundation, DNS, observability, and security
learning paths. `./jeannie map --dot` emits Graphviz DOT.
Focused doctor commands run narrower read-only checks and print the most likely
next step:
Focused doctor commands run narrower read-only checks. They use the same
problems-first UI as `status`; add `--all`, `--details`, `--json`, or
`--verbose` when you need more context:
```bash
./jeannie doctor-edge

View File

@ -341,11 +341,13 @@ Run a read-only health snapshot from the Debian server with:
and bootstrap through cluster lifecycle, reports, observability, recovery,
access, apps, security, AI, and destructive commands.
It reports host memory/disk, systemd services, Docker Compose stacks,
Kubernetes health when the API is reachable, Pimox worker VM status, RPi
services, Tailscale, key local/public HTTP endpoints, and "what broke" signals
such as deployment readiness, pod restarts, node pressure, disk pressure,
Traefik 5xx/404 log evidence, and recent Gitea errors.
`status` uses the reusable report UI and defaults to showing only failures and
warnings so the terminal stays readable. Use `./{{ main_script }} status --all`
to show every check, `--details` for row details, `--json` for structured
output, or `--verbose` for the older full host/Docker/Kubernetes/Pimox/RPi/
Tailscale/HTTP dump. The cascade includes "what broke" signals such as
deployment readiness, pod restarts, node pressure, disk pressure, Traefik
5xx/404 log evidence, and recent Gitea errors.
High-signal Prometheus alerts live in `apps/homelab-alerts`. They cover node
readiness, pod restart storms, unavailable deployments, node/PVC storage
@ -400,12 +402,12 @@ output. It can run safe report commands itself, such as
finding means, diagnostic commands, whether a Jeannie fix command already
exists, and risk/auto-fix safety.
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.
Rows may include separate `fix`, `graph`, and `query` fields so a warning can
point to both the next `jeannie` command and the matching Grafana panel or
Explore query.
Report-oriented commands share the reusable UI module in `scripts/report-ui`
and renderer in `scripts/report-render`. Commands emit status TSV rows, and the
renderer prints compact grouped summaries by default, with details or JSON
available for future TUI/web output. Rows may include separate `fix`, `graph`,
and `query` fields so a warning can point to both the next `jeannie` command and
the matching Grafana panel or Explore query.
`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,
@ -485,8 +487,9 @@ dirty file count, and command details.
entry, GitOps automation, cluster foundation, DNS, observability, and security
learning paths. `./{{ main_script }} map --dot` emits Graphviz DOT.
Focused doctor commands run narrower read-only checks and print the most likely
next step:
Focused doctor commands run narrower read-only checks. They use the same
problems-first UI as `status`; add `--all`, `--details`, `--json`, or
`--verbose` when you need more context:
```bash
./{{ main_script }} doctor-edge

View File

@ -55,11 +55,13 @@ cluster state.
: Start the Kubernetes control-plane runtime and desired worker VMs.
`status`
: Print a cascade health report for the configured homelab, from bootstrap
readiness through public website health. Set `LAB_STATUS_DETAILS=true` to append
the older detailed tables. The cascade includes "what broke" signals for recent
deployment readiness, pod restarts, node pressure, disk pressure, Traefik
5xx/404 log evidence, and recent Gitea errors.
: Print a problems-first cascade health report for the configured homelab, from
bootstrap readiness through public website health. By default it only shows
failures and warnings. Use `status --all` for every check, `status --details`
for row details, `status --json` for structured output, or `status --verbose`
for the older detailed tables. The cascade includes "what broke" signals for
recent deployment readiness, pod restarts, node pressure, disk pressure,
Traefik 5xx/404 log evidence, and recent Gitea errors.
`capacity`
: Print a compact read-only placement report covering Debian, Kubernetes,
@ -91,10 +93,12 @@ 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. Rows
may include separate `fix`, `graph`, and `query` fields for Grafana-backed
checks.
: Report-oriented commands share `scripts/report-ui` and `scripts/report-render`.
The UI module emits status TSV rows, and the renderer prints compact grouped
summaries, detailed output, or JSON. Rows may include separate `fix`, `graph`,
and `query` fields for Grafana-backed checks. Default doctor/status output is
problems-first; pass `--all`, `--details`, `--json`, or `--verbose` when more
context is needed.
`gitops-status`
: Print a focused Argo CD status view with application sync/health, out-of-sync
@ -265,23 +269,26 @@ Docker roots, Tailscale IPs, Gitea reachability, Pimox storage, and OCI edge SSH
It checks Debian free disk, Docker and containerd, Raspberry Pi Docker
writability, Pimox storage, OCI edge disk, and Pi-hole DNS.
`doctor-versions`
: Check Kubernetes component version alignment.
`doctor-versions [--all|--details|--verbose|--json]`
: Check Kubernetes component version alignment. Default output shows only
warnings and failures; `--verbose` prints the old version table.
`doctor-edge`
`doctor-edge [--all|--details|--verbose|--json]`
: Diagnose public edge routing, Traefik reachability, and Gitea edge exposure.
Default output shows only warnings and failures.
`doctor-gitea`
`doctor-gitea [--all|--details|--verbose|--json]`
: Diagnose Debian Gitea container, local access, edge access, and repository
health.
health. Default output shows only warnings and failures.
`doctor-rpi`
`doctor-rpi [--all|--details|--verbose|--json]`
: Diagnose Raspberry Pi services, Pi-hole DNS, Unbound DNS, fallback DNS,
Uptime Kuma, and Docker root state.
Uptime Kuma, and Docker root state. Default output shows only warnings and
failures.
`doctor-cluster`
`doctor-cluster [--all|--details|--verbose|--json]`
: Diagnose Kubernetes API, nodes, core pods, MetalLB, Traefik, app health, and
Pimox worker tailnet egress.
Pimox worker tailnet egress. Default output shows only warnings and failures.
`inventory-check`
: Validate `homelab.yml` with a real YAML parser when available.
@ -411,8 +418,8 @@ Defaults to `85`.
full output is written to the log file.
`LAB_STATUS_DETAILS=true`
: Append detailed host, Docker, Kubernetes, Pimox, RPi, Tailscale, and HTTP
tables after the normal status cascade.
: With `status --verbose`, append detailed host, Docker, Kubernetes, Pimox, RPi,
Tailscale, and HTTP tables after the normal status cascade.
`LAB_STATUS_SHOW_OK_OUTPUT=true`
: Show short command output for successful cascade checks. Failed and warning

247
jeannie
View File

@ -17,6 +17,9 @@ JEANNIE_LOG_FILE="${JEANNIE_LOG_FILE:-}"
JEANNIE_STEP_INDEX=0
JEANNIE_STEP_TOTAL=0
# shellcheck disable=SC1091
source "${REPO_ROOT}/scripts/report-ui"
trap 'rm -f "${BUILDX_CONFIG}"' EXIT
load_homelab_inventory_defaults() {
@ -661,9 +664,90 @@ doctor_versions_report() {
return "${status}"
}
doctor_versions_compact() {
local api_version
local api_minor
local node_rows
local kubelet_minor_count
local containerd_major_count
local mismatch_count
local failures=0
ensure_python3
report_ui_begin
if ! status_kubernetes_api_ok; then
report_ui_emit fail "Kubernetes" "API" "not reachable" "kubectl readyz failed" "./jeannie start-cluster"
report_ui_render "Doctor Versions"
report_ui_cleanup
return 1
fi
api_version="$(
kubectl --kubeconfig "${KUBECONFIG_PATH}" version -o json |
python3 -c 'import json, sys; print(json.load(sys.stdin)["serverVersion"]["gitVersion"])'
)"
api_minor="$(printf '%s\n' "${api_version}" | awk -F. '{gsub(/^v/, "", $1); print $1 "." $2}')"
node_rows="$(
kubectl --kubeconfig "${KUBECONFIG_PATH}" get nodes \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.kubeletVersion}{"\t"}{.status.nodeInfo.containerRuntimeVersion}{"\n"}{end}'
)"
if [[ -z "${node_rows}" ]]; then
report_ui_emit fail "Kubernetes" "Nodes" "none found" "kubectl returned no node rows" "./jeannie doctor-cluster"
report_ui_render "Doctor Versions"
report_ui_cleanup
return 1
fi
kubelet_minor_count="$(
printf '%s\n' "${node_rows}" |
awk -F '\t' '{ version = $2; gsub(/^v/, "", version); split(version, parts, "."); print parts[1] "." parts[2] }' |
sort -u |
wc -l |
tr -d ' '
)"
mismatch_count="$(
printf '%s\n' "${node_rows}" |
awk -F '\t' -v expected="${api_minor}" '{ version = $2; gsub(/^v/, "", version); split(version, parts, "."); if (parts[1] "." parts[2] != expected) count++ } END { print count + 0 }'
)"
containerd_major_count="$(
printf '%s\n' "${node_rows}" |
awk -F '\t' '$3 ~ /^containerd:\/\// { runtime = $3; sub(/^containerd:\/\//, "", runtime); split(runtime, parts, "."); print parts[1] }' |
sort -u |
wc -l |
tr -d ' '
)"
report_ui_emit ok "Kubernetes" "API version" "${api_version}"
if [[ "${kubelet_minor_count}" != "1" || "${mismatch_count}" != "0" ]]; then
report_ui_emit fail "Kubernetes" "Kubelet versions" "${mismatch_count} node(s) differ from ${api_minor}" "unique kubelet minors=${kubelet_minor_count}" "./jeannie doctor-versions --verbose"
failures=$((failures + 1))
else
report_ui_emit ok "Kubernetes" "Kubelet versions" "aligned to ${api_minor}"
fi
if [[ "${containerd_major_count}" -gt 1 ]]; then
report_ui_emit warn "Runtime" "Containerd versions" "mixed major versions" "unique majors=${containerd_major_count}" "./jeannie doctor-versions --verbose"
else
report_ui_emit ok "Runtime" "Containerd versions" "single major version"
fi
report_ui_render "Doctor Versions"
report_ui_cleanup
return "${failures}"
}
doctor_versions() {
require_debian_server "doctor-versions"
if ! report_ui_parse_args "$@"; then
return 1
fi
if [[ "${REPORT_UI_VERBOSE}" == "true" ]]; then
doctor_versions_report
exit "$?"
fi
doctor_versions_compact
exit "$?"
}
truthy() {
@ -4597,6 +4681,10 @@ ensure_existing_cluster_started_for_up() {
}
status_section() {
STATUS_CURRENT_SECTION="$1"
if truthy "${JEANNIE_REPORT_MODE:-false}"; then
return 0
fi
printf '\n== %s ==\n' "$1"
}
@ -4732,6 +4820,11 @@ status_cascade_check() {
local output
shift
if truthy "${JEANNIE_REPORT_MODE:-false}"; then
report_ui_check fail "${STATUS_CURRENT_SECTION:-Status}" "${description}" "./jeannie explain status" "" "" "$@" || return 1
return 0
fi
printf '%-36s ' "${description}"
if output="$("$@" 2>&1)"; then
printf 'ok\n'
@ -4753,6 +4846,11 @@ status_cascade_warn() {
local output
shift
if truthy "${JEANNIE_REPORT_MODE:-false}"; then
report_ui_check warn "${STATUS_CURRENT_SECTION:-Status}" "${description}" "./jeannie explain status" "" "" "$@"
return 0
fi
printf '%-36s ' "${description}"
if output="$("$@" 2>&1)"; then
printf 'ok\n'
@ -5024,6 +5122,10 @@ status_cascade_report() {
status_cascade_check "Website public URL" status_http_ok "${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/" || failures=$((failures + 1))
status_cascade_check "Gitea public route" status_gitea_public_ok || failures=$((failures + 1))
if truthy "${JEANNIE_REPORT_MODE:-false}"; then
return "${failures}"
fi
if ((failures > 0)); then
printf '\nStatus cascade found %s blocking failure(s).\n' "${failures}" >&2
return 1
@ -5336,9 +5438,21 @@ status_report() {
require_debian_server "status"
if ! report_ui_parse_args "$@"; then
return 1
fi
if [[ "${REPORT_UI_VERBOSE}" != "true" ]]; then
report_ui_begin
JEANNIE_REPORT_MODE=true status_cascade_report || cascade_status=$?
report_ui_render "Jeannie Status"
report_ui_cleanup
return "${cascade_status}"
fi
status_cascade_report || cascade_status=$?
if ! truthy "${LAB_STATUS_DETAILS:-false}"; then
if ! truthy "${LAB_STATUS_DETAILS:-false}" && [[ "${REPORT_UI_DETAILS}" != "true" ]]; then
return "${cascade_status}"
fi
@ -5385,14 +5499,61 @@ status_report() {
return "${cascade_status}"
}
check_edge_traefik_backend() {
local edge_host="${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}"
local edge_user="${LAB_EDGE_USER:-ubuntu}"
local traefik_ip="${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}"
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${edge_user}@${edge_host}" \
"curl -sS -o /dev/null --connect-timeout 5 --max-time 10 -w '%{http_code}' 'http://${traefik_ip}:80/' | grep -Eq '^[234]'"
}
check_edge_gitea_backend() {
local edge_host="${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}"
local edge_user="${LAB_EDGE_USER:-ubuntu}"
local gitea_ts_ip="${LAB_GITEA_TAILSCALE_IP:?LAB_GITEA_TAILSCALE_IP is required from homelab.yml}"
local gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}"
ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${edge_user}@${edge_host}" \
"curl -sS -o /dev/null --connect-timeout 5 --max-time 10 -w '%{http_code}' 'http://${gitea_ts_ip}:${gitea_http_port}/' | grep -Eq '^[234]'"
}
check_gitea_git_remote() {
git ls-remote gitea main >/dev/null
}
check_gitea_ssh() {
local gitea_host="${LAB_GITEA_HOST:-192.168.100.73}"
local gitea_ssh_port="${LAB_GITEA_SSH_PORT:-32222}"
ssh -T -p "${gitea_ssh_port}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "git@${gitea_host}" 2>&1 |
grep -Eiq 'successfully authenticated|Hi |shell access is disabled'
}
doctor_edge() {
local edge_host="${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}"
local edge_user="${LAB_EDGE_USER:-ubuntu}"
local traefik_ip="${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}"
local gitea_ts_ip="${LAB_GITEA_TAILSCALE_IP:?LAB_GITEA_TAILSCALE_IP is required from homelab.yml}"
local gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}"
local failures=0
require_debian_server "doctor-edge"
if ! report_ui_parse_args "$@"; then
return 1
fi
if [[ "${REPORT_UI_VERBOSE}" != "true" ]]; then
report_ui_begin
report_ui_check fail "Public URLs" "Website public" "./jeannie doctor-edge --verbose" "" "" status_http_ok "${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/" || failures=$((failures + 1))
report_ui_check fail "Public URLs" "Gitea public" "./jeannie doctor-gitea" "" "" status_gitea_public_ok || failures=$((failures + 1))
report_ui_check fail "Edge" "OCI edge SSH" "./jeannie preflight" "" "" check_edge_ssh || failures=$((failures + 1))
report_ui_check fail "Backends" "Traefik from edge" "./jeannie doctor-cluster" "" "" check_edge_traefik_backend || failures=$((failures + 1))
report_ui_check fail "Backends" "Gitea from edge" "./jeannie doctor-gitea" "" "" check_edge_gitea_backend || failures=$((failures + 1))
report_ui_render "Doctor Edge"
report_ui_cleanup
return "${failures}"
fi
status_section "Doctor Edge"
status_http "website public" "${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/"
@ -5438,8 +5599,25 @@ doctor_gitea() {
local gitea_host="${LAB_GITEA_HOST:-192.168.100.73}"
local gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}"
local gitea_ssh_port="${LAB_GITEA_SSH_PORT:-32222}"
local failures=0
require_debian_server "doctor-gitea"
if ! report_ui_parse_args "$@"; then
return 1
fi
if [[ "${REPORT_UI_VERBOSE}" != "true" ]]; then
report_ui_begin
report_ui_check fail "Local Gitea" "Container running" "./jeannie deploy-gitea" "" "" status_container_running "${GITEA_CONTAINER_NAME:-homelab-gitea}" || failures=$((failures + 1))
report_ui_check fail "Local Gitea" "Local HTTP" "./jeannie deploy-gitea" "" "" status_http_ok "http://127.0.0.1:${gitea_http_port}/" || failures=$((failures + 1))
report_ui_check fail "Public Gitea" "Public route" "./jeannie doctor-edge" "" "" status_gitea_public_ok || failures=$((failures + 1))
report_ui_check warn "Git" "Gitea remote" "git remote -v" "" "" check_gitea_git_remote
report_ui_check warn "Git" "Gitea SSH" "Check Gitea SSH key and remote URL." "" "" check_gitea_ssh
report_ui_check fail "Edge" "Edge backend reachability" "./jeannie doctor-edge" "" "" check_edge_gitea_backend || failures=$((failures + 1))
report_ui_render "Doctor Gitea"
report_ui_cleanup
return "${failures}"
fi
status_section "Doctor Gitea"
status_http "gitea local" "http://127.0.0.1:${gitea_http_port}/"
@ -5478,7 +5656,24 @@ EOF
}
doctor_rpi() {
local failures=0
require_debian_server "doctor-rpi"
if ! report_ui_parse_args "$@"; then
return 1
fi
if [[ "${REPORT_UI_VERBOSE}" != "true" ]]; then
report_ui_begin
report_ui_check fail "RPi" "SSH" "./jeannie rpi-services" "" "" check_rpi_ssh || failures=$((failures + 1))
report_ui_check fail "RPi" "Docker root state" "./jeannie rpi-services" "" "" check_rpi_docker_root_state || failures=$((failures + 1))
report_ui_check fail "DNS" "Pi-hole DNS" "./jeannie rpi-services" "" "" check_pihole_dns_query || failures=$((failures + 1))
report_ui_check warn "DNS" "RPi Tailscale IP" "Check tailscaled on RPi." "" "" check_rpi_tailscale_ip
report_ui_check warn "Services" "Uptime Kuma HTTP" "./jeannie rpi-services" "" "" status_rpi_uptime_kuma_ok
report_ui_render "Doctor RPi"
report_ui_cleanup
return "${failures}"
fi
status_rpi_services
@ -5502,7 +5697,28 @@ EOF
}
doctor_cluster() {
local failures=0
require_debian_server "doctor-cluster"
if ! report_ui_parse_args "$@"; then
return 1
fi
if [[ "${REPORT_UI_VERBOSE}" != "true" ]]; then
report_ui_begin
report_ui_check fail "Host Runtime" "kubelet" "./jeannie start-cluster" "" "" check_systemd_active kubelet || failures=$((failures + 1))
report_ui_check fail "Host Runtime" "containerd" "sudo systemctl restart containerd" "" "" check_systemd_active containerd || failures=$((failures + 1))
report_ui_check warn "Host Runtime" "docker" "sudo systemctl restart docker" "" "" check_systemd_active docker
report_ui_check fail "Kubernetes" "API" "./jeannie start-cluster" "" "" status_kubernetes_api_ok || failures=$((failures + 1))
report_ui_check fail "Kubernetes" "Nodes Ready" "kubectl get nodes -o wide" "" "" status_kubernetes_nodes_ready || failures=$((failures + 1))
report_ui_check warn "Kubernetes" "Problem pods" "kubectl get pods -A -o wide" "" "" status_no_problem_pods
report_ui_check fail "Pimox" "Workers running" "./jeannie start-cluster" "" "" status_pimox_workers_running || failures=$((failures + 1))
report_ui_check fail "Pimox" "Worker tailnet egress" "./jeannie workers tailnet" "" "" check_pimox_worker_tailnet_egress || failures=$((failures + 1))
report_ui_check fail "Networking" "Traefik LoadBalancer" "./jeannie doctor-edge" "" "" status_http_reachable "http://${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}/" || failures=$((failures + 1))
report_ui_render "Doctor Cluster"
report_ui_cleanup
return "${failures}"
fi
status_section "Doctor Cluster"
status_systemd_units kubelet containerd docker
@ -6200,10 +6416,11 @@ Cluster Lifecycle
workers <list|tailnet|start|stop|drain|uncordon|recreate-plan|rebalance>
Manage Pimox/Kubernetes workers.
move-prometheus-stack-workers Move monitoring workloads to worker nodes.
doctor-versions Check Kubernetes/container tooling versions.
doctor-versions [--all|--details|--verbose|--json]
Check Kubernetes/container tooling versions.
Operator View And Reports
status Cascading health check from host to public URLs.
status [--all|--details|--verbose|--json] Problems-first cascade from host to public URLs.
scorecard Compact pass/warn/fail operator scorecard.
capacity Compact capacity and placement report.
capacity-advisor Decide whether to add VMs or fix placement/resources.
@ -6236,10 +6453,14 @@ Recovery And State
state-backup Back up local OpenTofu state files.
Focused Doctors
doctor-edge Diagnose edge routing and public exposure.
doctor-gitea Diagnose Gitea local/public access.
doctor-rpi Diagnose RPi services and DNS.
doctor-cluster Diagnose Kubernetes cluster health.
doctor-edge [--all|--details|--verbose|--json]
Diagnose edge routing and public exposure.
doctor-gitea [--all|--details|--verbose|--json]
Diagnose Gitea local/public access.
doctor-rpi [--all|--details|--verbose|--json]
Diagnose RPi services and DNS.
doctor-cluster [--all|--details|--verbose|--json]
Diagnose Kubernetes cluster health.
Access, Secrets, And Policy
access-audit Audit SSH, Gitea, Kubernetes, and Tailscale access.
@ -6304,7 +6525,7 @@ case "${1:-}" in
start_cluster
;;
status)
status_report
status_report "${@:2}"
;;
capacity)
capacity_report "$@"
@ -6435,19 +6656,19 @@ case "${1:-}" in
move_prometheus_stack_workers
;;
doctor-versions)
doctor_versions
doctor_versions "${@:2}"
;;
doctor-edge)
doctor_edge
doctor_edge "${@:2}"
;;
doctor-gitea)
doctor_gitea
doctor_gitea "${@:2}"
;;
doctor-rpi)
doctor_rpi
doctor_rpi "${@:2}"
;;
doctor-cluster)
doctor_cluster
doctor_cluster "${@:2}"
;;
preflight)
homelab_preflight

View File

@ -121,6 +121,17 @@ print(
f"skip={counts.get('skip', 0)}"
)
if not rows:
if only == "problems":
print("\nNo failing or warning items.")
elif only == "failures":
print("\nNo failing items.")
elif only == "warnings":
print("\nNo warning items.")
else:
print("\nNo report rows.")
raise SystemExit(0)
groups = OrderedDict()
for row in rows:
groups.setdefault(row["area"], []).append(row)

157
scripts/report-ui Normal file
View File

@ -0,0 +1,157 @@
#!/usr/bin/env bash
REPORT_UI_ONLY="${REPORT_UI_ONLY:-problems}"
REPORT_UI_DETAILS=false
REPORT_UI_JSON=false
# shellcheck disable=SC2034
REPORT_UI_VERBOSE=false
REPORT_UI_ROWS_FILE=""
report_ui_usage_flags() {
cat <<'EOF'
Report UI flags:
--all Show ok, skip, warning, and failing items.
--details Show the compact report plus detail fields.
--json Print structured JSON.
--verbose Use the older full diagnostic output when supported.
EOF
}
report_ui_parse_args() {
REPORT_UI_ONLY="${REPORT_UI_ONLY:-problems}"
REPORT_UI_DETAILS=false
REPORT_UI_JSON=false
REPORT_UI_VERBOSE=false
while (($# > 0)); do
case "$1" in
--all)
REPORT_UI_ONLY="all"
shift
;;
--details)
REPORT_UI_DETAILS=true
shift
;;
--json)
REPORT_UI_JSON=true
shift
;;
--verbose)
# shellcheck disable=SC2034
REPORT_UI_VERBOSE=true
shift
;;
-h|--help)
report_ui_usage_flags
return 2
;;
*)
echo "unknown report option: $1" >&2
return 1
;;
esac
done
}
report_ui_begin() {
REPORT_UI_ROWS_FILE="$(mktemp "${TMPDIR:-/tmp}/jeannie-report-rows.XXXXXX")"
}
report_ui_cleanup() {
if [[ -n "${REPORT_UI_ROWS_FILE:-}" && -f "${REPORT_UI_ROWS_FILE}" ]]; then
rm -f "${REPORT_UI_ROWS_FILE}"
fi
REPORT_UI_ROWS_FILE=""
}
report_ui_one_line() {
local value="${1:-}"
value="${value//$'\t'/ }"
value="${value//$'\r'/ }"
value="${value//$'\n'/ | }"
printf '%s' "${value}"
}
report_ui_first_line() {
awk 'NF && $0 !~ /^\+/ { print; exit }'
}
report_ui_trim_detail() {
sed -n '1,20p'
}
report_ui_emit() {
local status="$1"
local area="$2"
local check="$3"
local summary="$4"
local detail="${5:-}"
local fix="${6:-}"
local graph="${7:-}"
local query="${8:-}"
if [[ -z "${REPORT_UI_ROWS_FILE:-}" ]]; then
echo "report_ui_begin was not called" >&2
return 1
fi
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$(report_ui_one_line "${status}")" \
"$(report_ui_one_line "${area}")" \
"$(report_ui_one_line "${check}")" \
"$(report_ui_one_line "${summary}")" \
"$(report_ui_one_line "${detail}")" \
"$(report_ui_one_line "${fix}")" \
"$(report_ui_one_line "${graph}")" \
"$(report_ui_one_line "${query}")" >>"${REPORT_UI_ROWS_FILE}"
}
report_ui_check() {
local severity="$1"
local area="$2"
local check="$3"
local fix="${4:-}"
local graph="${5:-}"
local query="${6:-}"
shift 6
local output
local status
local summary
local detail
set +e
output="$("$@" 2>&1)"
status=$?
set -e
if [[ "${status}" -eq 0 ]]; then
report_ui_emit ok "${area}" "${check}" "ok" "${output}"
return 0
fi
summary="$(printf '%s\n' "${output}" | report_ui_first_line)"
if [[ -z "${summary}" ]]; then
summary="check exited with status ${status}"
fi
detail="$(printf '%s\n' "${output}" | report_ui_trim_detail)"
if [[ "${severity}" == "fail" ]]; then
report_ui_emit fail "${area}" "${check}" "${summary}" "${detail}" "${fix}" "${graph}" "${query}"
return 1
fi
report_ui_emit warn "${area}" "${check}" "${summary}" "${detail}" "${fix}" "${graph}" "${query}"
return 0
}
report_ui_render() {
local title="$1"
local -a args=(--title "${title}" --only "${REPORT_UI_ONLY}")
if [[ "${REPORT_UI_DETAILS}" == "true" ]]; then
args+=(--details)
fi
if [[ "${REPORT_UI_JSON}" == "true" ]]; then
args+=(--json)
fi
"${REPO_ROOT}/scripts/report-render" "${args[@]}" <"${REPORT_UI_ROWS_FILE}"
}