feat(scheduler): slot gating + morning work-day gate
- _add_user_jobs now accepts config dict and skips disabled slots - _get_briefing_enabled_users returns 3-tuple (user_id, tz, config) - update_user_schedule passes config through to _add_user_jobs - _run_slot_for_user skips morning slot on non-work days via profile.work_schedule - Add tests for slot gating and work-day gate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,8 @@ def _resolve_timezone(tz_str: str) -> str:
|
||||
return "UTC"
|
||||
|
||||
|
||||
async def _get_briefing_enabled_users() -> list[tuple[int, str]]:
|
||||
"""Return [(user_id, iana_timezone)] for all users with briefing enabled."""
|
||||
async def _get_briefing_enabled_users() -> list[tuple[int, str, dict]]:
|
||||
"""Return [(user_id, iana_timezone, config)] for all users with briefing enabled."""
|
||||
import json
|
||||
async with async_session() as session:
|
||||
result = await session.execute(
|
||||
@@ -71,7 +71,7 @@ async def _get_briefing_enabled_users() -> list[tuple[int, str]]:
|
||||
if config.get("enabled"):
|
||||
tz_str = settings.get("user_timezone") or config.get("timezone", "UTC")
|
||||
tz = _resolve_timezone(tz_str)
|
||||
enabled.append((user_id, tz))
|
||||
enabled.append((user_id, tz, config))
|
||||
except Exception:
|
||||
pass
|
||||
return enabled
|
||||
@@ -81,16 +81,24 @@ def _job_id(user_id: int, slot: str) -> str:
|
||||
return f"briefing_{slot}_user_{user_id}"
|
||||
|
||||
|
||||
def _add_user_jobs(user_id: int, tz: str) -> None:
|
||||
"""Add (or replace) all 4 slot jobs for a user in their timezone."""
|
||||
def _add_user_jobs(user_id: int, tz: str, config: dict | None = None) -> None:
|
||||
"""Add (or replace) slot jobs for a user, skipping disabled slots."""
|
||||
if _scheduler is None or _loop is None:
|
||||
return
|
||||
enabled_slots = (config or {}).get("slots", {})
|
||||
for slot_name, hour, minute in SLOTS:
|
||||
jid = _job_id(user_id, slot_name)
|
||||
# compilation always runs; other slots default to True if not in config
|
||||
slot_on = enabled_slots.get(slot_name, True)
|
||||
if not slot_on:
|
||||
if _scheduler.get_job(jid):
|
||||
_scheduler.remove_job(jid)
|
||||
continue
|
||||
_scheduler.add_job(
|
||||
_run_user_slot_sync,
|
||||
CronTrigger(hour=hour, minute=minute, timezone=tz),
|
||||
args=[user_id, slot_name],
|
||||
id=_job_id(user_id, slot_name),
|
||||
id=jid,
|
||||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
@@ -119,7 +127,7 @@ def update_user_schedule(user_id: int, config: dict, tz_override: str | None = N
|
||||
if config.get("enabled"):
|
||||
tz_str = tz_override or config.get("timezone", "UTC")
|
||||
tz = _resolve_timezone(tz_str)
|
||||
_add_user_jobs(user_id, tz)
|
||||
_add_user_jobs(user_id, tz, config)
|
||||
else:
|
||||
_remove_user_jobs(user_id)
|
||||
|
||||
@@ -135,6 +143,24 @@ async def _run_slot_for_user(user_id: int, slot: str) -> None:
|
||||
from fabledassistant.services.settings import get_setting
|
||||
from fabledassistant.config import Config
|
||||
|
||||
# Morning slot: skip if today is not a configured work day
|
||||
if slot == "morning":
|
||||
from fabledassistant.services.user_profile import get_profile
|
||||
tz_str = await get_setting(user_id, "user_timezone") or "UTC"
|
||||
try:
|
||||
user_tz = ZoneInfo(tz_str)
|
||||
except Exception:
|
||||
user_tz = ZoneInfo("UTC")
|
||||
today_abbr = datetime.now(user_tz).strftime("%a") # 'Mon', 'Tue', …
|
||||
profile = await get_profile(user_id)
|
||||
work_days = (profile.work_schedule or {}).get("days", ["Mon", "Tue", "Wed", "Thu", "Fri"])
|
||||
if today_abbr not in work_days:
|
||||
logger.info(
|
||||
"Skipping morning slot for user %d — %s not a configured work day",
|
||||
user_id, today_abbr,
|
||||
)
|
||||
return
|
||||
|
||||
model = await get_setting(user_id, "default_model", Config.OLLAMA_MODEL)
|
||||
|
||||
if slot == "compilation":
|
||||
@@ -255,7 +281,7 @@ async def _catchup_missed_slots(loop: asyncio.AbstractEventLoop) -> None:
|
||||
(one catch-up per slot per user, evaluated in the user's local timezone).
|
||||
"""
|
||||
users = await _get_briefing_enabled_users()
|
||||
for user_id, tz in users:
|
||||
for user_id, tz, _config in users:
|
||||
user_tz = ZoneInfo(tz)
|
||||
now_local = datetime.now(user_tz)
|
||||
today_local = now_local.date()
|
||||
@@ -323,8 +349,8 @@ async def start_briefing_scheduler(loop: asyncio.AbstractEventLoop) -> None:
|
||||
logger.exception("Failed to load briefing users at startup")
|
||||
users = []
|
||||
|
||||
for user_id, tz in users:
|
||||
_add_user_jobs(user_id, tz)
|
||||
for user_id, tz, config in users:
|
||||
_add_user_jobs(user_id, tz, config)
|
||||
|
||||
from fabledassistant.services.recurrence import spawn_recurring_tasks as _spawn_recurring
|
||||
|
||||
|
||||
Reference in New Issue
Block a user