f624fee5e0
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
129 lines
5.5 KiB
Python
129 lines
5.5 KiB
Python
"""Дашборд: карточки серверов с 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()
|