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