diff --git a/handlers/backup.py b/handlers/backup.py index dba3703..5ff8ebb 100644 --- a/handlers/backup.py +++ b/handlers/backup.py @@ -12,6 +12,59 @@ from services.backup import backup_badge, restore_help from services.runner import run_cmd +def _parse_systemctl_kv(raw: str) -> dict[str, str]: + data: dict[str, str] = {} + for line in raw.splitlines(): + if "=" not in line: + continue + key, value = line.split("=", 1) + data[key.strip()] = value.strip() + return data + + +async def _unit_status(unit: str, props: list[str]) -> dict[str, str]: + args = ["systemctl", "show", unit] + [f"-p{prop}" for prop in props] + rc, out = await run_cmd(args, timeout=10) + if rc != 0: + return {"error": out.strip() or f"systemctl {unit} failed"} + return _parse_systemctl_kv(out) + + +async def send_backup_jobs_status(msg: Message): + services = [ + ("backup-auto", "backup-auto.timer"), + ("restic-check", "restic-check.timer"), + ("weekly-report", "weekly-report.timer"), + ] + service_props = ["ActiveState", "SubState", "Result", "ExecMainStatus", "ExecMainExitTimestamp"] + timer_props = ["LastTriggerUSecRealtime", "NextElapseUSecRealtime"] + + lines = ["🕒 Backup jobs\n"] + for service, timer in services: + svc = await _unit_status(f"{service}.service", service_props) + tmr = await _unit_status(timer, timer_props) + if "error" in svc: + lines.append(f"🔴 {service}: {svc['error']}") + continue + + active = svc.get("ActiveState", "n/a") + result = svc.get("Result", "n/a") + exit_status = svc.get("ExecMainStatus", "n/a") + last = svc.get("ExecMainExitTimestamp", "n/a") + next_run = tmr.get("NextElapseUSecRealtime", "n/a") + last_trigger = tmr.get("LastTriggerUSecRealtime", "n/a") + + lines.append( + f"🧊 {service}: {active} ({result}, rc={exit_status})" + ) + lines.append(f" Last run: {last}") + lines.append(f" Last trigger: {last_trigger}") + lines.append(f" Next: {next_run}") + lines.append("") + + await msg.answer("\n".join(lines).rstrip(), reply_markup=backup_kb) + + async def cmd_repo_stats(msg: Message): await msg.answer("⏳ Loading repo stats…", reply_markup=backup_kb) @@ -104,6 +157,7 @@ async def cmd_backup_status(msg: Message): f"📦 Snapshots ({len(snaps)})\n{badge}", reply_markup=kb ) + await send_backup_jobs_status(msg) asyncio.create_task(worker()) @@ -202,6 +256,34 @@ async def br(msg: Message): await cmd_backup_now(msg) +@dp.message(F.text == "🧪 Restic check") +async def rc(msg: Message): + if not is_admin_msg(msg): + return + + async def job(): + await msg.answer("🧪 Restic check запущен", reply_markup=backup_kb) + rc2, out = await run_cmd(["sudo", "/usr/local/bin/restic-check.sh"], timeout=6 * 3600) + await msg.answer(("✅ OK\n" if rc2 == 0 else "❌ FAIL\n") + out, reply_markup=backup_kb) + + pos = await enqueue("restic-check", job) + await msg.answer(f"🕓 Restic check queued (#{pos})", reply_markup=backup_kb) + + +@dp.message(F.text == "📬 Weekly report") +async def wr(msg: Message): + if not is_admin_msg(msg): + return + + async def job(): + await msg.answer("📬 Weekly report запущен", reply_markup=backup_kb) + rc2, out = await run_cmd(["sudo", "/usr/local/bin/weekly-report.sh"], timeout=3600) + await msg.answer(("✅ OK\n" if rc2 == 0 else "❌ FAIL\n") + out, reply_markup=backup_kb) + + pos = await enqueue("weekly-report", job) + await msg.answer(f"🕓 Weekly report queued (#{pos})", reply_markup=backup_kb) + + @dp.message(F.text == "🧯 Restore help") async def rh(msg: Message): if is_admin_msg(msg): diff --git a/keyboards.py b/keyboards.py index b659300..f4afbb0 100644 --- a/keyboards.py +++ b/keyboards.py @@ -38,7 +38,7 @@ backup_kb = ReplyKeyboardMarkup( [KeyboardButton(text="📦 Status"), KeyboardButton(text="📦 Last snapshot")], [KeyboardButton(text="📊 Repo stats"), KeyboardButton(text="🧯 Restore help")], [KeyboardButton(text="▶️ Run backup"), KeyboardButton(text="🧾 Queue")], - [KeyboardButton(text="⬅️ Назад")], + [KeyboardButton(text="🧪 Restic check"), KeyboardButton(text="📬 Weekly report"), KeyboardButton(text="⬅️ Назад")], ], resize_keyboard=True, )