"""Дашборд: карточки серверов с health-check и метриками.""" import asyncio import flet as ft from app.core import servers as servers_core from app.core import monitor from app.core.ssh import SSHClient, check_server from app.ui.components import fmt_mb, snack class DashboardPage(ft.Column): def __init__(self, page: ft.Page, go, group_filter: str = "Все"): super().__init__(expand=True, scroll=ft.ScrollMode.AUTO) self._page = page self.go = go self.group_filter = group_filter self.cards = ft.Row(wrap=True, spacing=16, run_spacing=16) self.controls = [ ft.Row([ ft.Text("Дашборд", size=28, weight=ft.FontWeight.BOLD), ft.IconButton(ft.Icons.REFRESH, tooltip="Обновить", on_click=lambda e: self.refresh()), ]), self.cards, ] self.refresh() def refresh(self): servers = servers_core.list_servers() if self.group_filter != "Все": servers = [s for s in servers if self.group_filter in [t.strip() for t in (s.tags or "").split(",")]] self.cards.controls = [self._placeholder(s) for s in servers] self._page.update() for srv, card in zip(servers, self.cards.controls): self._page.run_task(self._fill, srv, card) def _placeholder(self, srv): note_preview = (srv.note[:60] + "…") if len(srv.note) > 60 else srv.note return ft.Card( content=ft.Container( width=340, padding=16, content=ft.Column([ ft.Row([ ft.Icon(ft.Icons.CIRCLE, color=ft.Colors.GREY, size=14), ft.Text(srv.name, size=18, weight=ft.FontWeight.BOLD), ft.Text(f"{srv.host}:{srv.port}", size=12, color=ft.Colors.GREY_500), ft.Container(expand=True), ft.IconButton(ft.Icons.NOTE_ALT_OUTLINED, icon_size=18, tooltip="Заметка", on_click=lambda e, s=srv: self._note_dialog(s)), ]), ft.Text(note_preview, size=11, color=ft.Colors.GREY_500, visible=bool(srv.note)), ft.ProgressBar(height=3), ]), on_click=lambda e: self.go("server_detail", srv.id), ) ) def _note_dialog(self, srv): tf = ft.TextField(label="Заметка", value=srv.note, multiline=True, min_lines=3, max_lines=5, width=400, autofocus=True) def save(e): servers_core.update_server(srv.id, note=tf.value) self._page.pop_dialog() self.refresh() snack(self._page, "Заметка сохранена") dlg = ft.AlertDialog( modal=True, title=ft.Text(f"Заметка: {srv.name}"), content=tf, actions=[ ft.TextButton("Отмена", on_click=lambda e: self._page.pop_dialog()), ft.FilledButton("Сохранить", on_click=save), ], ) self._page.show_dialog(dlg) async def _fill(self, srv, card): info = await check_server(srv) if not info.get("online"): card.content.content.controls[1] = ft.Text( f"❌ {info.get('error', 'недоступен')}", color=ft.Colors.RED_400, size=13) card.content.content.controls[0].controls[0] = ft.Icon( ft.Icons.CIRCLE, color=ft.Colors.RED_400, size=14) self._page.update() return # метрики snap = monitor.latest(srv.id) try: async with SSHClient(srv) as c: snap = await monitor.collect(c, srv.id) except Exception: pass def bar(label, pct, color): return ft.Column([ ft.Row([ ft.Text(label, size=12, width=45), ft.ProgressBar(value=pct / 100, height=6, expand=True, color=color, bgcolor=ft.Colors.GREY_800), ft.Text(f"{pct:.0f}%", size=12, width=40, text_align=ft.TextAlign.RIGHT), ]), ], spacing=2) rows = [ft.Text(f"{info['os']} • {info['uptime']}", size=12, color=ft.Colors.GREY_400)] if snap: rows += [ ft.Container(height=4), 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}", size=11, color=ft.Colors.GREY_500), ] card.content.content.controls[0].controls[0] = ft.Icon( ft.Icons.CIRCLE, color=ft.Colors.GREEN_400, size=14) # сохраняем заметку (controls[1]) — она идёт после заголовка note_ctrl = card.content.content.controls[1] card.content.content.controls[2:] = rows card.content.content.controls[1] = note_ctrl self._page.update()