162 lines
7.4 KiB
Python
162 lines
7.4 KiB
Python
|
|
"""Планировщик: задачи по 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)
|