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

View File

@ -55,11 +55,13 @@ cluster state.
: Start the Kubernetes control-plane runtime and desired worker VMs. : Start the Kubernetes control-plane runtime and desired worker VMs.
`status` `status`
: Print a cascade health report for the configured homelab, from bootstrap : Print a problems-first cascade health report for the configured homelab, from
readiness through public website health. Set `LAB_STATUS_DETAILS=true` to append bootstrap readiness through public website health. By default it only shows
the older detailed tables. The cascade includes "what broke" signals for recent failures and warnings. Use `status --all` for every check, `status --details`
deployment readiness, pod restarts, node pressure, disk pressure, Traefik for row details, `status --json` for structured output, or `status --verbose`
5xx/404 log evidence, and recent Gitea errors. 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` `capacity`
: Print a compact read-only placement report covering Debian, Kubernetes, : 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. docs freshness into a compact pass/warn/fail report.
Report renderer Report renderer
: Report-oriented commands can share `scripts/report-render`, which reads status : Report-oriented commands share `scripts/report-ui` and `scripts/report-render`.
TSV rows and prints compact grouped summaries, detailed output, or JSON. Rows The UI module emits status TSV rows, and the renderer prints compact grouped
may include separate `fix`, `graph`, and `query` fields for Grafana-backed summaries, detailed output, or JSON. Rows may include separate `fix`, `graph`,
checks. 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` `gitops-status`
: Print a focused Argo CD status view with application sync/health, out-of-sync : 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 It checks Debian free disk, Docker and containerd, Raspberry Pi Docker
writability, Pimox storage, OCI edge disk, and Pi-hole DNS. writability, Pimox storage, OCI edge disk, and Pi-hole DNS.
`doctor-versions` `doctor-versions [--all|--details|--verbose|--json]`
: Check Kubernetes component version alignment. : 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. : 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 : 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, : 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 : 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` `inventory-check`
: Validate `homelab.yml` with a real YAML parser when available. : 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. full output is written to the log file.
`LAB_STATUS_DETAILS=true` `LAB_STATUS_DETAILS=true`
: Append detailed host, Docker, Kubernetes, Pimox, RPi, Tailscale, and HTTP : With `status --verbose`, append detailed host, Docker, Kubernetes, Pimox, RPi,
tables after the normal status cascade. Tailscale, and HTTP tables after the normal status cascade.
`LAB_STATUS_SHOW_OK_OUTPUT=true` `LAB_STATUS_SHOW_OK_OUTPUT=true`
: Show short command output for successful cascade checks. Failed and warning : 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_INDEX=0
JEANNIE_STEP_TOTAL=0 JEANNIE_STEP_TOTAL=0
# shellcheck disable=SC1091
source "${REPO_ROOT}/scripts/report-ui"
trap 'rm -f "${BUILDX_CONFIG}"' EXIT trap 'rm -f "${BUILDX_CONFIG}"' EXIT
load_homelab_inventory_defaults() { load_homelab_inventory_defaults() {
@ -661,9 +664,90 @@ doctor_versions_report() {
return "${status}" 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() { 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 doctor_versions_report
exit "$?" exit "$?"
fi
doctor_versions_compact
exit "$?"
} }
truthy() { truthy() {
@ -4597,6 +4681,10 @@ ensure_existing_cluster_started_for_up() {
} }
status_section() { status_section() {
STATUS_CURRENT_SECTION="$1"
if truthy "${JEANNIE_REPORT_MODE:-false}"; then
return 0
fi
printf '\n== %s ==\n' "$1" printf '\n== %s ==\n' "$1"
} }
@ -4732,6 +4820,11 @@ status_cascade_check() {
local output local output
shift 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}" printf '%-36s ' "${description}"
if output="$("$@" 2>&1)"; then if output="$("$@" 2>&1)"; then
printf 'ok\n' printf 'ok\n'
@ -4753,6 +4846,11 @@ status_cascade_warn() {
local output local output
shift 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}" printf '%-36s ' "${description}"
if output="$("$@" 2>&1)"; then if output="$("$@" 2>&1)"; then
printf 'ok\n' 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 "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)) 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 if ((failures > 0)); then
printf '\nStatus cascade found %s blocking failure(s).\n' "${failures}" >&2 printf '\nStatus cascade found %s blocking failure(s).\n' "${failures}" >&2
return 1 return 1
@ -5336,9 +5438,21 @@ status_report() {
require_debian_server "status" 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=$? 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}" return "${cascade_status}"
fi fi
@ -5385,14 +5499,61 @@ status_report() {
return "${cascade_status}" 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() { doctor_edge() {
local edge_host="${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}" local edge_host="${LAB_EDGE_HOST:?LAB_EDGE_HOST is required from homelab.yml}"
local edge_user="${LAB_EDGE_USER:-ubuntu}" local edge_user="${LAB_EDGE_USER:-ubuntu}"
local traefik_ip="${LAB_TRAEFIK_LB_IP:?LAB_TRAEFIK_LB_IP is required from homelab.yml}" 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_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 gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}"
local failures=0
require_debian_server "doctor-edge" 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_section "Doctor Edge"
status_http "website public" "${LAB_PUBLIC_URL:?LAB_PUBLIC_URL is required from homelab.yml}/" 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_host="${LAB_GITEA_HOST:-192.168.100.73}"
local gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}" local gitea_http_port="${LAB_GITEA_HTTP_PORT:-3000}"
local gitea_ssh_port="${LAB_GITEA_SSH_PORT:-32222}" local gitea_ssh_port="${LAB_GITEA_SSH_PORT:-32222}"
local failures=0
require_debian_server "doctor-gitea" 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_section "Doctor Gitea"
status_http "gitea local" "http://127.0.0.1:${gitea_http_port}/" status_http "gitea local" "http://127.0.0.1:${gitea_http_port}/"
@ -5478,7 +5656,24 @@ EOF
} }
doctor_rpi() { doctor_rpi() {
local failures=0
require_debian_server "doctor-rpi" 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 status_rpi_services
@ -5502,7 +5697,28 @@ EOF
} }
doctor_cluster() { doctor_cluster() {
local failures=0
require_debian_server "doctor-cluster" 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_section "Doctor Cluster"
status_systemd_units kubelet containerd docker status_systemd_units kubelet containerd docker
@ -6200,10 +6416,11 @@ Cluster Lifecycle
workers <list|tailnet|start|stop|drain|uncordon|recreate-plan|rebalance> workers <list|tailnet|start|stop|drain|uncordon|recreate-plan|rebalance>
Manage Pimox/Kubernetes workers. Manage Pimox/Kubernetes workers.
move-prometheus-stack-workers Move monitoring workloads to worker nodes. 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 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. scorecard Compact pass/warn/fail operator scorecard.
capacity Compact capacity and placement report. capacity Compact capacity and placement report.
capacity-advisor Decide whether to add VMs or fix placement/resources. 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. state-backup Back up local OpenTofu state files.
Focused Doctors Focused Doctors
doctor-edge Diagnose edge routing and public exposure. doctor-edge [--all|--details|--verbose|--json]
doctor-gitea Diagnose Gitea local/public access. Diagnose edge routing and public exposure.
doctor-rpi Diagnose RPi services and DNS. doctor-gitea [--all|--details|--verbose|--json]
doctor-cluster Diagnose Kubernetes cluster health. 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, Secrets, And Policy
access-audit Audit SSH, Gitea, Kubernetes, and Tailscale access. access-audit Audit SSH, Gitea, Kubernetes, and Tailscale access.
@ -6304,7 +6525,7 @@ case "${1:-}" in
start_cluster start_cluster
;; ;;
status) status)
status_report status_report "${@:2}"
;; ;;
capacity) capacity)
capacity_report "$@" capacity_report "$@"
@ -6435,19 +6656,19 @@ case "${1:-}" in
move_prometheus_stack_workers move_prometheus_stack_workers
;; ;;
doctor-versions) doctor-versions)
doctor_versions doctor_versions "${@:2}"
;; ;;
doctor-edge) doctor-edge)
doctor_edge doctor_edge "${@:2}"
;; ;;
doctor-gitea) doctor-gitea)
doctor_gitea doctor_gitea "${@:2}"
;; ;;
doctor-rpi) doctor-rpi)
doctor_rpi doctor_rpi "${@:2}"
;; ;;
doctor-cluster) doctor-cluster)
doctor_cluster doctor_cluster "${@:2}"
;; ;;
preflight) preflight)
homelab_preflight homelab_preflight

View File

@ -121,6 +121,17 @@ print(
f"skip={counts.get('skip', 0)}" 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() groups = OrderedDict()
for row in rows: for row in rows:
groups.setdefault(row["area"], []).append(row) 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}"
}