Track per-schedule last run time, only stamp on successful backup

Replace global last-run (based on log file mtime) with per-schedule
LAST_RUN field in the schedule config. New scheduled-run CLI command
runs backup and stamps LAST_RUN only on success.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-07 05:10:34 +02:00
parent 35db07c69d
commit 8486690011
3 changed files with 41 additions and 17 deletions

View File

@@ -146,7 +146,7 @@ fi
# Only create log files for operations that produce meaningful output
case "${SUBCOMMAND:-}" in
backup|restore|retention) init_logging ;;
backup|restore|retention|scheduled-run) init_logging ;;
esac
# ── Parse subcommand flags helper ────────────────────────────
@@ -499,6 +499,43 @@ run_cli() {
fi
;;
scheduled-run)
local sched_name="" target="" remote=""
sched_name=$(_parse_flag "--schedule" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
target=$(_parse_flag "--target" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
remote=$(_parse_flag "--remote" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
[[ -z "$sched_name" ]] && die "scheduled-run requires --schedule=NAME"
trap release_all_target_locks EXIT
local rc=0
if [[ -n "$target" && "$target" == *,* ]]; then
local IFS=','
local names
read -ra names <<< "$target"
for t in "${names[@]}"; do
t="${t#"${t%%[![:space:]]*}"}"
t="${t%"${t##*[![:space:]]}"}"
[[ -z "$t" ]] && continue
backup_target "$t" "$remote" || rc=$?
done
elif [[ -n "$target" ]]; then
backup_target "$target" "$remote" || rc=$?
else
backup_all_targets "$remote" || rc=$?
fi
# Stamp LAST_RUN on success
if (( rc == 0 )); then
local conf="$CONFIG_DIR/schedules.d/${sched_name}.conf"
if [[ -f "$conf" ]]; then
sed -i '/^LAST_RUN=/d' "$conf"
echo "LAST_RUN=\"$(date '+%Y-%m-%d %H:%M')\"" >> "$conf"
fi
fi
exit "$rc"
;;
version)
echo "gniza v${GNIZA4LINUX_VERSION}"
;;

View File

@@ -185,7 +185,7 @@ build_cron_line() {
extra_flags+=" --target=$SCHEDULE_TARGETS"
fi
echo "$cron_expr PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\" $bin_path backup${extra_flags} >>\"${LOG_DIR}/cron.log\" 2>&1"
echo "${cron_expr} PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\" ${bin_path} scheduled-run --schedule=${name}${extra_flags} >>\"${LOG_DIR}/cron.log\" 2>&1"
}
# ── Crontab Management ────────────────────────────────────────

View File

@@ -7,7 +7,7 @@ from tui.widgets.header import GnizaHeader as Header # noqa: F811
from textual.containers import Vertical, Horizontal
from textual import work
from tui.config import list_conf_dir, parse_conf, update_conf_key, CONFIG_DIR, LOG_DIR
from tui.config import list_conf_dir, parse_conf, update_conf_key, CONFIG_DIR
from tui.models import Schedule
from tui.backend import run_cli
from tui.widgets import ConfirmDialog, OperationLog, DocsPanel
@@ -40,28 +40,15 @@ class ScheduleScreen(Screen):
table = self.query_one("#sched-table", DataTable)
table.clear(columns=True)
table.add_columns("Name", "Active", "Type", "Time", "Last Run", "Next Run", "Sources", "Destinations")
last_run = self._get_last_run()
schedules = list_conf_dir("schedules.d")
for name in schedules:
data = parse_conf(CONFIG_DIR / "schedules.d" / f"{name}.conf")
s = Schedule.from_conf(name, data)
active = "" if s.active == "yes" else ""
last_run = data.get("LAST_RUN", "never") or "never"
next_run = self._calc_next_run(s) if s.active == "yes" else "inactive"
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 "never"
logs = sorted(log_dir.glob("gniza-*.log"), key=lambda p: p.stat().st_mtime, reverse=True)
if not logs:
return "never"
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()