added manual neuclear option to arrs cleanup
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user