nas16-scripts/fundtracker/fundtracker-backup.sh

1013 lines
27 KiB
Bash
Raw Permalink Normal View History

2026-08-20 18:41:02 -04:00
#!/bin/bash
# =============================================================================
# FundTracker Project Backup
# =============================================================================
# Backs up:
# 1. Complete FundTracker project/site files
# 2. FundTracker PostgreSQL database
#
# Creates ONE compressed archive containing both.
#
# Project files:
# /Kingdezignsnas16/fundtracker
#
# PostgreSQL:
# Container: fundtracker_db
# Database: fundtracker
# User: fundtracker
#
# Backup destination:
# /export/kingdezignsnas-16/Public/07 - Projects/NAS16/fundtracker
#
# Retention:
# Keeps the 10 most recent backups.
#
# Email:
# Uses OMV Postfix / Gmail relay via sendmail.
#
# Designed to run as ROOT from:
# OMV -> System -> Scheduled Jobs
#
# Backup destination: /export/kingdezignsnas-16/Public/07 - Projects/NAS16/fundtracker
2026-08-20 18:41:57 -04:00
# Script location: /usr/scripts/fundtracker/fundtracker-backup.sh
2026-08-20 18:41:02 -04:00
# =============================================================================
set -uo pipefail
# -----------------------------------------------------------------------------
# CONFIGURATION
# -----------------------------------------------------------------------------
HOSTNAME_LABEL=$(hostname -s 2>/dev/null || echo "NAS16")
REPORT_TO="rufus.king@kingdezigns.com"
REPORT_FROM="backup-monitor@nas16.local"
# FundTracker project files
PROJECT_DIR="/Kingdezignsnas16/fundtracker"
# PostgreSQL container
DB_CONTAINER="fundtracker_db"
DB_NAME="fundtracker"
DB_USER="fundtracker"
# Backup destination
BACKUP_ROOT="/export/kingdezignsnas-16/Public/07 - Projects/NAS16/fundtracker"
# Temporary working directory
TMP_BASE="/tmp/fundtracker-backup"
# Keep this many completed backups
RETAIN_COUNT=10
# Date/time
DATE=$(date +%Y-%m-%d)
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
REPORT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
TMP_DIR="${TMP_BASE}/fundtracker-${TIMESTAMP}"
LOG_FILE="${BACKUP_ROOT}/backup.log"
2026-08-20 18:41:57 -04:00
# Variable placeholder for version (populated dynamically during run)
SITE_VERSION="unknown"
ARCHIVE_NAME="fundtracker-backup-${TIMESTAMP}.tar.gz"
2026-08-20 18:41:02 -04:00
# -----------------------------------------------------------------------------
# STATUS TRACKING
# -----------------------------------------------------------------------------
OVERALL_STATUS="SUCCESS"
SECTION_ROWS_HTML=""
WARNING_COUNT=0
ERROR_COUNT=0
add_section_row() {
local section="$1"
local status="$2"
local 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"
}
# -----------------------------------------------------------------------------
# INITIALIZE
# -----------------------------------------------------------------------------
mkdir -p "$BACKUP_ROOT"
log_section "FundTracker Backup Starting"
ARCHIVE_SIZE="N/A"
BACKUP_COUNT=0
# -----------------------------------------------------------------------------
# PRE-FLIGHT CHECK 1 — NAS16 STORAGE
# -----------------------------------------------------------------------------
if [ ! -d "/export/kingdezignsnas-16" ]; then
log "ERROR: /export/kingdezignsnas-16 is not mounted or accessible."
add_section_row \
"NAS16 Storage" \
"ERROR" \
"/export/kingdezignsnas-16 is not accessible"
else
log "NAS16 storage is accessible."
add_section_row \
"NAS16 Storage" \
"OK" \
"/export/kingdezignsnas-16 is accessible"
fi
# -----------------------------------------------------------------------------
# PRE-FLIGHT CHECK 2 — FUNDTRACKER PROJECT
# -----------------------------------------------------------------------------
if [ ! -d "$PROJECT_DIR" ]; then
log "ERROR: FundTracker project directory not found:"
log "$PROJECT_DIR"
add_section_row \
"FundTracker Files" \
"ERROR" \
"$PROJECT_DIR does not exist"
else
log "FundTracker project directory found."
add_section_row \
"FundTracker Files" \
"OK" \
"$PROJECT_DIR is accessible"
fi
# -----------------------------------------------------------------------------
# PRE-FLIGHT CHECK 3 — DOCKER
# -----------------------------------------------------------------------------
if ! command -v docker >/dev/null 2>&1; then
log "ERROR: Docker command not found."
add_section_row \
"Docker" \
"ERROR" \
"Docker command is not available"
else
add_section_row \
"Docker" \
"OK" \
"Docker command available"
fi
# -----------------------------------------------------------------------------
# PRE-FLIGHT CHECK 4 — DATABASE CONTAINER
# -----------------------------------------------------------------------------
if docker ps --format '{{.Names}}' 2>/dev/null |
grep -qx "$DB_CONTAINER"; then
log "PostgreSQL container is running: $DB_CONTAINER"
add_section_row \
"PostgreSQL Container" \
"OK" \
"$DB_CONTAINER is running"
else
log "ERROR: PostgreSQL container $DB_CONTAINER is not running."
add_section_row \
"PostgreSQL Container" \
"ERROR" \
"$DB_CONTAINER is not running"
fi
# -----------------------------------------------------------------------------
# STOP HERE IF PRE-FLIGHT FAILED
# -----------------------------------------------------------------------------
if [ "$OVERALL_STATUS" = "FAILED" ]; then
log "Pre-flight checks failed. Backup will not proceed."
2026-08-20 18:41:57 -04:00
FINAL_ARCHIVE="${BACKUP_ROOT}/${ARCHIVE_NAME}"
2026-08-20 18:41:02 -04:00
else
2026-08-20 18:41:57 -04:00
# =========================================================================
# QUERY SITE VERSION FROM DATABASE
# =========================================================================
log_section "Retrieving site version from database"
QUERY_VERSION=$(docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -t -A -c \
"SELECT version_number FROM project_versions WHERE status = 'deployed' ORDER BY deployed_at DESC NULLS LAST LIMIT 1;" 2>/dev/null || true)
QUERY_VERSION="$(echo "${QUERY_VERSION}" | xargs)"
if [ -n "$QUERY_VERSION" ]; then
SITE_VERSION="$QUERY_VERSION"
ARCHIVE_NAME="fundtracker-v${SITE_VERSION}-backup-${TIMESTAMP}.tar.gz"
log "Deployed site version detected: v${SITE_VERSION}"
add_section_row \
"Project Version Check" \
"OK" \
"Detected deployed version v${SITE_VERSION}"
else
log "WARNING: Could not retrieve deployed version from 'project_versions' table. Using default naming."
add_section_row \
"Project Version Check" \
"WARNING" \
"Version query returned empty; falling back to standard naming"
fi
FINAL_ARCHIVE="${BACKUP_ROOT}/${ARCHIVE_NAME}"
2026-08-20 18:41:02 -04:00
# Create temporary working directory
mkdir -p "$TMP_DIR"
# =========================================================================
# SECTION 1 — DATABASE BACKUP
# =========================================================================
log_section "Backing up FundTracker PostgreSQL database"
DB_DIR="${TMP_DIR}/database"
mkdir -p "$DB_DIR"
DB_DUMP="${DB_DIR}/fundtracker.sql"
# pg_dump runs inside the PostgreSQL container.
# No database password is stored in this backup script.
if docker exec "$DB_CONTAINER" \
pg_dump \
-U "$DB_USER" \
"$DB_NAME" \
> "$DB_DUMP" 2>>"$LOG_FILE"; then
log "PostgreSQL dump completed successfully."
# Compress the SQL dump
if gzip "$DB_DUMP"; then
DB_SIZE=$(du -sh "${DB_DUMP}.gz" | cut -f1)
log "Database dump compressed."
log "Database backup size: $DB_SIZE"
add_section_row \
"PostgreSQL Database" \
"OK" \
"${DB_NAME} dumped and compressed · ${DB_SIZE}"
else
log "ERROR: Could not compress PostgreSQL dump."
add_section_row \
"PostgreSQL Database" \
"ERROR" \
"pg_dump succeeded but gzip failed"
fi
else
log "ERROR: PostgreSQL pg_dump failed."
rm -f "$DB_DUMP"
add_section_row \
"PostgreSQL Database" \
"ERROR" \
"pg_dump failed for database ${DB_NAME}"
fi
# =========================================================================
# SECTION 2 — SITE / PROJECT FILES
# =========================================================================
log_section "Backing up FundTracker project files"
SITE_DIR="${TMP_DIR}/fundtracker"
mkdir -p "$SITE_DIR"
if rsync -a "$PROJECT_DIR/" "$SITE_DIR/" 2>&1 |
tee -a "$LOG_FILE"; then
FILE_COUNT=$(find "$SITE_DIR" -type f 2>/dev/null | wc -l)
log "FundTracker project files copied successfully."
log "Files copied: $FILE_COUNT"
add_section_row \
"FundTracker Project Files" \
"OK" \
"${FILE_COUNT} file(s) backed up"
else
log "ERROR: rsync failed while backing up FundTracker files."
add_section_row \
"FundTracker Project Files" \
"ERROR" \
"File backup failed"
fi
# =========================================================================
# SECTION 3 — MANIFEST
# =========================================================================
log_section "Creating backup manifest"
MANIFEST="${TMP_DIR}/MANIFEST.txt"
cat > "$MANIFEST" <<MANIFEST_EOF
=============================================================
FUNDTRACKER BACKUP MANIFEST
=============================================================
Host:
$(hostname)
Backup date:
$(date)
2026-08-20 18:41:57 -04:00
Site Version:
v${SITE_VERSION}
2026-08-20 18:41:02 -04:00
Project source:
${PROJECT_DIR}
Backup destination:
${BACKUP_ROOT}
Archive:
${ARCHIVE_NAME}
=============================================================
FUNDTRACKER PROJECT
=============================================================
Source:
${PROJECT_DIR}
The complete FundTracker project directory is stored under:
fundtracker/
=============================================================
POSTGRESQL DATABASE
=============================================================
Container:
${DB_CONTAINER}
Database:
${DB_NAME}
User:
${DB_USER}
Database backup:
database/fundtracker.sql.gz
=============================================================
RESTORE OVERVIEW
=============================================================
1. Extract this archive.
2. Restore the FundTracker project files to:
${PROJECT_DIR}
3. Start the FundTracker Docker Compose project.
4. Recreate / start the PostgreSQL container.
5. Restore the database using:
docker exec -i fundtracker_db \
psql -U fundtracker fundtracker \
< database/fundtracker.sql
=============================================================
MANIFEST COMPLETE
=============================================================
MANIFEST_EOF
add_section_row \
"Backup Manifest" \
"OK" \
"MANIFEST.txt created"
# =========================================================================
# SECTION 4 — CREATE ARCHIVE
# =========================================================================
log_section "Creating compressed backup archive"
cd "$TMP_BASE" || {
log "ERROR: Could not change to temporary backup directory."
add_section_row \
"Archive Creation" \
"ERROR" \
"Could not access temporary directory"
OVERALL_STATUS="FAILED"
}
if [ "$OVERALL_STATUS" != "FAILED" ]; then
if tar -czf "$FINAL_ARCHIVE" \
"$(basename "$TMP_DIR")" \
2>>"$LOG_FILE"; then
ARCHIVE_SIZE=$(du -sh "$FINAL_ARCHIVE" | cut -f1)
log "Archive created successfully."
log "Archive: $FINAL_ARCHIVE"
log "Size: $ARCHIVE_SIZE"
add_section_row \
"Archive Creation" \
"OK" \
"${ARCHIVE_NAME} · ${ARCHIVE_SIZE}"
else
log "ERROR: tar failed while creating archive."
add_section_row \
"Archive Creation" \
"ERROR" \
"tar failed — archive was not created"
fi
fi
# =========================================================================
# SECTION 5 — VERIFY ARCHIVE
# =========================================================================
if [ -f "$FINAL_ARCHIVE" ]; then
log_section "Verifying backup archive"
if tar -tzf "$FINAL_ARCHIVE" >/dev/null 2>&1; then
log "Archive verification successful."
add_section_row \
"Archive Verification" \
"OK" \
"Archive can be opened and read successfully"
else
log "ERROR: Archive verification failed."
add_section_row \
"Archive Verification" \
"ERROR" \
"tar could not read the completed archive"
fi
else
log "ERROR: Final backup archive does not exist."
add_section_row \
"Archive Verification" \
"ERROR" \
"Final archive was not created"
fi
# =========================================================================
# SECTION 6 — CLEANUP
# =========================================================================
log_section "Cleaning up temporary files"
if rm -rf "$TMP_DIR"; then
log "Temporary backup files removed."
add_section_row \
"Temporary File Cleanup" \
"OK" \
"Temporary files removed"
else
log "WARNING: Could not completely remove temporary directory."
add_section_row \
"Temporary File Cleanup" \
"WARNING" \
"Temporary directory could not be completely removed"
fi
# =========================================================================
# SECTION 7 — ROTATE OLD BACKUPS
# =========================================================================
log_section "Rotating old FundTracker backups"
BACKUP_LIST=$(ls -1t \
2026-08-20 18:41:57 -04:00
"${BACKUP_ROOT}"/fundtracker-*.tar.gz \
2026-08-20 18:41:02 -04:00
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
[ -z "$OLD_BACKUP" ] && continue
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 backup(s)"
else
add_section_row \
"Backup Rotation" \
"OK" \
"${BACKUP_COUNT} / ${RETAIN_COUNT} backups retained"
fi
fi
# =============================================================================
# FINAL STATUS
# =============================================================================
log_section "FundTracker Backup Complete"
2026-08-20 18:41:57 -04:00
log "Site Version: ${SITE_VERSION}"
log "Archive: ${FINAL_ARCHIVE:-N/A}"
log "Size: ${ARCHIVE_SIZE:-N/A}"
log "Status: ${OVERALL_STATUS}"
2026-08-20 18:41:02 -04:00
# =============================================================================
# EMAIL STATUS BANNER
# =============================================================================
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;FundTracker 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;FundTracker 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;FundTracker backup failed — immediate attention required!</span></td></tr>"
;;
esac
# =============================================================================
# ARCHIVE CARD
# =============================================================================
if [ -f "${FINAL_ARCHIVE:-}" ]; 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;\">
🗜️ Backup 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
# =============================================================================
# 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;">
FundTracker 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;">
/usr/scripts/fundtracker/fundtracker-backup.sh
</div>
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">
$REPORT_TIME &nbsp;|&nbsp; $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 -->
<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 CARD -->
$ARCHIVE_CARD
<!-- SUMMARY -->
<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;">
📊 Backup 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>
2026-08-20 18:41:57 -04:00
<tr>
<td style="font-size:13px;color:#6b7280;">Site Version</td>
<td style="font-size:13px;color:#111827;font-weight:600;">v$SITE_VERSION</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
2026-08-20 18:41:02 -04:00
<tr>
<td style="font-size:13px;color:#6b7280;">Project Files</td>
<td style="font-family:monospace;font-size:11px;color:#111827;font-weight:600;">$PROJECT_DIR</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
<tr>
<td style="font-size:13px;color:#6b7280;">Database</td>
<td style="font-size:13px;color:#111827;font-weight:600;">PostgreSQL / $DB_NAME</td>
</tr>
<tr><td colspan="2" style="height:6px;"></td></tr>
<tr>
<td style="font-size:13px;color:#6b7280;">Destination</td>
<td style="font-family:monospace;font-size:11px;color:#111827;font-weight:600;">$BACKUP_ROOT</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:12px;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</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;">$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;">$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;">$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 FundTracker
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
EOF
)
# =============================================================================
# SEND EMAIL
# =============================================================================
SUBJECT="[FundTracker 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 "===== FundTracker backup script finished ====="
# =============================================================================
# EXIT STATUS
# =============================================================================
if [ "$OVERALL_STATUS" = "FAILED" ]; then
exit 1
else
exit 0
fi