8.8 KiB
Settings Consistency Pass — Design
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Fix five interrelated gaps in the settings UI — missing timezone field, SSO-unaware account tab, duplicated work schedule, ignored slot toggles, and timezone changes not propagating to the briefing scheduler.
Architecture: Primarily frontend cleanup with two focused backend hooks: settings PUT route gains a timezone→scheduler bridge; briefing scheduler gains slot-gating and work-day awareness.
Tech Stack: Vue 3 + TypeScript frontend; Python/Quart backend; APScheduler; zoneinfo.
Problem summary
-
No timezone field —
user_timezoneis read by the scheduler and the chat pipeline but is never exposed in the UI. The briefing tab displays the browser's detected timezone but never persists it. Scheduler falls back to UTC. -
Account tab ignores SSO — "Email Address" and "Change Password" sections are shown to SSO users (
has_password = false) even though they cannot change credentials here. -
Work schedule duplicated — Profile tab has the canonical work schedule (days + start/end time, stored in
profile.work_schedule). Briefing tab has a redundant "Office Days" section (briefing_config.work_days) that the backend never reads. -
Slot toggles are decorative — The briefing tab's four slot checkboxes are saved to
briefing_config.slotsbut_add_user_jobsschedules all four slots unconditionally. -
Timezone setting not propagated —
PUT /api/settingssavesuser_timezoneto the DB but does not callupdate_user_schedule, so the in-memory scheduler keeps the stale timezone until restart or briefing config re-save.
Components
1. General tab — Timezone field
File: frontend/src/views/SettingsView.vue
New section in the General tab (after the Assistant section, before Model Management):
<section class="settings-section full-width">
<h2>Timezone</h2>
<p class="section-desc">Used to schedule briefings and format times in chat.</p>
<div class="field">
<label for="user-timezone">Your timezone</label>
<div style="display:flex; gap:0.5rem; align-items:center">
<input id="user-timezone" v-model="userTimezone" type="text"
class="input" placeholder="e.g. America/New_York" />
<button class="btn-secondary" type="button" @click="detectTimezone">Detect</button>
</div>
<p class="field-hint">IANA timezone name (e.g. America/Chicago, Europe/London).</p>
</div>
<div class="actions">
<button class="btn-save" @click="saveTimezone" :disabled="savingTimezone">
{{ savingTimezone ? 'Saving…' : 'Save' }}
</button>
<span v-if="timezoneSaved" class="saved-msg">Saved!</span>
</div>
</section>
detectTimezone()setsuserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZonesaveTimezone()callsPUT /api/settingswith{ user_timezone: userTimezone }- Loaded in
onMounted/ general settings load alongsideassistantName,defaultModel
The briefing tab's "Firing in timezone" hint changes from the live Intl API to reading the stored user_timezone value:
Firing in timezone: <strong>{{ userTimezone || 'UTC (not set)' }}</strong>
2. Account tab — SSO guard
File: frontend/src/views/SettingsView.vue
Wrap the Email and Password sections:
<!-- SSO info banner (shown when no local password) -->
<section v-if="!authStore.user?.has_password" class="settings-section">
<h2>Account</h2>
<p class="section-desc">
Your account is managed by an external identity provider.
Email and password changes are made through your provider, not here.
</p>
</section>
<!-- Local-auth sections (hidden for SSO) -->
<template v-if="authStore.user?.has_password">
<section class="settings-section"> <!-- Email Address --> </section>
<section class="settings-section"> <!-- Change Password --> </section>
</template>
<!-- Active Sessions — always shown -->
<section class="settings-section"> ... </section>
No backend change needed — the API already rejects email/password changes for SSO accounts.
3. Briefing tab — Remove Office Days
File: frontend/src/views/SettingsView.vue
Delete the "Office Days" <section> (lines ~2068–2082). The briefing_config.work_days field can remain in the config object for backwards compatibility but the UI stops writing it.
The slot toggles section stays — it now actually drives scheduling (see §5).
4. Backend — settings PUT propagates timezone to scheduler
File: src/fabledassistant/routes/settings.py
After set_settings_batch, add:
if "user_timezone" in to_save:
import json
from fabledassistant.services.briefing_scheduler import update_user_schedule
config_raw = await get_setting(uid, "briefing_config", "{}")
try:
config = json.loads(config_raw) if isinstance(config_raw, str) else {}
except Exception:
config = {}
if config.get("enabled"):
update_user_schedule(uid, config, tz_override=to_save["user_timezone"] or None)
5. Backend — scheduler respects slot toggles and work days
File: src/fabledassistant/services/briefing_scheduler.py
5a. _add_user_jobs — only schedule enabled slots
Change signature to accept config: dict:
def _add_user_jobs(user_id: int, tz: str, config: dict | None = None) -> None:
enabled_slots = (config or {}).get("slots", {})
for slot_name, hour, minute in SLOTS:
# Default True for compilation (always run); others respect toggle
if slot_name != "compilation" and not enabled_slots.get(slot_name, True):
jid = _job_id(user_id, slot_name)
if _scheduler and _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),
replace_existing=True,
misfire_grace_time=3600,
)
Update callers:
update_user_schedule(user_id, config, tz_override)→ passconfigto_add_user_jobsstart_briefing_schedulerstartup loop → fetch full config to pass through
5b. _run_slot_for_user — skip morning on non-work days
For the morning slot, check today against profile.work_schedule.days:
if slot == "morning":
from fabledassistant.services.user_profile import get_profile
from datetime import datetime
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 work day", user_id, today_abbr)
return
Note: get_profile must be importable from user_profile.py — confirm signature during implementation.
Data flow
- User opens Settings → General tab loads, reads
user_timezonefromGET /api/settings, populates the field - User clicks Detect → browser timezone fills the field
- User clicks Save →
PUT /api/settings {user_timezone: "America/New_York"}→ backend saves and immediately callsupdate_user_scheduleif briefing enabled - Briefing tab "Firing in timezone" now shows stored value instead of live browser API
- Next 8am job: scheduler checks if
morningis enabled inbriefing_config.slots, then checks if today is inprofile.work_schedule.daysbefore running
Error handling
| Scenario | Behaviour |
|---|---|
user_timezone saved as empty string |
update_user_schedule called with tz_override=None → falls back to briefing_config.timezone or UTC |
| Invalid IANA string saved | _resolve_timezone already falls back to UTC with a warning log |
profile.work_schedule is None |
morning slot defaults to Mon–Fri |
| Slot toggles key missing from config | All non-compilation slots default to enabled (True) — no regression for existing users |
| SSO user visits Account tab | Sees info banner; email/password forms hidden; no API calls attempted |
What is NOT changing
- Profile "Interests" and Briefing "News Preferences" remain separate — they serve different purposes (system-prompt personalisation vs RSS topic filtering)
briefing_config.work_daysfield is not deleted from existing configs — just stops being written by the UI- No migration needed —
profile.work_schedule.daysalready exists; scheduler change is additive