Files
debian-manager/app/ui/pages/scheduler.py
T
dinlo f624fee5e0 Initial commit: Debian Manager — управление Debian-серверами
- SSH/SFTP управление без агентов
- Пакеты (apt), Docker, файлы, systemd, ufw, cron
- Плейбуки, групповые операции, планировщик
- Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy
- Терминал, Telegram-уведомления, тёмная/светлая тема
2026-09-05 19:46:45 +08:00

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)