Add per-schedule active toggle with crontab sync

Each schedule has SCHEDULE_ACTIVE field (yes/no). Table shows active
status with checkmark/cross. Toggle Active button flips state and
reinstalls crontab with only active schedules. Inactive schedules
are skipped during crontab install.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-06 04:38:51 +02:00
parent a8d67160a5
commit 1425c416eb
4 changed files with 37 additions and 32 deletions

View File

@@ -56,6 +56,7 @@ load_schedule() {
SCHEDULE_TIME="" SCHEDULE_TIME=""
SCHEDULE_DAY="" SCHEDULE_DAY=""
SCHEDULE_CRON="" SCHEDULE_CRON=""
SCHEDULE_ACTIVE="yes"
SCHEDULE_REMOTES="" SCHEDULE_REMOTES=""
SCHEDULE_TARGETS="" SCHEDULE_TARGETS=""
@@ -191,6 +192,11 @@ install_schedules() {
continue continue
fi fi
if [[ "${SCHEDULE_ACTIVE:-yes}" != "yes" ]]; then
log_debug "Schedule '$sname' is inactive, skipping"
continue
fi
local cron_line local cron_line
cron_line=$(build_cron_line "$sname") || { log_error "Skipping schedule '$sname': invalid schedule"; continue; } cron_line=$(build_cron_line "$sname") || { log_error "Skipping schedule '$sname': invalid schedule"; continue; }

View File

@@ -256,17 +256,6 @@ Switch {
margin: 0 1; margin: 0 1;
} }
#sched-active-row {
height: auto;
align: left middle;
margin: 1 0;
}
#sched-active-label {
width: auto;
margin: 0 1 0 0;
}
.section-label { .section-label {
text-style: bold; text-style: bold;
color: #00cc00; color: #00cc00;

View File

@@ -157,6 +157,7 @@ class Schedule:
cron: str = "" cron: str = ""
targets: str = "" targets: str = ""
remotes: str = "" remotes: str = ""
active: str = "yes"
def to_conf(self) -> dict[str, str]: def to_conf(self) -> dict[str, str]:
return { return {
@@ -164,6 +165,7 @@ class Schedule:
"SCHEDULE_TIME": self.time, "SCHEDULE_TIME": self.time,
"SCHEDULE_DAY": self.day, "SCHEDULE_DAY": self.day,
"SCHEDULE_CRON": self.cron, "SCHEDULE_CRON": self.cron,
"SCHEDULE_ACTIVE": self.active,
"TARGETS": self.targets, "TARGETS": self.targets,
"REMOTES": self.remotes, "REMOTES": self.remotes,
} }
@@ -178,6 +180,7 @@ class Schedule:
cron=data.get("SCHEDULE_CRON", ""), cron=data.get("SCHEDULE_CRON", ""),
targets=data.get("TARGETS", ""), targets=data.get("TARGETS", ""),
remotes=data.get("REMOTES", ""), remotes=data.get("REMOTES", ""),
active=data.get("SCHEDULE_ACTIVE", "yes"),
) )

View File

@@ -1,11 +1,11 @@
import re 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, Switch from textual.widgets import Header, Footer, Static, Button, DataTable, Input, Select, SelectionList
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, CONFIG_DIR from tui.config import list_conf_dir, parse_conf, write_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
@@ -40,12 +40,10 @@ class ScheduleScreen(Screen):
with Vertical(id="schedule-screen"): with Vertical(id="schedule-screen"):
yield Static("Schedules", id="screen-title") yield Static("Schedules", id="screen-title")
yield DataTable(id="sched-table") yield DataTable(id="sched-table")
with Horizontal(id="sched-active-row"):
yield Static("Active (crontab):", id="sched-active-label")
yield Switch(id="sched-active")
with Horizontal(id="sched-buttons"): with Horizontal(id="sched-buttons"):
yield Button("Add", variant="primary", id="btn-add") yield Button("Add", variant="primary", id="btn-add")
yield Button("Edit", id="btn-edit") yield Button("Edit", id="btn-edit")
yield Button("Toggle Active", variant="warning", id="btn-toggle")
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")
@@ -111,20 +109,6 @@ class ScheduleScreen(Screen):
def on_mount(self) -> None: def on_mount(self) -> None:
self._refresh_table() self._refresh_table()
self._update_type_visibility() self._update_type_visibility()
self._check_crontab_status()
@work
async def _check_crontab_status(self) -> None:
rc, stdout, stderr = await run_cli("schedule", "show")
has_entries = bool(stdout.strip()) and "no gniza" not in stdout.lower()
self.query_one("#sched-active", Switch).value = has_entries
def on_switch_changed(self, event: Switch.Changed) -> None:
if event.switch.id == "sched-active":
if event.value:
self._install_schedules()
else:
self._remove_schedules()
def on_select_changed(self, event: Select.Changed) -> None: def on_select_changed(self, event: Select.Changed) -> None:
if event.select.id == "sched-type": if event.select.id == "sched-type":
@@ -149,12 +133,13 @@ class ScheduleScreen(Screen):
def _refresh_table(self) -> None: def _refresh_table(self) -> None:
table = self.query_one("#sched-table", DataTable) table = self.query_one("#sched-table", DataTable)
table.clear(columns=True) table.clear(columns=True)
table.add_columns("Name", "Type", "Time", "Targets", "Remotes") table.add_columns("Name", "Active", "Type", "Time", "Targets", "Remotes")
schedules = list_conf_dir("schedules.d") schedules = list_conf_dir("schedules.d")
for name in schedules: for name in schedules:
data = parse_conf(CONFIG_DIR / "schedules.d" / f"{name}.conf") data = parse_conf(CONFIG_DIR / "schedules.d" / f"{name}.conf")
s = Schedule.from_conf(name, data) s = Schedule.from_conf(name, data)
table.add_row(name, s.schedule, s.time, s.targets or "all", s.remotes or "all", key=name) active = "" if s.active == "yes" else ""
table.add_row(name, active, s.schedule, s.time, s.targets or "all", s.remotes or "all", key=name)
def _selected_schedule(self) -> str | None: def _selected_schedule(self) -> str | None:
table = self.query_one("#sched-table", DataTable) table = self.query_one("#sched-table", DataTable)
@@ -182,6 +167,12 @@ class ScheduleScreen(Screen):
) )
else: else:
self.notify("Select a schedule first", severity="warning") self.notify("Select a schedule first", severity="warning")
elif event.button.id == "btn-toggle":
name = self._selected_schedule()
if name:
self._toggle_active(name)
else:
self.notify("Select a schedule first", severity="warning")
elif event.button.id == "btn-show": elif event.button.id == "btn-show":
self._show_crontab() self._show_crontab()
@@ -277,6 +268,22 @@ class ScheduleScreen(Screen):
self.query_one("#sched-name", Input).value = "" self.query_one("#sched-name", Input).value = ""
self.query_one("#sched-form-title", Static).update("Add Schedule") self.query_one("#sched-form-title", Static).update("Add Schedule")
def _toggle_active(self, name: str) -> None:
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
data = parse_conf(conf)
current = data.get("SCHEDULE_ACTIVE", "yes")
new_val = "no" if current == "yes" else "yes"
update_conf_key(conf, "SCHEDULE_ACTIVE", new_val)
state = "activated" if new_val == "yes" else "deactivated"
self.notify(f"Schedule '{name}' {state}")
self._refresh_table()
self._sync_crontab()
@work
async def _sync_crontab(self) -> None:
"""Reinstall crontab with only active schedules."""
await run_cli("schedule", "install")
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"
if conf.is_file(): if conf.is_file():