From 681eb144aef0e1eceb4fb0a170e80b122263bfd6 Mon Sep 17 00:00:00 2001 From: Timothy Kim Date: Mon, 13 Jul 2026 10:51:27 -0400 Subject: [PATCH] colorize cli --- scripts/disk-health-check.sh | 66 +++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/scripts/disk-health-check.sh b/scripts/disk-health-check.sh index af1d0ed..e657e44 100755 --- a/scripts/disk-health-check.sh +++ b/scripts/disk-health-check.sh @@ -83,8 +83,50 @@ state_html() { esac } +# Terminal colors (same palette as the HTML), only when stdout is a TTY +# so piped/redirected output stays clean. +COLOR=false +C_BOLD="" +C_RED="" +C_RESET="" +if [ -t 1 ]; then + COLOR=true + C_BOLD=$'\e[1m' + C_RED=$'\e[1;38;2;207;34;46m' + C_RESET=$'\e[0m' +fi + +state_txt() { + if [ "$COLOR" = true ]; then + local c + case "$1" in + PASSED|ONLINE) c=$'\e[1;38;2;26;127;55m' ;; + UNKNOWN) c=$'\e[1;38;2;154;103;0m' ;; + *) c=$'\e[1;38;2;207;34;46m' ;; + esac + printf '%s%s%s' "$c" "$1" "$C_RESET" + else + printf '%s' "$1" + fi +} + +# $1 = percentage (drives the gradient), $2 = text to print. Callers pad +# $2 to its column width BEFORE calling — ANSI codes are invisible but +# still count as characters, so padding after coloring breaks alignment. +usage_txt() { + if [ "$COLOR" = true ]; then + local hex + hex=$(usage_color "$1") + printf '\e[38;2;%d;%d;%dm%s%s' \ + "$((16#${hex:1:2}))" "$((16#${hex:3:2}))" "$((16#${hex:5:2}))" \ + "$2" "$C_RESET" + else + printf '%s' "$2" + fi +} + # ─── SMART Disk Health (SATA/SAS) ─────────────────────────────── -STATUS+="SMART (SATA):\n" +STATUS+="${C_BOLD}SMART (SATA):${C_RESET}\n" for disk in /dev/sd?; do [ -b "$disk" ] || continue @@ -103,7 +145,7 @@ for disk in /dev/sd?; do model=$(smartctl -i "$disk" 2>/dev/null | awk -F': +' '/Device Model|Model Number/{print $2; exit}') || true model="${model:-unknown}" - STATUS+=" $disk ($model): $health, ${temp}C, ${hours}h\n" + STATUS+=$(printf ' %-13s %-24s %5s %7s ' "$disk" "$model" "${temp}C" "${hours}h")$(state_txt "$health")"\n" HTML_SATA+="$disk$model$(state_html "$health")${temp}C${hours}h"$'\n' if [ "$health" != "PASSED" ] && [ "$health" != "UNKNOWN" ]; then @@ -130,7 +172,7 @@ for disk in /dev/sd?; do done # ─── SMART Disk Health (NVMe) ─────────────────────────────────── -STATUS+="\nSMART (NVMe):\n" +STATUS+="\n${C_BOLD}SMART (NVMe):${C_RESET}\n" for disk in /dev/nvme[0-9]*; do # Match only controller devices (nvme0, nvme1), not namespaces (nvme0n1) [[ "$disk" =~ ^/dev/nvme[0-9]+$ ]] || continue @@ -153,7 +195,7 @@ for disk in /dev/nvme[0-9]*; do # NVMe percentage used — 100% means full rated write endurance consumed pct_used=$(smartctl -A "$ns" 2>/dev/null | awk -F': +' '/Percentage Used/{print $2; exit}' | tr -d '%') || true - STATUS+=" $ns ($model): $health, ${temp}C, ${hours}h\n" + STATUS+=$(printf ' %-13s %-24s %5s %7s ' "$ns" "$model" "${temp}C" "${hours}h")$(state_txt "$health")"\n" HTML_NVME+="$ns$model$(state_html "$health")${temp}C${hours}h"$'\n' if [ "$health" != "PASSED" ] && [ "$health" != "UNKNOWN" ]; then @@ -166,16 +208,16 @@ for disk in /dev/nvme[0-9]*; do done # ─── ZFS Pool Health ───────────────────────────────────────────── -STATUS+="\nZFS Pools:\n" +STATUS+="\n${C_BOLD}ZFS Pools:${C_RESET}\n" while IFS= read -r pool; do [ -n "$pool" ] || continue state=$(zpool list -H -o health "$pool" 2>/dev/null) || true state="${state:-UNKNOWN}" pool_info=$(zpool list -H -o name,size,alloc,free,cap "$pool" 2>/dev/null) || true - STATUS+=" $pool_info $state\n" # zpool -H output is tab-separated IFS=$'\t' read -r p_name p_size p_alloc p_free p_cap <<< "$pool_info" || true + STATUS+=$(printf ' %-12s %7s %7s %7s ' "${p_name:-$pool}" "$p_size" "$p_alloc" "$p_free")$(usage_txt "${p_cap%\%}" "$(printf '%5s' "$p_cap")")" "$(state_txt "$state")"\n" cap_cell="$p_cap" HTML_ZFS+="${p_name:-$pool}$p_size$p_alloc$p_free$cap_cell$(state_html "$state")"$'\n' @@ -215,7 +257,7 @@ done < <(zpool list -H -o name 2>/dev/null || true) # Quotas are hard limits — writes fail at 100% — so warn before the kids # hit the wall. Lists every dataset with a quota set (with -p, an unset # quota reads as 0). -STATUS+="\nQuotas:\n" +STATUS+="\n${C_BOLD}Quotas:${C_RESET}\n" HTML_QUOTA="" while IFS=$'\t' read -r ds used quota; do [ -n "$ds" ] || continue @@ -223,7 +265,7 @@ while IFS=$'\t' read -r ds used quota; do pct=$(( used * 100 / quota )) used_h=$(numfmt --to=iec "$used" 2>/dev/null || echo "$used") quota_h=$(numfmt --to=iec "$quota" 2>/dev/null || echo "$quota") - STATUS+=" $ds: ${pct}% (${used_h}/${quota_h})\n" + STATUS+=$(printf ' %-18s ' "$ds")$(usage_txt "$pct" "$(printf '%4s' "${pct}%")")" (${used_h}/${quota_h})\n" weight="" if [ "$pct" -gt "$QUOTA_THRESHOLD" ]; then PROBLEMS+="[QUOTA] $ds is at ${pct}% of its ${quota_h} quota\n" @@ -233,7 +275,7 @@ while IFS=$'\t' read -r ds used quota; do done < <(zfs list -Hp -o name,used,quota -t filesystem 2>/dev/null || true) # ─── Disk Space ────────────────────────────────────────────────── -STATUS+="\nDisk Space:\n" +STATUS+="\n${C_BOLD}Disk Space:${C_RESET}\n" while IFS= read -r line; do [ -n "$line" ] || continue usage=$(echo "$line" | awk '{print $5}' | tr -d '%') @@ -241,7 +283,7 @@ while IFS= read -r line; do size=$(echo "$line" | awk '{print $2}') used=$(echo "$line" | awk '{print $3}') avail=$(echo "$line" | awk '{print $4}') - STATUS+=" $mount: ${usage}% (${used}/${size}, ${avail} free)\n" + STATUS+=$(printf ' %-12s ' "$mount")$(usage_txt "$usage" "$(printf '%4s' "${usage}%")")" (${used}/${size}, ${avail} free)\n" weight="" if [ "$usage" -gt "$SPACE_THRESHOLD" ] 2>/dev/null; then PROBLEMS+="[SPACE] $mount is ${usage}% full\n" @@ -259,7 +301,7 @@ if [ "$ERROR_ONLY" = true ]; then exit 0 fi SUBJECT="[ALERT] $HOSTNAME health check failed" - BODY="Health check found issues on $HOSTNAME at $(date):\n\n$PROBLEMS\n---\n" + BODY="Health check found issues on $HOSTNAME at $(date):\n\n${C_RED}$PROBLEMS${C_RESET}\n---\n" else if [ -n "$PROBLEMS" ]; then SUBJECT="[REPORT] $HOSTNAME health — ISSUES FOUND" @@ -268,7 +310,7 @@ else fi BODY="Health report for $HOSTNAME at $(date):\n\n" if [ -n "$PROBLEMS" ]; then - BODY+="*** ISSUES ***\n\n$PROBLEMS\n" + BODY+="${C_RED}*** ISSUES ***\n\n$PROBLEMS${C_RESET}\n" fi BODY+="--- Status ---\n\n$STATUS\n---\n" fi