Files
Varaverk/Orchestrators/sunday_morning_coffee_report.sh
T

633 lines
25 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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