Log scheduled refresh lifecycle to stdout
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 0.31.1 - 2026-05-17
|
||||
|
||||
- Added stdout lifecycle logging for scheduled watchlist refresh jobs so container logs now show when a scheduled run is queued, starts, finishes successfully, finishes with errors, gets interrupted, or fails.
|
||||
|
||||
## 0.31.0 - 2026-05-17
|
||||
|
||||
- Added scheduled background watchlist refresh support so the app can periodically recheck episode counts for tracked anime.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A local web UI for a system-wide `ani-cli` install.
|
||||
|
||||
Current version: `0.31.0`
|
||||
Current version: `0.31.1`
|
||||
|
||||
## Project Layout
|
||||
|
||||
@@ -158,6 +158,7 @@ The app currently includes:
|
||||
- The watchlist page now also avoids re-triggering the same unresolved thumbnail warm-up request on every silent poll cycle while a show's thumbnail state is unchanged.
|
||||
- Background `Refresh All` execution with status polling, parallel per-show refresh work, and no inline thumbnail downloads during the bulk job.
|
||||
- Optional scheduled background watchlist refreshes with Config page controls for enable/disable and refresh period.
|
||||
- Scheduled background refresh runs now log queue, start, and completion status to stdout so container logs show whether they finished successfully, with errors, or failed.
|
||||
- Search and Watchlist polling now use a shared serial browser polling helper so slow responses do not cause overlapping poll requests to pile up in the background.
|
||||
- That shared browser polling helper now also tears itself down on page unload and uses chained `setTimeout` scheduling instead of a bare repeating interval.
|
||||
- Controlled shutdown and runtime reset now cancel active download workers when waiting for teardown, reducing the chance of orphaned background downloads outliving the web process.
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ from uuid import uuid4
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
|
||||
APP_NAME = "ani-cli-web"
|
||||
VERSION = "0.31.0"
|
||||
VERSION = "0.31.1"
|
||||
ALLANIME_BASE = "allanime.day"
|
||||
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
||||
ALLANIME_REFERER = "https://allmanga.to"
|
||||
|
||||
+31
-2
@@ -178,6 +178,9 @@ class WatchlistRefreshJobs:
|
||||
def _copy_job(self, job):
|
||||
return json.loads(json.dumps(job))
|
||||
|
||||
def _stdout_log(self, message):
|
||||
print(f"[watchlist-refresh] {message}", flush=True)
|
||||
|
||||
def _find_locked(self, job_id):
|
||||
for job in self.jobs:
|
||||
if job["id"] == job_id:
|
||||
@@ -192,10 +195,15 @@ class WatchlistRefreshJobs:
|
||||
active = bool(job and job["status"] in {"pending", "running"})
|
||||
return {"job": self._copy_job(job) if job else None, "running": active}
|
||||
|
||||
def start(self):
|
||||
def start(self, source="manual"):
|
||||
source_name = str(source or "manual").strip().lower() or "manual"
|
||||
with self.lock:
|
||||
job = next((candidate for candidate in self.jobs if candidate["status"] in {"pending", "running"}), None)
|
||||
if job is not None:
|
||||
if source_name == "scheduled":
|
||||
self._stdout_log(
|
||||
f"Scheduled refresh skipped because another refresh is already {job['status']} (job={job['id']})."
|
||||
)
|
||||
return {
|
||||
"message": "Watchlist refresh already queued." if job["status"] == "pending" else "Watchlist refresh already running.",
|
||||
"job": self._copy_job(job) if job else None,
|
||||
@@ -205,6 +213,7 @@ class WatchlistRefreshJobs:
|
||||
job = {
|
||||
"id": os.urandom(16).hex(),
|
||||
"status": "pending",
|
||||
"source": source_name,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"started_at": None,
|
||||
@@ -220,6 +229,8 @@ class WatchlistRefreshJobs:
|
||||
self._save_job_locked(job)
|
||||
self.pending.append(job["id"])
|
||||
self.wakeup.set()
|
||||
if source_name == "scheduled":
|
||||
self._stdout_log(f"Scheduled refresh queued (job={job['id']}).")
|
||||
return {"message": "Watchlist refresh started.", "job": self._copy_job(job), "created": True}
|
||||
|
||||
def _next_pending(self):
|
||||
@@ -243,6 +254,7 @@ class WatchlistRefreshJobs:
|
||||
|
||||
def _run_job(self, job):
|
||||
interrupted = False
|
||||
source_name = str(job.get("source") or "manual").strip().lower() or "manual"
|
||||
try:
|
||||
show_ids = self.store.all_show_ids()
|
||||
started_at = now_iso()
|
||||
@@ -256,6 +268,8 @@ class WatchlistRefreshJobs:
|
||||
job["current_title"] = None
|
||||
job["message"] = f"Refreshing 0/{len(show_ids)} watchlist entries."
|
||||
self._save_job_locked(job)
|
||||
if source_name == "scheduled":
|
||||
self._stdout_log(f"Scheduled refresh started (job={job['id']}, total={len(show_ids)}).")
|
||||
|
||||
def refresh_one(show_id):
|
||||
title = show_id
|
||||
@@ -320,6 +334,19 @@ class WatchlistRefreshJobs:
|
||||
else:
|
||||
job["message"] = f"Refreshed {job['completed']} watchlist entries."
|
||||
self._save_job_locked(job)
|
||||
if source_name == "scheduled":
|
||||
if interrupted:
|
||||
self._stdout_log(
|
||||
f"Scheduled refresh interrupted (job={job['id']}, completed={job['completed']}/{job['total']})."
|
||||
)
|
||||
elif job["errors"]:
|
||||
self._stdout_log(
|
||||
f"Scheduled refresh finished with errors (job={job['id']}, completed={job['completed']}/{job['total']}, errors={job['errors']})."
|
||||
)
|
||||
else:
|
||||
self._stdout_log(
|
||||
f"Scheduled refresh finished successfully (job={job['id']}, completed={job['completed']}/{job['total']})."
|
||||
)
|
||||
except Exception as exc:
|
||||
finished_at = now_iso()
|
||||
with self.lock:
|
||||
@@ -329,6 +356,8 @@ class WatchlistRefreshJobs:
|
||||
job["current_title"] = None
|
||||
job["message"] = f"Watchlist refresh failed: {exc}"
|
||||
self._save_job_locked(job)
|
||||
if source_name == "scheduled":
|
||||
self._stdout_log(f"Scheduled refresh failed (job={job['id']}): {exc}")
|
||||
finally:
|
||||
with self.lock:
|
||||
self.current_executor = None
|
||||
@@ -397,7 +426,7 @@ class WatchlistAutoRefreshScheduler:
|
||||
return remaining
|
||||
self.last_attempt_monotonic = now
|
||||
try:
|
||||
result = self.refresh_start_fn()
|
||||
result = self.refresh_start_fn(source="scheduled")
|
||||
debug_log("watchlist.auto_refresh.tick", result=result)
|
||||
except Exception as exc:
|
||||
debug_log("watchlist.auto_refresh.error", error=exc)
|
||||
|
||||
+21
-1
@@ -461,6 +461,26 @@ class WatchlistRefreshJobTests(unittest.TestCase):
|
||||
self.assertEqual(second["job"]["id"], first["job"]["id"])
|
||||
self.assertEqual(second["job"]["status"], "pending")
|
||||
|
||||
def test_scheduled_refresh_logs_queue_and_completion(self):
|
||||
class FakeStore:
|
||||
def all_show_ids(self):
|
||||
return ["show-a"]
|
||||
|
||||
def refresh(self, _show_id, include_thumbnail=True):
|
||||
return {"title": "Show A", "status": "updated"}
|
||||
|
||||
service = APP.WatchlistRefreshJobs(FakeStore(), start_worker=False)
|
||||
with mock.patch("builtins.print") as print_mock:
|
||||
started = service.start(source="scheduled")
|
||||
job = service._next_pending()
|
||||
service._run_job(job)
|
||||
|
||||
self.assertTrue(started["created"])
|
||||
logged = "\n".join(call.args[0] for call in print_mock.call_args_list if call.args)
|
||||
self.assertIn("Scheduled refresh queued", logged)
|
||||
self.assertIn("Scheduled refresh started", logged)
|
||||
self.assertIn("Scheduled refresh finished successfully", logged)
|
||||
|
||||
def test_refresh_history_marks_pending_jobs_interrupted_after_restart(self):
|
||||
class FakeStore:
|
||||
def all_show_ids(self):
|
||||
@@ -1090,7 +1110,7 @@ class WatchlistAutoRefreshSchedulerTests(unittest.TestCase):
|
||||
self.assertEqual(scheduler.tick(now_monotonic=100.0), 600)
|
||||
self.assertEqual(scheduler.tick(now_monotonic=699.0), 1)
|
||||
self.assertEqual(scheduler.tick(now_monotonic=700.0), 600)
|
||||
start.assert_called_once()
|
||||
start.assert_called_once_with(source="scheduled")
|
||||
|
||||
def test_tick_resets_timer_when_schedule_changes(self):
|
||||
config = {
|
||||
|
||||
Reference in New Issue
Block a user