Add weekly report, multi-admin, docker health cmd, backup tail, openwrt filters
This commit is contained in:
@@ -5,7 +5,7 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKe
|
||||
from app import dp, bot, cfg, ADMIN_ID
|
||||
from auth import is_admin_msg
|
||||
from services.alert_mute import set_mute, clear_mute, list_mutes
|
||||
from services.incidents import read_recent
|
||||
from services.incidents import read_recent, log_incident
|
||||
from services.notify import notify
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ async def _handle_alerts(msg: Message, action: str, args: list[str]):
|
||||
key = f"test:{level}:{int(time.time())}"
|
||||
await notify(bot, msg.chat.id, f"[TEST] {level.upper()} alert", level=level, key=key, category="test")
|
||||
await msg.answer(f"Sent test alert: {level}")
|
||||
log_incident(cfg, f"alert_test level={level} by {msg.from_user.id}")
|
||||
return
|
||||
|
||||
if action == "mute":
|
||||
@@ -48,6 +49,7 @@ async def _handle_alerts(msg: Message, action: str, args: list[str]):
|
||||
until = set_mute(category, minutes * 60)
|
||||
dt = datetime.fromtimestamp(until, tz=timezone.utc).astimezone()
|
||||
await msg.answer(f"🔕 Muted {category} for {minutes}m (until {dt:%Y-%m-%d %H:%M:%S})")
|
||||
log_incident(cfg, f"alert_mute category={category} minutes={minutes} by {msg.from_user.id}")
|
||||
return
|
||||
|
||||
if action == "unmute":
|
||||
@@ -57,6 +59,7 @@ async def _handle_alerts(msg: Message, action: str, args: list[str]):
|
||||
category = args[0].lower()
|
||||
clear_mute(category)
|
||||
await msg.answer(f"🔔 Unmuted {category}")
|
||||
log_incident(cfg, f"alert_unmute category={category} by {msg.from_user.id}")
|
||||
return
|
||||
|
||||
if action in ("list", "mutes"):
|
||||
|
||||
@@ -38,13 +38,29 @@ def _sudo_cmd(cmd: list[str]) -> list[str]:
|
||||
|
||||
|
||||
def _format_backup_result(rc: int, out: str) -> str:
|
||||
log_hint = "log: /var/log/backup-auto.log"
|
||||
log_path = "/var/log/backup-auto.log"
|
||||
header = "✅ Backup finished" if rc == 0 else "❌ Backup failed"
|
||||
lines = out.strip().splitlines()
|
||||
body = "\n".join(lines[:20])
|
||||
if len(lines) > 20:
|
||||
body += f"\n… trimmed {len(lines) - 20} lines"
|
||||
return f"{header} (rc={rc})\n{log_hint}\n\n{body}" if body else f"{header} (rc={rc})\n{log_hint}"
|
||||
extra = ""
|
||||
if rc != 0 and os.path.exists(log_path):
|
||||
try:
|
||||
tail = ""
|
||||
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
||||
tail_lines = f.readlines()[-40:]
|
||||
tail = "".join(tail_lines).strip()
|
||||
if tail:
|
||||
extra = "\n\nLog tail:\n" + tail
|
||||
except Exception:
|
||||
pass
|
||||
base = f"{header} (rc={rc})\nlog: {log_path}"
|
||||
if body:
|
||||
base += "\n\n" + body
|
||||
if extra:
|
||||
base += extra
|
||||
return base
|
||||
|
||||
|
||||
def _load_json(raw: str, label: str) -> tuple[bool, object | None, str]:
|
||||
@@ -231,6 +247,11 @@ async def cmd_backup_now(msg: Message):
|
||||
|
||||
pos = await enqueue("backup", job)
|
||||
await msg.answer(f"🕓 Backup queued (#{pos})", reply_markup=backup_kb)
|
||||
try:
|
||||
from services.incidents import log_incident
|
||||
log_incident(cfg, f"backup_queued by {msg.from_user.id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def cmd_last_snapshot(msg: Message):
|
||||
|
||||
@@ -4,8 +4,10 @@ from app import dp
|
||||
from auth import is_admin_msg
|
||||
from keyboards import docker_kb, docker_inline_kb
|
||||
from services.docker import container_uptime, docker_cmd
|
||||
from services.incidents import log_incident
|
||||
from state import DOCKER_MAP, LOG_FILTER_PENDING
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
async def cmd_docker_status(msg: Message):
|
||||
@@ -42,6 +44,7 @@ async def cmd_docker_status(msg: Message):
|
||||
lines.append(f"{icon} {alias}: {status} ({up})")
|
||||
|
||||
await msg.answer("\n".join(lines), reply_markup=docker_kb)
|
||||
log_incident(cfg, f"docker_status by {msg.from_user.id}")
|
||||
|
||||
except Exception as e:
|
||||
# ⬅️ КРИТИЧЕСКИ ВАЖНО
|
||||
@@ -83,6 +86,45 @@ async def ds_cmd(msg: Message):
|
||||
await cmd_docker_status(msg)
|
||||
|
||||
|
||||
@dp.message(F.text.startswith("/docker_health"))
|
||||
async def docker_health(msg: Message):
|
||||
if not is_admin_msg(msg):
|
||||
return
|
||||
parts = msg.text.split()
|
||||
if len(parts) < 2:
|
||||
await msg.answer("Usage: /docker_health <alias>")
|
||||
return
|
||||
alias = parts[1]
|
||||
real = DOCKER_MAP.get(alias)
|
||||
if not real:
|
||||
await msg.answer(f"⚠️ Unknown container: {alias}", reply_markup=docker_kb)
|
||||
return
|
||||
rc, out = await docker_cmd(["inspect", "-f", "{{json .State.Health}}", real], timeout=10)
|
||||
if rc != 0 or not out.strip():
|
||||
await msg.answer(f"⚠️ Failed to get health for {alias}", reply_markup=docker_kb)
|
||||
return
|
||||
try:
|
||||
data = json.loads(out)
|
||||
except json.JSONDecodeError:
|
||||
await msg.answer(f"⚠️ Invalid health JSON for {alias}", reply_markup=docker_kb)
|
||||
return
|
||||
status = data.get("Status", "n/a")
|
||||
fail = data.get("FailingStreak", "n/a")
|
||||
logs = data.get("Log") or []
|
||||
lines = [f"🐳 {alias} health", f"Status: {status}", f"Failing streak: {fail}"]
|
||||
if logs:
|
||||
lines.append("Recent logs:")
|
||||
for entry in logs[-5:]:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
ts = entry.get("Start") or entry.get("End") or ""
|
||||
exitc = entry.get("ExitCode", "")
|
||||
out_line = entry.get("Output", "").strip()
|
||||
lines.append(f"- {ts} rc={exitc} {out_line}")
|
||||
await msg.answer("\n".join(lines), reply_markup=docker_kb)
|
||||
log_incident(cfg, f"docker_health alias={alias} by {msg.from_user.id}")
|
||||
|
||||
|
||||
@dp.message(F.text == "📈 Stats")
|
||||
async def dstats(msg: Message):
|
||||
if not is_admin_msg(msg):
|
||||
|
||||
@@ -219,6 +219,38 @@ async def openwrt_cmd(msg: Message):
|
||||
await openwrt_status(msg)
|
||||
|
||||
|
||||
@dp.message(F.text == "/openwrt_wan")
|
||||
async def openwrt_wan(msg: Message):
|
||||
if not is_admin_msg(msg):
|
||||
return
|
||||
await msg.answer("⏳ Checking OpenWrt WAN…", reply_markup=system_info_kb)
|
||||
|
||||
async def worker():
|
||||
try:
|
||||
text = await get_openwrt_status(cfg, mode="wan")
|
||||
except Exception as e:
|
||||
text = f"⚠️ OpenWrt error: {e}"
|
||||
await msg.answer(text, reply_markup=system_info_kb)
|
||||
|
||||
asyncio.create_task(worker())
|
||||
|
||||
|
||||
@dp.message(F.text == "/openwrt_clients")
|
||||
async def openwrt_clients(msg: Message):
|
||||
if not is_admin_msg(msg):
|
||||
return
|
||||
await msg.answer("⏳ Checking OpenWrt clients…", reply_markup=system_info_kb)
|
||||
|
||||
async def worker():
|
||||
try:
|
||||
text = await get_openwrt_status(cfg, mode="clients")
|
||||
except Exception as e:
|
||||
text = f"⚠️ OpenWrt error: {e}"
|
||||
await msg.answer(text, reply_markup=system_info_kb)
|
||||
|
||||
asyncio.create_task(worker())
|
||||
|
||||
|
||||
@dp.message(F.text == "🧾 Audit")
|
||||
async def audit_log(msg: Message):
|
||||
if not is_admin_msg(msg):
|
||||
|
||||
Reference in New Issue
Block a user