"""Вкладка Сервисы: 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)