Move schedule form to separate edit screen and fix crontab sync
- Extract schedule add/edit form into ScheduleEditScreen (follows target_edit pattern) - Fix toggle active: now properly installs/removes crontab entries with error reporting - Delete also syncs crontab to remove deleted schedule entries - Handle case where all schedules deactivated (calls remove instead of install) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from tui.screens.remote_edit import RemoteEditScreen
|
|||||||
from tui.screens.snapshots import SnapshotsScreen
|
from tui.screens.snapshots import SnapshotsScreen
|
||||||
from tui.screens.retention import RetentionScreen
|
from tui.screens.retention import RetentionScreen
|
||||||
from tui.screens.schedule import ScheduleScreen
|
from tui.screens.schedule import ScheduleScreen
|
||||||
|
from tui.screens.schedule_edit import ScheduleEditScreen
|
||||||
from tui.screens.logs import LogsScreen
|
from tui.screens.logs import LogsScreen
|
||||||
from tui.screens.settings import SettingsScreen
|
from tui.screens.settings import SettingsScreen
|
||||||
from tui.screens.wizard import WizardScreen
|
from tui.screens.wizard import WizardScreen
|
||||||
@@ -32,6 +33,7 @@ class GnizaApp(App):
|
|||||||
"snapshots": SnapshotsScreen,
|
"snapshots": SnapshotsScreen,
|
||||||
"retention": RetentionScreen,
|
"retention": RetentionScreen,
|
||||||
"schedule": ScheduleScreen,
|
"schedule": ScheduleScreen,
|
||||||
|
"schedule_edit": ScheduleEditScreen,
|
||||||
"logs": LogsScreen,
|
"logs": LogsScreen,
|
||||||
"settings": SettingsScreen,
|
"settings": SettingsScreen,
|
||||||
"wizard": WizardScreen,
|
"wizard": WizardScreen,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ DataTable {
|
|||||||
/* Form screens */
|
/* Form screens */
|
||||||
#target-edit,
|
#target-edit,
|
||||||
#remote-edit,
|
#remote-edit,
|
||||||
|
#schedule-edit,
|
||||||
#settings-screen,
|
#settings-screen,
|
||||||
#schedule-screen,
|
#schedule-screen,
|
||||||
#backup-screen,
|
#backup-screen,
|
||||||
@@ -93,6 +94,7 @@ SelectionList {
|
|||||||
#remotes-buttons,
|
#remotes-buttons,
|
||||||
#logs-buttons,
|
#logs-buttons,
|
||||||
#sched-buttons,
|
#sched-buttons,
|
||||||
|
#sched-edit-buttons,
|
||||||
#te-buttons,
|
#te-buttons,
|
||||||
#re-buttons,
|
#re-buttons,
|
||||||
#set-buttons {
|
#set-buttons {
|
||||||
@@ -107,6 +109,7 @@ SelectionList {
|
|||||||
#remotes-buttons Button,
|
#remotes-buttons Button,
|
||||||
#logs-buttons Button,
|
#logs-buttons Button,
|
||||||
#sched-buttons Button,
|
#sched-buttons Button,
|
||||||
|
#sched-edit-buttons Button,
|
||||||
#te-buttons Button,
|
#te-buttons Button,
|
||||||
#re-buttons Button,
|
#re-buttons Button,
|
||||||
#set-buttons Button {
|
#set-buttons Button {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from tui.screens.remote_edit import RemoteEditScreen
|
|||||||
from tui.screens.snapshots import SnapshotsScreen
|
from tui.screens.snapshots import SnapshotsScreen
|
||||||
from tui.screens.retention import RetentionScreen
|
from tui.screens.retention import RetentionScreen
|
||||||
from tui.screens.schedule import ScheduleScreen
|
from tui.screens.schedule import ScheduleScreen
|
||||||
|
from tui.screens.schedule_edit import ScheduleEditScreen
|
||||||
from tui.screens.logs import LogsScreen
|
from tui.screens.logs import LogsScreen
|
||||||
from tui.screens.settings import SettingsScreen
|
from tui.screens.settings import SettingsScreen
|
||||||
from tui.screens.wizard import WizardScreen
|
from tui.screens.wizard import WizardScreen
|
||||||
|
|||||||
@@ -1,35 +1,14 @@
|
|||||||
import re
|
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import Header, Footer, Static, Button, DataTable, Input, Select, SelectionList
|
from textual.widgets import Header, Footer, Static, Button, DataTable
|
||||||
from textual.containers import Vertical, Horizontal
|
from textual.containers import Vertical, Horizontal
|
||||||
from textual import work
|
from textual import work
|
||||||
|
|
||||||
from tui.config import list_conf_dir, parse_conf, write_conf, update_conf_key, CONFIG_DIR
|
from tui.config import list_conf_dir, parse_conf, update_conf_key, CONFIG_DIR
|
||||||
from tui.models import Schedule
|
from tui.models import Schedule
|
||||||
from tui.backend import run_cli
|
from tui.backend import run_cli
|
||||||
from tui.widgets import ConfirmDialog, OperationLog
|
from tui.widgets import ConfirmDialog, OperationLog
|
||||||
|
|
||||||
_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$')
|
|
||||||
|
|
||||||
SCHEDULE_TYPES = [
|
|
||||||
("Hourly", "hourly"),
|
|
||||||
("Daily", "daily"),
|
|
||||||
("Weekly", "weekly"),
|
|
||||||
("Monthly", "monthly"),
|
|
||||||
("Custom cron", "custom"),
|
|
||||||
]
|
|
||||||
|
|
||||||
HOURLY_INTERVALS = [
|
|
||||||
("Every Hour", "1"),
|
|
||||||
("Every 2 Hours", "2"),
|
|
||||||
("Every 3 Hours", "3"),
|
|
||||||
("Every 4 Hours", "4"),
|
|
||||||
("Every 6 Hours", "6"),
|
|
||||||
("Every 8 Hours", "8"),
|
|
||||||
("Every 12 Hours", "12"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ScheduleScreen(Screen):
|
class ScheduleScreen(Screen):
|
||||||
|
|
||||||
@@ -47,88 +26,10 @@ class ScheduleScreen(Screen):
|
|||||||
yield Button("Delete", variant="error", id="btn-delete")
|
yield Button("Delete", variant="error", id="btn-delete")
|
||||||
yield Button("Show crontab", id="btn-show")
|
yield Button("Show crontab", id="btn-show")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
yield Static("", id="sched-divider")
|
|
||||||
yield Static("Add Schedule", id="sched-form-title")
|
|
||||||
yield Static("Name:")
|
|
||||||
yield Input(id="sched-name", placeholder="Schedule name")
|
|
||||||
yield Static("Type:")
|
|
||||||
yield Select(SCHEDULE_TYPES, id="sched-type", value="daily")
|
|
||||||
yield Static("Schedule Hours:", classes="sched-hourly-field")
|
|
||||||
yield Select(HOURLY_INTERVALS, id="sched-interval", value="1", classes="sched-hourly-field")
|
|
||||||
yield Static("Time (HH:MM):", classes="sched-time-field")
|
|
||||||
yield Input(id="sched-time", value="02:00", placeholder="02:00", classes="sched-time-field")
|
|
||||||
yield Static("Schedule Days:", classes="sched-daily-days-field")
|
|
||||||
yield SelectionList[str](
|
|
||||||
("Sunday", "0"),
|
|
||||||
("Monday", "1"),
|
|
||||||
("Tuesday", "2"),
|
|
||||||
("Wednesday", "3"),
|
|
||||||
("Thursday", "4"),
|
|
||||||
("Friday", "5"),
|
|
||||||
("Saturday", "6"),
|
|
||||||
id="sched-daily-days",
|
|
||||||
classes="sched-daily-days-field",
|
|
||||||
)
|
|
||||||
yield Static("Schedule Day:", classes="sched-weekly-day-field")
|
|
||||||
yield Select(
|
|
||||||
[("Sunday", "0"), ("Monday", "1"), ("Tuesday", "2"), ("Wednesday", "3"),
|
|
||||||
("Thursday", "4"), ("Friday", "5"), ("Saturday", "6")],
|
|
||||||
id="sched-weekly-day",
|
|
||||||
value="0",
|
|
||||||
classes="sched-weekly-day-field",
|
|
||||||
)
|
|
||||||
yield Static("Schedule Day:", classes="sched-monthly-field")
|
|
||||||
yield Select(
|
|
||||||
[("1st of the month", "1"), ("7th of the month", "7"),
|
|
||||||
("14th of the month", "14"), ("21st of the month", "21"),
|
|
||||||
("28th of the month", "28")],
|
|
||||||
id="sched-monthly-day",
|
|
||||||
value="1",
|
|
||||||
classes="sched-monthly-field",
|
|
||||||
)
|
|
||||||
yield Static("Custom cron (5 fields):", classes="sched-cron-field")
|
|
||||||
yield Input(id="sched-cron", placeholder="0 2 * * *", classes="sched-cron-field")
|
|
||||||
yield Static("Targets (empty=all):")
|
|
||||||
yield SelectionList[str](
|
|
||||||
*self._build_target_choices(),
|
|
||||||
id="sched-targets",
|
|
||||||
)
|
|
||||||
yield Static("Remotes (empty=all):")
|
|
||||||
yield SelectionList[str](
|
|
||||||
*self._build_remote_choices(),
|
|
||||||
id="sched-remotes",
|
|
||||||
)
|
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def _build_target_choices(self) -> list[tuple[str, str]]:
|
|
||||||
return [(name, name) for name in list_conf_dir("targets.d")]
|
|
||||||
|
|
||||||
def _build_remote_choices(self) -> list[tuple[str, str]]:
|
|
||||||
return [(name, name) for name in list_conf_dir("remotes.d")]
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self._refresh_table()
|
self._refresh_table()
|
||||||
self._update_type_visibility()
|
|
||||||
|
|
||||||
def on_select_changed(self, event: Select.Changed) -> None:
|
|
||||||
if event.select.id == "sched-type":
|
|
||||||
self._update_type_visibility()
|
|
||||||
|
|
||||||
def _update_type_visibility(self) -> None:
|
|
||||||
type_sel = self.query_one("#sched-type", Select)
|
|
||||||
stype = str(type_sel.value) if isinstance(type_sel.value, str) else "daily"
|
|
||||||
for w in self.query(".sched-hourly-field"):
|
|
||||||
w.display = stype == "hourly"
|
|
||||||
for w in self.query(".sched-time-field"):
|
|
||||||
w.display = stype in ("daily", "weekly", "monthly")
|
|
||||||
for w in self.query(".sched-daily-days-field"):
|
|
||||||
w.display = stype == "daily"
|
|
||||||
for w in self.query(".sched-weekly-day-field"):
|
|
||||||
w.display = stype == "weekly"
|
|
||||||
for w in self.query(".sched-monthly-field"):
|
|
||||||
w.display = stype == "monthly"
|
|
||||||
for w in self.query(".sched-cron-field"):
|
|
||||||
w.display = stype == "custom"
|
|
||||||
|
|
||||||
def _refresh_table(self) -> None:
|
def _refresh_table(self) -> None:
|
||||||
table = self.query_one("#sched-table", DataTable)
|
table = self.query_one("#sched-table", DataTable)
|
||||||
@@ -151,11 +52,13 @@ class ScheduleScreen(Screen):
|
|||||||
if event.button.id == "btn-back":
|
if event.button.id == "btn-back":
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
elif event.button.id == "btn-add":
|
elif event.button.id == "btn-add":
|
||||||
self._save_schedule()
|
from tui.screens.schedule_edit import ScheduleEditScreen
|
||||||
|
self.app.push_screen(ScheduleEditScreen(), callback=lambda _: self._refresh_table())
|
||||||
elif event.button.id == "btn-edit":
|
elif event.button.id == "btn-edit":
|
||||||
name = self._selected_schedule()
|
name = self._selected_schedule()
|
||||||
if name:
|
if name:
|
||||||
self._load_schedule(name)
|
from tui.screens.schedule_edit import ScheduleEditScreen
|
||||||
|
self.app.push_screen(ScheduleEditScreen(name), callback=lambda _: self._refresh_table())
|
||||||
else:
|
else:
|
||||||
self.notify("Select a schedule first", severity="warning")
|
self.notify("Select a schedule first", severity="warning")
|
||||||
elif event.button.id == "btn-delete":
|
elif event.button.id == "btn-delete":
|
||||||
@@ -176,98 +79,6 @@ class ScheduleScreen(Screen):
|
|||||||
elif event.button.id == "btn-show":
|
elif event.button.id == "btn-show":
|
||||||
self._show_crontab()
|
self._show_crontab()
|
||||||
|
|
||||||
def _load_schedule(self, name: str) -> None:
|
|
||||||
"""Load a schedule's config into the form for editing."""
|
|
||||||
data = parse_conf(CONFIG_DIR / "schedules.d" / f"{name}.conf")
|
|
||||||
s = Schedule.from_conf(name, data)
|
|
||||||
self.query_one("#sched-name", Input).value = name
|
|
||||||
self.query_one("#sched-type", Select).value = s.schedule
|
|
||||||
self.query_one("#sched-time", Input).value = s.time
|
|
||||||
self.query_one("#sched-cron", Input).value = s.cron
|
|
||||||
# Hourly interval
|
|
||||||
if s.schedule == "hourly" and s.day:
|
|
||||||
self.query_one("#sched-interval", Select).value = s.day
|
|
||||||
# Daily days
|
|
||||||
if s.schedule == "daily" and s.day:
|
|
||||||
days_list = self.query_one("#sched-daily-days", SelectionList)
|
|
||||||
day_vals = set(s.day.split(","))
|
|
||||||
for idx in range(days_list.option_count):
|
|
||||||
opt = days_list.get_option_at_index(idx)
|
|
||||||
if opt.value in day_vals:
|
|
||||||
days_list.select(opt.value)
|
|
||||||
else:
|
|
||||||
days_list.deselect(opt.value)
|
|
||||||
# Weekly day
|
|
||||||
if s.schedule == "weekly" and s.day:
|
|
||||||
self.query_one("#sched-weekly-day", Select).value = s.day
|
|
||||||
# Monthly day
|
|
||||||
if s.schedule == "monthly" and s.day:
|
|
||||||
self.query_one("#sched-monthly-day", Select).value = s.day
|
|
||||||
# Targets
|
|
||||||
if s.targets:
|
|
||||||
target_vals = set(s.targets.split(","))
|
|
||||||
tlist = self.query_one("#sched-targets", SelectionList)
|
|
||||||
for idx in range(tlist.option_count):
|
|
||||||
opt = tlist.get_option_at_index(idx)
|
|
||||||
if opt.value in target_vals:
|
|
||||||
tlist.select(opt.value)
|
|
||||||
else:
|
|
||||||
tlist.deselect(opt.value)
|
|
||||||
# Remotes
|
|
||||||
if s.remotes:
|
|
||||||
remote_vals = set(s.remotes.split(","))
|
|
||||||
rlist = self.query_one("#sched-remotes", SelectionList)
|
|
||||||
for idx in range(rlist.option_count):
|
|
||||||
opt = rlist.get_option_at_index(idx)
|
|
||||||
if opt.value in remote_vals:
|
|
||||||
rlist.select(opt.value)
|
|
||||||
else:
|
|
||||||
rlist.deselect(opt.value)
|
|
||||||
self._update_type_visibility()
|
|
||||||
self.query_one("#sched-form-title", Static).update("Edit Schedule")
|
|
||||||
self.notify(f"Editing schedule '{name}'")
|
|
||||||
|
|
||||||
def _save_schedule(self) -> None:
|
|
||||||
name = self.query_one("#sched-name", Input).value.strip()
|
|
||||||
if not name:
|
|
||||||
self.notify("Name is required", severity="error")
|
|
||||||
return
|
|
||||||
if not _NAME_RE.match(name):
|
|
||||||
self.notify("Invalid name.", severity="error")
|
|
||||||
return
|
|
||||||
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
|
|
||||||
type_sel = self.query_one("#sched-type", Select)
|
|
||||||
stype = str(type_sel.value) if isinstance(type_sel.value, str) else "daily"
|
|
||||||
if stype == "hourly":
|
|
||||||
interval_sel = self.query_one("#sched-interval", Select)
|
|
||||||
day_val = str(interval_sel.value) if isinstance(interval_sel.value, str) else "1"
|
|
||||||
elif stype == "daily":
|
|
||||||
selected_days = sorted(self.query_one("#sched-daily-days", SelectionList).selected)
|
|
||||||
day_val = ",".join(selected_days)
|
|
||||||
elif stype == "weekly":
|
|
||||||
wday_sel = self.query_one("#sched-weekly-day", Select)
|
|
||||||
day_val = str(wday_sel.value) if isinstance(wday_sel.value, str) else "0"
|
|
||||||
elif stype == "monthly":
|
|
||||||
mday_sel = self.query_one("#sched-monthly-day", Select)
|
|
||||||
day_val = str(mday_sel.value) if isinstance(mday_sel.value, str) else "1"
|
|
||||||
else:
|
|
||||||
day_val = ""
|
|
||||||
sched = Schedule(
|
|
||||||
name=name,
|
|
||||||
schedule=stype,
|
|
||||||
time=self.query_one("#sched-time", Input).value.strip() or "02:00",
|
|
||||||
day=day_val,
|
|
||||||
cron=self.query_one("#sched-cron", Input).value.strip(),
|
|
||||||
targets=",".join(self.query_one("#sched-targets", SelectionList).selected),
|
|
||||||
remotes=",".join(self.query_one("#sched-remotes", SelectionList).selected),
|
|
||||||
)
|
|
||||||
is_new = not conf.exists()
|
|
||||||
write_conf(conf, sched.to_conf())
|
|
||||||
self.notify(f"Schedule '{name}' {'created' if is_new else 'updated'}.")
|
|
||||||
self._refresh_table()
|
|
||||||
self.query_one("#sched-name", Input).value = ""
|
|
||||||
self.query_one("#sched-form-title", Static).update("Add Schedule")
|
|
||||||
|
|
||||||
def _toggle_active(self, name: str) -> None:
|
def _toggle_active(self, name: str) -> None:
|
||||||
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
|
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
|
||||||
data = parse_conf(conf)
|
data = parse_conf(conf)
|
||||||
@@ -282,7 +93,19 @@ class ScheduleScreen(Screen):
|
|||||||
@work
|
@work
|
||||||
async def _sync_crontab(self) -> None:
|
async def _sync_crontab(self) -> None:
|
||||||
"""Reinstall crontab with only active schedules."""
|
"""Reinstall crontab with only active schedules."""
|
||||||
await run_cli("schedule", "install")
|
# Check if any schedule is active
|
||||||
|
has_active = False
|
||||||
|
for name in list_conf_dir("schedules.d"):
|
||||||
|
data = parse_conf(CONFIG_DIR / "schedules.d" / f"{name}.conf")
|
||||||
|
if data.get("SCHEDULE_ACTIVE", "yes") == "yes":
|
||||||
|
has_active = True
|
||||||
|
break
|
||||||
|
if has_active:
|
||||||
|
rc, stdout, stderr = await run_cli("schedule", "install")
|
||||||
|
else:
|
||||||
|
rc, stdout, stderr = await run_cli("schedule", "remove")
|
||||||
|
if rc != 0:
|
||||||
|
self.notify(f"Crontab sync failed: {stderr or stdout}", severity="error")
|
||||||
|
|
||||||
def _delete_schedule(self, name: str) -> None:
|
def _delete_schedule(self, name: str) -> None:
|
||||||
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
|
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
|
||||||
@@ -290,6 +113,7 @@ class ScheduleScreen(Screen):
|
|||||||
conf.unlink()
|
conf.unlink()
|
||||||
self.notify(f"Schedule '{name}' deleted.")
|
self.notify(f"Schedule '{name}' deleted.")
|
||||||
self._refresh_table()
|
self._refresh_table()
|
||||||
|
self._sync_crontab()
|
||||||
|
|
||||||
@work
|
@work
|
||||||
async def _install_schedules(self) -> None:
|
async def _install_schedules(self) -> None:
|
||||||
|
|||||||
234
tui/screens/schedule_edit.py
Normal file
234
tui/screens/schedule_edit.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import re
|
||||||
|
from textual.app import ComposeResult
|
||||||
|
from textual.screen import Screen
|
||||||
|
from textual.widgets import Header, Footer, Static, Button, Input, Select, SelectionList
|
||||||
|
from textual.containers import Vertical, Horizontal
|
||||||
|
|
||||||
|
from tui.config import list_conf_dir, parse_conf, write_conf, CONFIG_DIR
|
||||||
|
from tui.models import Schedule
|
||||||
|
|
||||||
|
_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$')
|
||||||
|
|
||||||
|
SCHEDULE_TYPES = [
|
||||||
|
("Hourly", "hourly"),
|
||||||
|
("Daily", "daily"),
|
||||||
|
("Weekly", "weekly"),
|
||||||
|
("Monthly", "monthly"),
|
||||||
|
("Custom cron", "custom"),
|
||||||
|
]
|
||||||
|
|
||||||
|
HOURLY_INTERVALS = [
|
||||||
|
("Every Hour", "1"),
|
||||||
|
("Every 2 Hours", "2"),
|
||||||
|
("Every 3 Hours", "3"),
|
||||||
|
("Every 4 Hours", "4"),
|
||||||
|
("Every 6 Hours", "6"),
|
||||||
|
("Every 8 Hours", "8"),
|
||||||
|
("Every 12 Hours", "12"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleEditScreen(Screen):
|
||||||
|
|
||||||
|
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||||
|
|
||||||
|
def __init__(self, name: str = ""):
|
||||||
|
super().__init__()
|
||||||
|
self._edit_name = name
|
||||||
|
self._is_new = not name
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
title = "Add Schedule" if self._is_new else f"Edit Schedule: {self._edit_name}"
|
||||||
|
sched = Schedule()
|
||||||
|
if not self._is_new:
|
||||||
|
data = parse_conf(CONFIG_DIR / "schedules.d" / f"{self._edit_name}.conf")
|
||||||
|
sched = Schedule.from_conf(self._edit_name, data)
|
||||||
|
|
||||||
|
with Vertical(id="schedule-edit"):
|
||||||
|
yield Static(title, id="screen-title")
|
||||||
|
if self._is_new:
|
||||||
|
yield Static("Name:")
|
||||||
|
yield Input(value="", placeholder="Schedule name", id="sched-name")
|
||||||
|
yield Static("Type:")
|
||||||
|
yield Select(SCHEDULE_TYPES, id="sched-type", value=sched.schedule)
|
||||||
|
yield Static("Schedule Hours:", classes="sched-hourly-field")
|
||||||
|
yield Select(
|
||||||
|
HOURLY_INTERVALS,
|
||||||
|
id="sched-interval",
|
||||||
|
value=sched.day if sched.schedule == "hourly" and sched.day else "1",
|
||||||
|
classes="sched-hourly-field",
|
||||||
|
)
|
||||||
|
yield Static("Time (HH:MM):", classes="sched-time-field")
|
||||||
|
yield Input(
|
||||||
|
id="sched-time",
|
||||||
|
value=sched.time,
|
||||||
|
placeholder="02:00",
|
||||||
|
classes="sched-time-field",
|
||||||
|
)
|
||||||
|
yield Static("Schedule Days:", classes="sched-daily-days-field")
|
||||||
|
yield SelectionList[str](
|
||||||
|
("Sunday", "0"),
|
||||||
|
("Monday", "1"),
|
||||||
|
("Tuesday", "2"),
|
||||||
|
("Wednesday", "3"),
|
||||||
|
("Thursday", "4"),
|
||||||
|
("Friday", "5"),
|
||||||
|
("Saturday", "6"),
|
||||||
|
id="sched-daily-days",
|
||||||
|
classes="sched-daily-days-field",
|
||||||
|
)
|
||||||
|
yield Static("Schedule Day:", classes="sched-weekly-day-field")
|
||||||
|
yield Select(
|
||||||
|
[("Sunday", "0"), ("Monday", "1"), ("Tuesday", "2"), ("Wednesday", "3"),
|
||||||
|
("Thursday", "4"), ("Friday", "5"), ("Saturday", "6")],
|
||||||
|
id="sched-weekly-day",
|
||||||
|
value=sched.day if sched.schedule == "weekly" and sched.day else "0",
|
||||||
|
classes="sched-weekly-day-field",
|
||||||
|
)
|
||||||
|
yield Static("Schedule Day:", classes="sched-monthly-field")
|
||||||
|
yield Select(
|
||||||
|
[("1st of the month", "1"), ("7th of the month", "7"),
|
||||||
|
("14th of the month", "14"), ("21st of the month", "21"),
|
||||||
|
("28th of the month", "28")],
|
||||||
|
id="sched-monthly-day",
|
||||||
|
value=sched.day if sched.schedule == "monthly" and sched.day else "1",
|
||||||
|
classes="sched-monthly-field",
|
||||||
|
)
|
||||||
|
yield Static("Custom cron (5 fields):", classes="sched-cron-field")
|
||||||
|
yield Input(
|
||||||
|
id="sched-cron",
|
||||||
|
value=sched.cron,
|
||||||
|
placeholder="0 2 * * *",
|
||||||
|
classes="sched-cron-field",
|
||||||
|
)
|
||||||
|
yield Static("Targets (empty=all):")
|
||||||
|
yield SelectionList[str](
|
||||||
|
*self._build_target_choices(),
|
||||||
|
id="sched-targets",
|
||||||
|
)
|
||||||
|
yield Static("Remotes (empty=all):")
|
||||||
|
yield SelectionList[str](
|
||||||
|
*self._build_remote_choices(),
|
||||||
|
id="sched-remotes",
|
||||||
|
)
|
||||||
|
with Horizontal(id="sched-edit-buttons"):
|
||||||
|
yield Button("Save", variant="primary", id="btn-save")
|
||||||
|
yield Button("Cancel", id="btn-cancel")
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
def _build_target_choices(self) -> list[tuple[str, str]]:
|
||||||
|
return [(name, name) for name in list_conf_dir("targets.d")]
|
||||||
|
|
||||||
|
def _build_remote_choices(self) -> list[tuple[str, str]]:
|
||||||
|
return [(name, name) for name in list_conf_dir("remotes.d")]
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self._update_type_visibility()
|
||||||
|
if not self._is_new:
|
||||||
|
self._load_selections()
|
||||||
|
|
||||||
|
def _load_selections(self) -> None:
|
||||||
|
"""Pre-select items in SelectionLists when editing."""
|
||||||
|
data = parse_conf(CONFIG_DIR / "schedules.d" / f"{self._edit_name}.conf")
|
||||||
|
sched = Schedule.from_conf(self._edit_name, data)
|
||||||
|
# Daily days
|
||||||
|
if sched.schedule == "daily" and sched.day:
|
||||||
|
days_list = self.query_one("#sched-daily-days", SelectionList)
|
||||||
|
day_vals = set(sched.day.split(","))
|
||||||
|
for idx in range(days_list.option_count):
|
||||||
|
opt = days_list.get_option_at_index(idx)
|
||||||
|
if opt.value in day_vals:
|
||||||
|
days_list.select(opt.value)
|
||||||
|
# Targets
|
||||||
|
if sched.targets:
|
||||||
|
target_vals = set(sched.targets.split(","))
|
||||||
|
tlist = self.query_one("#sched-targets", SelectionList)
|
||||||
|
for idx in range(tlist.option_count):
|
||||||
|
opt = tlist.get_option_at_index(idx)
|
||||||
|
if opt.value in target_vals:
|
||||||
|
tlist.select(opt.value)
|
||||||
|
# Remotes
|
||||||
|
if sched.remotes:
|
||||||
|
remote_vals = set(sched.remotes.split(","))
|
||||||
|
rlist = self.query_one("#sched-remotes", SelectionList)
|
||||||
|
for idx in range(rlist.option_count):
|
||||||
|
opt = rlist.get_option_at_index(idx)
|
||||||
|
if opt.value in remote_vals:
|
||||||
|
rlist.select(opt.value)
|
||||||
|
|
||||||
|
def on_select_changed(self, event: Select.Changed) -> None:
|
||||||
|
if event.select.id == "sched-type":
|
||||||
|
self._update_type_visibility()
|
||||||
|
|
||||||
|
def _update_type_visibility(self) -> None:
|
||||||
|
type_sel = self.query_one("#sched-type", Select)
|
||||||
|
stype = str(type_sel.value) if isinstance(type_sel.value, str) else "daily"
|
||||||
|
for w in self.query(".sched-hourly-field"):
|
||||||
|
w.display = stype == "hourly"
|
||||||
|
for w in self.query(".sched-time-field"):
|
||||||
|
w.display = stype in ("daily", "weekly", "monthly")
|
||||||
|
for w in self.query(".sched-daily-days-field"):
|
||||||
|
w.display = stype == "daily"
|
||||||
|
for w in self.query(".sched-weekly-day-field"):
|
||||||
|
w.display = stype == "weekly"
|
||||||
|
for w in self.query(".sched-monthly-field"):
|
||||||
|
w.display = stype == "monthly"
|
||||||
|
for w in self.query(".sched-cron-field"):
|
||||||
|
w.display = stype == "custom"
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
if event.button.id == "btn-cancel":
|
||||||
|
self.dismiss(None)
|
||||||
|
elif event.button.id == "btn-save":
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def _save(self) -> None:
|
||||||
|
if self._is_new:
|
||||||
|
name = self.query_one("#sched-name", Input).value.strip()
|
||||||
|
if not name:
|
||||||
|
self.notify("Name is required", severity="error")
|
||||||
|
return
|
||||||
|
if not _NAME_RE.match(name):
|
||||||
|
self.notify("Invalid name. Use letters, digits, _ - (max 32 chars, start with letter).", severity="error")
|
||||||
|
return
|
||||||
|
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
|
||||||
|
if conf.exists():
|
||||||
|
self.notify(f"Schedule '{name}' already exists.", severity="error")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
name = self._edit_name
|
||||||
|
|
||||||
|
type_sel = self.query_one("#sched-type", Select)
|
||||||
|
stype = str(type_sel.value) if isinstance(type_sel.value, str) else "daily"
|
||||||
|
if stype == "hourly":
|
||||||
|
interval_sel = self.query_one("#sched-interval", Select)
|
||||||
|
day_val = str(interval_sel.value) if isinstance(interval_sel.value, str) else "1"
|
||||||
|
elif stype == "daily":
|
||||||
|
selected_days = sorted(self.query_one("#sched-daily-days", SelectionList).selected)
|
||||||
|
day_val = ",".join(selected_days)
|
||||||
|
elif stype == "weekly":
|
||||||
|
wday_sel = self.query_one("#sched-weekly-day", Select)
|
||||||
|
day_val = str(wday_sel.value) if isinstance(wday_sel.value, str) else "0"
|
||||||
|
elif stype == "monthly":
|
||||||
|
mday_sel = self.query_one("#sched-monthly-day", Select)
|
||||||
|
day_val = str(mday_sel.value) if isinstance(mday_sel.value, str) else "1"
|
||||||
|
else:
|
||||||
|
day_val = ""
|
||||||
|
|
||||||
|
sched = Schedule(
|
||||||
|
name=name,
|
||||||
|
schedule=stype,
|
||||||
|
time=self.query_one("#sched-time", Input).value.strip() or "02:00",
|
||||||
|
day=day_val,
|
||||||
|
cron=self.query_one("#sched-cron", Input).value.strip(),
|
||||||
|
targets=",".join(self.query_one("#sched-targets", SelectionList).selected),
|
||||||
|
remotes=",".join(self.query_one("#sched-remotes", SelectionList).selected),
|
||||||
|
)
|
||||||
|
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
|
||||||
|
write_conf(conf, sched.to_conf())
|
||||||
|
self.notify(f"Schedule '{name}' saved.")
|
||||||
|
self.dismiss(name)
|
||||||
|
|
||||||
|
def action_cancel(self) -> None:
|
||||||
|
self.dismiss(None)
|
||||||
Reference in New Issue
Block a user