@@ -0,0 +1,633 @@
#!/bin/bash
# -----------------------------------------------------------------------------------------------
# ----------------------------- Sunday Morning Coffee Report -----------------------------------
# -----------------------------------------------------------------------------------------------
# Weekly system overview — everything that happened this week in one clean read.
# Designed to be read over coffee Sunday morning while the system is fully caught up
# from the 2:30am maintenance window.
# Schedule: 0 7 * * 0 (7am Sunday — after weekly_sync_maintenance.sh finishes at ~3am)
# Maintenance window completes → 4 hours of fresh data → report ready ☕
#
# Sections:
# 🖥️ System — uptime, memory, boot drive, cache drive, reboots
# 📀 Array — disk count, parity status, ZFS health, drive temps
# 🔀 Failover — state, last change, Tailscale connectivity
# 🎬 Transcodes — ramdisk usage, weekly peak, flips, session split
# 🎵 Media Activity — arr cleanup stats, arr recovery stats, library health
# 🌐 Rsync — weekly transfer totals, per-share breakdown
# 🛡️ Watchdog — container strikes, restarts, system strikes, skip list
# 🔐 Security — SSL cert expiry per domain
# 📊 Emby — weekly stream count, top users, top content
# ⚙️ Health — SMART summary, docker container count, Gitea sync status
# ⚠️ Issues — anything requiring attention collected above
#
# Data sources (reads only — no writes except the notification):
# DATA_DIR stats files — arr cleanup, recovery, transcode, bandwidth history
# /boot/config — failover state, watchdog reboot log
# /tmp — watchdog strike state files
# /proc, /sys — system memory, uptime
# /var/local/emhttp/ — unRAID array info
# Emby API — session history
# Arr APIs — current queue depth
# tailscale — network status
# openssl — live SSL cert check
# smartctl — drive health
#
# All configuration in Master.conf.
# Supports --dry-run to preview report without sending notification.
# -----------------------------------------------------------------------------------------------
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
source " $SCRIPT_DIR /../Master.conf "
source " $SCRIPT_DIR /../common.sh "
parse_args " $@ "
# -----------------------------------------------------------------------------------------------
# ━━━ Setup ━━━
# -----------------------------------------------------------------------------------------------
acquire_lock
detect_hosts
REPORT_DATE = $( date '+%A, %B %-d, %Y' )
WEEK_START = $( date -d "7 days ago" '+%Y-%m-%d' )
TODAY = $( date '+%Y-%m-%d' )
NOW = $( date +%s)
REPORT = ( ) # all report lines
ISSUES = ( ) # items needing attention
FINDINGS = ( ) # notable but not critical
section( ) {
REPORT += ( "" )
REPORT += ( " $1 " )
REPORT += ( " $( printf '%.0s─' { 1..50} ) " )
}
line( ) {
REPORT += ( " $1 " )
}
issue( ) {
ISSUES += ( " $1 " )
REPORT += ( " ⚠️ $1 " )
}
finding( ) {
FINDINGS += ( " $1 " )
REPORT += ( " ℹ ️ $1 " )
}
format_bytes( ) {
local bytes = $1
if ( ( bytes > 1073741824 ) ) ; then
awk " BEGIN {printf \"%.1fGB\", $bytes / 1073741824} "
elif ( ( bytes > 1048576 ) ) ; then
awk " BEGIN {printf \"%.1fMB\", $bytes / 1048576} "
else
echo " ${ bytes } B "
fi
}
# -----------------------------------------------------------------------------------------------
# ━━━ 🖥️ System ━━━
# -----------------------------------------------------------------------------------------------
section "🖥️ SYSTEM"
# Uptime
UPTIME_SECONDS = $( awk '{print int($1)}' /proc/uptime)
UPTIME_DAYS = $(( UPTIME_SECONDS / 86400 ))
UPTIME_HOURS = $(( ( UPTIME_SECONDS % 86400 ) / 3600 ))
BOOT_TIME = $( date -d " @ $(( NOW - UPTIME_SECONDS )) " '+%A %-d %b at %-I:%M%p' )
line " Uptime: ${ UPTIME_DAYS } d ${ UPTIME_HOURS } hr (up since $BOOT_TIME ) "
# Reboot history this week
REBOOT_COUNT = 0
if [ [ -f " $SYS_WATCHDOG_REBOOT_LOG " ] ] ; then
WEEK_EPOCH = $( date -d " $WEEK_START " +%s)
REBOOT_COUNT = $( awk -v cutoff = " $WEEK_EPOCH " '$1 >= cutoff' \
" $SYS_WATCHDOG_REBOOT_LOG " 2>/dev/null | wc -l)
fi
if [ [ " $REBOOT_COUNT " -gt 0 ] ] ; then
issue " Reboots this week: $REBOOT_COUNT (system watchdog triggered) "
else
line "Reboots this week: 0 ✅"
fi
# Memory
MEM_TOTAL_KB = $( awk '/MemTotal/ {print $2}' /proc/meminfo)
MEM_AVAIL_KB = $( awk '/MemAvailable/ {print $2}' /proc/meminfo)
MEM_USED_KB = $(( MEM_TOTAL_KB - MEM_AVAIL_KB ))
MEM_TOTAL_GB = $( awk " BEGIN {printf \"%.0f\", $MEM_TOTAL_KB / 1048576} " )
MEM_USED_GB = $( awk " BEGIN {printf \"%.1f\", $MEM_USED_KB / 1048576} " )
MEM_FREE_GB = $( awk " BEGIN {printf \"%.1f\", $MEM_AVAIL_KB / 1048576} " )
ARC_SIZE = 0
if [ [ -f /proc/spl/kstat/zfs/arcstats ] ] ; then
ARC_BYTES = $( awk '/^size / {print $3}' /proc/spl/kstat/zfs/arcstats 2>/dev/null || echo 0)
ARC_SIZE = $( awk " BEGIN {printf \"%.1f\", $ARC_BYTES / 1073741824} " )
fi
line " Memory: ${ MEM_USED_GB } GB used / ${ MEM_TOTAL_GB } GB total (ARC: ${ ARC_SIZE } GB) "
# Boot drive
BOOT_PCT = $( df /boot --output= pcent 2>/dev/null | tail -1 | tr -d ' %' )
BOOT_USED = $( df /boot -h --output= used 2>/dev/null | tail -1 | tr -d ' ' )
BOOT_SIZE = $( df /boot -h --output= size 2>/dev/null | tail -1 | tr -d ' ' )
if [ [ " ${ BOOT_PCT :- 0 } " -ge 80 ] ] ; then
issue " Boot drive: ${ BOOT_PCT } % used ( ${ BOOT_USED } / ${ BOOT_SIZE } ) — getting full "
else
line " Boot drive: ${ BOOT_PCT } % used ( ${ BOOT_USED } / ${ BOOT_SIZE } ) "
fi
# Cache drive
CACHE_PCT = $( df /mnt/cache --output= pcent 2>/dev/null | tail -1 | tr -d ' %' )
CACHE_AVAIL = $( df /mnt/cache -h --output= avail 2>/dev/null | tail -1 | tr -d ' ' )
CACHE_SIZE = $( df /mnt/cache -h --output= size 2>/dev/null | tail -1 | tr -d ' ' )
if [ [ " ${ CACHE_PCT :- 0 } " -ge 85 ] ] ; then
issue " Cache drive: ${ CACHE_PCT } % used ( ${ CACHE_AVAIL } free of ${ CACHE_SIZE } ) "
else
line " Cache drive: ${ CACHE_PCT } % used ( ${ CACHE_AVAIL } free of ${ CACHE_SIZE } ) "
fi
# -----------------------------------------------------------------------------------------------
# ━━━ 📀 Array ━━━
# -----------------------------------------------------------------------------------------------
section "📀 ARRAY"
# unRAID array info
if [ [ -f /var/local/emhttp/disks.ini ] ] ; then
DISK_COUNT = $( grep -c "^\[disk" /var/local/emhttp/disks.ini 2>/dev/null || echo "?" )
line " Array disks: $DISK_COUNT "
fi
# Parity
if [ [ -f /var/local/emhttp/parity-date.txt ] ] ; then
PARITY_INFO = $( cat /var/local/emhttp/parity-date.txt 2>/dev/null)
if echo " $PARITY_INFO " | grep -q "progress" ; then
finding "Parity check in progress"
else
PARITY_DATE = $( echo " $PARITY_INFO " | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1)
PARITY_ERRORS = $( echo " $PARITY_INFO " | grep -oE 'errors=[0-9]+' | cut -d= -f2 || echo 0)
if [ [ " ${ PARITY_ERRORS :- 0 } " -gt 0 ] ] ; then
issue " Parity: $PARITY_ERRORS errors on last check ( $PARITY_DATE ) "
else
line " Parity: OK (last check: ${ PARITY_DATE :- unknown } , 0 errors) "
fi
fi
fi
# ZFS pool health
if command -v zpool >/dev/null 2>& 1; then
POOLS = $( zpool list -H -o name,health 2>/dev/null)
while IFS = $'\t' read -r pool health; do
[ [ -z " $pool " ] ] && continue
# Skip single-disk unRAID array pools
if [ [ " $health " = = "ONLINE" ] ] ; then
line " ZFS $pool : ONLINE ✅ "
else
issue " ZFS $pool : $health — check immediately "
fi
done <<< " $POOLS "
fi
# Drive temperatures from SMART
if command -v smartctl >/dev/null 2>& 1; then
TEMP_WARN = false
TEMP_SUMMARY = ""
for disk in /dev/sd? /dev/nvme?; do
[ [ ! -e " $disk " ] ] && continue
DISK_NAME = $( basename " $disk " )
# Skip ignored drives
SKIP = false
for ignore in " ${ SMART_IGNORE_DRIVES [@] :- } " ; do
[ [ " $DISK_NAME " = = " $ignore " ] ] && SKIP = true && break
done
[ [ " $SKIP " = = true ] ] && continue
TEMP = $( smartctl -A " $disk " 2>/dev/null | \
awk '/Temperature_Celsius|Temperature:/ {print $NF; exit}' | tr -d '°C' )
[ [ -z " $TEMP " ] ] && continue
TEMP_INT = $( printf "%.0f" " $TEMP " 2>/dev/null || echo 0)
if [ [ " $TEMP_INT " -ge " ${ SMART_TEMP_CRIT :- 55 } " ] ] ; then
issue " Drive $DISK_NAME : ${ TEMP_INT } °C — CRITICAL "
TEMP_WARN = true
elif [ [ " $TEMP_INT " -ge " ${ SMART_TEMP_WARN :- 45 } " ] ] ; then
finding " Drive $DISK_NAME : ${ TEMP_INT } °C — warm "
TEMP_WARN = true
fi
[ [ -n " $TEMP_SUMMARY " ] ] && TEMP_SUMMARY += ", "
TEMP_SUMMARY += " ${ DISK_NAME } : ${ TEMP_INT } °C "
done
if [ [ " $TEMP_WARN " = = false ] ] ; then
line "Drive temps: all normal"
fi
[ [ -n " $TEMP_SUMMARY " ] ] && line " Temps: $TEMP_SUMMARY "
fi
# -----------------------------------------------------------------------------------------------
# ━━━ 🔀 Failover ━━━
# -----------------------------------------------------------------------------------------------
section "🔀 FAILOVER"
if [ [ -f " $FAILOVER_STATE_FILE " ] ] ; then
FAILOVER_STATE = $( grep "^state=" " $FAILOVER_STATE_FILE " 2>/dev/null | cut -d= -f2)
FAILOVER_CHANGE = $( grep "^last_change=" " $FAILOVER_STATE_FILE " 2>/dev/null | cut -d= -f2)
if [ [ " $FAILOVER_STATE " = = "NORMAL" ] ] ; then
line "State: NORMAL ✅"
else
issue " Failover state: $FAILOVER_STATE — not normal "
fi
line " Last change: ${ FAILOVER_CHANGE :- unknown } "
else
issue "Failover state file not found"
fi
# Tailscale connectivity
if command -v tailscale >/dev/null 2>& 1; then
TS_STATUS = $( tailscale status 2>/dev/null)
REMOTE_VISIBLE = $( echo " $TS_STATUS " | grep -c " $HOST2 " 2>/dev/null || echo 0)
if [ [ " $REMOTE_VISIBLE " -gt 0 ] ] ; then
REMOTE_IP = $( tailscale ip -4 " $HOST2 " 2>/dev/null || echo "unknown" )
line " Tailscale: $HOST2 visible at $REMOTE_IP ✅ "
else
issue " Tailscale: $HOST2 not visible — check connectivity "
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ 🎬 Transcodes ━━━
# -----------------------------------------------------------------------------------------------
section "🎬 TRANSCODES"
if mountpoint -q " $RAMDISK_PATH " 2>/dev/null; then
RAMDISK_USED_KB = $( df " $RAMDISK_PATH " --output= used | tail -1 | tr -d ' ' )
RAMDISK_USED_GB = $( awk " BEGIN {printf \"%.2f\", $RAMDISK_USED_KB / 1048576} " )
RAMDISK_AVAIL_KB = $( df " $RAMDISK_PATH " --output= avail | tail -1 | tr -d ' ' )
RAMDISK_AVAIL_GB = $( awk " BEGIN {printf \"%.2f\", $RAMDISK_AVAIL_KB / 1048576} " )
SYMLINK_TARGET = $( readlink " $TRANSCODE_LINK " 2>/dev/null | xargs basename 2>/dev/null || echo "unknown" )
line " Ramdisk now: ${ RAMDISK_USED_GB } GB used / ${ RAMDISK_AVAIL_GB } GB free → $SYMLINK_TARGET "
else
issue "Ramdisk not mounted"
fi
if [ [ -f " $TRANSCODE_DAILY_LOG " ] ] ; then
WEEK_PEAK = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff {if ($2 > max) max=$2} END {printf "%.2f", max+0}' \
" $TRANSCODE_DAILY_LOG " )
WEEK_FLIPS = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff {sum+=$3} END {print sum+0}' " $TRANSCODE_DAILY_LOG " )
WEEK_RAM = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff {sum+=$4} END {print sum+0}' " $TRANSCODE_DAILY_LOG " )
WEEK_SSD = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff {sum+=$5} END {print sum+0}' " $TRANSCODE_DAILY_LOG " )
WEEK_FILES = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff {sum+=$6} END {print sum+0}' " $TRANSCODE_DAILY_LOG " )
line " Week peak: ${ WEEK_PEAK } GB | flips: ${ WEEK_FLIPS } | files cleaned: ${ WEEK_FILES } "
line " Sessions: ${ WEEK_RAM } ramdisk / ${ WEEK_SSD } SSD "
PEAK_INT = $( printf "%.0f" " $WEEK_PEAK " 2>/dev/null || echo 0)
WARN_INT = $( printf "%.0f" " $RAMDISK_WARN_GB " 2>/dev/null || echo 0)
if [ [ " $PEAK_INT " -ge " $WARN_INT " ] ] ; then
finding " Transcode peak ${ WEEK_PEAK } GB reached warn threshold — consider increasing RAMDISK_SIZE "
fi
if [ [ " ${ WEEK_FLIPS :- 0 } " -ge " ${ TRANSCODE_FLIP_WARN :- 3 } " ] ] ; then
finding " Transcode flips this week: $WEEK_FLIPS — monitor ramdisk headroom "
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ 🎵 Media Activity ━━━
# -----------------------------------------------------------------------------------------------
section "🎵 MEDIA ACTIVITY"
# Arr cleanup stats
if [ [ -f " $ARR_CLEANUP_STATS " ] ] ; then
for arr in lidarr sonarr radarr; do
WEEK_ORPHANS = $( awk -F'|' -v cutoff = " $WEEK_START " -v a = " $arr " \
'$1 >= cutoff && $2 == a {sum+=$3} END {print sum+0}' " $ARR_CLEANUP_STATS " )
WEEK_BYTES = $( awk -F'|' -v cutoff = " $WEEK_START " -v a = " $arr " \
'$1 >= cutoff && $2 == a {sum+=$4} END {print sum+0}' " $ARR_CLEANUP_STATS " )
WEEK_TRACKED = $( awk -F'|' -v cutoff = " $WEEK_START " -v a = " $arr " \
'BEGIN{max=0} $1 >= cutoff && $2 == a && $8 > max {max=$8} END {print max+0}' \
" $ARR_CLEANUP_STATS " )
if [ [ " ${ WEEK_ORPHANS :- 0 } " -gt 0 ] ] ; then
FREED = $( format_bytes " ${ WEEK_BYTES :- 0 } " )
line " ${ arr ^ } cleanup: $WEEK_ORPHANS orphans removed ( $FREED freed) | tracked: $WEEK_TRACKED files "
else
line " ${ arr ^ } cleanup: clean ✅ (tracked: $WEEK_TRACKED files) "
fi
done
else
line "Arr cleanup stats: no data yet (runs after first cleanup)"
fi
# Arr recovery stats
if [ [ -f " $ARR_RECOVERY_STATS " ] ] ; then
WEEK_ACTIONED = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff {sum+=$3} END {print sum+0}' " $ARR_RECOVERY_STATS " )
WEEK_RUNS = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff {count++} END {print count+0}' " $ARR_RECOVERY_STATS " )
if [ [ " ${ WEEK_ACTIONED :- 0 } " -gt 0 ] ] ; then
line " Arr recovery: $WEEK_ACTIONED items auto-recovered across $WEEK_RUNS runs "
else
line "Arr recovery: no failed imports this week ✅"
fi
fi
# Arr queue depth — current snapshot
for arr_name in " Sonarr| ${ HOST1_SONARR_URL } | ${ HOST1_SONARR_API_KEY } |v3 " \
" Radarr| ${ HOST1_RADARR_URL } | ${ HOST1_RADARR_API_KEY } |v3 " \
" Lidarr| ${ HOST1_LIDARR_URL } | ${ HOST1_LIDARR_API_KEY } |v1 " ; do
IFS = '|' read -r name url key ver <<< " $arr_name "
if [ [ " $url " = = *"your-" * ] ] || [ [ -z " $key " ] ] ; then continue ; fi
QUEUE = $( curl -sf --max-time 5 -H " X-Api-Key: $key " \
" ${ url } /api/ ${ ver } /queue?pageSize=1 " 2>/dev/null | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('totalRecords',0))" \
2>/dev/null || echo "?" )
if [ [ " $QUEUE " = = "0" ] ] || [ [ -z " $QUEUE " ] ] ; then
line " $name queue: empty ✅ "
else
line " $name queue: $QUEUE items "
fi
done
# -----------------------------------------------------------------------------------------------
# ━━━ 🌐 Rsync ━━━
# -----------------------------------------------------------------------------------------------
section "🌐 RSYNC"
if [ [ -f " $BANDWIDTH_LOG " ] ] && [ [ -s " $BANDWIDTH_LOG " ] ] ; then
WEEK_TOTAL_BYTES = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff {sum+=$3} END {print sum+0}' " $BANDWIDTH_LOG " )
WEEK_SYNCS = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff' " $BANDWIDTH_LOG " | wc -l)
WEEK_FAILED = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff && $5 != "success"' " $BANDWIDTH_LOG " | wc -l)
WEEK_GB = $( awk " BEGIN {printf \"%.1f\", $WEEK_TOTAL_BYTES / 1073741824} " )
line " Total transferred: ${ WEEK_GB } GB across $WEEK_SYNCS syncs "
if [ [ " ${ WEEK_FAILED :- 0 } " -gt 0 ] ] ; then
issue " Failed syncs this week: $WEEK_FAILED "
else
line "Sync failures: none ✅"
fi
# Top 3 shares by transfer this week
TOP_SHARES = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$1 >= cutoff {bytes[$4]+=$3} END {for(s in bytes) print bytes[s], s}' \
" $BANDWIDTH_LOG " | sort -rn | head -3)
if [ [ -n " $TOP_SHARES " ] ] ; then
while IFS = ' ' read -r bytes share; do
[ [ -z " $share " ] ] && continue
SIZE = $( format_bytes " $bytes " )
line " → $share : $SIZE "
done <<< " $TOP_SHARES "
fi
else
line "No bandwidth data yet"
fi
# -----------------------------------------------------------------------------------------------
# ━━━ 🛡️ Watchdog ━━━
# -----------------------------------------------------------------------------------------------
section "🛡️ WATCHDOG"
# Container watchdog strikes
if [ [ -f " $WATCHDOG_STATE_FILE " ] ] ; then
ACTIVE_STRIKES = $( grep -v " :0 $" " $WATCHDOG_STATE_FILE " 2>/dev/null | grep -v " ^ $" | wc -l)
if [ [ " $ACTIVE_STRIKES " -gt 0 ] ] ; then
STRIKE_LIST = $( grep -v " :0 $" " $WATCHDOG_STATE_FILE " 2>/dev/null | tr '\n' ' ' )
issue " Container watchdog: $ACTIVE_STRIKES active strikes — $STRIKE_LIST "
else
line "Container watchdog: no active strikes ✅"
fi
fi
# System watchdog strikes
if [ [ -f " $SYS_WATCHDOG_STATE_FILE " ] ] ; then
SYS_STRIKES = $( grep -v " :0 $" " $SYS_WATCHDOG_STATE_FILE " 2>/dev/null | grep -v " ^ $" | wc -l)
if [ [ " $SYS_STRIKES " -gt 0 ] ] ; then
SYS_LIST = $( grep -v " :0 $" " $SYS_WATCHDOG_STATE_FILE " 2>/dev/null | tr '\n' ' ' )
issue " System watchdog: $SYS_STRIKES active strikes — $SYS_LIST "
else
line "System watchdog: no active strikes ✅"
fi
fi
# Container skip list
if [ [ -f " $SYS_WATCHDOG_FAILED_FILE " ] ] && [ [ -s " $SYS_WATCHDOG_FAILED_FILE " ] ] ; then
SKIP_COUNT = $( wc -l < " $SYS_WATCHDOG_FAILED_FILE " )
SKIP_LIST = $( cat " $SYS_WATCHDOG_FAILED_FILE " | tr '\n' ' ' )
issue " Skip list: $SKIP_COUNT containers — $SKIP_LIST — manual intervention needed "
else
line "Skip list: empty ✅"
fi
# Container restarts this week
if [ [ -f " $WATCHDOG_CONTAINER_RESTART_LOG " ] ] ; then
WEEK_RESTARTS = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$2 >= cutoff' " $WATCHDOG_CONTAINER_RESTART_LOG " 2>/dev/null | wc -l)
if [ [ " ${ WEEK_RESTARTS :- 0 } " -gt 0 ] ] ; then
RESTARTED = $( awk -F'|' -v cutoff = " $WEEK_START " \
'$2 >= cutoff {print $1}' " $WATCHDOG_CONTAINER_RESTART_LOG " 2>/dev/null | \
sort | uniq -c | sort -rn | head -5 | \
awk '{print $2 "(" $1 ")"}' | tr '\n' ' ' )
finding " Watchdog restarted $WEEK_RESTARTS containers this week: $RESTARTED "
else
line "Watchdog restarts: none this week ✅"
fi
fi
# Docker container count
if command -v docker >/dev/null 2>& 1; then
RUNNING = $( docker ps -q 2>/dev/null | wc -l)
TOTAL = $( docker ps -aq 2>/dev/null | wc -l)
STOPPED = $(( TOTAL - RUNNING ))
if [ [ " $STOPPED " -gt 0 ] ] ; then
STOPPED_NAMES = $( docker ps -af "status=exited" --format "{{.Names}}" 2>/dev/null | \
head -5 | tr '\n' ' ' )
finding " Docker: $RUNNING / $TOTAL running — $STOPPED stopped: $STOPPED_NAMES "
else
line " Docker: $RUNNING / $TOTAL containers running ✅ "
fi
fi
# -----------------------------------------------------------------------------------------------
# ━━━ 🔐 Security ━━━
# -----------------------------------------------------------------------------------------------
section "🔐 SECURITY"
for domain in " ${ CERT_MONITOR_DOMAINS [@] :- } " ; do
[ [ -z " $domain " ] ] && continue
EXPIRY = $( echo | timeout " ${ CERT_TIMEOUT :- 10 } " openssl s_client \
-connect " ${ domain } :443 " -servername " $domain " 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ [ -z " $EXPIRY " ] ] ; then
issue " $domain : could not check certificate "
continue
fi
EXPIRY_EPOCH = $( date -d " $EXPIRY " +%s 2>/dev/null || echo 0)
DAYS_LEFT = $(( ( EXPIRY_EPOCH - NOW) / 86400 ))
if [ [ " $DAYS_LEFT " -le " ${ CERT_CRIT_DAYS :- 7 } " ] ] ; then
issue " $domain : ${ DAYS_LEFT } days remaining — CRITICAL, renew now "
elif [ [ " $DAYS_LEFT " -le " ${ CERT_WARN_DAYS :- 30 } " ] ] ; then
finding " $domain : ${ DAYS_LEFT } days remaining — renew soon "
else
line " $domain : ${ DAYS_LEFT } days remaining ✅ "
fi
done
# -----------------------------------------------------------------------------------------------
# ━━━ 📊 Emby ━━━
# -----------------------------------------------------------------------------------------------
section "📊 EMBY"
# Select correct Emby URL and key for this host
if [ [ " $LOCAL_SERVER_NAME " = = " $HOST1 " ] ] ; then
EMBY_URL = " $HOST1_EMBY_URL "
EMBY_KEY = " $HOST1_EMBY_API_KEY "
else
EMBY_URL = " $HOST2_EMBY_URL "
EMBY_KEY = " $HOST2_EMBY_API_KEY "
fi
if [ [ " $EMBY_KEY " != *"your-" * ] ] && [ [ -n " $EMBY_KEY " ] ] ; then
WEEK_MS = $(( 7 * 24 * 3600 * 10000000 ))
ACTIVITY = $( curl -sf --max-time 10 \
-H " X-Emby-Token: $EMBY_KEY " \
" ${ EMBY_URL } /Sessions?ControllableByUserId=&api_key= $EMBY_KEY " \
2>/dev/null)
# Use activity log for weekly stats
ACTIVITY_LOG = $( curl -sf --max-time 10 \
-H " X-Emby-Token: $EMBY_KEY " \
" ${ EMBY_URL } /user_usage_stats/user_activity?days=7&api_key= $EMBY_KEY " \
2>/dev/null)
if [ [ -n " $ACTIVITY_LOG " ] ] ; then
TOTAL_PLAYS = $( echo " $ACTIVITY_LOG " | \
python3 -c "import sys,json; d=json.load(sys.stdin); \
print(sum(u.get('total_plays',0) for u in d))" 2>/dev/null || echo "?" )
line " Streams this week: $TOTAL_PLAYS total plays "
# Top 3 users
TOP_USERS = $( echo " $ACTIVITY_LOG " | \
python3 -c "
import sys,json
d=json.load(sys.stdin)
users=sorted(d,key=lambda x:x.get('total_plays',0),reverse=True)[:3]
for u in users:
print(f\" → {u.get('user_name','?')}: {u.get('total_plays',0)} plays\")
" 2>/dev/null)
[ [ -n " $TOP_USERS " ] ] && echo " $TOP_USERS " | while IFS = read -r l; do line " $l " ; done
else
# Fallback — just show active sessions
ACTIVE = $( curl -sf --max-time 5 \
-H " X-Emby-Token: $EMBY_KEY " \
" ${ EMBY_URL } /Sessions?api_key= $EMBY_KEY " 2>/dev/null | \
python3 -c "import sys,json; \
d=json.load(sys.stdin); \
active=[s for s in d if s.get('NowPlayingItem')]; \
print(f'{len(active)} active streams now')" 2>/dev/null || echo "API unavailable" )
line " Emby: $ACTIVE "
fi
else
line "Emby: API key not configured"
fi
# -----------------------------------------------------------------------------------------------
# ━━━ ⚙️ System Health ━━━
# -----------------------------------------------------------------------------------------------
section "⚙️ SYSTEM HEALTH"
# SMART summary
if command -v smartctl >/dev/null 2>& 1; then
SMART_ISSUES = 0
for disk in /dev/sd? /dev/nvme?; do
[ [ ! -e " $disk " ] ] && continue
DISK_NAME = $( basename " $disk " )
SKIP = false
for ignore in " ${ SMART_IGNORE_DRIVES [@] :- } " ; do
[ [ " $DISK_NAME " = = " $ignore " ] ] && SKIP = true && break
done
[ [ " $SKIP " = = true ] ] && continue
HEALTH = $( smartctl -H " $disk " 2>/dev/null | grep "SMART overall-health" | \
awk '{print $NF}' )
if [ [ " $HEALTH " != "PASSED" ] ] && [ [ -n " $HEALTH " ] ] ; then
issue " SMART $DISK_NAME : $HEALTH — check immediately "
( ( SMART_ISSUES++) )
fi
done
if [ [ " $SMART_ISSUES " -eq 0 ] ] ; then
line "SMART: all drives PASSED ✅"
fi
fi
# Gitea sync status
if command -v git >/dev/null 2>& 1 && [ [ -d " $TARGET_DIR /.git " ] ] ; then
CURRENT_COMMIT = $( git -C " $TARGET_DIR " rev-parse --short HEAD 2>/dev/null || echo "unknown" )
LAST_PULL = $( git -C " $TARGET_DIR " log -1 --format= "%ar" 2>/dev/null || echo "unknown" )
line " Gitea: commit $CURRENT_COMMIT (pulled $LAST_PULL ) "
fi
# -----------------------------------------------------------------------------------------------
# ━━━ ⚠️ Issues Requiring Attention ━━━
# -----------------------------------------------------------------------------------------------
if [ [ ${# ISSUES [@] } -gt 0 ] ] ; then
REPORT += ( "" )
REPORT += ( "⚠️ ISSUES REQUIRING ATTENTION" )
REPORT += ( " $( printf '%.0s─' { 1..50} ) " )
for issue_line in " ${ ISSUES [@] } " ; do
REPORT += ( " ❌ $issue_line " )
done
fi
if [ [ ${# FINDINGS [@] } -gt 0 ] ] ; then
REPORT += ( "" )
REPORT += ( "ℹ ️ NOTABLE" )
REPORT += ( " $( printf '%.0s─' { 1..50} ) " )
for finding_line in " ${ FINDINGS [@] } " ; do
REPORT += ( " → $finding_line " )
done
fi
# -----------------------------------------------------------------------------------------------
# ━━━ Footer ━━━
# -----------------------------------------------------------------------------------------------
REPORT += ( "" )
if [ [ ${# ISSUES [@] } -eq 0 ] ] ; then
STATUS = "✅ All systems healthy — enjoy your Sunday"
else
STATUS = " ⚠️ ${# ISSUES [@] } issue(s) need attention "
fi
REPORT += ( " $STATUS " )
REPORT += ( "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" )
# -----------------------------------------------------------------------------------------------
# ━━━ Output and Send ━━━
# -----------------------------------------------------------------------------------------------
HEADER = " ☕ SUNDAY MORNING COFFEE REPORT — $REPORT_DATE "
DIVIDER = " $( printf '%.0s━' { 1..50} ) "
echo ""
echo " $DIVIDER "
echo " $HEADER "
echo " $DIVIDER "
for report_line in " ${ REPORT [@] } " ; do
echo " $report_line "
done
echo ""
# Send notification
BODY = $( printf '%s\n' " $HEADER " " ${ REPORT [@] } " )
if [ [ " $DRY_RUN " = = true ] ] ; then
warn "DRY RUN — notification not sent"
else
notify " $BODY " "☕ Weekly Report" "normal"
success "Report sent"
fi