Initial commit: Debian Manager — управление Debian-серверами

- SSH/SFTP управление без агентов
- Пакеты (apt), Docker, файлы, systemd, ufw, cron
- Плейбуки, групповые операции, планировщик
- Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy
- Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
dinlo
2026-09-05 19:46:45 +08:00
commit f624fee5e0
65 changed files with 6981 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
"""Вкладка Обзор: метрики, update/upgrade/reboot, история."""
import flet as ft
from app.core import monitor
from app.core.packages import apt_update, list_upgradable
from app.core.ssh import SSHClient, check_server
from app.core.tasks import tasks
from app.db.models import Server
from app.ui.components import append_log, confirm_dialog, fmt_mb, log_view, snack
class OverviewTab(ft.Column):
def __init__(self, page: ft.Page, srv: Server):
super().__init__(expand=True, scroll=ft.ScrollMode.AUTO, spacing=12)
self._page = page
self.srv = srv
self.info_text = ft.Text("загрузка…", size=13)
self.upg_text = ft.Text("", size=13, color=ft.Colors.ORANGE_300)
self.metrics_col = ft.Column(spacing=4)
self.log = log_view()
self.log_container = ft.Container(
height=220, border=ft.Border.all(1, ft.Colors.GREY_800),
border_radius=8, padding=8, content=self.log, visible=False,
)
self.controls = [
ft.Container(height=8),
ft.Row([
ft.FilledButton("apt update", icon=ft.Icons.REFRESH,
on_click=self._update),
ft.FilledButton("Upgrade", icon=ft.Icons.UPGRADE,
on_click=lambda e: self._upgrade(False)),
ft.OutlinedButton("Dist-upgrade",
on_click=lambda e: self._upgrade(True)),
ft.FilledButton("Reboot", icon=ft.Icons.RESTART_ALT,
style=ft.ButtonStyle(bgcolor=ft.Colors.RED_800),
on_click=self._reboot),
self.upg_text,
], spacing=10),
ft.Card(content=ft.Container(padding=14, content=ft.Column([
ft.Text("Система", weight=ft.FontWeight.BOLD),
self.info_text,
ft.Container(height=6),
self.metrics_col,
]))),
self.log_container,
ft.Text("История метрик", weight=ft.FontWeight.BOLD),
self._chart(),
]
self._page.run_task(self._load)
# ---------- данные ----------
async def _load(self):
info = await check_server(self.srv)
if info.get("online"):
self.info_text.value = (
f"🟢 {info['hostname']} • {info['os']}\n"
f"{info['uptime']} • CPU: {info['cpu_cores']} ядер • RAM: {info['mem_mb']} МБ"
)
try:
async with SSHClient(self.srv) as c:
upg = await list_upgradable(c)
self.upg_text.value = (
f"доступно обновлений: {len(upg)}" if upg else "система обновлена"
)
snap = await monitor.collect(c, self.srv.id)
self._show_metrics(snap)
except Exception as e:
self.info_text.value += f"\n⚠ {e}"
else:
self.info_text.value = f"🔴 {info.get('error')}"
self._page.update()
def _show_metrics(self, snap):
def bar(label, pct, color):
return ft.Row([
ft.Text(label, size=12, width=45),
ft.ProgressBar(value=pct / 100, height=8, expand=True, color=color,
bgcolor=ft.Colors.GREY_800),
ft.Text(f"{pct:.0f}%", size=12, width=42,
text_align=ft.TextAlign.RIGHT),
])
self.metrics_col.controls = [
bar("CPU", snap.cpu_percent, ft.Colors.BLUE_400),
bar("RAM", snap.mem_percent, ft.Colors.PURPLE_300),
bar("Диск", snap.disk_percent,
ft.Colors.RED_400 if snap.disk_percent > 85 else ft.Colors.GREEN_400),
ft.Text(f"RAM {fmt_mb(snap.mem_used_mb)} / {fmt_mb(snap.mem_total_mb)}"
f" • load {snap.load1:.2f}"
f" • net ↓{snap.net_rx_kbs:.0f} ↑{snap.net_tx_kbs:.0f} КБ/с",
size=11, color=ft.Colors.GREY_500),
]
def _chart(self):
hist = monitor.history(self.srv.id, 30)
if len(hist) < 2:
return ft.Text("данных пока мало — график появится после нескольких замеров",
size=12, color=ft.Colors.GREY_500)
# простое текстовое представление истории (LineChart недоступен в flet 0.86)
cpu_vals = [h.cpu_percent for h in hist]
ram_vals = [h.mem_percent for h in hist]
rows = []
for i, h in enumerate(hist[-15:]):
bar_cpu = "█" * int(h.cpu_percent / 5) + "░" * (20 - int(h.cpu_percent / 5))
bar_ram = "█" * int(h.mem_percent / 5) + "░" * (20 - int(h.mem_percent / 5))
rows.append(ft.Text(
f"{h.ts:%H:%M} CPU {bar_cpu} {h.cpu_percent:5.1f}% RAM {bar_ram} {h.mem_percent:5.1f}%",
size=10, font_family="Consolas", color=ft.Colors.GREY_400,
))
return ft.Column(rows, spacing=1, scroll=ft.ScrollMode.AUTO, height=180)
# ---------- действия ----------
def _show_log(self, show=True):
self.log_container.visible = show
self._page.update()
def _update(self, e):
self._show_log()
async def job(log):
async with SSHClient(self.srv) as c:
log("$ apt-get update")
out = await apt_update(c)
for line in out.splitlines()[-30:]:
log(line)
tasks.start("update", "apt update", self.srv.id, job,
on_log=lambda l: (append_log(self.log, l), self._page.update()))
snack(self._page, "apt update запущен")
def _upgrade(self, dist: bool):
label = "dist-upgrade" if dist else "upgrade"
def do():
self._show_log()
async def job(log):
async with SSHClient(self.srv) as c:
log(f"$ apt-get {label} -y")
stream = await __import__("app.core.packages", fromlist=["apt_upgrade"]).apt_upgrade(c, dist)
async for line in stream:
log(line)
tasks.start("upgrade", label, self.srv.id, job,
on_log=lambda l: (append_log(self.log, l), self._page.update()))
confirm_dialog(self._page, f"Запустить {label}?",
f"На сервере «{self.srv.name}» будут обновлены пакеты.", do)
def _reboot(self, e):
def do():
self._show_log()
from app.core.group_ops import op_reboot
async def job(log):
async with SSHClient(self.srv) as c:
await op_reboot(wait=True)(c, self.srv, log)
tasks.start("reboot", "reboot", self.srv.id, job,
on_log=lambda l: (append_log(self.log, l), self._page.update()))
confirm_dialog(self._page, "Перезагрузить сервер?",
f"Сервер «{self.srv.name}» будет перезагружен. Приложение дождётся его возврата.",
do)