refactor: centralise user timezone as a standalone setting

Browser timezone is now synced to user_settings["user_timezone"] on
every login/page load (App.vue). The briefing scheduler and LLM context
both read from this single source, falling back to the legacy
briefing_config.timezone for existing users during migration.

- App.vue: PUT /api/settings with browser IANA timezone on startAppServices
- routes/chat.py: fall back to stored user_timezone when not sent in request
- briefing_scheduler: read user_timezone setting; briefing_config.timezone
  kept as fallback only
- routes/briefing.py: pass tz_override from user_timezone to live-patched scheduler
- Remove timezone field from BriefingConfig interface and all briefing UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 20:17:51 -04:00
parent fd05c65018
commit 62dbb8d496
7 changed files with 24 additions and 55 deletions
+3 -2
View File
@@ -44,9 +44,10 @@ async def get_config():
async def put_config():
data = await request.get_json()
await set_settings_batch(g.user.id, {"briefing_config": json.dumps(data)})
# Live-patch the scheduler so the new timezone takes effect immediately.
# Live-patch the scheduler using the stored user_timezone.
from fabledassistant.services.briefing_scheduler import update_user_schedule
update_user_schedule(g.user.id, data)
tz_override = await get_setting(g.user.id, "user_timezone") or None
update_user_schedule(g.user.id, data, tz_override=tz_override)
return jsonify({"ok": True})
+2
View File
@@ -149,6 +149,8 @@ async def send_message_route(conv_id: int):
rag_project_id = data.get("rag_project_id") or None
workspace_project_id = data.get("workspace_project_id") or None
user_timezone = data.get("user_timezone") or None
if not user_timezone:
user_timezone = await get_setting(uid, "user_timezone") or None
effective_rag_project_id = workspace_project_id or rag_project_id
# Reject if generation already running for this conversation
@@ -56,17 +56,22 @@ async def _get_briefing_enabled_users() -> list[tuple[int, str]]:
import json
async with async_session() as session:
result = await session.execute(
select(Setting).where(Setting.key == "briefing_config")
select(Setting).where(Setting.key.in_(["briefing_config", "user_timezone"]))
)
rows = list(result.scalars().all())
enabled = []
by_user: dict[int, dict[str, str]] = {}
for row in rows:
by_user.setdefault(row.user_id, {})[row.key] = row.value or ""
enabled = []
for user_id, settings in by_user.items():
try:
config = json.loads(row.value) if row.value else {}
config = json.loads(settings.get("briefing_config", "{}") or "{}")
if config.get("enabled"):
tz = _resolve_timezone(config.get("timezone", "UTC"))
enabled.append((row.user_id, tz))
tz_str = settings.get("user_timezone") or config.get("timezone", "UTC")
tz = _resolve_timezone(tz_str)
enabled.append((user_id, tz))
except Exception:
pass
return enabled
@@ -105,13 +110,15 @@ def _remove_user_jobs(user_id: int) -> None:
# ── Public API ────────────────────────────────────────────────────────────────
def update_user_schedule(user_id: int, config: dict) -> None:
def update_user_schedule(user_id: int, config: dict, tz_override: str | None = None) -> None:
"""
Called when a user saves their briefing config via the settings UI.
Live-patches the scheduler — no restart required.
tz_override takes priority over any timezone in config.
"""
if config.get("enabled"):
tz = _resolve_timezone(config.get("timezone", "UTC"))
tz_str = tz_override or config.get("timezone", "UTC")
tz = _resolve_timezone(tz_str)
_add_user_jobs(user_id, tz)
else:
_remove_user_jobs(user_id)