feat(journal): register per-user closeout job at day_rollover_hour

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 14:35:57 -04:00
parent 020bd6614b
commit b88d5ee6b3
2 changed files with 91 additions and 16 deletions
@@ -89,30 +89,65 @@ def _run_daily_prep_threadsafe(user_id: int) -> None:
asyncio.run_coroutine_threadsafe(_do_daily_prep(user_id), _loop)
async def _do_closeout(user_id: int) -> None:
try:
tz_str = await get_user_timezone(user_id)
tz = _resolve_tz(tz_str)
now = datetime.datetime.now(tz)
# We just rolled into a new day in user-local time. The day that
# just ended is yesterday's calendar date regardless of whether
# rollover_hour is 0 or 4 — APScheduler fires precisely at the
# configured hour so no clock-skew correction is needed.
yesterday = now.date() - datetime.timedelta(days=1)
from fabledassistant.services.journal_closeout import run_for_user
await run_for_user(user_id=user_id, yesterday=yesterday)
except Exception:
logger.exception("Closeout failed for user %d", user_id)
def _run_closeout_threadsafe(user_id: int) -> None:
if _loop is None:
return
asyncio.run_coroutine_threadsafe(_do_closeout(user_id), _loop)
async def update_user_schedule(user_id: int) -> None:
"""Add or replace this user's daily-prep job using their current config."""
"""Add or replace this user's daily-prep + closeout jobs from current config."""
if _scheduler is None:
return
job_id = f"journal_prep_{user_id}"
if _scheduler.get_job(job_id):
_scheduler.remove_job(job_id)
config = await get_journal_config(user_id)
if not config.get("prep_enabled", True):
return
tz_str = await get_user_timezone(user_id)
tz = _resolve_tz(tz_str)
prep_hour = int(config.get("prep_hour", 5))
prep_minute = int(config.get("prep_minute", 0))
_scheduler.add_job(
_run_daily_prep_threadsafe,
trigger=CronTrigger(hour=prep_hour, minute=prep_minute, timezone=tz),
args=[user_id],
id=job_id,
replace_existing=True,
)
# ── Prep job ──────────────────────────────────────────────────────────
prep_job_id = f"journal_prep_{user_id}"
if _scheduler.get_job(prep_job_id):
_scheduler.remove_job(prep_job_id)
if config.get("prep_enabled", True):
prep_hour = int(config.get("prep_hour", 5))
prep_minute = int(config.get("prep_minute", 0))
_scheduler.add_job(
_run_daily_prep_threadsafe,
trigger=CronTrigger(hour=prep_hour, minute=prep_minute, timezone=tz),
args=[user_id],
id=prep_job_id,
replace_existing=True,
)
# ── Closeout job ──────────────────────────────────────────────────────
closeout_job_id = f"journal_closeout_{user_id}"
if _scheduler.get_job(closeout_job_id):
_scheduler.remove_job(closeout_job_id)
if config.get("closeout_enabled", True):
rollover_hour = int(config.get("day_rollover_hour", 4))
_scheduler.add_job(
_run_closeout_threadsafe,
trigger=CronTrigger(hour=rollover_hour, minute=0, timezone=tz),
args=[user_id],
id=closeout_job_id,
replace_existing=True,
)
async def _register_all_user_jobs() -> None: