Add weekly report, multi-admin, docker health cmd, backup tail, openwrt filters
This commit is contained in:
@@ -308,7 +308,7 @@ def _parse_leases_fallback(raw: str) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
async def get_openwrt_status(cfg: dict[str, Any]) -> str:
|
||||
async def get_openwrt_status(cfg: dict[str, Any], mode: str = "full") -> str:
|
||||
ow_cfg = cfg.get("openwrt", {})
|
||||
host = ow_cfg.get("host")
|
||||
user = ow_cfg.get("user", "root")
|
||||
@@ -353,19 +353,11 @@ async def get_openwrt_status(cfg: dict[str, Any]) -> str:
|
||||
if len(parts) < 4:
|
||||
return "⚠️ OpenWrt response incomplete"
|
||||
|
||||
sys_info = None
|
||||
wan_status = None
|
||||
wireless = None
|
||||
leases = None
|
||||
leases_fallback = ""
|
||||
sys_info = _safe_json_load(parts[0])
|
||||
if sys_info is None:
|
||||
sys_info = None
|
||||
wan_status = _safe_json_load(parts[1]) or {}
|
||||
wireless = _safe_json_load(parts[2]) or {}
|
||||
leases = _safe_json_load(parts[3])
|
||||
if leases is None:
|
||||
leases_fallback = parts[3]
|
||||
leases_fallback = "" if leases is not None else parts[3]
|
||||
|
||||
if isinstance(sys_info, dict):
|
||||
uptime_raw = sys_info.get("uptime")
|
||||
@@ -419,35 +411,40 @@ async def get_openwrt_status(cfg: dict[str, Any]) -> str:
|
||||
else:
|
||||
leases_list = _parse_leases_fallback(leases_fallback)
|
||||
|
||||
lines = [
|
||||
header = [
|
||||
"📡 OpenWrt",
|
||||
f"🕒 Uptime: {uptime}",
|
||||
f"⚙️ Load: {load}",
|
||||
f"🌐 WAN: {wan_ip} ({wan_state})",
|
||||
"",
|
||||
]
|
||||
wifi_section: list[str] = []
|
||||
if wifi_net_counts:
|
||||
lines.append("📶 Wi-Fi networks:")
|
||||
wifi_section.append("📶 Wi-Fi networks:")
|
||||
for label, count in sorted(wifi_net_counts.items()):
|
||||
lines.append(f" - {label}: {count}")
|
||||
lines.append("")
|
||||
wifi_section.append(f" - {label}: {count}")
|
||||
wifi_section.append("")
|
||||
|
||||
lines.append(f"📶 Wi-Fi clients: {len(wifi_clients)}")
|
||||
wifi_section.append(f"📶 Wi-Fi clients: {len(wifi_clients)}")
|
||||
if wifi_clients:
|
||||
for line in wifi_clients[:20]:
|
||||
lines.append(f" - {line}")
|
||||
wifi_section.append(f" - {line}")
|
||||
if len(wifi_clients) > 20:
|
||||
lines.append(f" … and {len(wifi_clients) - 20} more")
|
||||
wifi_section.append(f" … and {len(wifi_clients) - 20} more")
|
||||
else:
|
||||
lines.append(" (none)")
|
||||
wifi_section.append(" (none)")
|
||||
|
||||
lines += ["", f"🧾 DHCP leases: {len(leases_list)}"]
|
||||
lease_section: list[str] = ["", f"🧾 DHCP leases: {len(leases_list)}"]
|
||||
if leases_list:
|
||||
for line in leases_list[:20]:
|
||||
lines.append(f" - {line}")
|
||||
lease_section.append(f" - {line}")
|
||||
if len(leases_list) > 20:
|
||||
lines.append(f" … and {len(leases_list) - 20} more")
|
||||
lease_section.append(f" … and {len(leases_list) - 20} more")
|
||||
else:
|
||||
lines.append(" (none)")
|
||||
lease_section.append(" (none)")
|
||||
|
||||
return "\n".join(lines)
|
||||
if mode == "wan":
|
||||
return "\n".join(header)
|
||||
if mode == "clients":
|
||||
return "\n".join(header + wifi_section)
|
||||
return "\n".join(header + wifi_section + lease_section)
|
||||
|
||||
107
services/weekly_report.py
Normal file
107
services/weekly_report.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import asyncio
|
||||
import socket
|
||||
from datetime import datetime, timedelta
|
||||
import psutil
|
||||
from services.system import worst_disk_usage
|
||||
from services.alert_mute import list_mutes
|
||||
from services.incidents import read_recent
|
||||
from services.docker import docker_cmd
|
||||
|
||||
|
||||
def _parse_hhmm(value: str) -> tuple[int, int]:
|
||||
try:
|
||||
h, m = value.split(":", 1)
|
||||
h = int(h)
|
||||
m = int(m)
|
||||
if 0 <= h <= 23 and 0 <= m <= 59:
|
||||
return h, m
|
||||
except Exception:
|
||||
pass
|
||||
return 8, 0
|
||||
|
||||
|
||||
def _next_run(day: str, time_str: str) -> datetime:
|
||||
day = (day or "Sun").lower()
|
||||
day_map = {"mon": 0, "tue": 1, "wed": 2, "thu": 3, "fri": 4, "sat": 5, "sun": 6}
|
||||
target_wd = day_map.get(day[:3], 6)
|
||||
hour, minute = _parse_hhmm(time_str or "08:00")
|
||||
now = datetime.now()
|
||||
candidate = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
# find next target weekday/time
|
||||
while candidate <= now or candidate.weekday() != target_wd:
|
||||
candidate = candidate + timedelta(days=1)
|
||||
candidate = candidate.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
return candidate
|
||||
|
||||
|
||||
async def _docker_running_counts(docker_map: dict) -> tuple[int, int]:
|
||||
total = len(docker_map)
|
||||
running = 0
|
||||
for real in docker_map.values():
|
||||
rc, raw = await docker_cmd(["inspect", "-f", "{{.State.Status}}", real], timeout=10)
|
||||
if rc == 0 and raw.strip() == "running":
|
||||
running += 1
|
||||
return running, total
|
||||
|
||||
|
||||
def _format_uptime(seconds: int) -> str:
|
||||
days, rem = divmod(seconds, 86400)
|
||||
hours, rem = divmod(rem, 3600)
|
||||
minutes, _ = divmod(rem, 60)
|
||||
return f"{days}d {hours:02d}:{minutes:02d}"
|
||||
|
||||
|
||||
async def build_weekly_report(cfg, docker_map: dict) -> str:
|
||||
host = socket.gethostname()
|
||||
uptime = int(datetime.now().timestamp() - psutil.boot_time())
|
||||
load1, load5, load15 = psutil.getloadavg()
|
||||
mem = psutil.virtual_memory()
|
||||
disk_usage, disk_mount = worst_disk_usage()
|
||||
running, total = await _docker_running_counts(docker_map)
|
||||
mutes = list_mutes()
|
||||
incidents_24 = len(read_recent(cfg, 24, limit=1000))
|
||||
incidents_7d = len(read_recent(cfg, 24 * 7, limit=2000))
|
||||
|
||||
lines = [
|
||||
f"🧾 Weekly report — {host}",
|
||||
f"⏱ Uptime: {_format_uptime(uptime)}",
|
||||
f"⚙️ Load: {load1:.2f} {load5:.2f} {load15:.2f}",
|
||||
f"🧠 RAM: {mem.percent}%",
|
||||
]
|
||||
if disk_usage is None:
|
||||
lines.append("💾 Disk: n/a")
|
||||
else:
|
||||
lines.append(f"💾 Disk: {disk_usage}% ({disk_mount})")
|
||||
|
||||
lines.append(f"🐳 Docker: {running}/{total} running")
|
||||
lines.append(f"📓 Incidents: 24h={incidents_24}, 7d={incidents_7d}")
|
||||
|
||||
if mutes:
|
||||
lines.append("🔕 Active mutes:")
|
||||
for cat, secs in mutes.items():
|
||||
mins = max(0, secs) // 60
|
||||
lines.append(f"- {cat}: {mins}m left")
|
||||
else:
|
||||
lines.append("🔔 Mutes: none")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def weekly_reporter(cfg, bot, admin_ids: list[int], docker_map: dict):
|
||||
reports_cfg = cfg.get("reports", {}).get("weekly", {})
|
||||
if not reports_cfg.get("enabled", False):
|
||||
return
|
||||
day = reports_cfg.get("day", "Sun")
|
||||
time_str = reports_cfg.get("time", "08:00")
|
||||
while True:
|
||||
target = _next_run(day, time_str)
|
||||
wait_sec = (target - datetime.now()).total_seconds()
|
||||
if wait_sec > 0:
|
||||
await asyncio.sleep(wait_sec)
|
||||
try:
|
||||
text = await build_weekly_report(cfg, docker_map)
|
||||
for admin_id in admin_ids:
|
||||
await bot.send_message(admin_id, text)
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(60) # small delay to avoid tight loop if time skew
|
||||
Reference in New Issue
Block a user