From c0186119597ac5532a269713a848e3d802eb49bc Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 18 Jul 2026 15:43:11 -0400 Subject: [PATCH] driveAtlas: revamp PBS/NUC layout + fix post-outage drive map Remap the pbs (NUC5i5RYB/RYH) USB bays after a power outage: the ...615 NVMe dropped to 0B/hardware-error in the hot stacked rear-top bay and was moved to the cooler front-left port. Slot keys are now position-explicit (usb-fl/usb-fr/usb-rt/usb-rb) keyed by USB port path, with the front-left = 3-2 (usb-0:2) move recorded and the root cause (rear-stack thermals, exhaust vent directly above the rear USB pair) documented. Rebuild generate_nuc_layout() as a cabinet-projection schematic: 88-col, metal-shaded isometric chassis + rear panel with the USB pair stacked under the fan vents and a red "bakes in CPU exhaust" callout, dashed ghost boxes for empty ports, and green LED bars on occupied drives. Add reusable helpers: da_color_init (capability-gated 256/truecolor/16 "hardware schematic" palette, honors NO_COLOR/tty/tput/COLORTERM; structure grey, color encodes state only so it reads in monochrome), da_repeat, and da_gauge (eighth-block capacity gauge). Every line is a uniform 88 display columns in mono and color; only SAFE width-1 glyphs used. Co-Authored-By: Claude Opus 4.8 --- driveAtlas.sh | 214 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 182 insertions(+), 32 deletions(-) diff --git a/driveAtlas.sh b/driveAtlas.sh index 471dc1d..1d0f777 100644 --- a/driveAtlas.sh +++ b/driveAtlas.sh @@ -575,34 +575,174 @@ generate_micro_layout() { # # Side effects: Calls build_drive_map() to populate DRIVE_MAP #------------------------------------------------------------------------------ +# ══ driveAtlas :: color + glyph helpers ═══════════════════════════════════════ + +# da_repeat GLYPH N -- print GLYPH N times (multibyte-safe) +da_repeat() { + local out='' i + for ((i = 0; i < $2; i++)); do out+="$1"; done + printf '%s' "$out" +} + +# da_color_init -- set the driveAtlas "hardware schematic" palette globals. +# Structure is steel grey; color encodes STATE only (green=ok, amber/yellow, +# red=hot, blue=cool). All vars are empty strings when color is off, so +# rendering is byte-identical to pure monochrome. +# USE_COLOR=true force on (depth still auto-picked) +# USE_COLOR=false force off +# unset/auto auto: off if NO_COLOR set, stdout not a tty, +# or terminal reports < 256 colors +# Depth: truecolor only when COLORTERM=truecolor/24bit, else 256, else 16. +da_color_init() { + DA_ST='' DA_HL='' DA_DIM='' DA_OK='' DA_YEL='' DA_HOT='' DA_BLU='' DA_CYN='' + DA_SR='' DA_RST='' + local mode="${USE_COLOR:-auto}" ncolors + ncolors="$(tput colors 2>/dev/null)"; ncolors="${ncolors:-0}" + case "$mode" in + false) return 0 ;; + auto) + [ -n "${NO_COLOR-}" ] && return 0 + [ ! -t 1 ] && return 0 + [ "$ncolors" -lt 256 ] 2>/dev/null && return 0 + ;; + esac + if [ "${COLORTERM-}" = "truecolor" ] || [ "${COLORTERM-}" = "24bit" ]; then + DA_ST=$'\033[38;2;138;144;151m' # steel mid #8a9097 + DA_HL=$'\033[38;2;200;205;211m' # steel bright #c8cdd3 + DA_DIM=$'\033[38;2;86;93;102m' # steel shadow #565d66 + DA_OK=$'\033[38;2;51;209;122m' # LED green #33d17a + DA_YEL=$'\033[38;2;246;196;69m' # LED amber #f6c445 + DA_HOT=$'\033[38;2;255;77;77m' # LED red #ff4d4d + DA_BLU=$'\033[38;2;77;184;255m' # cool blue #4db8ff + DA_CYN=$'\033[38;2;77;216;230m' # cyan accent #4dd8e6 + elif [ "$ncolors" -ge 256 ] 2>/dev/null; then + DA_ST=$'\033[38;5;245m' DA_HL=$'\033[38;5;252m' DA_DIM=$'\033[38;5;238m' + DA_OK=$'\033[38;5;40m' DA_YEL=$'\033[38;5;214m' DA_HOT=$'\033[38;5;196m' + DA_BLU=$'\033[38;5;39m' DA_CYN=$'\033[38;5;51m' + else + DA_ST=$'\033[37m' DA_HL=$'\033[1;37m' DA_DIM=$'\033[90m' + DA_OK=$'\033[32m' DA_YEL=$'\033[33m' DA_HOT=$'\033[31m' + DA_BLU=$'\033[34m' DA_CYN=$'\033[36m' + fi + DA_RST=$'\033[0m' + DA_SR="${DA_RST}${DA_ST}" # "return to structure": reset attrs, steel mid +} + +# da_gauge PCT WIDTH -- smooth eighth-block capacity gauge over a dim track, +# e.g. "da_gauge 62 15" -> "████████▋░░░░░░". Color it by level at call site +# (DA_OK < 70, DA_YEL < 90, DA_HOT >= 90); pad-then-color as usual. +da_gauge() { + local pct="${1:-0}" w="${2:-10}" full rem cells out='' i + local eighth=('' '▏' '▎' '▍' '▌' '▋' '▊' '▉') + [ "$pct" -lt 0 ] 2>/dev/null && pct=0 + [ "$pct" -gt 100 ] 2>/dev/null && pct=100 + full=$(( pct * w / 100 )); rem=$(( (pct * w * 8 / 100) % 8 )); cells=$full + for ((i = 0; i < full; i++)); do out+='█'; done + if [ "$rem" -gt 0 ] && [ "$full" -lt "$w" ]; then + out+="${eighth[rem]}"; cells=$((cells + 1)) + fi + for ((i = cells; i < w; i++)); do out+='░'; done + printf '%s' "$out" +} + +# ══ driveAtlas :: NUC5i5RYB/RYH chassis + drive-slot map ══════════════════════ +# 88-col cabinet-projection rendering with live device names from the global +# associative array DRIVE_MAP (keys: int-os usb-fl usb-fr usb-rt usb-rb). +# Occupied ports: solid light boxes, green LED bar + device. EMPTY ports: +# dashed ghost boxes, dim. Alignment: every variable field is padded to its +# final width BEFORE color codes wrap it; borders are literal strings. generate_nuc_layout() { - local hostname="$1" build_drive_map + local host="${1:-unknown}" + local model="" + if command -v maybe_sudo >/dev/null 2>&1; then + model="$(maybe_sudo dmidecode -s baseboard-product-name 2>/dev/null | head -n1)" + fi + [ -n "$model" ] || model="NUC5i5RYB/RYH" - local nuc_model - nuc_model="$(maybe_sudo dmidecode -s baseboard-product-name 2>/dev/null | head -n1)" - [[ -z "$nuc_model" ]] && nuc_model="Intel NUC" + da_color_init - printf "┌─────────────────────────────────────────────────────────────┐\n" - printf "│ %-57s │\n" "$hostname - $nuc_model (Mini PC)" - printf "│ │\n" - printf "│ Storage Controllers: │\n" - while IFS= read -r ctrl; do - [[ -n "$ctrl" ]] && printf "│ %-59.59s│\n" "$ctrl" - done < <(get_storage_controllers) - printf "│ │\n" - printf "│ Internal 2.5\" SATA (OS / boot): │\n" - printf "│ ┌────────────────┐ │\n" - printf "│ │ %-14s │ │\n" "${DRIVE_MAP[int-os]:-EMPTY}" - printf "│ └────────────────┘ │\n" - printf "│ │\n" - printf "│ Rear USB 3.0 (stacked): │\n" - printf "│ ┌─────────────────────────┐ │\n" - printf "│ │ TOP (3-4): %-9s │ │\n" "${DRIVE_MAP[usb-top]:-EMPTY}" - printf "│ ├─────────────────────────┤ │\n" - printf "│ │ BOTTOM (3-3): %-9s │ │\n" "${DRIVE_MAP[usb-bot]:-EMPTY}" - printf "│ └─────────────────────────┘ │\n" - printf "└─────────────────────────────────────────────────────────────┘\n" + # fixed-width header fields (pad/truncate so the frame never moves) + local hostp modelp + printf -v hostp '%-22.22s' "$host" + printf -v modelp '%-14.14s' "$model" + + # per-USB-slot pieces: device field (6 cols: LED bar + 5-char name), + # box side glyph, label color, border dash -- ghost styling for EMPTY + local fl fr rt rb io fls frs rts rbs flc frc rtc rbc fld frd rtd rbd + local flt frt flb frb rtt rbb + local pre key col v p + for pre in fl:usb-fl:B fr:usb-fr:Y rt:usb-rt:B rb:usb-rb:B; do + key="${pre#*:}"; key="${key%:*}"; col="${pre##*:}"; pre="${pre%%:*}" + v="${DRIVE_MAP[$key]:-EMPTY}" + if [ "$v" = "EMPTY" ]; then + printf -v p ' %-5.5s' "$v" + printf -v "$pre" '%s' "${DA_DIM}${p}${DA_SR}" + printf -v "${pre}s" '%s' "${DA_DIM}╎${DA_SR}" + printf -v "${pre}c" '%s' "${DA_DIM}" + printf -v "${pre}d" '%s' '╌' + else + printf -v p '%-5.5s' "$v" + printf -v "$pre" '%s' "${DA_OK}▌${p}${DA_SR}" + printf -v "${pre}s" '%s' '│' + [ "$col" = "Y" ] && printf -v "${pre}c" '%s' "${DA_YEL}" \ + || printf -v "${pre}c" '%s' "${DA_BLU}" + printf -v "${pre}d" '%s' '─' + fi + done + # box edges (11 cols; front tops carry the callout stem junction) + flt="$(da_repeat "$fld" 5)┴$(da_repeat "$fld" 5)" + frt="┴$(da_repeat "$frd" 10)" + flb="$(da_repeat "$fld" 11)"; frb="$(da_repeat "$frd" 11)" + rtt="$(da_repeat "$rtd" 11)"; rbb="$(da_repeat "$rbd" 11)" + [ "$fld" = '╌' ] && { flt="${DA_DIM}${flt}${DA_SR}"; flb="${DA_DIM}${flb}${DA_SR}"; } + [ "$frd" = '╌' ] && { frt="${DA_DIM}${frt}${DA_SR}"; frb="${DA_DIM}${frb}${DA_SR}"; } + [ "$rtd" = '╌' ] && rtt="${DA_DIM}${rtt}${DA_SR}" + [ "$rbd" = '╌' ] && rbb="${DA_DIM}${rbb}${DA_SR}" + # internal 2.5" SATA (int-os) + v="${DRIVE_MAP[int-os]:-EMPTY}" + if [ "$v" = "EMPTY" ]; then + printf -v p ' %-5.5s' "$v"; io="${DA_DIM}${p}${DA_SR}" + else + printf -v p '%-5.5s' "$v"; io="${DA_OK}▌${p}${DA_SR}" + fi + + printf "${DA_ST}${DA_HL}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${DA_SR}${DA_RST}\n" + printf "${DA_ST}${DA_HL}┃${DA_SR} ${DA_CYN}driveAtlas${DA_SR} :: ${DA_HL}%s${DA_SR} %s - 115x111x49 mm - tall (RYH) kit ${DA_HL}┃${DA_SR}${DA_RST}\n" "$hostp" "$modelp" + printf "${DA_ST}${DA_HL}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${DA_SR}${DA_RST}\n" + printf " \n" + printf "${DA_ST} FRONT 3/4 ── cool intake side REAR ── hot exhaust side ${DA_RST}\n" + printf " \n" + printf "${DA_ST} ${DA_DIM}▄▄▄▄▄▄ ▄▄▄▄▄▄${DA_SR} < CPU fan vents ${DA_RST}\n" + printf "${DA_ST} ${DA_HOT} v v v hot air${DA_SR} ${DA_RST}\n" + printf "${DA_ST} ${DA_HL}┌──────────────────────────┐${DA_SR} ${DA_HL}╭────────────────────────────────╮${DA_SR} ${DA_RST}\n" + printf "${DA_ST} ${DA_HL}╱${DA_SR}${DA_DIM}░░░─░░░░░░░─░░░░░░░░─░░░░░${DA_SR}${DA_HL}╱│${DA_SR} ${DA_HL}│${DA_SR} ┌%s┐ ${DA_HL}│${DA_SR} ${DA_RST}\n" "$rtt" + printf "${DA_ST} ${DA_HL}╱${DA_SR}${DA_DIM}░${DA_SR}${DA_OK}(o)${DA_SR} pwr ${DA_DIM}░░─░░░░░─░░░░░░░─${DA_SR}${DA_HL}╱${DA_SR}${DA_DIM}▒${DA_SR}${DA_HL}│${DA_SR} ${DA_HL}│${DA_SR} (o) ▐═▌ ▐╬▌ %s %s3-4 BLUE${DA_SR} %s ▐▄▌ ${DA_HL}│${DA_SR} ${DA_RST}\n" "$rts" "$rtc" "$rts" + printf "${DA_ST} ${DA_HL}┌──────────────────────────┐${DA_SR}${DA_DIM}▒▒${DA_SR}${DA_HL}│${DA_SR} ${DA_HL}│${DA_SR} DC mDP GbE %s %s %smHDMI${DA_HL}│${DA_SR} ${DA_RST}\n" "$rts" "$rt" "$rts" + printf "${DA_ST} ${DA_HL}│${DA_SR} ╌ brushed aluminum band ╌${DA_HL}│${DA_SR}${DA_DIM}▒${DA_SR}${DA_HL}╱${DA_SR} ${DA_HL}│${DA_SR} ├───────────┤ ${DA_HL}│${DA_SR} ${DA_RST}\n" + printf "${DA_ST} ${DA_HL}│${DA_SR} ${DA_BLU}▐███▌${DA_SR} ${DA_YEL}▐███▌${DA_SR} (o) . ${DA_HL}│╱${DA_SR} ${DA_HL}│${DA_SR} %s %s3-3 BLUE${DA_SR} %s ${DA_HL}│${DA_SR}${DA_HOT}< bakes in${DA_SR} ${DA_RST}\n" "$rbs" "$rbc" "$rbs" + printf "${DA_ST} ${DA_HL}└────┬───────┬─────────────┘${DA_SR} ${DA_HL}│${DA_SR} %s %s %s ${DA_HL}│${DA_SR}${DA_HOT} CPU exhaust${DA_SR}${DA_RST}\n" "$rbs" "$rb" "$rbs" + printf "${DA_ST} │ │ audio IR ${DA_HL}│${DA_SR} └%s┘ ${DA_HL}│${DA_SR} ${DA_RST}\n" "$rbb" + printf "${DA_ST} ┌%s┐┌%s┐ ${DA_HL}╰────────────────────────────────╯${DA_SR} ${DA_RST}\n" "$flt" "$frt" + printf "${DA_ST} %s %s3-2 BLUE${DA_SR} %s%s%s3-1? YELLOW${DA_SR}%s ${DA_DIM}▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀${DA_SR} ${DA_RST}\n" "$fls" "$flc" "$fls" "$frs" "$frc" "$frs" + printf "${DA_ST} %s %s %s%s %s %s ${DA_RST}\n" "$fls" "$fl" "$fls" "$frs" "$fr" "$frs" + printf "${DA_ST} %s USB3 data %s%s charging %s ${DA_RST}\n" "$fls" "$fls" "$frs" "$frs" + printf "${DA_ST} └%s┘└%s┘ ${DA_RST}\n" "$flb" "$frb" + printf " \n" + printf "${DA_ST} ${DA_HL}UNDERSIDE / INSIDE${DA_SR} ${DA_HL}NOTES${DA_SR} ${DA_RST}\n" + printf "${DA_ST} ┌──────────────────────────────────┐ ${DA_RST}\n" + printf "${DA_ST} │ internal 2.5\" SATA ── mounted │ - all 4 USB3 jacks hang off ONE xHCI ${DA_RST}\n" + printf "${DA_ST} │ INVERTED in the bottom lid │ controller ── one shared kernel bus ${DA_RST}\n" + printf "${DA_ST} │ %s < int-os - OS / boot │ - M.2 2280 slot ── flat on mainboard ${DA_RST}\n" "$io" + printf "${DA_ST} │${DA_DIM} ╌╌╌╌╌ flex cable to mainboard ╌╌ ${DA_SR}│ - \"?\" = port 3-1 id inferred (unverified) ${DA_RST}\n" + printf "${DA_ST} └──────────────────────────────────┘ ${DA_RST}\n" + printf "${DA_ST} ${DA_DIM}o feet o────── VESA ──────o feet o${DA_SR} ${DA_RST}\n" + + # optional flourish: controller inventory from the existing helper + if command -v get_storage_controllers >/dev/null 2>&1; then + printf '\n %sSTORAGE CONTROLLERS%s\n' "${DA_HL}" "${DA_RST}" + get_storage_controllers 2>/dev/null | sed "s/^/ /" + fi } #------------------------------------------------------------------------------ @@ -778,18 +918,28 @@ declare -A SERVER_MAPPINGS=( # pbs # Intel NUC5i5RYB - Proxmox Backup Server # int-os : internal 2.5\" SATA SSD (Samsung MZNLN128) - OS/boot, controller 00:1f.2 - # usb-top : TOP rear USB 3.0 NVMe (Patriot P300 / JMicron bridge) - halfTbNVMeMirror - # usb-bot : BOTTOM rear USB 3.0 NVMe (Patriot P300 / JMicron bridge) - halfTbNVMeMirror + # usb-fl : FRONT-LEFT USB 3.0 (blue) NVMe (Patriot P300 / JMicron bridge) - halfTbNVMeMirror + # usb-fr : FRONT-RIGHT USB 3.0 (yellow, always-on charging) - normally empty + # usb-rt : REAR-TOP USB 3.0 (blue) - now empty + # usb-rb : REAR-BOTTOM USB 3.0 (blue) NVMe (Patriot P300 / JMicron bridge) - halfTbNVMeMirror # # The two USB bridges report an IDENTICAL serial, so these drives are keyed by USB - # *port path* (usb-0:N = kernel bus3 port N), NOT by-id/serial. Physical top/bottom - # confirmed by an LED blink test on 2026-07-15: - # TOP rear jack = port 3-4 (usb-0:4) = fw serial P300WCBA24090617615 (label 022411042502235) - # BOTTOM rear jack = port 3-3 (usb-0:3) = fw serial P300WCBA24090617536 + # *port path* (usb-0:N = kernel bus3 port N), NOT by-id/serial. + # Port jacks (LED-blink verified 2026-07-15; front-left re-verified 2026-07-18): + # REAR-TOP jack = port 3-4 (usb-0:4) [blue] + # REAR-BOTTOM jack = port 3-3 (usb-0:3) [blue] = fw serial P300WCBA24090617536 + # FRONT-LEFT jack = port 3-2 (usb-0:2) [blue] = fw serial P300WCBA24090617615 + # FRONT-RIGHT jack = port 3-1 (usb-0:1) [yellow] (port # unverified - jack empty) + # + # 2026-07-18: after a power outage the ...615 drive dropped to 0B / hardware-error in the + # hot stacked REAR-TOP bay; moving it to FRONT-LEFT (3-2) revived it. Root cause was + # thermal - the two NVMe USB bridges sat flush against each other in the rear stack. ["pbs"]=" pci-0000:00:1f.2-ata-4 int-os - pci-0000:00:14.0-usb-0:4:1.0-scsi-0:0:0:0 usb-top - pci-0000:00:14.0-usb-0:3:1.0-scsi-0:0:0:0 usb-bot + pci-0000:00:14.0-usb-0:2:1.0-scsi-0:0:0:0 usb-fl + pci-0000:00:14.0-usb-0:1:1.0-scsi-0:0:0:0 usb-fr + pci-0000:00:14.0-usb-0:4:1.0-scsi-0:0:0:0 usb-rt + pci-0000:00:14.0-usb-0:3:1.0-scsi-0:0:0:0 usb-rb " )