nas08-scripts/Backup Scripts/nas08-backup.sh
2026-07-26 21:34:22 -04:00

595 lines
26 KiB
Bash

#!/bin/bash
# =============================================================================
# NAS08 System Backup Script
# =============================================================================
# Backs up OMV config, Docker Compose files, app data, and system /etc
# Writes a single dated .tar.gz archive to the NAS data drive
# Retains the last 10 backups (~30 days), auto-removes older ones
# Sends an HTML email report on completion (via OMV Postfix / Gmail relay).
#
# Schedule via OMV Scheduled Jobs (cron):
# 0 2 */3 * * (2:00 AM every 3 days)
#
# Backup destination: /export/kingdezigns-all/Backups/NAS08/
# Script location: /usr/scripts/omv/nas08-backup.sh
# =============================================================================
set -uo pipefail
# -----------------------------------------------------------------------------
# CONFIGURATION
# -----------------------------------------------------------------------------
HOSTNAME_LABEL=$(hostname -s 2>/dev/null || echo "NAS08")
REPORT_TO="rufus.king@kingdezigns.com"
REPORT_FROM="backup-monitor@nas08.local" # Display only — Postfix uses your Gmail relay
BACKUP_ROOT="/export/kingdezigns-all/Backups/NAS08"
TMP_BASE="/export/kingdezigns-all/Backups/.tmp"
DOCKER_DIR="/export/kingdezigns-all/Docker"
DATE=$(date +%Y-%m-%d)
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
REPORT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
ARCHIVE_NAME="nas08-backup-${DATE}.tar.gz"
TMP_DIR="${TMP_BASE}/nas08-backup-${TIMESTAMP}"
RETAIN_COUNT=10
LOG_FILE="${BACKUP_ROOT}/backup.log"
# -----------------------------------------------------------------------------
# STATUS TRACKING
# OVERALL_STATUS: SUCCESS | WARNING | FAILED
# -----------------------------------------------------------------------------
OVERALL_STATUS="SUCCESS"
SECTION_ROWS_HTML=""
WARNING_COUNT=0
ERROR_COUNT=0
# Append a result row to the section table
# Usage: add_section_row "Section name" "STATUS" "Detail message"
# STATUS: OK | WARNING | ERROR | SKIPPED
add_section_row() {
local section="$1" status="$2" detail="$3"
case "$status" in
OK)
BADGE="<span style=\"background:#d1fae5;color:#065f46;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">✓ OK</span>"
;;
WARNING)
BADGE="<span style=\"background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">⚠ WARNING</span>"
WARNING_COUNT=$((WARNING_COUNT + 1))
[[ "$OVERALL_STATUS" == "SUCCESS" ]] && OVERALL_STATUS="WARNING"
;;
ERROR)
BADGE="<span style=\"background:#fee2e2;color:#991b1b;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">✗ ERROR</span>"
ERROR_COUNT=$((ERROR_COUNT + 1))
OVERALL_STATUS="FAILED"
;;
SKIPPED)
BADGE="<span style=\"background:#f3f4f6;color:#6b7280;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">— SKIPPED</span>"
;;
esac
SECTION_ROWS_HTML+="
<tr>
<td style=\"padding:9px 14px;border-bottom:1px solid #f3f4f6;font-size:13px;color:#111827;font-weight:600;\">$section</td>
<td style=\"padding:9px 14px;border-bottom:1px solid #f3f4f6;\">$BADGE</td>
<td style=\"padding:9px 14px;border-bottom:1px solid #f3f4f6;font-size:12px;color:#6b7280;\">$detail</td>
</tr>"
}
# -----------------------------------------------------------------------------
# LOGGING
# -----------------------------------------------------------------------------
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log_section() {
echo "" | tee -a "$LOG_FILE"
echo "------------------------------------------------------------" | tee -a "$LOG_FILE"
log "$1"
echo "------------------------------------------------------------" | tee -a "$LOG_FILE"
}
# -----------------------------------------------------------------------------
# PRE-FLIGHT CHECKS
# -----------------------------------------------------------------------------
log_section "NAS08 Backup Starting"
if [ ! -d "/export/kingdezigns-all" ]; then
log "ERROR: /export/kingdezigns-all is not mounted or accessible. Aborting."
add_section_row "Pre-flight check" "ERROR" "/export/kingdezigns-all not mounted or accessible"
OVERALL_STATUS="FAILED"
exit 1
fi
mkdir -p "$BACKUP_ROOT"
mkdir -p "$TMP_DIR"
log "Temporary working directory: $TMP_DIR"
# -----------------------------------------------------------------------------
# SECTION 1: OMV CONFIGURATION
# -----------------------------------------------------------------------------
log_section "Backing up OMV configuration"
OMV_DIR="${TMP_DIR}/omv-config"
mkdir -p "$OMV_DIR"
OMV_STATUS="OK"
OMV_DETAIL=""
# omv-confbak is optional — raw config.xml is the primary backup and fully sufficient.
if command -v omv-confbak &>/dev/null; then
if omv-confbak "${OMV_DIR}/omv-config.xml" 2>/dev/null; then
log "omv-confbak completed successfully"
OMV_DETAIL="omv-confbak exported + raw config.xml copied"
else
log "WARNING: omv-confbak failed or returned non-zero — raw config.xml still captured"
OMV_DETAIL="omv-confbak failed (non-fatal) + raw config.xml copied"
fi
else
log "omv-confbak not present — using raw config.xml (this is normal)"
fi
if [ -f /etc/openmediavault/config.xml ]; then
cp /etc/openmediavault/config.xml "${OMV_DIR}/config.xml.raw"
log "Raw OMV config.xml copied"
[[ -z "$OMV_DETAIL" ]] && OMV_DETAIL="raw config.xml copied"
else
log "WARNING: /etc/openmediavault/config.xml not found — OMV config not backed up"
OMV_STATUS="WARNING"
OMV_DETAIL="config.xml not found at /etc/openmediavault/config.xml"
fi
add_section_row "OMV Configuration" "$OMV_STATUS" "$OMV_DETAIL"
# -----------------------------------------------------------------------------
# SECTION 2: SYSTEM /etc
# -----------------------------------------------------------------------------
log_section "Backing up /etc (system configuration)"
ETC_DIR="${TMP_DIR}/etc"
mkdir -p "$ETC_DIR"
if rsync -a --quiet \
--exclude='/etc/mtab' \
--exclude='/etc/fstab.d' \
--exclude='/etc/blkid.tab' \
/etc/ "${ETC_DIR}/" 2>/dev/null; then
log "/etc backed up successfully"
add_section_row "System /etc" "OK" "rsync completed successfully"
else
log "WARNING: /etc rsync encountered errors"
add_section_row "System /etc" "WARNING" "rsync completed with errors"
fi
# -----------------------------------------------------------------------------
# SECTION 3: DOCKER COMPOSE FILES & GLOBAL ENV
# -----------------------------------------------------------------------------
log_section "Backing up Docker Compose files"
COMPOSE_DIR="${TMP_DIR}/docker-compose"
mkdir -p "$COMPOSE_DIR"
if [ -d "${DOCKER_DIR}/Compose" ]; then
if rsync -a --quiet \
--exclude='vaultwarden/icon_cache/' \
"${DOCKER_DIR}/Compose/" "${COMPOSE_DIR}/" 2>/dev/null; then
log "Docker Compose directory backed up (Vaultwarden icon_cache excluded)"
# Count compose files for the detail line
COMPOSE_COUNT=$(find "${COMPOSE_DIR}" -name "docker-compose.yml" -o -name "compose.yml" 2>/dev/null | wc -l)
add_section_row "Docker Compose Files" "OK" "${COMPOSE_COUNT} compose file(s) · vaultwarden/icon_cache excluded"
else
log "WARNING: Docker Compose rsync encountered errors"
add_section_row "Docker Compose Files" "WARNING" "rsync completed with errors"
fi
else
log "WARNING: ${DOCKER_DIR}/Compose not found — skipping"
add_section_row "Docker Compose Files" "WARNING" "${DOCKER_DIR}/Compose not found"
fi
# -----------------------------------------------------------------------------
# SECTION 4: APP DATA (Pi-hole, Plex, Shared)
# -----------------------------------------------------------------------------
log_section "Backing up application data"
APPDATA_DIR="${TMP_DIR}/app-data"
mkdir -p "$APPDATA_DIR"
APPDATA_WARN=0
APPDATA_DETAIL=""
# Pi-hole
if [ -d "${DOCKER_DIR}/pihole" ]; then
if rsync -a --quiet "${DOCKER_DIR}/pihole/" "${APPDATA_DIR}/pihole/" 2>/dev/null; then
log "Pi-hole data backed up"
APPDATA_DETAIL="${APPDATA_DETAIL} Pi-hole ✓"
else
log "WARNING: Pi-hole rsync encountered errors"
APPDATA_DETAIL="${APPDATA_DETAIL} Pi-hole ⚠"
APPDATA_WARN=$((APPDATA_WARN + 1))
fi
else
log "WARNING: ${DOCKER_DIR}/pihole not found — skipping"
APPDATA_DETAIL="${APPDATA_DETAIL} Pi-hole —"
fi
# Plex (Cache, Logs, Crash Reports excluded)
if [ -d "${DOCKER_DIR}/plex" ]; then
if rsync -a --quiet \
--exclude='Plex Media Server/Cache/' \
--exclude='Plex Media Server/Logs/' \
--exclude='Plex Media Server/Crash Reports/' \
"${DOCKER_DIR}/plex/" "${APPDATA_DIR}/plex/" 2>/dev/null; then
log "Plex data backed up (Cache, Logs, Crash Reports excluded)"
APPDATA_DETAIL="${APPDATA_DETAIL} · Plex ✓"
else
log "WARNING: Plex rsync encountered errors"
APPDATA_DETAIL="${APPDATA_DETAIL} · Plex ⚠"
APPDATA_WARN=$((APPDATA_WARN + 1))
fi
else
log "WARNING: ${DOCKER_DIR}/plex not found — skipping"
APPDATA_DETAIL="${APPDATA_DETAIL} · Plex —"
fi
# Shared folder
if [ -d "${DOCKER_DIR}/Shared" ]; then
if rsync -a --quiet "${DOCKER_DIR}/Shared/" "${APPDATA_DIR}/shared/" 2>/dev/null; then
log "Shared folder backed up"
APPDATA_DETAIL="${APPDATA_DETAIL} · Shared ✓"
else
log "WARNING: Shared rsync encountered errors"
APPDATA_DETAIL="${APPDATA_DETAIL} · Shared ⚠"
APPDATA_WARN=$((APPDATA_WARN + 1))
fi
else
log "WARNING: ${DOCKER_DIR}/Shared not found — skipping"
APPDATA_DETAIL="${APPDATA_DETAIL} · Shared —"
fi
if [ "$APPDATA_WARN" -eq 0 ]; then
add_section_row "App Data (Pi-hole / Plex / Shared)" "OK" "${APPDATA_DETAIL# }"
else
add_section_row "App Data (Pi-hole / Plex / Shared)" "WARNING" "$APPDATA_WARN item(s) missing or failed ·${APPDATA_DETAIL}"
fi
# -----------------------------------------------------------------------------
# SECTION 5: CRONTAB & SCHEDULED JOBS
# -----------------------------------------------------------------------------
log_section "Backing up crontab and scheduled jobs"
CRON_DIR="${TMP_DIR}/cron"
mkdir -p "$CRON_DIR"
CRON_WARN=0
crontab -l > "${CRON_DIR}/root-crontab.txt" 2>/dev/null \
&& log "Root crontab saved" \
|| { log "No root crontab found"; CRON_WARN=$((CRON_WARN + 1)); }
if [ -d /var/spool/cron/crontabs ]; then
cp -r /var/spool/cron/crontabs/ "${CRON_DIR}/crontabs/" \
&& log "All crontabs copied" \
|| { log "WARNING: crontabs copy failed"; CRON_WARN=$((CRON_WARN + 1)); }
fi
if [ -d /etc/cron.d ]; then
cp -r /etc/cron.d/ "${CRON_DIR}/cron.d/" \
&& log "cron.d copied" \
|| { log "WARNING: cron.d copy failed"; CRON_WARN=$((CRON_WARN + 1)); }
fi
if [ "$CRON_WARN" -eq 0 ]; then
add_section_row "Crontab / Scheduled Jobs" "OK" "root crontab, crontabs/, cron.d/ saved"
else
add_section_row "Crontab / Scheduled Jobs" "WARNING" "$CRON_WARN item(s) missing or failed to copy"
fi
# -----------------------------------------------------------------------------
# SECTION 6: BACKUP MANIFEST
# -----------------------------------------------------------------------------
log_section "Generating backup manifest"
OMV_VER=$(dpkg -l openmediavault 2>/dev/null | awk '/^ii/{print $3}' || echo "Unknown")
KERNEL_VER=$(uname -r)
HOST_IP=$(hostname -I | awk '{print $1}')
DOCKER_VER=$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ',' || echo "Unknown")
MANIFEST="${TMP_DIR}/MANIFEST.txt"
cat > "$MANIFEST" <<MANIFEST_EOF
=============================================================
NAS08 BACKUP MANIFEST
=============================================================
Hostname: $(hostname)
Date: $(date)
Kernel: ${KERNEL_VER}
OMV Version: ${OMV_VER}
Docker: ${DOCKER_VER}
IP Address: ${HOST_IP}
CONTENTS:
omv-config/ - OMV exported config + raw config.xml
etc/ - Full /etc system configuration
docker-compose/ - All Docker Compose files and global.env
app-data/ - Pi-hole, Plex, Shared app data
cron/ - Root crontab and scheduled jobs
MANIFEST.txt - This file
RESTORE NOTES:
1. Flash fresh OMV to SD card
2. SSH in and restore /etc from etc/ folder
3. Import OMV config via omv-confbak or config.xml.raw
4. Restore Docker Compose files to /export/kingdezigns-all/Docker/Compose/
5. Restore app data to /export/kingdezigns-all/Docker/
6. Recreate containers: docker compose up -d (in each compose folder)
7. Restore crontab: crontab cron/root-crontab.txt
=============================================================
MANIFEST_EOF
log "Manifest written"
add_section_row "Backup Manifest" "OK" "MANIFEST.txt written to archive"
# -----------------------------------------------------------------------------
# SECTION 7: CREATE ARCHIVE
# -----------------------------------------------------------------------------
log_section "Creating compressed archive"
cd "$TMP_BASE"
if tar -czf "${BACKUP_ROOT}/${ARCHIVE_NAME}" -C "$TMP_BASE" "$(basename "$TMP_DIR")" 2>/dev/null; then
log "Archive created: ${BACKUP_ROOT}/${ARCHIVE_NAME}"
ARCHIVE_SIZE=$(du -sh "${BACKUP_ROOT}/${ARCHIVE_NAME}" | cut -f1)
log "Archive size: ${ARCHIVE_SIZE}"
add_section_row "Archive Creation" "OK" "${ARCHIVE_NAME} · ${ARCHIVE_SIZE}"
else
log "ERROR: Archive creation failed"
ARCHIVE_SIZE="N/A"
add_section_row "Archive Creation" "ERROR" "tar failed — archive not created"
rm -rf "$TMP_DIR"
OVERALL_STATUS="FAILED"
fi
# -----------------------------------------------------------------------------
# SECTION 8: CLEANUP TEMP FILES
# -----------------------------------------------------------------------------
log_section "Cleaning up temporary files"
if rm -rf "$TMP_DIR" 2>/dev/null; then
log "Temporary directory removed"
else
log "WARNING: Failed to remove temp directory: $TMP_DIR"
fi
rmdir "$TMP_BASE" 2>/dev/null && log ".tmp directory removed (empty)" || true
# -----------------------------------------------------------------------------
# SECTION 9: ROTATE OLD BACKUPS
# -----------------------------------------------------------------------------
log_section "Rotating old backups (keeping last ${RETAIN_COUNT})"
BACKUP_LIST=$(ls -1t "${BACKUP_ROOT}"/nas08-backup-*.tar.gz 2>/dev/null || true)
BACKUP_COUNT=$(echo "$BACKUP_LIST" | grep -c . || true)
DELETED_COUNT=0
if [ "$BACKUP_COUNT" -gt "$RETAIN_COUNT" ]; then
DELETE_LIST=$(echo "$BACKUP_LIST" | tail -n +$((RETAIN_COUNT + 1)))
while IFS= read -r OLD_BACKUP; do
rm -f "$OLD_BACKUP"
log "Removed old backup: $(basename "$OLD_BACKUP")"
DELETED_COUNT=$((DELETED_COUNT + 1))
done <<< "$DELETE_LIST"
add_section_row "Backup Rotation" "OK" "Kept $RETAIN_COUNT most recent · removed $DELETED_COUNT old archive(s)"
else
log "Backup count (${BACKUP_COUNT}) within retention limit — no rotation needed"
add_section_row "Backup Rotation" "OK" "$BACKUP_COUNT / $RETAIN_COUNT slots used — no rotation needed"
fi
# -----------------------------------------------------------------------------
# DONE — log summary
# -----------------------------------------------------------------------------
log_section "Backup Complete"
log "Archive: ${BACKUP_ROOT}/${ARCHIVE_NAME}"
log "Size: ${ARCHIVE_SIZE:-N/A}"
log "Status: ${OVERALL_STATUS}"
log ""
# -----------------------------------------------------------------------------
# BUILD & SEND HTML EMAIL (pattern: nas08_zfs_scrub.sh)
# -----------------------------------------------------------------------------
# Overall banner & badge
case "$OVERALL_STATUS" in
SUCCESS)
BADGE_HTML="<div style=\"background:#1a7f4b;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;\">✅ SUCCESS</div>"
BANNER_HTML="<tr><td style=\"background:#1a7f4b;padding:12px 32px;\"><span style=\"color:white;font-size:14px;font-weight:600;\">✅ &nbsp;Backup completed successfully — no action required.</span></td></tr>"
;;
WARNING)
BADGE_HTML="<div style=\"background:#b45309;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;\">⚠️ WARNING</div>"
BANNER_HTML="<tr><td style=\"background:#b45309;padding:12px 32px;\"><span style=\"color:white;font-size:14px;font-weight:600;\">⚠️ &nbsp;Backup completed with warnings — review recommended.</span></td></tr>"
;;
FAILED)
BADGE_HTML="<div style=\"background:#b91c1c;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;\">🚨 FAILED</div>"
BANNER_HTML="<tr><td style=\"background:#b91c1c;padding:12px 32px;\"><span style=\"color:white;font-size:14px;font-weight:600;\">🚨 &nbsp;Backup failed — immediate attention required!</span></td></tr>"
;;
esac
# Archive info card — only shown when archive was created
if [[ "${ARCHIVE_SIZE:-N/A}" != "N/A" ]]; then
ARCHIVE_CARD="
<tr><td style=\"padding:0 24px 8px;\">
<div style=\"background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;\">
<div style=\"font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;\">🗜️ Archive</div>
<table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">
<tr>
<td style=\"width:50%;padding:8px 12px;background:#f9fafb;border-radius:6px;text-align:center;\">
<div style=\"font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;\">Filename</div>
<div style=\"font-family:monospace;font-size:13px;font-weight:600;color:#111827;margin-top:4px;\">$ARCHIVE_NAME</div>
</td>
<td style=\"width:4%;\"></td>
<td style=\"width:20%;padding:8px 12px;background:#f9fafb;border-radius:6px;text-align:center;\">
<div style=\"font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;\">Size</div>
<div style=\"font-size:20px;font-weight:700;color:#111827;margin-top:2px;\">$ARCHIVE_SIZE</div>
</td>
<td style=\"width:4%;\"></td>
<td style=\"width:20%;padding:8px 12px;background:#f9fafb;border-radius:6px;text-align:center;\">
<div style=\"font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;\">Retained</div>
<div style=\"font-size:20px;font-weight:700;color:#111827;margin-top:2px;\">$BACKUP_COUNT / $RETAIN_COUNT</div>
</td>
</tr>
</table>
</div>
</td></tr>"
else
ARCHIVE_CARD=""
fi
# System versions card
VERSIONS_CARD="
<tr><td style=\"padding:0 24px 8px;\">
<div style=\"background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;\">
<div style=\"font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;\">🖥️ System Versions</div>
<table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">
<tr>
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">OMV</td>
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${OMV_VER}</td>
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">Kernel</td>
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${KERNEL_VER}</td>
</tr>
<tr><td colspan=\"4\" style=\"height:4px;\"></td></tr>
<tr>
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">Docker</td>
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${DOCKER_VER}</td>
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">IP Address</td>
<td style=\"font-size:13px;color:#111827;font-weight:600;font-family:monospace;\">${HOST_IP}</td>
</tr>
</table>
</div>
</td></tr>"
# Compose full HTML email
HTML_BODY=$(cat <<EOF
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
<table width="100%" cellspacing="0" cellpadding="0" style="background:#f1f5f9;padding:30px 0;">
<tr><td align="center">
<table width="680" cellspacing="0" cellpadding="0" style="max-width:680px;width:100%;">
<!-- Header -->
<tr><td style="background:#0f172a;border-radius:10px 10px 0 0;padding:28px 32px;">
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
<td>
<div style="font-size:11px;color:#94a3b8;letter-spacing:1px;text-transform:uppercase;">System Backup Monitor</div>
<div style="font-size:26px;font-weight:800;color:white;margin-top:4px;">💾 $HOSTNAME_LABEL</div>
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">$REPORT_TIME &nbsp;|&nbsp; 7 sections · $WARNING_COUNT warning(s) · $ERROR_COUNT error(s)</div>
</td>
<td align="right">$BADGE_HTML</td>
</tr>
</table>
</td></tr>
<!-- Status banner -->
$BANNER_HTML
<!-- Section results table -->
<tr><td style="padding:28px 24px 8px;">
<div style="background:white;border-radius:10px;box-shadow:0 1px 6px rgba(0,0,0,.10);overflow:hidden;border:1px solid #e5e7eb;">
<div style="background:#f8fafc;padding:14px 22px;border-bottom:1px solid #e5e7eb;">
<span style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.5px;">📋 Backup Section Results</span>
</div>
<table width="100%" cellspacing="0" cellpadding="0">
<tr style="background:#f9fafb;">
<th style="text-align:left;padding:8px 14px;font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid #e5e7eb;width:30%;">Section</th>
<th style="text-align:left;padding:8px 14px;font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid #e5e7eb;width:15%;">Status</th>
<th style="text-align:left;padding:8px 14px;font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid #e5e7eb;">Detail</th>
</tr>
$SECTION_ROWS_HTML
</table>
</div>
</td></tr>
<!-- Archive info card -->
$ARCHIVE_CARD
<!-- System versions card -->
$VERSIONS_CARD
<!-- Run summary card -->
<tr><td style="padding:0 24px 28px;">
<div style="background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;">
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:10px;text-transform:uppercase;letter-spacing:.5px;">📊 Run Summary</div>
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
<td style="font-size:13px;color:#6b7280;">Host</td>
<td style="font-size:13px;color:#111827;font-weight:600;">$HOSTNAME_LABEL</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
<tr>
<td style="font-size:13px;color:#6b7280;">Run Time</td>
<td style="font-size:13px;color:#111827;font-weight:600;">$REPORT_TIME</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
<tr>
<td style="font-size:13px;color:#6b7280;">Archive</td>
<td style="font-family:monospace;font-size:13px;color:#111827;font-weight:600;">${ARCHIVE_NAME}</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
<tr>
<td style="font-size:13px;color:#6b7280;">Archive Size</td>
<td style="font-size:13px;color:#111827;font-weight:600;">${ARCHIVE_SIZE:-N/A}</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
<tr>
<td style="font-size:13px;color:#6b7280;">Backups on Disk</td>
<td style="font-size:13px;color:#111827;font-weight:600;">$BACKUP_COUNT of $RETAIN_COUNT max retained</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
<tr>
<td style="font-size:13px;color:#6b7280;">Warnings</td>
<td style="font-size:13px;font-weight:700;color:$([ "$WARNING_COUNT" -gt 0 ] && echo "#b45309" || echo "#1a7f4b");">$WARNING_COUNT</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
<tr>
<td style="font-size:13px;color:#6b7280;">Errors</td>
<td style="font-size:13px;font-weight:700;color:$([ "$ERROR_COUNT" -gt 0 ] && echo "#b91c1c" || echo "#1a7f4b");">$ERROR_COUNT</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
<tr>
<td style="font-size:13px;color:#6b7280;">Overall Status</td>
<td style="font-size:13px;font-weight:700;color:$(
case "$OVERALL_STATUS" in
SUCCESS) echo "#1a7f4b" ;;
WARNING) echo "#b45309" ;;
*) echo "#b91c1c" ;;
esac
);">$OVERALL_STATUS</td>
</tr>
</table>
</div>
</td></tr>
<!-- Footer -->
<tr><td style="background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;">
<span style="font-size:11px;color:#64748b;">Automated Backup Monitor &nbsp;|&nbsp; $HOSTNAME_LABEL &nbsp;|&nbsp; KingDezigns Infrastructure</span>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
EOF
)
# Send email via OMV Postfix (Gmail relay)
SUBJECT="[Backup] $HOSTNAME_LABEL$OVERALL_STATUS | $REPORT_TIME"
{
echo "To: $REPORT_TO"
echo "From: $REPORT_FROM"
echo "Subject: $SUBJECT"
echo "MIME-Version: 1.0"
echo "Content-Type: text/html; charset=UTF-8"
echo ""
echo "$HTML_BODY"
} | sendmail -t
log "Email sent to $REPORT_TO — Overall status: $OVERALL_STATUS"
log "===== NAS08 Backup script finished ====="