159 lines
7.3 KiB
Python
159 lines
7.3 KiB
Python
|
|
"""Вкладка Обзор: метрики, 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)
|