diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3412abe..3021118 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -8,7 +8,7 @@ import { useShortcuts } from "@/composables/useShortcuts"; import { useAuthStore } from "@/stores/auth"; import { useChatStore } from "@/stores/chat"; import { useSettingsStore } from "@/stores/settings"; -import { apiGet } from "@/api/client"; +import { apiGet, apiPut } from "@/api/client"; useTheme(); @@ -22,6 +22,8 @@ const { showShortcuts, toggleShortcuts, closeShortcuts } = useShortcuts(); function startAppServices() { chatStore.startStatusPolling(); settingsStore.fetchSettings(); + // Sync browser timezone to the server on every login/page load. + apiPut("/api/settings", { user_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }).catch(() => {}); } function stopAppServices() { diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 09724a7..5ae97d0 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -329,7 +329,6 @@ export interface BriefingConfig { slots: BriefingSlots; notifications: boolean; temp_unit: 'C' | 'F'; - timezone: string; } export interface BriefingFeed { @@ -364,7 +363,6 @@ const DEFAULT_BRIEFING_CONFIG: BriefingConfig = { slots: { compilation: true, morning: true, midday: false, afternoon: false }, notifications: true, temp_unit: 'C', - timezone: '', }; export async function getBriefingConfig(): Promise { diff --git a/frontend/src/components/BriefingSetupWizard.vue b/frontend/src/components/BriefingSetupWizard.vue index f1b7c67..d7e5c54 100644 --- a/frontend/src/components/BriefingSetupWizard.vue +++ b/frontend/src/components/BriefingSetupWizard.vue @@ -20,7 +20,6 @@ const config = reactive({ slots: { compilation: true, morning: true, midday: false, afternoon: false }, notifications: true, temp_unit: 'C', - timezone: '', }) // Step 2 — locations diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index d230735..0b011e3 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -258,7 +258,6 @@ const briefingConfig = ref({ slots: { compilation: true, morning: true, midday: false, afternoon: false }, notifications: true, temp_unit: 'C', - timezone: '', }); const briefingFeeds = ref([]); const briefingSaving = ref(false); @@ -284,10 +283,6 @@ function _parseTopics(raw: unknown): string[] { async function loadBriefingTab() { briefingConfig.value = await getBriefingConfig(); briefingFeeds.value = await getBriefingFeeds(); - // Auto-populate timezone from browser if not already stored. - if (!briefingConfig.value.timezone) { - briefingConfig.value.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - } const allSettings = await apiGet>('/api/settings').catch(() => ({} as Record)); briefingIncludeTopics.value = _parseTopics(allSettings['briefing_include_topics'] ?? '[]'); briefingExcludeTopics.value = _parseTopics(allSettings['briefing_exclude_topics'] ?? '[]'); @@ -1694,35 +1689,6 @@ function formatUserDate(iso: string): string { - -
-

Timezone

-

- Briefing slots fire at the times below in this timezone. - Auto-detected from your browser — override if the server should use a different zone. -

-
- - -
-

- Use an - IANA timezone name - (e.g. Europe/London, America/Chicago). - Your browser reports: {{ Intl.DateTimeFormat().resolvedOptions().timeZone }} -

-
-

Scheduled Slots

@@ -1747,8 +1713,8 @@ function formatUserDate(iso: string): string { -

- Firing in timezone: {{ briefingConfig.timezone }} +

+ Firing in timezone: {{ Intl.DateTimeFormat().resolvedOptions().timeZone }}

@@ -3313,12 +3279,6 @@ FABLE_API_KEY=<your-api-key> flex-wrap: wrap; margin-top: 0.5rem; } -.briefing-timezone-row { - display: flex; - gap: 0.5rem; - align-items: center; - margin-bottom: 0.5rem; -} .briefing-unit-toggle { display: flex; gap: 0; diff --git a/src/fabledassistant/routes/briefing.py b/src/fabledassistant/routes/briefing.py index b950849..eba9411 100644 --- a/src/fabledassistant/routes/briefing.py +++ b/src/fabledassistant/routes/briefing.py @@ -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}) diff --git a/src/fabledassistant/routes/chat.py b/src/fabledassistant/routes/chat.py index 63cd5e1..e5b3328 100644 --- a/src/fabledassistant/routes/chat.py +++ b/src/fabledassistant/routes/chat.py @@ -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 diff --git a/src/fabledassistant/services/briefing_scheduler.py b/src/fabledassistant/services/briefing_scheduler.py index fd5d5c6..44f1ed0 100644 --- a/src/fabledassistant/services/briefing_scheduler.py +++ b/src/fabledassistant/services/briefing_scheduler.py @@ -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)