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:
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user