"""Планировщик: задачи по cron, пресеты, запуск.""" import flet as ft from croniter import croniter from app.core import scheduler as sch from app.ui.components import confirm_dialog, snack class SchedulerPage(ft.Column): def __init__(self, page: ft.Page, go): super().__init__(expand=True, scroll=ft.ScrollMode.AUTO, spacing=12) self._page = page self.table = ft.DataTable(columns=[ ft.DataColumn(ft.Text("Вкл")), ft.DataColumn(ft.Text("Название")), ft.DataColumn(ft.Text("Расписание")), ft.DataColumn(ft.Text("Действие")), ft.DataColumn(ft.Text("Цель")), ft.DataColumn(ft.Text("Последний запуск")), ft.DataColumn(ft.Text("Статус")), ft.DataColumn(ft.Text("")), ], rows=[]) self.controls = [ ft.Row([ ft.Text("Планировщик", size=28, weight=ft.FontWeight.BOLD), ft.FilledButton("Добавить задачу", icon=ft.Icons.ADD, on_click=self._add_dialog), ft.OutlinedButton("Из пресета", icon=ft.Icons.LIST_ALT, on_click=self._preset_dialog), ft.IconButton(ft.Icons.REFRESH, on_click=lambda e: self._reload()), ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), ft.Card(content=ft.Container(padding=10, content=self.table)), ] self._reload() def _reload(self): self.table.rows = [] for t in sch.list_tasks(): try: nxt = croniter(t.cron).get_next(__import__("datetime").datetime) sched_txt = f"{t.cron} → {nxt:%d.%m %H:%M}" except Exception: sched_txt = f"{t.cron} (неверно)" self.table.rows.append(ft.DataRow(cells=[ ft.DataCell(ft.Checkbox(value=t.enabled, on_change=lambda e, t=t: self._toggle(t))), ft.DataCell(ft.Text(t.name, weight=ft.FontWeight.BOLD)), ft.DataCell(ft.Text(sched_txt, size=11)), ft.DataCell(ft.Text(t.action, size=11)), ft.DataCell(ft.Text(t.target, size=11)), ft.DataCell(ft.Text(f"{t.last_run:%d.%m %H:%M}" if t.last_run else "—", size=11)), ft.DataCell(ft.Text(t.last_status or "—", size=11, color=ft.Colors.GREEN_300 if t.last_status == "success" else ft.Colors.RED_300 if t.last_status == "error" else ft.Colors.GREY_500)), ft.DataCell(ft.Row([ ft.IconButton(ft.Icons.PLAY_ARROW, icon_size=16, tooltip="Запустить сейчас", on_click=lambda e, t=t: self._run_now(t)), ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_size=16, on_click=lambda e, t=t: self._delete(t)), ], spacing=0)), ])) self._page.update() def _toggle(self, t): sch.update_task(t.id, enabled=not t.enabled) self._reload() def _run_now(self, t): async def _r(): status = await sch.execute_task(t.id) snack(self._page, f"{t.name}: {status}", error=status != "success") self._reload() self._page.run_task(_r) def _delete(self, t): def do(): sch.delete_task(t.id) self._reload() confirm_dialog(self._page, f"Удалить задачу «{t.name}»?", "", do) def _add_dialog(self, e): self._form("Новая задача", None) def _form(self, title, t): name_tf = ft.TextField(label="Название", value=t.name if t else "", width=350) cron_tf = ft.TextField(label="Cron (мин час день мес день_нед)", value=t.cron if t else "0 3 * * *", width=350) hint = ft.Text("", size=11, color=ft.Colors.GREY_500) def upd(e): try: nxt = croniter(cron_tf.value).get_next(__import__("datetime").datetime) hint.value = f"следующий запуск: {nxt:%Y-%m-%d %H:%M}" hint.color = ft.Colors.GREY_500 except Exception: hint.value = "неверное выражение" hint.color = ft.Colors.RED_300 dlg.content.update() cron_tf.on_change = upd action_dd = ft.Dropdown(label="Действие", value=t.action if t else "apt_update", width=350, options=[ ft.dropdown.Option("apt_update", "apt update"), ft.dropdown.Option("upgrade", "upgrade"), ft.dropdown.Option("dist_upgrade", "dist-upgrade"), ft.dropdown.Option("shell", "команда"), ft.dropdown.Option("backup", "бэкап"), ft.dropdown.Option("playbook", "плейбук"), ft.dropdown.Option("report", "отчёт"), ft.dropdown.Option("cleanup_metrics", "очистка метрик"), ]) target_tf = ft.TextField(label="Цель (all или тег)", value=t.target if t else "all", width=350) params_tf = ft.TextField(label="Параметры (команда / пути / плейбук)", value=t.params if t else "", width=350) def save(e): if "неверное" in (hint.value or ""): snack(self._page, "Исправьте cron-выражение", error=True) return fields = dict(name=name_tf.value, cron=cron_tf.value, action=action_dd.value, target=target_tf.value, params=params_tf.value) if t: sch.update_task(t.id, **fields) else: sch.add_task(**fields) self._page.pop_dialog() self._reload() snack(self._page, "Сохранено") dlg = ft.AlertDialog( modal=True, title=ft.Text(title), content=ft.Column([name_tf, cron_tf, hint, action_dd, target_tf, params_tf], tight=True), actions=[ ft.TextButton("Отмена", on_click=lambda e: self._page.pop_dialog()), ft.FilledButton("Сохранить", on_click=save), ], ) self._page.show_dialog(dlg) def _preset_dialog(self, e): def add(idx): name, cron, action, target, params = sch.PRESETS[idx] sch.add_task(name=name, cron=cron, action=action, target=target, params=params) self._page.pop_dialog() self._reload() snack(self._page, f"Добавлено: {name}") dlg = ft.AlertDialog( modal=True, title=ft.Text("Пресеты"), content=ft.Column([ ft.TextButton(f"{name} ({cron})", on_click=lambda e, i=i: add(i)) for i, (name, cron, *_ ) in enumerate(sch.PRESETS) ], tight=True), actions=[ft.TextButton("Отмена", on_click=lambda e: self._page.pop_dialog())], ) self._page.show_dialog(dlg)