Initial commit: Debian Manager — управление Debian-серверами
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""Вкладка Сервисы: systemd-юниты, журнал."""
|
||||
import flet as ft
|
||||
|
||||
from app.core import systemd_svc as sd
|
||||
from app.core.ssh import SSHClient
|
||||
from app.db.models import Server
|
||||
from app.ui.components import confirm_dialog, snack
|
||||
|
||||
|
||||
class SystemdTab(ft.Column):
|
||||
def __init__(self, page: ft.Page, srv: Server):
|
||||
super().__init__(expand=True, spacing=8)
|
||||
self._page = page
|
||||
self.srv = srv
|
||||
self._units = []
|
||||
|
||||
self.search = ft.TextField(hint_text="Поиск юнита…", prefix_icon=ft.Icons.SEARCH,
|
||||
on_change=lambda e: self._render(), expand=True, dense=True)
|
||||
self.only_failed = ft.Checkbox(label="только failed", value=False,
|
||||
on_change=lambda e: self._render())
|
||||
self.table = ft.DataTable(columns=[
|
||||
ft.DataColumn(ft.Text("Юнит")),
|
||||
ft.DataColumn(ft.Text("Состояние")),
|
||||
ft.DataColumn(ft.Text("Enabled")),
|
||||
ft.DataColumn(ft.Text("Описание")),
|
||||
ft.DataColumn(ft.Text("Действия")),
|
||||
], rows=[])
|
||||
self.log_text = ft.Column(scroll=ft.ScrollMode.AUTO, spacing=1)
|
||||
|
||||
self.controls = [
|
||||
ft.Container(height=8),
|
||||
ft.Row([
|
||||
ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH,
|
||||
on_click=lambda e: self._page.run_task(self._load)),
|
||||
self.search, self.only_failed,
|
||||
]),
|
||||
ft.Row([
|
||||
ft.Container(expand=3, content=ft.Column([self.table],
|
||||
scroll=ft.ScrollMode.AUTO)),
|
||||
ft.VerticalDivider(width=1),
|
||||
ft.Container(expand=2, content=self.log_text),
|
||||
], expand=True),
|
||||
]
|
||||
self._page.run_task(self._load)
|
||||
|
||||
async def _load(self):
|
||||
try:
|
||||
async with SSHClient(self.srv) as c:
|
||||
self._units = await sd.list_units(c)
|
||||
except Exception as e:
|
||||
snack(self._page, f"systemd: {e}", error=True)
|
||||
self._render()
|
||||
|
||||
def _render(self):
|
||||
q = (self.search.value or "").lower()
|
||||
units = self._units
|
||||
if self.only_failed.value:
|
||||
units = [u for u in units if u.active == "failed"]
|
||||
if q:
|
||||
units = [u for u in units if q in u.name.lower() or q in u.description.lower()]
|
||||
|
||||
def state_color(u):
|
||||
return {"active": ft.Colors.GREEN_300, "failed": ft.Colors.RED_400,
|
||||
"inactive": ft.Colors.GREY_500}.get(u.active, ft.Colors.GREY_500)
|
||||
|
||||
self.table.rows = [
|
||||
ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(u.name, size=12, selectable=True)),
|
||||
ft.DataCell(ft.Text(f"{u.active}/{u.sub}", size=12, color=state_color(u))),
|
||||
ft.DataCell(ft.Text(u.enabled or "—", size=11)),
|
||||
ft.DataCell(ft.Text(u.description[:50], size=11)),
|
||||
ft.DataCell(ft.Row([
|
||||
ft.IconButton(ft.Icons.PLAY_ARROW, icon_size=16, tooltip="start",
|
||||
on_click=lambda e, n=u.name: self._act(n, "start")),
|
||||
ft.IconButton(ft.Icons.STOP, icon_size=16, tooltip="stop",
|
||||
on_click=lambda e, n=u.name: self._act(n, "stop")),
|
||||
ft.IconButton(ft.Icons.RESTART_ALT, icon_size=16, tooltip="restart",
|
||||
on_click=lambda e, n=u.name: self._act(n, "restart")),
|
||||
ft.IconButton(ft.Icons.ARTICLE, icon_size=16, tooltip="логи",
|
||||
on_click=lambda e, n=u.name: self._logs(n)),
|
||||
], spacing=0)),
|
||||
]) for u in units[:200]
|
||||
]
|
||||
self._page.update()
|
||||
|
||||
def _act(self, name, action):
|
||||
def do():
|
||||
async def _d():
|
||||
try:
|
||||
async with SSHClient(self.srv) as c:
|
||||
await sd.action(c, name, action)
|
||||
snack(self._page, f"{name}: {action} ✓")
|
||||
await self._load()
|
||||
except Exception as ex:
|
||||
snack(self._page, f"{ex}", error=True)
|
||||
self._page.run_task(_d)
|
||||
if name in sd.CRITICAL_UNITS and action in ("stop", "restart"):
|
||||
confirm_dialog(self._page, f"⚠ {action} {name}?",
|
||||
"Это критичный системный сервис! Можно потерять связь с сервером.", do)
|
||||
else:
|
||||
do()
|
||||
|
||||
def _logs(self, name):
|
||||
async def _l():
|
||||
self.log_text.controls = [ft.Text(f"journalctl -u {name}", weight=ft.FontWeight.BOLD, size=12)]
|
||||
self._page.update()
|
||||
try:
|
||||
async with SSHClient(self.srv) as c:
|
||||
out = await sd.logs(c, name)
|
||||
for line in out.splitlines()[-80:]:
|
||||
self.log_text.controls.append(
|
||||
ft.Text(line, size=10, font_family="Consolas", selectable=True))
|
||||
except Exception as ex:
|
||||
self.log_text.controls.append(ft.Text(f"ошибка: {ex}", size=11))
|
||||
self._page.update()
|
||||
self._page.run_task(_l)
|
||||
Reference in New Issue
Block a user