2026-07-26 21:35:57 -04:00
#!/bin/bash
# =============================================================
# PLEX32 — Daily Update Check & Notification
# KingDezigns Infrastructure
# Schedule: Daily at 7:00 AM
# Location: /usr/scripts/plex32/plex32_update_check.sh
# =============================================================
2026-08-04 16:25:36 -04:00
# Explicit PATH for root's cron environment (minimal default PATH
# under cron doesn't reliably match the interactive shell PATH).
export PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
2026-07-26 21:35:57 -04:00
HOSTNAME = "plex32"
SMTP_SERVER = "smtppro.zoho.com"
SMTP_PORT = "465"
SMTP_USER = "rufus.king@kingdezigns.com"
SMTP_PASS = "Valerie8545"
EMAIL_TO = "rufus.king@kingdezigns.com"
EMAIL_FROM = "rufus.king@kingdezigns.com"
TIMESTAMP = $( date "+%A, %B %d, %Y at %I:%M %p" )
DATE_SHORT = $( date "+%Y-%m-%d" )
LOG_DIR = "/var/log/plex32-updates"
LOG_FILE = " $LOG_DIR /update-check- $DATE_SHORT .log "
mkdir -p " $LOG_DIR "
exec > >( tee -a " $LOG_FILE " ) 2>& 1
echo " [ $TIMESTAMP ] Starting update check... "
# =============================================================
# STEP 1 — CHECK OS/APT UPDATES
# =============================================================
echo "[INFO] Checking apt updates..."
apt_output = $( apt-get update -qq 2>& 1)
apt_check_status = $?
if [ $apt_check_status -ne 0 ] ; then
APT_FAILED = true
APT_UPDATES = 0
APT_DETAILS = " apt-get update failed:\n $apt_output "
else
APT_FAILED = false
apt_list = $( apt list --upgradable 2>/dev/null | grep -v "Listing..." | grep -v " ^ $" )
APT_UPDATES = $( echo " $apt_list " | grep -c "/" || true )
APT_DETAILS = " $apt_list "
fi
echo " [INFO] APT updates available: $APT_UPDATES "
# =============================================================
# STEP 2 — CHECK PLEX DOCKER IMAGE UPDATE
# =============================================================
echo "[INFO] Checking Plex Docker image..."
PLEX_FAILED = false
PLEX_UPDATE_AVAILABLE = false
PLEX_ERROR_HTML = ""
2026-08-04 16:25:36 -04:00
# lscr.io occasionally returns "error from registry: unauthorized" on the
# final manifest step of a large multi-layer pull, even after every layer
# downloaded successfully — a transient token/rate-limit issue on their end,
# not a credentials problem. Retry a few times before treating it as a real
# failure, since it usually resolves within one or two attempts.
PLEX_PULL_MAX_RETRIES = 3
PLEX_PULL_RETRY_DELAY = 30
plex_pull_attempt = 1
PLEX_PULL_STATUS = 1
PLEX_PULL_OUTPUT = ""
while [ $plex_pull_attempt -le $PLEX_PULL_MAX_RETRIES ] ; do
PLEX_PULL_OUTPUT = $( docker pull lscr.io/linuxserver/plex:latest 2>& 1)
PLEX_PULL_STATUS = $?
if [ $PLEX_PULL_STATUS -eq 0 ] ; then
break
fi
echo " [WARN] docker pull attempt $plex_pull_attempt / $PLEX_PULL_MAX_RETRIES failed (exit $PLEX_PULL_STATUS ). "
if [ $plex_pull_attempt -lt $PLEX_PULL_MAX_RETRIES ] ; then
echo " [WARN] Retrying in ${ PLEX_PULL_RETRY_DELAY } s... "
sleep $PLEX_PULL_RETRY_DELAY
fi
plex_pull_attempt = $(( plex_pull_attempt + 1 ))
done
2026-07-26 21:35:57 -04:00
PLEX_IMAGE_VERSION = $( docker inspect lscr.io/linuxserver/plex:latest --format '{{index .Config.Labels "build_version"}}' 2>/dev/null || echo "unknown" )
CURRENT_IMAGE_DATE = $( docker inspect --format= '{{.Created}}' lscr.io/linuxserver/plex:latest 2>/dev/null | cut -d'T' -f1)
if [ $PLEX_PULL_STATUS -ne 0 ] ; then
PLEX_FAILED = true
2026-08-04 16:25:36 -04:00
PLEX_STATUS_TEXT = " Check failed after $PLEX_PULL_MAX_RETRIES attempts — see error detail below "
echo " [ERROR] docker pull failed after $PLEX_PULL_MAX_RETRIES attempts. Final output: "
2026-07-26 21:35:57 -04:00
echo " $PLEX_PULL_OUTPUT "
2026-08-04 16:25:36 -04:00
PLEX_ERROR_HTML = " <div style='margin-top:8px;padding:10px 14px;background:#fff1f2;border-left:4px solid #b91c1c;border-radius:4px;'><div style='font-size:11px;font-weight:700;color:#b91c1c;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;'>⚠ Docker Pull Error (exit $PLEX_PULL_STATUS , after $PLEX_PULL_MAX_RETRIES attempts)</div><pre style='margin:0;font-family:monospace;font-size:12px;color:#7f1d1d;white-space:pre-wrap;'> $( echo " $PLEX_PULL_OUTPUT " | sed 's/&/\&/g; s/</\</g; s/>/\>/g' ) </pre></div> "
2026-07-26 21:35:57 -04:00
elif echo " $PLEX_PULL_OUTPUT " | grep -q "Pull complete" ; then
PLEX_UPDATE_AVAILABLE = true
PLEX_STATUS_TEXT = "Update available — new image pulled"
2026-08-04 16:25:36 -04:00
if [ $plex_pull_attempt -gt 1 ] ; then
PLEX_STATUS_TEXT = " $PLEX_STATUS_TEXT (succeeded on retry, attempt $plex_pull_attempt ) "
fi
2026-07-26 21:35:57 -04:00
else
PLEX_STATUS_TEXT = "Up to date"
fi
echo " [INFO] Plex status: $PLEX_STATUS_TEXT "
2026-07-28 21:07:42 -04:00
# =============================================================
# STEP 2B — VERIFY RUNNING CONTAINER (independent of pull result)
# =============================================================
echo "[INFO] Checking actual running Plex container..."
CONTAINER_STATUS = $( docker inspect -f '{{.State.Status}}' plex 2>/dev/null)
CONTAINER_STARTED = $( docker inspect -f '{{.State.StartedAt}}' plex 2>/dev/null | cut -d'.' -f1 | tr 'T' ' ' )
CONTAINER_HEALTH = $( docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}n/a{{end}}' plex 2>/dev/null)
if [ " $CONTAINER_STATUS " = "running" ] ; then
CONTAINER_OK = true
CONTAINER_STATUS_TEXT = " Running since $CONTAINER_STARTED UTC "
else
CONTAINER_OK = false
CONTAINER_STATUS_TEXT = " NOT running (status: ${ CONTAINER_STATUS :- unknown } ) "
fi
echo " [INFO] Plex container status: $CONTAINER_STATUS_TEXT "
2026-07-26 21:35:57 -04:00
# =============================================================
# STEP 3 — CHECK FAIL2BAN VERSION
# =============================================================
echo "[INFO] Checking Fail2Ban..."
F2B_UPDATE_AVAILABLE = false
F2B_CANDIDATE = $( apt-cache policy fail2ban 2>/dev/null | grep Candidate | awk '{print $2}' | cut -d'-' -f1)
F2B_INSTALLED = $( apt-cache policy fail2ban 2>/dev/null | grep Installed | awk '{print $2}' | cut -d'-' -f1)
if [ " $F2B_CANDIDATE " != " $F2B_INSTALLED " ] && [ -n " $F2B_CANDIDATE " ] ; then
F2B_UPDATE_AVAILABLE = true
F2B_STATUS_TEXT = " Update available: $F2B_INSTALLED → $F2B_CANDIDATE "
else
F2B_STATUS_TEXT = " Up to date ( $F2B_INSTALLED ) "
fi
echo " [INFO] Fail2Ban status: $F2B_STATUS_TEXT "
# =============================================================
# STEP 4 — CHECK DOCKER ENGINE UPDATE
# =============================================================
echo "[INFO] Checking Docker..."
DOCKER_UPDATE_AVAILABLE = false
DOCKER_CANDIDATE = $( apt-cache policy docker-ce 2>/dev/null | grep Candidate | awk '{print $2}' )
DOCKER_INSTALLED = $( apt-cache policy docker-ce 2>/dev/null | grep Installed | awk '{print $2}' )
if [ " $DOCKER_CANDIDATE " != " $DOCKER_INSTALLED " ] && [ -n " $DOCKER_CANDIDATE " ] ; then
DOCKER_UPDATE_AVAILABLE = true
DOCKER_STATUS_TEXT = " Update available: $DOCKER_INSTALLED → $DOCKER_CANDIDATE "
else
DOCKER_STATUS_TEXT = " Up to date ( $DOCKER_INSTALLED ) "
fi
echo " [INFO] Docker status: $DOCKER_STATUS_TEXT "
# =============================================================
# STEP 5 — DETERMINE OVERALL STATUS
# =============================================================
ANY_FAILED = false
ANY_UPDATES = false
[ " $APT_FAILED " = true ] && ANY_FAILED = true
[ " $PLEX_FAILED " = true ] && ANY_FAILED = true
2026-07-28 21:07:42 -04:00
[ " $CONTAINER_OK " = false ] && ANY_FAILED = true
2026-07-26 21:35:57 -04:00
[ " $APT_UPDATES " -gt 0 ] && ANY_UPDATES = true
[ " $PLEX_UPDATE_AVAILABLE " = true ] && ANY_UPDATES = true
[ " $F2B_UPDATE_AVAILABLE " = true ] && ANY_UPDATES = true
[ " $DOCKER_UPDATE_AVAILABLE " = true ] && ANY_UPDATES = true
2026-07-28 21:07:42 -04:00
# A failed registry pull (rate-limit, transient network blip, etc.) is not the
# same severity as the actual Plex container being down. If the pull check
# failed but the running container is confirmed healthy, badge it as a lower
# severity "registry check failed" rather than a full CHECK FAILED/critical state.
if [ " $CONTAINER_OK " = false ] ; then
STATUS_COLOR = "#b91c1c"
STATUS_BADGE = "🔴 CONTAINER DOWN"
STATUS_BANNER_MSG = "🔴 Plex container is NOT running — investigate immediately"
SUBJECT = " 🔴 PLEX32 — Plex Container Down — $DATE_SHORT "
elif [ " $ANY_FAILED " = true ] ; then
2026-07-26 21:35:57 -04:00
STATUS_COLOR = "#b45309"
STATUS_BADGE = "⚠ CHECK FAILED"
2026-07-28 21:07:42 -04:00
STATUS_BANNER_MSG = "⚠ Registry/update check failed, but Plex container itself is confirmed running — no action required unless this repeats daily"
2026-07-26 21:35:57 -04:00
SUBJECT = " ⚠ PLEX32 — Update Check Failed — $DATE_SHORT "
elif [ " $ANY_UPDATES " = true ] ; then
STATUS_COLOR = "#1d6fa8"
STATUS_BADGE = "🔄 UPDATES AVAILABLE"
STATUS_BANNER_MSG = "🔄 Updates are available for one or more components"
SUBJECT = " 🔄 PLEX32 — Updates Available — $DATE_SHORT "
else
STATUS_COLOR = "#1a7f4b"
STATUS_BADGE = "✅ ALL CURRENT"
STATUS_BANNER_MSG = "✅ All components are up to date — no action required"
SUBJECT = " ✅ PLEX32 — All Systems Current — $DATE_SHORT "
fi
# =============================================================
# STEP 6 — BUILD COMMAND BLOCKS (click to select)
# =============================================================
make_cmd_block( ) {
local step_num = " $1 "
local step_label = " $2 "
local cmd = " $3 "
local cmd_html = $( echo " $cmd " | sed 's/&/\&/g; s/</\</g; s/>/\>/g' )
echo " <div style='margin-bottom:16px;'>
<div style = 'font-size:12px;font-weight:600;color:#374151;margin-bottom:4px;' >$step_num . $step_label </div>
<div style = 'font-size:11px;color:#6b7280;margin-bottom:4px;' >👆 Click inside to select all — then Ctrl+C</div>
<pre style = 'margin:0;background:#1e293b;color:#e2e8f0;padding:14px;border-radius:6px;font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-all;cursor:pointer;user-select:all;-webkit-user-select:all;-moz-user-select:all;' >$cmd_html </pre>
</div>"
}
UPDATE_COMMANDS_HTML = ""
if [ " $ANY_UPDATES " = true ] ; then
UPDATE_COMMANDS_HTML = "<div style='margin-top:24px;'><div style='font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;'>🛠 Commands to Apply Updates (run in order)</div>"
STEP = 1
if [ " $APT_UPDATES " -gt 0 ] ; then
CMD = "sudo apt update && sudo apt upgrade -y"
UPDATE_COMMANDS_HTML += $( make_cmd_block " $STEP " "OS & Package Updates" " $CMD " )
STEP = $(( STEP+1))
fi
if [ " $PLEX_UPDATE_AVAILABLE " = true ] ; then
CMD = " cd ~/docker/plex
docker compose up -d"
UPDATE_COMMANDS_HTML += $( make_cmd_block " $STEP " "Plex Media Server (Docker)" " $CMD " )
STEP = $(( STEP+1))
fi
if [ " $DOCKER_UPDATE_AVAILABLE " = true ] ; then
CMD = " sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
docker --version"
UPDATE_COMMANDS_HTML += $( make_cmd_block " $STEP " "Docker Engine" " $CMD " )
STEP = $(( STEP+1))
fi
if [ " $F2B_UPDATE_AVAILABLE " = true ] ; then
CMD = " sudo apt update && sudo apt install -y fail2ban
sudo systemctl restart fail2ban
sudo systemctl status fail2ban"
UPDATE_COMMANDS_HTML += $( make_cmd_block " $STEP " "Fail2Ban" " $CMD " )
fi
UPDATE_COMMANDS_HTML += "</div>"
fi
# =============================================================
# STEP 7 — BUILD APT PACKAGES TABLE
# =============================================================
APT_PACKAGES_HTML = ""
if [ " $APT_UPDATES " -gt 0 ] && [ " $APT_FAILED " = false ] ; then
APT_PACKAGES_HTML = ' <div style = "margin-top:24px;" >
<div style = "font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;" >📦 Packages Awaiting Update</div>
<table width = "100%" cellspacing = "0" cellpadding = "0" style = "background:white;border-radius:8px;border:1px solid #e5e7eb;overflow:hidden;" >
<tr style = "background:#f8fafc;" >
<th style = "text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;" >Package</th>
<th style = "text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;" >Available Version</th>
</tr>'
while IFS = read -r line; do
[ -z " $line " ] && continue
pkg = $( echo " $line " | awk '{print $1}' | cut -d'/' -f1)
ver = $( echo " $line " | awk '{print $2}' )
APT_PACKAGES_HTML += " <tr><td style='padding:8px 14px;font-size:13px;color:#111827;border-bottom:1px solid #f3f4f6;font-family:monospace;'> $pkg </td><td style='padding:8px 14px;font-size:13px;color:#1d6fa8;font-weight:600;border-bottom:1px solid #f3f4f6;'> $ver </td></tr> "
done <<< " $APT_DETAILS "
APT_PACKAGES_HTML += '</table></div>'
fi
# =============================================================
# STEP 8 — BADGES
# =============================================================
make_badge( ) {
local update = " $1 "
local failed = " $2 "
if [ " $failed " = true ] ; then
echo "<span style='background:#b45309;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>CHECK FAILED</span>"
elif [ " $update " = true ] ; then
echo "<span style='background:#1d6fa8;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>UPDATE AVAILABLE</span>"
else
echo "<span style='background:#1a7f4b;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>CURRENT</span>"
fi
}
PLEX_BADGE = $( make_badge " $PLEX_UPDATE_AVAILABLE " " $PLEX_FAILED " )
F2B_BADGE = $( make_badge " $F2B_UPDATE_AVAILABLE " "false" )
DOCKER_BADGE = $( make_badge " $DOCKER_UPDATE_AVAILABLE " "false" )
2026-07-28 21:07:42 -04:00
if [ " $CONTAINER_OK " = true ] ; then
CONTAINER_BADGE = "<span style='background:#1a7f4b;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>RUNNING</span>"
else
CONTAINER_BADGE = "<span style='background:#b91c1c;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>DOWN</span>"
fi
2026-07-26 21:35:57 -04:00
# =============================================================
# STEP 9 — BUILD & SEND EMAIL
# =============================================================
python3 << PYEOF
import smtplib, ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
apt_updates = $APT_UPDATES
plex_update = " $PLEX_UPDATE_AVAILABLE " = = "true"
f2b_update = " $F2B_UPDATE_AVAILABLE " = = "true"
docker_update = " $DOCKER_UPDATE_AVAILABLE " = = "true"
any_failed = " $ANY_FAILED " = = "true"
status_color = " $STATUS_COLOR "
status_badge = " $STATUS_BADGE "
status_banner = " $STATUS_BANNER_MSG "
subject = " $SUBJECT "
plex_status = " $PLEX_STATUS_TEXT "
f2b_status = " $F2B_STATUS_TEXT "
docker_status = " $DOCKER_STATUS_TEXT "
2026-07-28 21:07:42 -04:00
container_ok = " $CONTAINER_OK " = = "true"
container_status = " $CONTAINER_STATUS_TEXT "
2026-07-26 21:35:57 -04:00
plex_badge = "" " $PLEX_BADGE " ""
f2b_badge = "" " $F2B_BADGE " ""
docker_badge = "" " $DOCKER_BADGE " ""
2026-07-28 21:07:42 -04:00
container_badge = "" " $CONTAINER_BADGE " ""
2026-07-26 21:35:57 -04:00
update_commands = "" " $UPDATE_COMMANDS_HTML " ""
apt_packages_html = "" " $APT_PACKAGES_HTML " ""
plex_error_html = "" " $PLEX_ERROR_HTML " ""
timestamp = " $TIMESTAMP "
plex_image_ver = " $PLEX_IMAGE_VERSION "
current_image_date = " $CURRENT_IMAGE_DATE "
from_email = " $EMAIL_FROM "
total_updates = apt_updates + ( 1 if plex_update else 0) + ( 1 if f2b_update else 0) + ( 1 if docker_update else 0)
apt_card_color = "#1d6fa8" if apt_updates > 0 else "#111827"
plex_card_color = "#1d6fa8" if plex_update else "#111827"
total_card_color = "#1d6fa8" if total_updates > 0 else "#1a7f4b"
other_update = f2b_update or docker_update
other_card_color = "#1d6fa8" if other_update else "#111827"
other_count = sum( [ 1 if f2b_update else 0, 1 if docker_update else 0] )
html = f"" " <!DOCTYPE html>
<html><head><meta charset = "UTF-8" ><meta name = "viewport" content = "width=device-width,initial-scale=1.0" ></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:32px 16px;" >
<tr><td align = "center" >
<table width = "100%" cellspacing = "0" cellpadding = "0" style = "max-width:720px;background:white;border-radius:10px;box-shadow:0 1px 3px rgba(0,0,0,.1);border:1px solid #e5e7eb;overflow:hidden;" >
<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;" >Update Monitor</div>
<div style = "font-size:26px;font-weight:800;color:white;margin-top:4px;" >🖥 PLEX32</div>
<div style = "font-size:13px;color:#94a3b8;margin-top:4px;" >{ timestamp} | VLAN 50 — 192.168.150.45</div>
</td>
<td align = "right" >
<div style = "background:{status_color};color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;" >{ status_badge} </div>
</td>
</tr></table>
</td></tr>
<tr><td style = "background:{status_color};padding:12px 32px;" >
<span style = "color:white;font-size:14px;font-weight:600;" >{ status_banner} </span>
</td></tr>
<tr><td style = "padding:28px 32px;" >
<table width = "100%" cellspacing = "0" cellpadding = "0" >
<tr>
<td style = "width:24%;padding:8px 6px 8px 0;" >
<div style = "background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;" >
<div style = "font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;" >Total Updates</div>
<div style = "font-size:28px;font-weight:700;color:{total_card_color};margin-top:4px;" >{ total_updates} </div>
</div>
</td>
<td style = "width:24%;padding:8px 6px;" >
<div style = "background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;" >
<div style = "font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;" >OS Packages</div>
<div style = "font-size:28px;font-weight:700;color:{apt_card_color};margin-top:4px;" >{ apt_updates} </div>
</div>
</td>
<td style = "width:24%;padding:8px 6px;" >
<div style = "background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;" >
<div style = "font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;" >Plex</div>
<div style = "font-size:28px;font-weight:700;color:{plex_card_color};margin-top:4px;" >{ '1' if plex_update else '0' } </div>
</div>
</td>
<td style = "width:24%;padding:8px 0 8px 6px;" >
<div style = "background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;" >
<div style = "font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;" >Other</div>
<div style = "font-size:28px;font-weight:700;color:{other_card_color};margin-top:4px;" >{ other_count} </div>
</div>
</td>
</tr>
</table>
<div style = "margin-top:24px;" >
<div style = "font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;" >📋 Component Status</div>
<table width = "100%" cellspacing = "0" cellpadding = "0" style = "background:white;border-radius:8px;border:1px solid #e5e7eb;overflow:hidden;" >
<tr style = "background:#f8fafc;" >
<th style = "text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;" >Component</th>
<th style = "text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;" >Status</th>
</tr>
<tr>
<td style = "padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;" >🐧 Ubuntu Server ( apt) </td>
<td style = "padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;" >{ apt_updates} package( s) available</td>
</tr>
<tr>
<td style = "padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;" >🎬 Plex Media Server { plex_badge} </td>
<td style = "padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;" >{ plex_status} <br><span style = "font-size:11px;color:#9ca3af;" >Image: { plex_image_ver} | Built: { current_image_date} </span>{ plex_error_html} </td>
</tr>
2026-07-28 21:07:42 -04:00
<tr>
<td style = "padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;" >✅ Plex Container ( actual) { container_badge} </td>
<td style = "padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;" >{ container_status} <br><span style = "font-size:11px;color:#9ca3af;" >This is the real, running-right-now status — independent of the registry check above.</span></td>
</tr>
2026-07-26 21:35:57 -04:00
<tr>
<td style = "padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;" >🛡 Fail2Ban { f2b_badge} </td>
<td style = "padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;" >{ f2b_status} </td>
</tr>
<tr>
<td style = "padding:10px 14px;font-size:13px;color:#374151;font-weight:600;" >🐳 Docker Engine { docker_badge} </td>
<td style = "padding:10px 14px;font-size:13px;color:#374151;" >{ docker_status} </td>
</tr>
</table>
</div>
{ update_commands}
{ apt_packages_html}
</td></tr>
<tr><td style = "background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;" >
<span style = "font-size:11px;color:#64748b;" >Update Monitor & nbsp; | & nbsp; PLEX32 & nbsp; | & nbsp; KingDezigns Infrastructure</span>
</td></tr>
</table>
</td></tr>
</table>
</body></html>"" "
msg = MIMEMultipart( "alternative" )
msg[ "Subject" ] = subject
msg[ "From" ] = f"KingDezigns PLEX32 <{from_email}>"
msg[ "To" ] = " $EMAIL_TO "
msg.attach( MIMEText( html, "html" ) )
context = ssl.create_default_context( )
try:
with smtplib.SMTP_SSL( " $SMTP_SERVER " , $SMTP_PORT , context = context, timeout = 15) as server:
server.login( " $SMTP_USER " , " $SMTP_PASS " )
server.sendmail( " $EMAIL_FROM " , " $EMAIL_TO " , msg.as_string( ) )
print( " [ $TIMESTAMP ] Email sent successfully. " )
except Exception as e:
print( f" [ $TIMESTAMP ] Email send failed: {e} " )
exit( 1)
PYEOF
echo " [ $TIMESTAMP ] Update check complete. "