diff --git a/bin/gniza b/bin/gniza index 55caeca..60eccf3 100755 --- a/bin/gniza +++ b/bin/gniza @@ -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}" ;; diff --git a/lib/schedule.sh b/lib/schedule.sh index c78dbc2..754df55 100644 --- a/lib/schedule.sh +++ b/lib/schedule.sh @@ -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 ──────────────────────────────────────── diff --git a/tui/screens/schedule.py b/tui/screens/schedule.py index f49f151..6032808 100644 --- a/tui/screens/schedule.py +++ b/tui/screens/schedule.py @@ -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()