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