Same class as #46 (a PATCH /api/user/update body that silently does nothing), found while
reviewing light/dark/system theme support.
Symptom
Settings → Appearance → Theme → System does not stick. Pick Dark, then pick System: the app
stays dark, and the picker snaps back to Dark the next time the sheet syncs from the user.
Root cause
SettingsView.swift:87 maps "system" to nil:
Task { await save(theme: newValue == "system" ? nil : newValue) }
updateUserSettings puts that in a synthesized Encodable struct, and Swift encodes optionals with
encodeIfPresent — so a nil field is omitted, not sent as null. Verified:
The route applies ...(theme !== undefined && { theme }) (app/api/user/update/route.ts:102,157),
so an absent key is a no-op. The response then returns the unchanged user (theme: "dark"),
authState.updateUser stores it, and syncFromUser resets the picker to Dark.
The comment justifying it is wrong
SettingsView.swift:85-86 says:
"system" means no explicit preference — send nil so the server clears it.
Sending the string "system" is rejected or treated as default (light) by the server.
The route performs no validation on theme — it stores whatever string arrives. And the web
stores the literal "system": ThemeProvider defaults to theme = 'system' and writes
localStorage.setItem('theme', themeValue) unconditionally
(components/layout/ThemeProvider.tsx:11,17). "system" is the web's own persisted value.
Fix
Acceptance criteria
Files: Views/SettingsView.swift:81-88, InterlinedListTests/APIClientTests/APIClientProfileTests.swift.
Same class as #46 (a
PATCH /api/user/updatebody that silently does nothing), found whilereviewing light/dark/system theme support.
Symptom
Settings → Appearance → Theme → System does not stick. Pick Dark, then pick System: the app
stays dark, and the picker snaps back to Dark the next time the sheet syncs from the user.
Root cause
SettingsView.swift:87maps "system" tonil:updateUserSettingsputs that in a synthesizedEncodablestruct, and Swift encodes optionals withencodeIfPresent— so a nil field is omitted, not sent asnull. Verified:The route applies
...(theme !== undefined && { theme })(app/api/user/update/route.ts:102,157),so an absent key is a no-op. The response then returns the unchanged user (
theme: "dark"),authState.updateUserstores it, andsyncFromUserresets the picker to Dark.The comment justifying it is wrong
SettingsView.swift:85-86says:The route performs no validation on
theme— it stores whatever string arrives. And the webstores the literal
"system":ThemeProviderdefaults totheme = 'system'and writeslocalStorage.setItem('theme', themeValue)unconditionally(
components/layout/ThemeProvider.tsx:11,17)."system"is the web's own persisted value.Fix
"system"verbatim; delete thenilmapping and the incorrect comment.{"theme":"system"}— it fails today, where the body is{}.RootView.preferredSchemealready treats any non-light/darkvalue (including"system") as "follow the OS", so no change is needed there.Acceptance criteria
GET /api/userrefresh and app relaunch.Files:
Views/SettingsView.swift:81-88,InterlinedListTests/APIClientTests/APIClientProfileTests.swift.