Files
FabledScribe/docs/superpowers/specs/2026-04-06-settings-consistency-design.md
T

8.8 KiB
Raw Blame History

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

  1. No timezone fielduser_timezone is 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.

  2. 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.

  3. 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.

  4. Slot toggles are decorative — The briefing tab's four slot checkboxes are saved to briefing_config.slots but _add_user_jobs schedules all four slots unconditionally.

  5. Timezone setting not propagatedPUT /api/settings saves user_timezone to the DB but does not call update_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() sets userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
  • saveTimezone() calls PUT /api/settings with { user_timezone: userTimezone }
  • Loaded in onMounted / general settings load alongside assistantName, 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 ~20682082). 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) → pass config to _add_user_jobs
  • start_briefing_scheduler startup 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

  1. User opens Settings → General tab loads, reads user_timezone from GET /api/settings, populates the field
  2. User clicks Detect → browser timezone fills the field
  3. User clicks Save → PUT /api/settings {user_timezone: "America/New_York"} → backend saves and immediately calls update_user_schedule if briefing enabled
  4. Briefing tab "Firing in timezone" now shows stored value instead of live browser API
  5. Next 8am job: scheduler checks if morning is enabled in briefing_config.slots, then checks if today is in profile.work_schedule.days before 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 MonFri
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_days field is not deleted from existing configs — just stops being written by the UI
  • No migration needed — profile.work_schedule.days already exists; scheduler change is additive