fix: flush child form before modal save, guard localStorage parse, explicit dialect check

- InterfaceDefaultsModal submitHandler now requestSubmits the embedded
  tab-interface form and awaits a Svelte tick before POSTing. That
  flushes submit-scoped writes in the InterfaceSettings component
  (updateInterfaceHandler -> saveSettings({ models, imageCompressionSize }))
  through the saveAdminSettings callback so they land in adminDefaults
  instead of being silently dropped on save.
- +layout.svelte only reads localStorage.settings when the server
  didn't return user UI, and wraps the JSON.parse in try/catch with a
  {} fallback. A stale or hand-edited localStorage value would
  otherwise throw and abort the whole authenticated-load path even
  when valid userSettings came back from the backend.
- users.py reset_all_users_interface_settings branches explicitly on
  postgresql and sqlite; any other dialect now responds 501 with a
  clear message naming the unsupported dialect. The previous
  "everything that isn't postgres runs SQLite-specific SQL" assumption
  would fail on MySQL/MariaDB with an opaque syntax error.
This commit is contained in:
DrMelone 2026-04-13 00:40:36 +02:00
parent e7249118da
commit 6cd3c7238b
3 changed files with 51 additions and 6 deletions

View file

@ -701,6 +701,10 @@ async def reset_all_users_interface_settings(
Clears the 'ui' key in all users' settings. Admin only.
"""
try:
# Match dialects explicitly. Falling through to SQLite-specific SQL
# (json_set / json_extract) for every non-postgres backend would
# silently fail on MySQL / MariaDB / other engines with an opaque
# syntax error instead of a clear "unsupported dialect" response.
dialect = db.bind.dialect.name
if dialect == 'postgresql':
@ -723,10 +727,10 @@ async def reset_all_users_interface_settings(
)
result = await db.execute(stmt)
reset_count = result.rowcount
else:
# SQLite: use native json_set (available since SQLite 3.9+).
# rowcount on the UPDATE result reflects the rows actually
# changed, so no follow-up SELECT changes() round-trip is needed.
elif dialect == 'sqlite':
# Native json_set is available since SQLite 3.9+. rowcount on the
# UPDATE result reflects the rows actually changed, so no
# follow-up SELECT changes() round-trip is needed.
result = await db.execute(
text(
"""
@ -738,6 +742,14 @@ async def reset_all_users_interface_settings(
)
)
reset_count = result.rowcount
else:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail=(
f"Resetting interface settings is not implemented for the '{dialect}' "
'database dialect; supported dialects are postgresql and sqlite.'
),
)
await db.commit()

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { getContext } from 'svelte';
import { getContext, tick } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
@ -48,6 +48,19 @@
const submitHandler = async () => {
saving = true;
try {
// The embedded InterfaceSettings form has submit-scoped writes
// (updateInterfaceHandler -> saveSettings({ models, imageCompressionSize }))
// that don't fire on per-control change. Request-submit the child
// form first so those fields land in adminDefaults via the
// saveAdminSettings callback, then wait one tick for reactivity
// to flush before we POST. Without this, admins could save
// defaults and silently lose submit-only fields.
const innerForm = document.getElementById('tab-interface') as HTMLFormElement | null;
if (innerForm && typeof innerForm.requestSubmit === 'function') {
innerForm.requestSubmit();
}
await tick();
await setInterfaceDefaults(localStorage.token, prepareForBackend(adminDefaults));
toast.success($i18n.t('Interface defaults saved successfully'));
show = false;

View file

@ -935,7 +935,27 @@
// defaults is that THOSE users get them, so always merge
// against adminDefaults instead of skipping straight to
// localStorage when userSettings is falsy.
const localStorageSettings = JSON.parse(localStorage.getItem('settings') ?? '{}');
//
// Guard the localStorage read: a stale or hand-edited
// localStorage.settings value can be invalid JSON, and
// letting JSON.parse throw here would abort the whole
// authenticated-load path even when we already have valid
// userSettings from the backend.
let localStorageSettings: any = {};
if (!userSettings?.ui) {
const raw = localStorage.getItem('settings');
if (raw) {
try {
localStorageSettings = JSON.parse(raw) ?? {};
} catch (parseErr) {
console.warn(
'Ignoring malformed localStorage.settings during init:',
parseErr
);
localStorageSettings = {};
}
}
}
const userUI = userSettings?.ui ?? localStorageSettings ?? {};
settings.set(deepMerge(adminDefaults ?? {}, userUI));
setTextScale($settings?.textScale ?? 1);