Prefs: expose user_ai_prefs — GET/PUT /prefs + GET /timezones (existing columns, no migration) #126
Notifications
Due Date
No due date set.
Blocks
Reference: Apps/ImageApi#126
Reference in new issue
Block a user
Part of #113
What to build
First-class settings surface for the
user_ai_prefsrow. Todaydisplay_tz,voice, andlibraryare only writable incidentally — aPOST /reelsbody mirrors them via
capture_prefs. An owner who never creates a reel hasno way to set their display zone, and there is no read path for any pref.
FileViewer-React currently persists prefs on-device; these endpoints are the
sync/migration target for that state.
Endpoints:
GET /prefs— returns the current row (voice, library, display_tz,updated_at) or an empty shape when no row exists. Auth required like the
other protected routes.
PUT /prefs— partial update: a field omitted from the body isunchanged; a field present is set (explicit null clears).
display_tzis validated against chrono-tz (same check as
display_zone_for); aninvalid zone name → 400, the row is not partially written.
GET /timezones— the IANA zone list from chrono-tz, for picker UIs(unauthenticated is fine; it's static data).
Scope: existing columns only — no migration.
capture_prefsonreel creation stays as-is (idempotent with this surface; same write).
Follow-on owner defaults (model/backend/persona/reels) are a separate
ticket that adds the columns.
Acceptance criteria
GET /prefsreturns the stored row; empty/nullable shape when none.PUT /prefsupdates only the fields present in the body; others untouched.display_tz(e.g.Not/A_Zone) → 400, nothing written.GET /timezonesreturns the IANA list (includesAmerica/New_York).PUT /prefsdisplay_tzchange is reflected by/memoriesgroupingand the reels pregen zone on the next read/pregen (prefs row is the
first source in
load_pregen_params).Cross-references
Implemented — PR #128 (branch
feature/prefs-endpoints, commit2258a0e).What shipped:
GET /prefs(JWT) →{voice, library, display_tz, updated_at}or the all-null shape; 200 when no row.PUT /prefs(JWT) → three-state partial update: omitted = unchanged, explicit null = clear, value = set.display_tzvalidated against chrono-tz before any write — invalid → 400, nothing written (not even other valid fields). Empty body → 200 no-op, no row created. Frozentz_offset_minutespreserved verbatim.GET /timezones(public) →chrono_tz::TZ_VARIANTS— exactly the domainTz::from_straccepts, so the picker list and the validation check can't drift.capture_prefson POST /reels untouched — both writers upsert the same single row.Evidence: 14 new tests (3 GET, 7 PUT incl. auth-gate, 2 timezones, 2 pure merge_patch) — the 3-state semantics needed a hand-rolled
TriStatedeserializer because serde derive maps explicit null onOption<Option<T>>to the outerNone, making "clear" indistinguishable from "omitted". Full suite 1436 passed / 0 failed. Clippy + fmt clean. Two-axis review (spec + standards) clean — all 9 spec requirements and all 10 house conventions verified.Awaiting merge.
Resolved — PR #128 merged to master.
src/prefs.rs: GET/PUT/prefs(JWT) + GET/timezones(public). Three-state partial update via a hand-rolledTriStatedeserializer (omitted = unchanged, explicit null = clear, value = set) — bareOption<Option<T>>derive conflates omitted with null, andserde_with::double_optionwas considered but the repo has noserde_withdep.display_tzvalidated against chrono-tz before any write; invalid → 400, nothing written. Frozentz_offset_minutespreserved verbatim on every write./timezonesreturnschrono_tz::TZ_VARIANTS— exactly the domainTz::from_straccepts.capture_prefs(POST /reels) untouched.Unblocks #127 (owner defaults).