#!/bin/bash
# ================================================================
#  Server Diagnostic  v5.0  — GRML PXE
#  Collects: system specs, drive health, I/O speeds, RAM test
#  Output: HTML report served at http://<public-ip>:<random-port>
# ================================================================

RED='\033[1;31m'; GRN='\033[1;32m'; YLW='\033[1;33m'
CYN='\033[1;36m'; BLU='\033[1;34m'; WHT='\033[1;37m'
DIM='\033[2m'; RST='\033[0m'; BLD='\033[1m'

ok()   { echo -e "  ${GRN}✔${RST}  $1"; }
warn() { echo -e "  ${YLW}⚠${RST}  $1"; }
err()  { echo -e "  ${RED}✘${RST}  $1"; }
info() { echo -e "  ${WHT}•${RST}  $1"; }
hdr()  { echo -e "\n${BLU}${BLD}━━━ $1 ━━━${RST}\n"; }

[[ $EUID -ne 0 ]] && { echo "Run as root."; exit 1; }

# ── Install required tools ───────────────────────────────────
hdr "Checking tools"
for PKG in nvme-cli smartmontools fio dmidecode pciutils \
           memtester ethtool lm-sensors; do
    if ! command -v "${PKG%%-*}" &>/dev/null && ! dpkg -s "$PKG" &>/dev/null 2>&1; then
        info "Installing $PKG..."
        apt-get install -y "$PKG" -qq &>/dev/null \
            && ok "$PKG installed" \
            || warn "Could not install $PKG — some data may be missing"
    fi
done
ok "Tools ready"

# ── Session info ─────────────────────────────────────────────
clear
echo -e "${BLU}${BLD}"
echo "  ╔══════════════════════════════════════════════════════╗"
echo "  ║      Shift Hosting LLC — Server Diagnostic v5.0     ║"
echo "  ╚══════════════════════════════════════════════════════╝"
echo -e "${RST}"

echo -n "  Technician name  : "; read -r TECH_NAME;  TECH_NAME="${TECH_NAME:-Unknown}"
echo -n "  Server label     : "; read -r DEDI_LABEL; DEDI_LABEL="${DEDI_LABEL:-$(hostname)}"

HOSTNAME_VAL=$(hostname 2>/dev/null || echo "unknown")
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
TIMESTAMP_DISP=$(date "+%Y-%m-%d %I:%M %p %Z")
TIMEZONE=$(timedatectl show 2>/dev/null | awk -F= '/^Timezone/{print $2}' \
           || cat /etc/timezone 2>/dev/null || echo "Unknown")
NTP_OK=$(timedatectl show 2>/dev/null | awk -F= '/NTPSynchronized/{print $2}')

# Work directory for all temp data
WORKDIR=$(mktemp -d /tmp/diag_XXXXXX)
trap 'rm -rf "$WORKDIR"' EXIT

# ── Helper: write a key=value to the Python data file ────────
# All values are JSON-encoded by Python so special chars are safe
DATAFILE="$WORKDIR/data.py"
echo "# Auto-generated diag data" > "$DATAFILE"
echo "D = {}" >> "$DATAFILE"
echo "DRIVES = []" >> "$DATAFILE"
echo "RAM_SLOTS = []" >> "$DATAFILE"
echo "NICS = []" >> "$DATAFILE"

pset() {
    # pset KEY VALUE  — appends  D['KEY'] = <json-encoded VALUE>  to DATAFILE
    python3 -c "
import json, sys
k, v = sys.argv[1], sys.argv[2]
print(\"D[\" + json.dumps(k) + \"] = \" + json.dumps(v))
" "$1" "$2" >> "$DATAFILE"
}

pnum() {
    # pnum KEY NUMBER
    python3 -c "
import sys
k, v = sys.argv[1], sys.argv[2]
try:
    n = int(float(v))
except:
    n = 0
print(\"D[\" + repr(k) + \"] = \" + str(n))
" "$1" "$2" >> "$DATAFILE"
}

# ── System Info ──────────────────────────────────────────────
hdr "System Info"

pset "timestamp"      "$TIMESTAMP"
pset "timestamp_disp" "$TIMESTAMP_DISP"
pset "timezone"       "$TIMEZONE"
pset "ntp_ok"         "${NTP_OK:-no}"
pset "technician"     "$TECH_NAME"
pset "dedi_label"     "$DEDI_LABEL"
pset "hostname"       "$HOSTNAME_VAL"

SYS_MAKE=$(dmidecode -t system  2>/dev/null | awk -F: '/Manufacturer/{gsub(/^[ \t]+/,"",$2); print $2; exit}')
SYS_MODEL=$(dmidecode -t system 2>/dev/null | awk -F: '/Product Name/{gsub(/^[ \t]+/,"",$2); print $2; exit}')
SYS_SERIAL=$(dmidecode -t system 2>/dev/null | awk -F: '/Serial Number/{gsub(/^[ \t]+/,"",$2); print $2; exit}')
MB_MAKE=$(dmidecode -t baseboard 2>/dev/null | awk -F: '/Manufacturer/{gsub(/^[ \t]+/,"",$2); print $2; exit}')
MB_MODEL=$(dmidecode -t baseboard 2>/dev/null | awk -F: '/Product Name/{gsub(/^[ \t]+/,"",$2); print $2; exit}')
MB_SERIAL=$(dmidecode -t baseboard 2>/dev/null | awk -F: '/Serial Number/{gsub(/^[ \t]+/,"",$2); print $2; exit}')
BIOS_VER=$(dmidecode -t bios 2>/dev/null | awk -F: '/^[[:space:]]*Version/{gsub(/^[ \t]+/,"",$2); print $2; exit}')
BIOS_DATE=$(dmidecode -t bios 2>/dev/null | awk -F: '/Release Date/{gsub(/^[ \t]+/,"",$2); print $2; exit}')

pset "sys_make"   "${SYS_MAKE:-Unknown}"
pset "sys_model"  "${SYS_MODEL:-Unknown}"
pset "sys_serial" "${SYS_SERIAL:-Unknown}"
pset "mb_make"    "${MB_MAKE:-Unknown}"
pset "mb_model"   "${MB_MODEL:-Unknown}"
pset "mb_serial"  "${MB_SERIAL:-Unknown}"
pset "bios_ver"   "${BIOS_VER:-Unknown}"
pset "bios_date"  "${BIOS_DATE:-Unknown}"

info "System : $SYS_MAKE $SYS_MODEL  (S/N: $SYS_SERIAL)"
info "Board  : $MB_MAKE $MB_MODEL"
info "BIOS   : $BIOS_VER  ($BIOS_DATE)"

# ── CPU ──────────────────────────────────────────────────────
CPU_MODEL=$(grep "model name" /proc/cpuinfo | head -1 | awk -F: '{gsub(/^[ \t]+/,"",$2); print $2}')
CPU_THREADS=$(grep -c "^processor" /proc/cpuinfo)
CPU_SOCKETS=$(grep "physical id" /proc/cpuinfo 2>/dev/null | sort -u | wc -l)
CPU_SOCKETS=${CPU_SOCKETS:-1}
CPU_CORES=$(grep "cpu cores" /proc/cpuinfo | head -1 | awk -F: '{print $2+0}')
CPU_FREQ=$(grep "cpu MHz" /proc/cpuinfo | head -1 | awk -F: '{printf "%.0f", $2+0}')
CPU_CACHE=$(grep "cache size" /proc/cpuinfo | head -1 | awk -F: '{gsub(/^[ \t]+/,"",$2); print $2}')

pset "cpu_model"   "${CPU_MODEL:-Unknown}"
pnum "cpu_threads" "$CPU_THREADS"
pnum "cpu_sockets" "$CPU_SOCKETS"
pnum "cpu_cores"   "${CPU_CORES:-0}"
pset "cpu_freq_mhz" "${CPU_FREQ:-0}"
pset "cpu_cache"   "${CPU_CACHE:-Unknown}"

info "CPU    : $CPU_MODEL"
info "Threads: ${CPU_SOCKETS}S × ${CPU_CORES}C = ${CPU_THREADS} threads  @${CPU_FREQ}MHz"

# ── RAM — installed capacity + slot details ──────────────────
hdr "Memory"
RAM_TOTAL_MB=0
while IFS= read -r _line; do
    if [[ "$_line" =~ [Ss]ize:[[:space:]]*([0-9]+)[[:space:]]*(MB|GB|MiB|GiB) ]]; then
        _n="${BASH_REMATCH[1]}"; _u="${BASH_REMATCH[2]}"
        case "$_u" in MB|MiB) (( RAM_TOTAL_MB += _n )) ;;
                      GB|GiB) (( RAM_TOTAL_MB += _n * 1024 )) ;; esac
    fi
done < <(dmidecode -t memory 2>/dev/null | grep -i "^\s*Size:")
RAM_TOTAL_GB=$(( RAM_TOTAL_MB / 1024 ))
pnum "ram_total_gb" "$RAM_TOTAL_GB"
info "Total installed: ${RAM_TOTAL_GB} GB"

# Per-slot details — parse dmidecode blocks split on blank lines (most reliable)
python3 - "$DATAFILE" << 'RAMPY'
import subprocess, sys, json, re

datafile = sys.argv[1]

try:
    raw = subprocess.check_output(['dmidecode', '-t', '17'],
                                  stderr=subprocess.DEVNULL, text=True)
except Exception:
    raw = ''

# Split into blocks on blank lines
blocks = re.split(r'\n\s*\n', raw)

appends = []
printed = 0

for blk in blocks:
    lines = blk.strip().splitlines()
    if not lines:
        continue
    # Only process Memory Device blocks
    if not any('Memory Device' in l and not 'Array' in l for l in lines[:3]):
        continue

    # Parse every Key: Value line in this block
    s = {}
    for line in lines:
        m = re.match(r'^\s+(.+?):\s*(.*)', line)
        if m:
            s[m.group(1).strip()] = m.group(2).strip()

    # Skip unpopulated slots
    size = s.get('Size', '')
    skip_vals = {'', 'No Module Installed', 'Not Installed', '0 MB', '0 GB',
                 'None', 'Not Present', '0'}
    if size in skip_vals or size.startswith('0 '):
        continue

    # Grab fields — try every known field name variant
    loc  = (s.get('Locator') or s.get('Bank Locator') or
            s.get('Socket Designation') or '—')
    # Strip bank locator prefix if it crept in
    loc = loc.split('\t')[0].strip()

    typ  = (s.get('Type') or s.get('Memory Type') or '—')
    # Speed: prefer configured, fall back to max speed
    spd  = (s.get('Configured Memory Speed') or
            s.get('Configured Clock Speed') or
            s.get('Speed') or '—')
    mfr  = (s.get('Manufacturer') or '—')
    part = (s.get('Part Number') or '—').strip()
    rank = (s.get('Rank') or '—')
    form = (s.get('Form Factor') or '—')
    ecc  = (s.get('Total Width', '')) != (s.get('Data Width', ''))  # rough ECC detect
    tech = (s.get('Memory Technology') or s.get('Type Detail') or '—')
    sn   = (s.get('Serial Number') or '')

    # Replace "Unknown" / "Not Specified" with em-dash
    def clean(v):
        return v if v not in ('Unknown','Not Specified','Not Provided','') else '—'
    loc=clean(loc); typ=clean(typ); spd=clean(spd)
    mfr=clean(mfr); part=clean(part); rank=clean(rank); form=clean(form)

    col_r = '\033[0m'
    col_w = '\033[37m'
    col_c = '\033[36m'
    print(f"  {col_c}{'Slot':<6}{col_w}{loc:<22}{col_r}  "
          f"{size:<10}  {typ:<8}  {spd:<18}  {mfr:<24}  {part}")
    printed += 1

    d = {
        'slot': loc, 'size': size, 'type': typ, 'speed': spd,
        'manufacturer': mfr, 'part': part, 'rank': rank,
        'form_factor': form, 'tech': tech, 'serial': sn
    }
    appends.append('RAM_SLOTS.append(' + json.dumps(d) + ')')

if printed == 0:
    print("  (no populated DIMM slots found — dmidecode may need root or DMI data unavailable)")

with open(datafile, 'a') as f:
    f.write('\n'.join(appends) + '\n')
RAMPY

# ── NICs — PCI-backed only ───────────────────────────────────
hdr "Network Interfaces"
_SKIP_DRIVERS="rndis_host cdc_ether cdc_ncm cdc_mbim usbnet qmi_wwan \
               veth bridge tun tap dummy ifb bonding team wireguard \
               vxlan sit gre ipip ip6tnl ovs-system"
for iface in $(ls /sys/class/net/ 2>/dev/null | grep -v "^lo$" | sort); do
    _devpath=$(readlink -f /sys/class/net/$iface/device 2>/dev/null)
    [[ -z "$_devpath" ]] && continue
    # Accept any device that has a PCI-style address component
    _pciaddr=$(echo "$_devpath" | grep -oP '[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]' | tail -1)
    # Also try getting PCI address from subsystem link
    if [[ -z "$_pciaddr" ]]; then
        _pciaddr=$(ls /sys/class/net/$iface/device/ 2>/dev/null | grep -oP '^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$' | head -1)
    fi
    [[ -z "$_pciaddr" ]] && continue
    _driver=$(readlink /sys/class/net/$iface/device/driver 2>/dev/null | awk -F/ '{print $NF}')
    # Skip USB and virtual drivers
    echo " $_SKIP_DRIVERS " | grep -qw "${_driver:-__NONE__}" && continue
    _mac=$(cat /sys/class/net/$iface/address 2>/dev/null | tr '[:upper:]' '[:lower:]')
    _state=$(cat /sys/class/net/$iface/operstate 2>/dev/null)
    _speed=$(cat /sys/class/net/$iface/speed 2>/dev/null 2>/dev/null || echo "")
    [[ "$_speed" == "-1" || -z "$_speed" ]] && _speed="—"
    _lspci=$(lspci -s "$_pciaddr" 2>/dev/null | sed 's/.*: //')
    # If lspci empty, try from device uevent
    [[ -z "$_lspci" ]] && _lspci=$(cat /sys/class/net/$iface/device/uevent 2>/dev/null \
        | grep "^DRIVER=" | cut -d= -f2)
    [[ "$_state" == "up" ]] && SC="$GRN" || SC="$DIM"
    printf "  ${SC}%-16s${RST}  %-19s  %-10s  %-6s  %-12s  %s\n" \
        "$iface" "${_mac:-??:??:??:??:??:??}" "${_speed} Mbps" "$_state" "$_driver" "$_lspci"
    python3 -c "
import json, sys
a = sys.argv[1:]
print('NICS.append(' + json.dumps({
    'name':a[0],'mac':a[1],'speed':a[2],'state':a[3],
    'driver':a[4],'lspci':a[5]}) + ')')
" "$iface" "${_mac:-}" "${_speed:-}" "${_state:-}" \
  "${_driver:-}" "${_lspci:-}" >> "$DATAFILE"
done

# ── Drive inventory ──────────────────────────────────────────
hdr "Drive Inventory"

_pcie_gen() {
    # Returns Gen number from GT/s string
    local s="$1"
    case "$s" in *2.5*) echo 1;; *5.0*) echo 2;; *8.0*) echo 3;;
                 *16.*) echo 4;; *32.*) echo 5;; *) echo "?";; esac
}

mapfile -t ALL_DEVS < <(lsblk -d -o NAME,TYPE --noheadings 2>/dev/null \
    | awk '$2=="disk"{print $1}' | grep -v "^loop\|^sr\|^fd")

for DEV_NAME in "${ALL_DEVS[@]}"; do
    DRIVE="/dev/$DEV_NAME"
    TRAN=$(lsblk -d -o TRAN --noheadings "$DRIVE" 2>/dev/null | tr -d ' ')
    SIZE=$(lsblk -d -o SIZE --noheadings "$DRIVE" 2>/dev/null | tr -d ' ')
    SIZE_B=$(lsblk -b -d -o SIZE --noheadings "$DRIVE" 2>/dev/null | tr -d ' ')
    ROTA=$(lsblk -d -o ROTA --noheadings "$DRIVE" 2>/dev/null | tr -d ' ')

    if   [[ "$TRAN" == "nvme" ]];                     then DTYPE="NVMe SSD"
    elif [[ "$TRAN" == "sata" && "$ROTA" == "0" ]];   then DTYPE="SATA SSD"
    elif [[ "$TRAN" == "sata" || "$TRAN" == "ata" ]]; then DTYPE="HDD"
    elif [[ "$TRAN" == "sas" ]];                      then DTYPE="SAS"
    else DTYPE="${TRAN:-Unknown}"; fi

    echo -e "\n  ${CYN}${DEV_NAME}${RST}  ${DTYPE}  ${SIZE}"

    # ── NVMe path ────────────────────────────────────────────
    if [[ "$TRAN" == "nvme" ]]; then
        CTRL=$(nvme id-ctrl "$DRIVE" 2>/dev/null)
        SMART=$(nvme smart-log "$DRIVE" 2>/dev/null)
        ERRLOG=$(nvme error-log "$DRIVE" 2>/dev/null)

        D_MODEL=$(echo "$CTRL"  | awk -F: '/^mn /{gsub(/^[ \t]+/,"",$2); print $2}' | xargs)
        D_SERIAL=$(echo "$CTRL" | awk -F: '/^sn /{gsub(/^[ \t]+/,"",$2); print $2}' | xargs)
        D_FW=$(echo "$CTRL"     | awk -F: '/^fr /{gsub(/^[ \t]+/,"",$2); print $2}' | xargs)
        NVME_CTRL_NAME=$(basename "$DRIVE" | sed 's/n[0-9]*$//')  # nvme0

        # Temperature — parse Kelvin from parens (most reliable across nvme-cli versions)
        TEMP_LINE=$(echo "$SMART" | grep -i "^temperature\b" | head -1)
        TEMP_K=$(echo "$TEMP_LINE" | grep -oP '\((\d+)\s*K\)' | grep -oP '\d+' | head -1)
        if [[ -n "$TEMP_K" && "$TEMP_K" =~ ^[0-9]+$ ]]; then
            TEMP_C=$(( TEMP_K - 273 ))
            TEMP_F=$(( TEMP_C * 9 / 5 + 32 ))
        elif echo "$TEMP_LINE" | grep -q "°F"; then
            # Line already has °F — grab it directly
            TEMP_F=$(echo "$TEMP_LINE" | grep -oP '\d+(?=\s*°F)' | head -1)
            TEMP_C=$(( (TEMP_F - 32) * 5 / 9 ))
        else
            # Fall back: first number, treat as Celsius
            TEMP_C=$(echo "$TEMP_LINE" | grep -oP '\d+' | head -1)
            [[ -n "$TEMP_C" && "$TEMP_C" =~ ^[0-9]+$ && $TEMP_C -gt 200 ]] && TEMP_C=$(( TEMP_C - 273 ))
            [[ -n "$TEMP_C" && "$TEMP_C" =~ ^[0-9]+$ ]] && TEMP_F=$(( TEMP_C * 9 / 5 + 32 ))
        fi

        PCT_USED=$(echo "$SMART" | grep -i "percentage_used" | awk '{print $NF}')
        PCT_NUM=${PCT_USED//%/}; PCT_NUM=${PCT_NUM:-0}
        LIFE_LEFT=$(( 100 - PCT_NUM ))
        SPARE=$(echo "$SMART" | grep -i "available_spare\b" | awk '{print $NF}')
        POH=$(echo "$SMART" | grep -i "power_on_hours" | awk '{print $NF}')
        PC=$(echo "$SMART"  | grep -i "power_cycles"   | awk '{print $NF}')
        ME=$(echo "$SMART"  | grep -i "media_errors"   | awk '{print $NF}')
        EL=$(echo "$SMART"  | grep -i "num_err_log_entries" | awk '{print $NF}')
        US=$(echo "$SMART"  | grep -i "unsafe_shutdowns"    | awk '{print $NF}')
        CRIT=$(echo "$SMART"| grep -i "critical_warning"    | awk '{print $NF}')

        # Data written/read — nvme-cli outputs either "Data Units Written" or "data_units_written"
        # Value line looks like:  Data Units Written  : 210389 (107.72 GB)
        # Grab the human-readable part in parens if present, else compute from raw units
        _DW_LINE=$(echo "$SMART" | grep -i "data.units.written")
        _DR_LINE=$(echo "$SMART" | grep -i "data.units.read")
        DATA_W_DISP=$(echo "$_DW_LINE" | grep -oP '\([\d.]+ \w+\)' | tr -d '()' | head -1)
        DATA_R_DISP=$(echo "$_DR_LINE" | grep -oP '\([\d.]+ \w+\)' | tr -d '()' | head -1)
        # Fall back to computing from raw units if no parens
        if [[ -z "$DATA_W_DISP" ]]; then
            DATA_W_RAW=$(echo "$_DW_LINE" | awk '{print $NF}' | tr -d ',')
            [[ "$DATA_W_RAW" =~ ^[0-9]+$ ]] && {
                _TB=$(python3 -c "print(f'{int(\"$DATA_W_RAW\")*512000/1e12:.2f}')" 2>/dev/null)
                DATA_W_DISP="${_TB} TB"
            }
        fi
        if [[ -z "$DATA_R_DISP" ]]; then
            DATA_R_RAW=$(echo "$_DR_LINE" | awk '{print $NF}' | tr -d ',')
            [[ "$DATA_R_RAW" =~ ^[0-9]+$ ]] && {
                _TB=$(python3 -c "print(f'{int(\"$DATA_R_RAW\")*512000/1e12:.2f}')" 2>/dev/null)
                DATA_R_DISP="${_TB} TB"
            }
        fi
        DATA_W_DISP="${DATA_W_DISP:-N/A}"
        DATA_R_DISP="${DATA_R_DISP:-N/A}"

        # Health
        HEALTH="PASS"
        [[ "${CRIT:-0}" != "0" || "${ME:-0}" != "0" ]] && HEALTH="FAIL"
        [[ "${EL:-0}"   != "0" || "${US:-0}" != "0" ]] && [[ "$HEALTH" != "FAIL" ]] && HEALTH="WARN"

        # PCIe link
        PCI_PATH=$(readlink -f /sys/class/nvme/${NVME_CTRL_NAME}/device 2>/dev/null)
        PCI_ADDR=$(basename "$PCI_PATH" 2>/dev/null)
        CURR_SPEED=$(cat "/sys/bus/pci/devices/${PCI_ADDR}/current_link_speed" 2>/dev/null)
        CURR_WIDTH=$(cat "/sys/bus/pci/devices/${PCI_ADDR}/current_link_width" 2>/dev/null)
        MAX_SPEED=$(cat  "/sys/bus/pci/devices/${PCI_ADDR}/max_link_speed"     2>/dev/null)
        MAX_WIDTH=$(cat  "/sys/bus/pci/devices/${PCI_ADDR}/max_link_width"     2>/dev/null)
        CURR_GEN=$(_pcie_gen "$CURR_SPEED")
        MAX_GEN=$(_pcie_gen  "$MAX_SPEED")
        PCIE_BW=0
        [[ "$CURR_GEN" =~ ^[0-9]+$ && "$CURR_WIDTH" =~ ^[0-9]+$ ]] && {
            _bw_per_lane=( 0 250 500 985 1969 3938 )
            PCIE_BW=$(( ${_bw_per_lane[$CURR_GEN]:-0} * CURR_WIDTH ))
        }
        LSPCI_NAME=$(lspci -s "$PCI_ADDR" 2>/dev/null | sed 's/.*: //')

    # ── SATA / SAS / HDD path ─────────────────────────────────
    else
        SMART_OUT=$(smartctl -i -A -H "$DRIVE" 2>/dev/null)
        D_MODEL=$(echo "$SMART_OUT"  | awk -F: '/Device Model|Product/{gsub(/^[ \t]+/,"",$2); print $2; exit}' | xargs)
        D_SERIAL=$(echo "$SMART_OUT" | awk -F: '/Serial/{gsub(/^[ \t]+/,"",$2); print $2; exit}' | xargs)
        D_FW=$(echo "$SMART_OUT"     | awk -F: '/Firmware/{gsub(/^[ \t]+/,"",$2); print $2; exit}' | xargs)
        SMART_STATUS=$(echo "$SMART_OUT" | grep -i "overall-health\|result:" | awk -F: '{gsub(/^[ \t]+/,"",$2); print $2}' | tr -d ' ')
        HEALTH="PASS"; [[ "$SMART_STATUS" != *PASSED* ]] && HEALTH="FAIL"

        POH=$(echo "$SMART_OUT" | awk '/Power_On_Hours/{print $10}')
        PC=$(echo "$SMART_OUT"  | awk '/Power_Cycle_Count/{print $10}')
        TEMP_C=$(echo "$SMART_OUT" | awk '/Temperature_Celsius/{print $10}')
        TEMP_F=""; [[ -n "$TEMP_C" && "$TEMP_C" =~ ^[0-9]+$ ]] && TEMP_F=$(( TEMP_C * 9 / 5 + 32 ))
        ME="0"; EL="0"; US="0"; CRIT="0"; SPARE="N/A"
        LIFE_LEFT="N/A"; PCT_NUM=0
        DATA_W_DISP="N/A"; DATA_R_DISP="N/A"
        PCIE_BW=0; CURR_GEN="N/A"; CURR_WIDTH="N/A"
        MAX_GEN="N/A"; MAX_WIDTH="N/A"; CURR_SPEED=""; LSPCI_NAME=""
        ERRLOG=$(smartctl -l error "$DRIVE" 2>/dev/null | tail -60)
    fi

    # Write errlog to file BEFORE python3 reads it
    printf '%s' "${ERRLOG:-}" > "$WORKDIR/errlog_${DEV_NAME}"

    # Print summary line
    [[ "$HEALTH" == "PASS" ]] && HC="$GRN" || HC="$RED"
    [[ "$HEALTH" == "WARN" ]] && HC="$YLW"
    printf "    Model   : %s\n" "$D_MODEL"
    printf "    Serial  : %s\n" "$D_SERIAL"
    printf "    FW      : %s\n" "$D_FW"
    printf "    Health  : ${HC}%s${RST}   Life: %s%%  (used: %s%%)   Spare: %s\n" \
        "$HEALTH" "$LIFE_LEFT" "$PCT_NUM" "${SPARE:-N/A}"
    printf "    Written : %s   Read: %s\n" "$DATA_W_DISP" "$DATA_R_DISP"
    printf "    Temp    : %s°F (%s°C)   POH: %s hrs   Power Cycles: %s\n" \
        "${TEMP_F:-?}" "${TEMP_C:-?}" "${POH:-?}" "${PC:-?}"
    [[ "$TRAN" == "nvme" ]] && \
        printf "    PCIe    : Gen%s x%s  (~%s MB/s peak)  [%s]\n" \
            "$CURR_GEN" "$CURR_WIDTH" "$PCIE_BW" "$LSPCI_NAME"
    [[ "${ME:-0}" != "0" ]] && err "Media errors: $ME"
    [[ "${EL:-0}" != "0" ]] && warn "Error log entries: $EL  |  Unsafe shutdowns: ${US:-0}"

    # Write to Python data file
    python3 -c "
import json, sys
args = sys.argv[1:]
def n(v):
    try: return int(float(v))
    except: return 0

errlog_path = args[0]
try:
    errlog = open(errlog_path).read()
except:
    errlog = ''

d = {
    'device':args[1], 'type':args[2], 'size':args[3], 'transport':args[4],
    'model':args[5], 'serial':args[6], 'firmware':args[7], 'health':args[8],
    'life_pct':n(args[9]), 'pct_used':n(args[10]),
    'data_written':args[11], 'data_read':args[12],
    'temp_f':n(args[13]) if args[13].strip() else None,
    'temp_c':n(args[14]) if args[14].strip() else None,
    'poh':n(args[15]), 'power_cycles':n(args[16]),
    'media_errors':n(args[17]), 'error_entries':n(args[18]),
    'unsafe_shutdowns':n(args[19]), 'critical_warning':args[20],
    'spare':args[21],
    'pcie_gen':args[22], 'pcie_width':args[23],
    'pcie_max_gen':args[24], 'pcie_max_width':args[25],
    'pcie_bw_mbs':n(args[26]), 'lspci':args[27],
    'seq_r_mb':0, 'seq_w_mb':0, 'seq_r_iops':0, 'seq_w_iops':0,
    'rnd_r_iops':0, 'rnd_w_iops':0, 'rnd_r_mb':0, 'rnd_w_mb':0,
    'error_log': errlog
}
print('DRIVES.append(' + json.dumps(d) + ')')
" "$WORKDIR/errlog_${DEV_NAME}" \
  "$DEV_NAME" "$DTYPE" "$SIZE" "${TRAN:-unknown}" \
  "${D_MODEL:-Unknown}" "${D_SERIAL:-}" "${D_FW:-}" "$HEALTH" \
  "${LIFE_LEFT:-0}" "${PCT_NUM:-0}" \
  "${DATA_W_DISP:-N/A}" "${DATA_R_DISP:-N/A}" \
  "${TEMP_F:-}" "${TEMP_C:-}" \
  "${POH:-0}" "${PC:-0}" "${ME:-0}" "${EL:-0}" "${US:-0}" "${CRIT:-0}" \
  "${SPARE:-}" \
  "${CURR_GEN:-?}" "${CURR_WIDTH:-?}" \
  "${MAX_GEN:-?}"  "${MAX_WIDTH:-?}" \
  "${PCIE_BW:-0}"  "${LSPCI_NAME:-}" >> "$DATAFILE"
done

# ── Disk I/O Tests ────────────────────────────────────────────
hdr "Disk I/O Tests (fio)"
info "Testing each drive — 5 sec per test on raw block device"

DRIVE_IDX=0
for DEV_NAME in "${ALL_DEVS[@]}"; do
    DRIVE="/dev/$DEV_NAME"
    echo ""
    echo -e "  ${CYN}${DEV_NAME}${RST}  ($(lsblk -d -o SIZE --noheadings "$DRIVE" | tr -d ' '))"

    FIO_TMP="$WORKDIR/fio_${DEV_NAME}.json"
    TESTS=( "seqread:read:128k:4:read" "seqwrite:write:128k:4:write"
            "randread:randread:4k:32:read" "randwrite:randwrite:4k:32:write" )
    declare -A FIO_RESULTS=()

    for T in "${TESTS[@]}"; do
        IFS=: read -r TNAME RW BS QD RWFIELD <<< "$T"
        printf "    %-14s  " "$TNAME..."
        # Pipe through awk to strip fio's progress lines (Jobs: 1...) before the JSON starts
        fio --name="$TNAME" --filename="$DRIVE" \
            --rw="$RW" --bs="$BS" --iodepth="$QD" \
            --numjobs=1 --direct=1 \
            --runtime=5 --time_based \
            --output-format=json \
            2>/dev/null | awk '/^\{/{found=1} found{print}' > "$FIO_TMP"
        VAL=$(python3 -c "
import json
try:
    with open('$FIO_TMP') as f:
        d = json.load(f)
    j = d['jobs'][0]
    rw = '$RWFIELD'
    rd = j['read']; wr = j['write']
    bw   = int((rd if rw=='read' else wr)['bw'] / 1024)
    iops = int((rd if rw=='read' else wr)['iops'])
    lat  = (rd if rw=='read' else wr)['lat_ns']['mean'] / 1e6  # ms
    print(f'{bw} MB/s  |  {iops:,} IOPS  |  {lat:.2f}ms avg lat')
    print(bw)
    print(iops)
except Exception as e:
    print('ERROR: ' + str(e))
    print(0)
    print(0)
" 2>/dev/null)
        DISPLAY=$(echo "$VAL" | head -1)
        BW=$(echo "$VAL" | sed -n '2p')
        IOPS=$(echo "$VAL" | sed -n '3p')
        echo "$DISPLAY"
        FIO_RESULTS["${TNAME}_bw"]="${BW:-0}"
        FIO_RESULTS["${TNAME}_iops"]="${IOPS:-0}"
    done

    # Update DRIVES entry with I/O results
    python3 -c "
import json, sys
idx = int(sys.argv[1])
sr = int(sys.argv[2]); sw = int(sys.argv[3])
rr = int(sys.argv[4]); rw = int(sys.argv[5])
DRIVES[idx]['seq_r_mb'] = sr
DRIVES[idx]['seq_w_mb'] = sw
DRIVES[idx]['rnd_r_iops'] = rr
DRIVES[idx]['rnd_w_iops'] = rw
" "$DRIVE_IDX" \
  "${FIO_RESULTS[seqread_bw]:-0}" "${FIO_RESULTS[seqwrite_bw]:-0}" \
  "${FIO_RESULTS[randread_iops]:-0}" "${FIO_RESULTS[randwrite_iops]:-0}" \
  2>/dev/null || true

    # Append corrective update to data file
    cat >> "$DATAFILE" << PYUPDATE
DRIVES[${DRIVE_IDX}]['seq_r_mb']    = ${FIO_RESULTS[seqread_bw]:-0}
DRIVES[${DRIVE_IDX}]['seq_r_iops']  = ${FIO_RESULTS[seqread_iops]:-0}
DRIVES[${DRIVE_IDX}]['seq_w_mb']    = ${FIO_RESULTS[seqwrite_bw]:-0}
DRIVES[${DRIVE_IDX}]['seq_w_iops']  = ${FIO_RESULTS[seqwrite_iops]:-0}
DRIVES[${DRIVE_IDX}]['rnd_r_iops']  = ${FIO_RESULTS[randread_iops]:-0}
DRIVES[${DRIVE_IDX}]['rnd_r_mb']    = ${FIO_RESULTS[randread_bw]:-0}
DRIVES[${DRIVE_IDX}]['rnd_w_iops']  = ${FIO_RESULTS[randwrite_iops]:-0}
DRIVES[${DRIVE_IDX}]['rnd_w_mb']    = ${FIO_RESULTS[randwrite_bw]:-0}
PYUPDATE
    (( DRIVE_IDX++ ))
done

# ── RAM Test ──────────────────────────────────────────────────
hdr "RAM Test (memtester)"
RAM_AVAIL_KB=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
RAM_RESERVE_KB=$(( 384 * 1024 ))
RAM_TEST_KB=$(( RAM_AVAIL_KB - RAM_RESERVE_KB ))
[[ $RAM_TEST_KB -lt $(( 128 * 1024 )) ]] && RAM_TEST_KB=$(( 128 * 1024 ))
RAM_TEST_MB_FULL=$(( RAM_TEST_KB / 1024 ))
RAM_TEST_MB_SHORT=512

echo ""
echo -e "  ${WHT}Memory test options:${RST}"
echo -e "    ${GRN}[1]${RST}  Short — 512MB      (~1–2 min, quick sanity check)"
echo -e "    ${GRN}[2]${RST}  Full  — ${RAM_TEST_MB_FULL}MB  (tests all available RAM, recommended)"
echo -e "    ${GRN}[3]${RST}  Skip memory test"
echo ""
echo -n "  Select [2]: "
read -r MEM_SEL; MEM_SEL=${MEM_SEL:-2}
case "$MEM_SEL" in
    1) RAM_TEST_MB=$RAM_TEST_MB_SHORT ;;
    3) RAM_TEST_MB=0 ;;
    *) RAM_TEST_MB=$RAM_TEST_MB_FULL  ;;
esac

RAM_RESULT="SKIP"
RAM_FAIL=0

if [[ "$MEM_SEL" == "3" ]]; then
    warn "Memory test skipped."
else
    info "Testing ${RAM_TEST_MB}M  (installed: ${RAM_TOTAL_GB} GB  •  OS using ~$(( RAM_TEST_MB_FULL - RAM_TEST_MB + 384 )) MB)"
    info "For 100% coverage boot memtest86+ from USB.  (Ctrl+C to abort)"
    echo ""

    MEM_LOGFILE=$(mktemp /tmp/memtest_XXXXXX.log)
    TESTS_TOTAL=19   # memtester always runs exactly 19 sub-tests per loop

    # stdbuf -oL forces line-buffered output so log file updates live instead of at process exit
    stdbuf -oL memtester "${RAM_TEST_MB}M" 1 > "$MEM_LOGFILE" 2>&1 &
    MEM_PID=$!

    START_S=$SECONDS
    LAST_OK=0
    LAST_FAIL=0

    # Poll log file using awk — returns clean integer, no grep -c exit-code trap
    while kill -0 "$MEM_PID" 2>/dev/null; do
        sleep 0.8

        DONE=$(awk '/: ok/{c++} END{print c+0}' "$MEM_LOGFILE" 2>/dev/null)
        FAILS=$(awk '/FAIL/{c++} END{print c+0}' "$MEM_LOGFILE" 2>/dev/null)
        DONE=${DONE:-0}; FAILS=${FAILS:-0}
        [[ $DONE -gt $TESTS_TOTAL ]] && DONE=$TESTS_TOTAL

        PCT=$(( DONE * 100 / TESTS_TOTAL ))
        FILL=$(( DONE * 40 / TESTS_TOTAL ))
        EMPTY=$(( 40 - FILL ))
        BAR_GREEN=$(printf '█%.0s' $(seq 1 $FILL)  2>/dev/null)
        BAR_GREY=$( printf '░%.0s' $(seq 1 $EMPTY) 2>/dev/null)

        ELAPSED=$(( SECONDS - START_S ))
        if [[ $DONE -gt 0 && $ELAPSED -gt 0 ]]; then
            SECS_LEFT=$(( (TESTS_TOTAL - DONE) * ELAPSED / DONE ))
            ETA=$(printf "%d:%02d left" $(( SECS_LEFT/60 )) $(( SECS_LEFT%60 )))
        else
            ETA="starting..."
        fi

        FAIL_TAG=""
        [[ $FAILS -gt 0 ]] && FAIL_TAG="  ${RED}${FAILS} FAIL!${RST}"

        printf "\r  \033[32m%s\033[2m%s\033[0m  %3d%%  %2d/%-2d  %-20s%s   " \
            "$BAR_GREEN" "$BAR_GREY" \
            "$PCT" "$DONE" "$TESTS_TOTAL" \
            "$ETA" "$FAIL_TAG"
    done

    # Final counts via awk
    wait "$MEM_PID" 2>/dev/null
    DONE=$(awk  '/: ok/{c++} END{print c+0}' "$MEM_LOGFILE" 2>/dev/null); DONE=${DONE:-0}
    RAM_FAIL=$(awk '/FAIL/{c++}     END{print c+0}' "$MEM_LOGFILE" 2>/dev/null);  RAM_FAIL=${RAM_FAIL:-0}

    # Print final full bar
    printf "\r  \033[32m%s\033[0m  100%%  %d/%-d  complete!                       \n\n" \
        "$(printf '%0.s█' $(seq 1 40) 2>/dev/null)" \
        "$TESTS_TOTAL" "$TESTS_TOTAL"

    rm -f "$MEM_LOGFILE"

    if [[ "$RAM_FAIL" -eq 0 ]]; then
        ok "RAM PASSED — ${RAM_TEST_MB}M tested, no errors"
        RAM_RESULT="PASS"
    else
        err "RAM FAILED — ${RAM_FAIL} failure(s) found. Verify with memtest86+ from USB."
        RAM_RESULT="FAIL"
    fi
fi

pset "ram_test_result" "$RAM_RESULT"
pnum "ram_test_mb"     "$RAM_TEST_MB"
pnum "ram_test_errors" "$RAM_FAIL"

# ── Generate JSON + HTML report ───────────────────────────────
hdr "Generating Report"

REPORT_DIR="$WORKDIR/report"
mkdir -p "$REPORT_DIR"

python3 - <<PYEOF2
import json, os, sys
os.chdir('$WORKDIR')
exec(open('$DATAFILE').read())

def esc(s): return str(s).replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
def badge(h):
    c={'PASS':'#3fb950','WARN':'#d29922','FAIL':'#f85149'}.get(h,'#8b949e')
    return f'<span style="background:{c}22;color:{c};border:1px solid {c}55;padding:2px 10px;border-radius:4px;font-size:12px;font-weight:700">{esc(h)}</span>'
def bar(pct,w=120):
    if pct is None or str(pct)=='N/A': return '<span style="color:#8b949e">N/A</span>'
    n=int(pct); c='#3fb950' if n>=50 else '#d29922' if n>=20 else '#f85149'
    return (f'<div style="display:flex;align-items:center;gap:6px">'
            f'<div style="width:{w}px;height:8px;background:#21262d;border-radius:4px;overflow:hidden">'
            f'<div style="width:{n}%;height:100%;background:{c};border-radius:4px"></div></div>'
            f'<span style="color:{c};font-size:12px">{n}%</span></div>')
def spd(v,unit):
    n=int(v) if v else 0
    if not n: return '<span style="color:#8b949e">—</span>'
    c=('#3fb950' if (unit=='MB/s' and n>=2000) or (unit=='IOPS' and n>=200000)
       else '#d29922' if (unit=='MB/s' and n>=500) or (unit=='IOPS' and n>=50000)
       else '#f85149')
    return f'<span style="color:{c};font-family:monospace;font-weight:700">{n:,}</span><span style="color:#8b949e;font-size:11px"> {unit}</span>'
def temp_cell(tf,tc):
    if not tf: return '<span style="color:#8b949e">—</span>'
    c='#3fb950' if tf<158 else '#d29922' if tf<176 else '#f85149'
    return f'<span style="color:{c}">{tf}°F<br><span style="font-size:11px;color:#8b949e">{tc}°C</span></span>'

ntp_c='#3fb950' if D.get('ntp_ok')=='yes' else '#d29922'
ram_test_c='#3fb950' if D.get('ram_test_result')=='PASS' else '#f85149'

specs_html=f'''<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px 28px">
<div><div class="lbl">LABEL / DEDI</div><div style="font-size:20px;font-weight:700;color:#58a6ff">{esc(D.get("dedi_label",""))}</div></div>
<div><div class="lbl">HOSTNAME</div><div class="val">{esc(D.get("hostname",""))}</div></div>
<div><div class="lbl">DATE / TIME</div><div class="val">{esc(D.get("timestamp_disp",""))}</div>
     <div style="font-size:11px;color:#8b949e">{esc(D.get("timezone",""))} &nbsp;<span style="color:{ntp_c}">NTP:{esc(D.get("ntp_ok","?"))}</span></div></div>
<div><div class="lbl">TECHNICIAN</div><div class="val">{esc(D.get("technician",""))}</div></div>
<div><div class="lbl">MAKE / MODEL</div><div class="val">{esc(D.get("sys_make",""))} {esc(D.get("sys_model",""))}</div></div>
<div><div class="lbl">SYSTEM S/N</div><div style="font-family:monospace">{esc(D.get("sys_serial","—"))}</div></div>
<div><div class="lbl">MOTHERBOARD</div><div class="val">{esc(D.get("mb_make",""))} {esc(D.get("mb_model",""))}</div></div>
<div><div class="lbl">BIOS</div><div style="font-family:monospace;font-size:13px">{esc(D.get("bios_ver",""))} <span style="color:#8b949e;font-size:11px">({esc(D.get("bios_date",""))})</span></div></div>
<div><div class="lbl">CPU</div><div class="val">{esc(D.get("cpu_model",""))}</div>
     <div style="color:#8b949e;font-size:11px">{D.get("cpu_sockets",1)}S &times; {D.get("cpu_cores",0)}C &mdash; {D.get("cpu_threads",0)} threads</div></div>
<div><div class="lbl">RAM INSTALLED</div><div style="font-size:20px;font-weight:700;color:#3fb950">{D.get("ram_total_gb",0)} GB</div></div>
</div>'''

ram_rows=''.join(f'<tr><td style="font-family:monospace;color:#39d5c3">{esc(s.get("slot",""))}</td><td>{esc(s.get("size",""))}</td><td>{esc(s.get("type",""))}</td><td>{esc(s.get("speed",""))}</td><td>{esc(s.get("manufacturer",""))}</td><td style="font-family:monospace;font-size:12px">{esc(s.get("part",""))}</td><td>{esc(s.get("rank",""))}</td></tr>' for s in RAM_SLOTS) or '<tr><td colspan="7" style="color:#8b949e">No slot data</td></tr>'

nic_rows=''.join(f'<tr><td style="font-family:monospace;color:#39d5c3">{esc(n.get("name",""))}</td><td style="font-family:monospace">{esc(n.get("mac",""))}</td><td>{"<span style=color:#3fb950>"+esc(n.get("speed",""))+" Mbps</span>" if n.get("state")=="up" else esc(n.get("speed","—"))}</td><td style="color:{"#3fb950" if n.get("state")=="up" else "#8b949e"}">{esc(n.get("state",""))}</td><td style="font-size:12px;color:#8b949e">{esc(n.get("driver",""))}</td><td style="font-size:12px;color:#8b949e">{esc(n.get("lspci",""))}</td></tr>' for n in NICS) or '<tr><td colspan="6" style="color:#8b949e">No interfaces</td></tr>'

drive_rows=''
for dr in DRIVES:
    me=dr.get('media_errors',0); el=dr.get('error_entries',0)
    pcie='—'
    if str(dr.get('pcie_gen','?')) not in ('?','N/A',''):
        pcie=(f'Gen{dr["pcie_gen"]} x{dr["pcie_width"]}<br>'
              f'<span style="color:#8b949e;font-size:11px">~{dr.get("pcie_bw_mbs",0):,} MB/s</span>')
        if dr.get('lspci'): pcie+=f'<br><span style="color:#58a6ff;font-size:11px">{esc(dr["lspci"])}</span>'
    errlog=dr.get('error_log','').strip()
    elog_html=''
    if errlog and '(none)' not in errlog.lower():
        lines=[l for l in errlog.replace('\\\\n','\n').splitlines() if l.strip()]
        rows_html=''.join(f'<div style="font-family:monospace;font-size:12px;padding:2px 0;color:#e6edf3;border-bottom:1px solid #21262d">{esc(l)}</div>' for l in lines)
        elog_html=(f'<tr><td colspan="15" style="padding:0"><details>'
                   f'<summary style="cursor:pointer;padding:8px 14px;background:#1c2128;color:#d29922;font-size:12px;font-weight:600;list-style:none">'
                   f'&#9658; Error Log ({len(lines)} entries &mdash; click to expand)</summary>'
                   f'<div style="padding:10px 14px;background:#161b22;max-height:400px;overflow-y:auto">{rows_html}</div>'
                   f'</details></td></tr>')
    drive_rows+=(f'<tr>'
        f'<td style="font-family:monospace;color:#39d5c3;font-weight:700">{esc(dr["device"])}</td>'
        f'<td>{esc(dr.get("type",""))}</td>'
        f'<td><span style="font-weight:600">{esc(dr.get("model","—"))}</span><br>'
        f'<span style="font-family:monospace;color:#8b949e;font-size:11px">S/N:{esc(dr.get("serial",""))}</span><br>'
        f'<span style="color:#8b949e;font-size:11px">FW:{esc(dr.get("firmware",""))}</span></td>'
        f'<td>{esc(dr.get("size",""))}</td>'
        f'<td>{badge(dr.get("health","?"))}</td>'
        f'<td style="min-width:140px">{bar(dr.get("life_pct"))}<span style="font-size:11px;color:#8b949e">used: {dr.get("pct_used",0)}%</span></td>'
        f'<td>{temp_cell(dr.get("temperature_f"),dr.get("temperature_c"))}</td>'
        f'<td>{"<span style=color:#f85149;font-weight:700>"+str(me)+"</span>" if me else "<span style=color:#3fb950>0</span>"}</td>'
        f'<td>{"<span style=color:#d29922>"+str(el)+"</span>" if el else "0"}</td>'
        f'<td style="font-size:12px;font-family:monospace">W: {esc(dr.get("data_written","N/A"))}<br>R: {esc(dr.get("data_read","N/A"))}</td>'
        f'<td style="font-size:12px">{pcie}</td>'
        f'<td style="min-width:110px">'
        f'<span style="font-family:monospace;font-weight:700;color:#3fb950">{dr.get("seq_r_mb",0):,}</span>'
        f'<span style="color:#8b949e;font-size:11px"> MB/s</span><br>'
        f'<span style="font-family:monospace;font-weight:600;color:#58a6ff">{dr.get("seq_r_iops",0):,}</span>'
        f'<span style="color:#8b949e;font-size:11px"> IOPS</span></td>'
        f'<td style="min-width:110px">'
        f'<span style="font-family:monospace;font-weight:700;color:#3fb950">{dr.get("seq_w_mb",0):,}</span>'
        f'<span style="color:#8b949e;font-size:11px"> MB/s</span><br>'
        f'<span style="font-family:monospace;font-weight:600;color:#58a6ff">{dr.get("seq_w_iops",0):,}</span>'
        f'<span style="color:#8b949e;font-size:11px"> IOPS</span></td>'
        f'<td style="min-width:110px">'
        f'<span style="font-family:monospace;font-weight:700;color:#3fb950">{dr.get("rnd_r_mb",0):,}</span>'
        f'<span style="color:#8b949e;font-size:11px"> MB/s</span><br>'
        f'<span style="font-family:monospace;font-weight:600;color:#58a6ff">{dr.get("rnd_r_iops",0):,}</span>'
        f'<span style="color:#8b949e;font-size:11px"> IOPS</span></td>'
        f'<td style="min-width:110px">'
        f'<span style="font-family:monospace;font-weight:700;color:#3fb950">{dr.get("rnd_w_mb",0):,}</span>'
        f'<span style="color:#8b949e;font-size:11px"> MB/s</span><br>'
        f'<span style="font-family:monospace;font-weight:600;color:#58a6ff">{dr.get("rnd_w_iops",0):,}</span>'
        f'<span style="color:#8b949e;font-size:11px"> IOPS</span></td>'
        f'</tr>{elog_html}')

label=D.get('dedi_label','') or D.get('hostname','Server')
html=f"""<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Shift Hosting LLC — Server Diagnostic</title>
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
body{{background:#0d1117;color:#e6edf3;font-family:system-ui,sans-serif;font-size:14px;padding:24px 32px}}
h1{{color:#58a6ff;font-size:22px;margin-bottom:6px;font-weight:700}}
h2{{color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.8px;margin:28px 0 12px;padding-bottom:6px;border-bottom:1px solid #30363d}}
.card{{background:#161b22;border:1px solid #30363d;border-radius:10px;padding:20px 24px;margin-bottom:20px}}
.lbl{{font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}}
.val{{font-size:14px;font-weight:600}}
table{{width:100%;border-collapse:collapse;background:#161b22;border:1px solid #30363d;border-radius:8px;overflow:hidden;margin-bottom:20px;font-size:13px}}
th{{text-align:left;padding:8px 12px;font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #30363d;background:#21262d;white-space:nowrap}}
td{{padding:9px 12px;border-bottom:1px solid #21262d;vertical-align:middle}}
tr:last-child td{{border-bottom:none}}
tr:hover>td{{background:#1c2128}}
details summary::-webkit-details-marker{{display:none}}
</style></head><body>
<h1>&#128187;&nbsp; Shift Hosting LLC &mdash; Server Diagnostic</h1>
<div style="color:#58a6ff;font-size:15px;font-weight:600;margin-bottom:4px">{esc(label)}</div>
<div style="color:#8b949e;font-size:13px;margin-bottom:20px">{esc(D.get("timestamp_disp",""))} &nbsp;&middot;&nbsp; {esc(D.get("timezone",""))} &nbsp;&middot;&nbsp; Tech: {esc(D.get("technician",""))}</div>
<h2>System</h2><div class="card">{specs_html}</div>
<h2>Memory Slots</h2>
<table><thead><tr><th>Slot</th><th>Size</th><th>Type</th><th>Speed</th><th>Manufacturer</th><th>Part Number</th><th>Rank</th></tr></thead>
<tbody>{ram_rows}</tbody></table>
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:20px">
  <div style="background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px 20px;text-align:center">
    <div class="lbl">RAM Test</div><div style="font-size:22px;font-weight:700;color:{ram_test_c}">{D.get("ram_test_result","—")}</div></div>
  <div style="background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px 20px;text-align:center">
    <div class="lbl">Tested</div><div style="font-size:18px;font-weight:600">{D.get("ram_test_mb",0):,} MB</div></div>
  <div style="background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px 20px;text-align:center">
    <div class="lbl">Errors</div><div style="font-size:22px;font-weight:700;color:{'#f85149' if D.get('ram_test_errors',0)>0 else '#3fb950'}">{D.get("ram_test_errors",0)}</div></div>
</div>
<h2>Network Interfaces</h2>
<table><thead><tr><th>Interface</th><th>MAC</th><th>Speed</th><th>State</th><th>Driver</th><th>Controller</th></tr></thead>
<tbody>{nic_rows}</tbody></table>
<h2>Storage Drives</h2>
<table><thead><tr><th>Device</th><th>Type</th><th>Model / S/N / FW</th><th>Size</th><th>Health</th><th>Life Left / Used</th><th>Temp</th><th>Media Err</th><th>Err Log</th><th>Written / Read</th><th>PCIe</th><th>Seq Read</th><th>Seq Write</th><th>Rand Read</th><th>Rand Write</th></tr></thead>
<tbody>{drive_rows}</tbody></table>
<div style="color:#8b949e;font-size:12px;margin-top:16px;text-align:center">
Server Diagnostic v5.0 &nbsp;&middot;&nbsp; Shift Hosting LLC &nbsp;&middot;&nbsp; <a href="results.json" style="color:#58a6ff">Download raw JSON</a></div>
</body></html>"""

with open('$REPORT_DIR/index.html','w') as f: f.write(html)
report={**D,'drives':DRIVES,'ram_slots':RAM_SLOTS,'nics':NICS}
with open('$REPORT_DIR/results.json','w') as f: json.dump(report,f,indent=2)
print("ok")
PYEOF2

ok "Report generated"

# ── Serve report ──────────────────────────────────────────────
hdr "Serving Report"

check_public_ip() {
    local ip _url
    for _url in \
        "https://api.ipify.org" \
        "https://ifconfig.me/ip" \
        "https://ipv4.icanhazip.com" \
        "https://api4.my-ip.io/ip" \
        "https://checkip.amazonaws.com"; do
        ip=$(curl -s --max-time 8 "$_url" 2>/dev/null | tr -d '[:space:]')
        [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || continue
        [[ "$ip" =~ ^10\.|^192\.168\.|^127\.|^172\.(1[6-9]|2[0-9]|3[01])\. ]] && continue
        echo "$ip"; return 0
    done
    return 1
}

# Get local IP as fallback
LOCAL_IP=$(ip -4 route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
[[ -z "$LOCAL_IP" ]] && LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}')

SRV_PORT=$(( RANDOM % 7000 + 2000 ))
SRV_PID=""

python3 -m http.server "$SRV_PORT" --directory "$REPORT_DIR" --bind 0.0.0.0 \
    &>/tmp/diag_srv.log &
SRV_PID=$!
sleep 1

if ! kill -0 "$SRV_PID" 2>/dev/null; then
    warn "Server failed to start on port $SRV_PORT, trying another..."
    SRV_PORT=$(( RANDOM % 7000 + 2000 ))
    python3 -m http.server "$SRV_PORT" --directory "$REPORT_DIR" --bind 0.0.0.0 \
        &>/tmp/diag_srv.log &
    SRV_PID=$!
    sleep 1
fi

# Save report locally regardless
SAVE_PATH="/tmp/diag_${HOSTNAME_VAL}_${TIMESTAMP//[:T]/_}.json"
cp "$REPORT_DIR/results.json" "$SAVE_PATH" 2>/dev/null
ok "JSON saved to: $SAVE_PATH"

PUBLIC_IP=$(check_public_ip)
DISPLAY_IP="${PUBLIC_IP:-$LOCAL_IP}"
if [[ -n "$SRV_PID" ]] && kill -0 "$SRV_PID" 2>/dev/null; then
    URL="http://${DISPLAY_IP}:${SRV_PORT}/"
    ( sleep 1200; kill "$SRV_PID" 2>/dev/null ) &

    echo ""
    echo -e "${GRN}${BLD}"
    echo "  ╔══════════════════════════════════════════════════════╗"
    printf  "  ║  %-52s║\n" "  $URL"
    if [[ -n "$PUBLIC_IP" ]]; then
        echo "  ║  Report is live on PUBLIC IP — auto-stops in 20 min ║"
    else
        echo "  ║  Report is live on local network (no public IP)     ║"
    fi
    echo "  ╚══════════════════════════════════════════════════════╝"
    echo -e "${RST}"
    info "Server PID $SRV_PID — stop early: kill $SRV_PID"
else
    warn "Web server did not start. Report saved to $SAVE_PATH"
fi

echo ""
echo -e "  ${DIM}Re-run with:  sudo ./server_diag.sh |& tee diag_\$(date +%F).txt${RST}"
echo ""

# Prevent EXIT trap from deleting REPORT_DIR before server serves files
# Server is background — wait for it or user kills it
trap '' EXIT
