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,172 @@
|
||||
"""Вкладка Cron: редактор crontab."""
|
||||
import flet as ft
|
||||
|
||||
from app.core import cron_svc as cron
|
||||
from app.core.ssh import SSHClient
|
||||
from app.db.models import Server
|
||||
from app.ui.components import confirm_dialog, snack
|
||||
|
||||
|
||||
class CronTab(ft.Column):
|
||||
def __init__(self, page: ft.Page, srv: Server):
|
||||
super().__init__(expand=True, spacing=8, scroll=ft.ScrollMode.AUTO)
|
||||
self._page = page
|
||||
self.srv = srv
|
||||
self._lines: list[cron.CronLine] = []
|
||||
self._user = "" # пусто = текущий sudo-пользовательский контекст
|
||||
|
||||
self.entries_col = ft.Column(spacing=4)
|
||||
self.controls = [
|
||||
ft.Container(height=8),
|
||||
ft.Row([
|
||||
ft.FilledButton("Добавить задачу", icon=ft.Icons.ADD,
|
||||
on_click=self._add_dialog),
|
||||
ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH,
|
||||
on_click=lambda e: self._page.run_task(self._load)),
|
||||
ft.OutlinedButton("Откатить к бэкапу", icon=ft.Icons.HISTORY,
|
||||
on_click=self._restore_dialog),
|
||||
]),
|
||||
self.entries_col,
|
||||
]
|
||||
self._page.run_task(self._load)
|
||||
|
||||
async def _load(self):
|
||||
try:
|
||||
async with SSHClient(self.srv) as c:
|
||||
content = await cron.get_crontab(c)
|
||||
self._lines = cron.parse_crontab(content)
|
||||
except Exception as e:
|
||||
snack(self._page, f"cron: {e}", error=True)
|
||||
self._render()
|
||||
|
||||
def _render(self):
|
||||
self.entries_col.controls = []
|
||||
for i, l in enumerate(self._lines):
|
||||
if not l.is_entry:
|
||||
if l.raw.strip() and not l.raw.strip().startswith(("#", "SHELL", "PATH", "MAILTO")):
|
||||
self.entries_col.controls.append(
|
||||
ft.Text(l.raw, size=11, color=ft.Colors.GREY_600,
|
||||
font_family="Consolas"))
|
||||
continue
|
||||
self.entries_col.controls.append(
|
||||
ft.Card(content=ft.Container(padding=8, content=ft.Row([
|
||||
ft.Checkbox(value=l.enabled,
|
||||
on_change=lambda e, idx=i: self._toggle(idx)),
|
||||
ft.Column([
|
||||
ft.Text(l.schedule, size=13, weight=ft.FontWeight.BOLD,
|
||||
font_family="Consolas"),
|
||||
ft.Text(l.command, size=12, font_family="Consolas"),
|
||||
ft.Text(cron.humanize(l.schedule), size=10,
|
||||
color=ft.Colors.GREY_500),
|
||||
], spacing=1, expand=True),
|
||||
ft.IconButton(ft.Icons.EDIT, icon_size=16,
|
||||
on_click=lambda e, idx=i: self._edit_dialog(idx)),
|
||||
ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_size=16,
|
||||
on_click=lambda e, idx=i: self._delete(idx)),
|
||||
])))
|
||||
)
|
||||
if not any(l.is_entry for l in self._lines):
|
||||
self.entries_col.controls.append(
|
||||
ft.Text("crontab пуст", color=ft.Colors.GREY_500))
|
||||
self._page.update()
|
||||
|
||||
async def _save(self):
|
||||
content = cron.render(self._lines)
|
||||
async with SSHClient(self.srv) as c:
|
||||
await cron.set_crontab(c, content, self.srv.id)
|
||||
snack(self._page, "crontab сохранён (бэкап записан)")
|
||||
|
||||
def _toggle(self, idx):
|
||||
self._lines[idx].enabled = not self._lines[idx].enabled
|
||||
self._page.run_task(self._save)
|
||||
|
||||
def _delete(self, idx):
|
||||
def do():
|
||||
del self._lines[idx]
|
||||
self._page.run_task(self._save_and_reload)
|
||||
confirm_dialog(self._page, "Удалить задачу?", self._lines[idx].command[:80], do)
|
||||
|
||||
async def _save_and_reload(self):
|
||||
await self._save()
|
||||
await self._load()
|
||||
|
||||
def _edit_dialog(self, idx):
|
||||
l = self._lines[idx]
|
||||
sched_tf = ft.TextField(label="Расписание (мин час день мес день_нед)",
|
||||
value=l.schedule, width=400)
|
||||
cmd_tf = ft.TextField(label="Команда", value=l.command, width=400)
|
||||
hint = ft.Text(cron.humanize(l.schedule), size=11, color=ft.Colors.GREY_500)
|
||||
|
||||
def upd_hint(e):
|
||||
hint.value = cron.humanize(sched_tf.value)
|
||||
dlg.content.update()
|
||||
sched_tf.on_change = upd_hint
|
||||
|
||||
def save(e):
|
||||
if "неверное" in hint.value:
|
||||
snack(self._page, "Неверное cron-выражение", error=True)
|
||||
return
|
||||
self._lines[idx] = cron.CronLine("", l.enabled, sched_tf.value.strip(),
|
||||
cmd_tf.value.strip())
|
||||
self._page.pop_dialog()
|
||||
self._page.run_task(self._save_and_reload)
|
||||
|
||||
dlg = ft.AlertDialog(
|
||||
modal=True, title=ft.Text("Редактировать задачу"),
|
||||
content=ft.Column([sched_tf, cmd_tf, hint], tight=True),
|
||||
actions=[ft.TextButton("Отмена", on_click=lambda e: self._close(dlg)),
|
||||
ft.FilledButton("Сохранить", on_click=save)],
|
||||
)
|
||||
self._page.show_dialog(dlg)
|
||||
|
||||
def _add_dialog(self, e):
|
||||
sched_tf = ft.TextField(label="Расписание", hint_text="0 3 * * *", width=400)
|
||||
cmd_tf = ft.TextField(label="Команда", width=400)
|
||||
hint = ft.Text("", size=11, color=ft.Colors.GREY_500)
|
||||
|
||||
def upd_hint(ev):
|
||||
hint.value = cron.humanize(sched_tf.value)
|
||||
dlg.content.update()
|
||||
sched_tf.on_change = upd_hint
|
||||
|
||||
def save(e):
|
||||
if not sched_tf.value or not cmd_tf.value:
|
||||
return
|
||||
self._lines.append(cron.CronLine("", True, sched_tf.value.strip(),
|
||||
cmd_tf.value.strip()))
|
||||
self._page.pop_dialog()
|
||||
self._page.run_task(self._save_and_reload)
|
||||
|
||||
dlg = ft.AlertDialog(
|
||||
modal=True, title=ft.Text("Новая cron-задача"),
|
||||
content=ft.Column([sched_tf, cmd_tf, hint], tight=True),
|
||||
actions=[ft.TextButton("Отмена", on_click=lambda e: self._close(dlg)),
|
||||
ft.FilledButton("Добавить", on_click=save)],
|
||||
)
|
||||
self._page.show_dialog(dlg)
|
||||
|
||||
def _restore_dialog(self, e):
|
||||
backups = cron.backups(self.srv.id, "current")
|
||||
if not backups:
|
||||
snack(self._page, "Бэкапов пока нет", error=True)
|
||||
return
|
||||
def restore(b):
|
||||
self._page.pop_dialog()
|
||||
async def _r():
|
||||
async with SSHClient(self.srv) as c:
|
||||
await cron.set_crontab(c, b.content, self.srv.id)
|
||||
await self._load()
|
||||
self._page.run_task(_r)
|
||||
dlg = ft.AlertDialog(
|
||||
modal=True, title=ft.Text("Откатить к бэкапу"),
|
||||
content=ft.Column([
|
||||
ft.TextButton(f"{b.created_at:%Y-%m-%d %H:%M} ({len(b.content)} символов)",
|
||||
on_click=lambda e, b=b: restore(b))
|
||||
for b in backups
|
||||
], tight=True),
|
||||
actions=[ft.TextButton("Отмена", on_click=lambda e: self._close(dlg))],
|
||||
)
|
||||
self._page.show_dialog(dlg)
|
||||
|
||||
def _close(self, dlg):
|
||||
self._page.pop_dialog()
|
||||
Reference in New Issue
Block a user