Add Last Run and Next Run columns to Schedules screen

- Last Run: timestamp of most recent backup log file
- Next Run: calculated from schedule type, time, and day settings
- Handles hourly, daily, weekly, monthly schedule types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-06 06:31:49 +02:00
parent 220d30e515
commit 5c1fa04657

View File

@@ -1,10 +1,12 @@
from datetime import datetime, timedelta
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 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, update_conf_key, CONFIG_DIR from tui.config import list_conf_dir, parse_conf, update_conf_key, CONFIG_DIR, LOG_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
@@ -34,13 +36,69 @@ 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", "Active", "Type", "Time", "Targets", "Remotes") table.add_columns("Name", "Active", "Type", "Time", "Last Run", "Next Run", "Targets", "Remotes")
last_run = self._get_last_run()
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)
active = "" if s.active == "yes" else "" 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) next_run = self._calc_next_run(s) if s.active == "yes" else "--"
table.add_row(name, active, s.schedule, s.time, last_run, next_run, s.targets or "all", s.remotes or "all", key=name)
def _get_last_run(self) -> str:
"""Get the timestamp of the most recent backup log."""
from pathlib import Path
log_dir = Path(str(LOG_DIR))
if not log_dir.is_dir():
return "--"
logs = sorted(log_dir.glob("gniza-*.log"), key=lambda p: p.stat().st_mtime, reverse=True)
if not logs:
return "--"
mtime = logs[0].stat().st_mtime
dt = datetime.fromtimestamp(mtime)
return dt.strftime("%Y-%m-%d %H:%M")
def _calc_next_run(self, s: Schedule) -> str:
"""Calculate the next run time from schedule config."""
now = datetime.now()
try:
hour, minute = (int(x) for x in s.time.split(":")) if s.time else (2, 0)
except (ValueError, IndexError):
hour, minute = 2, 0
if s.schedule == "hourly":
next_dt = now.replace(minute=minute, second=0, microsecond=0)
if next_dt <= now:
next_dt += timedelta(hours=1)
elif s.schedule == "daily":
next_dt = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
if next_dt <= now:
next_dt += timedelta(days=1)
elif s.schedule == "weekly":
try:
target_dow = int(s.day) if s.day else 0
except ValueError:
target_dow = 0
next_dt = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
days_ahead = (target_dow - now.weekday()) % 7
if days_ahead == 0 and next_dt <= now:
days_ahead = 7
next_dt += timedelta(days=days_ahead)
elif s.schedule == "monthly":
try:
target_dom = int(s.day) if s.day else 1
except ValueError:
target_dom = 1
next_dt = now.replace(day=target_dom, hour=hour, minute=minute, second=0, microsecond=0)
if next_dt <= now:
if now.month == 12:
next_dt = next_dt.replace(year=now.year + 1, month=1)
else:
next_dt = next_dt.replace(month=now.month + 1)
else:
return "--"
return next_dt.strftime("%Y-%m-%d %H:%M")
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)