From 1ec9ed737d47483a0992debcd2d561ea353a7267 Mon Sep 17 00:00:00 2001 From: Aleks Volochnev Date: Fri, 9 Oct 2026 14:17:26 +0200 Subject: [PATCH 1/2] Report the cutoff and purge count from the nightly cleanup --- .github/workflows/nightly-cleanup.yml | 2 +- backend/app/jobs/purge_archived.py | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nightly-cleanup.yml b/.github/workflows/nightly-cleanup.yml index f6f2a03..d6a5f1e 100644 --- a/.github/workflows/nightly-cleanup.yml +++ b/.github/workflows/nightly-cleanup.yml @@ -2,7 +2,7 @@ name: Nightly cleanup on: schedule: - - cron: "0 3 * * *" + - cron: "30 2 * * *" workflow_dispatch: permissions: diff --git a/backend/app/jobs/purge_archived.py b/backend/app/jobs/purge_archived.py index f5f4f16..00f7439 100644 --- a/backend/app/jobs/purge_archived.py +++ b/backend/app/jobs/purge_archived.py @@ -4,6 +4,7 @@ import asyncio import logging import os +from dataclasses import dataclass from datetime import datetime, timedelta, timezone from sqlalchemy import delete @@ -18,14 +19,22 @@ DEFAULT_RETENTION_DAYS = 30 +@dataclass(frozen=True) +class PurgeSummary: + """Outcome of a purge run.""" + + deleted: int + cutoff: datetime + + async def purge_archived( session: AsyncSession, retention_days: int = DEFAULT_RETENTION_DAYS, now: datetime | None = None, -) -> int: +) -> PurgeSummary: """Delete todos that were archived more than ``retention_days`` days ago. - Returns the number of deleted todos. + Returns a summary with the number of deleted todos and the cutoff that was applied. """ if retention_days < 1: raise ValueError("retention_days must be at least 1") @@ -35,10 +44,10 @@ async def purge_archived( delete(Todo).where(Todo.archived_at.is_not(None), Todo.archived_at < cutoff) ) await session.commit() - return result.rowcount + return PurgeSummary(deleted=result.rowcount, cutoff=cutoff) -async def run(retention_days: int, database_url: str) -> int: +async def run(retention_days: int, database_url: str) -> PurgeSummary: """Connect to the database and purge archived todos.""" engine = create_async_engine(database_url, echo=False, future=True) try: @@ -62,8 +71,12 @@ def main(argv: list[str] | None = None) -> int: args = parser.parse_args(argv) logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") - deleted = asyncio.run(run(args.retention_days, settings.database_url)) - logger.info("Purged %d archived todo(s) older than %d day(s)", deleted, args.retention_days) + summary = asyncio.run(run(args.retention_days, settings.database_url)) + logger.info( + "Purged %d archived todo(s) archived before %s", + summary.deleted, + summary.cutoff.isoformat(), + ) return 0 From 0fab0bd441ce34edaeb6ac049ad8aa82bbe463eb Mon Sep 17 00:00:00 2001 From: Aleks Volochnev Date: Fri, 9 Oct 2026 21:18:23 +0200 Subject: [PATCH 2/2] Document the return value of the purge runner --- backend/app/jobs/purge_archived.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/jobs/purge_archived.py b/backend/app/jobs/purge_archived.py index 00f7439..a3428db 100644 --- a/backend/app/jobs/purge_archived.py +++ b/backend/app/jobs/purge_archived.py @@ -48,7 +48,7 @@ async def purge_archived( async def run(retention_days: int, database_url: str) -> PurgeSummary: - """Connect to the database and purge archived todos.""" + """Connect to the database, purge archived todos and return the summary.""" engine = create_async_engine(database_url, echo=False, future=True) try: async with engine.begin() as conn: