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
+128
View File
@@ -0,0 +1,128 @@
"""Дашборд: карточки серверов с 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()