From 24b0e5b8dd351a50c14668e5f849ff2709376531 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Fri, 12 Jun 2026 16:54:47 +0200 Subject: [PATCH] improve settings UI Signed-off-by: Ronni Skansing --- .../src/routes/settings/panels/Data.svelte | 19 +++-- .../src/routes/settings/panels/Scim.svelte | 73 ++++++++++--------- .../src/routes/settings/panels/System.svelte | 46 ++++++++++-- 3 files changed, 90 insertions(+), 48 deletions(-) diff --git a/frontend/src/routes/settings/panels/Data.svelte b/frontend/src/routes/settings/panels/Data.svelte index 766c883..c1b6ea1 100644 --- a/frontend/src/routes/settings/panels/Data.svelte +++ b/frontend/src/routes/settings/panels/Data.svelte @@ -22,6 +22,7 @@ let autoPruneOption = { enabled: false, companies: [] }; let autoPruneEnabled = false; let autoPruneError = ''; + let isSavingAutoPrune = false; // backup let isBackupModalVisible = false; @@ -74,10 +75,12 @@ } } - async function setAutoPruneValue(enabled) { + // save the auto-prune setting, only applied when the user clicks Save + async function saveAutoPrune() { autoPruneError = ''; + isSavingAutoPrune = true; // read-modify-write: preserve per-company entries - const updated = { ...autoPruneOption, enabled }; + const updated = { ...autoPruneOption, enabled: autoPruneEnabled }; try { const res = await api.option.setAutoPrune(updated); if (!res.success) { @@ -85,11 +88,12 @@ return; } autoPruneOption = updated; - autoPruneEnabled = enabled; addToast('Auto-prune setting saved', 'Success'); } catch (e) { autoPruneError = 'Failed to save auto-prune setting'; console.error('failed to set auto-prune setting', e); + } finally { + isSavingAutoPrune = false; } } @@ -319,16 +323,21 @@ checked={autoPruneEnabled} label="Enabled" description="Orphaned recipients are deleted automatically each hour" - on:change={() => setAutoPruneValue(true)} + on:change={() => (autoPruneEnabled = true)} /> setAutoPruneValue(false)} + on:change={() => (autoPruneEnabled = false)} /> +
+ Save Changes +
diff --git a/frontend/src/routes/settings/panels/Scim.svelte b/frontend/src/routes/settings/panels/Scim.svelte index e3793d1..04376cf 100644 --- a/frontend/src/routes/settings/panels/Scim.svelte +++ b/frontend/src/routes/settings/panels/Scim.svelte @@ -5,14 +5,18 @@ import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsLoading from '$lib/components/SettingsLoading.svelte'; import Form from '$lib/components/Form.svelte'; + import FormButton from '$lib/components/FormButton.svelte'; + import FormError from '$lib/components/FormError.svelte'; import TextField from '$lib/components/TextField.svelte'; import TextFieldSelect from '$lib/components/TextFieldSelect.svelte'; let loaded = false; + let scimError = ''; + let isSaving = false; // SCIM provisioning: single global domain that serves the SCIM endpoints let scimDomain = ''; - let scimDomainOptions = [{ value: '', label: '— Disabled —' }]; + let scimDomainOptions = [{ value: '', label: '- Disabled -' }]; // retention window (days) before a SCIM-disabled recipient is pruned let scimRetentionDays = 30; @@ -36,24 +40,35 @@ } } - async function setScimRetention() { + // save both SCIM settings together, like the other settings panels, so a + // change is only applied when the user explicitly clicks Save + async function saveScim() { + scimError = ''; + const days = parseInt(scimRetentionDays, 10); + if (isNaN(days) || days < 0) { + scimError = 'Retention days must be zero or positive'; + return; + } + isSaving = true; try { - const days = parseInt(scimRetentionDays, 10); - if (isNaN(days) || days < 0) { - addToast('Retention days must be zero or positive', 'Error'); + const domainRes = await api.option.setScimDomain(scimDomain); + if (!domainRes.success) { + scimError = domainRes.error || 'Failed to update SCIM domain'; + await refreshScimDomain(); + return; + } + const retentionRes = await api.option.setScimRetentionDays(days); + if (!retentionRes.success) { + scimError = retentionRes.error || 'Failed to update SCIM retention'; await refreshScimRetention(); return; } - const res = await api.option.setScimRetentionDays(days); - if (res.success) { - addToast('SCIM retention updated', 'Success'); - } else { - addToast(res.error || 'Failed to update SCIM retention', 'Error'); - await refreshScimRetention(); - } + addToast('SCIM settings updated', 'Success'); } catch (e) { - addToast('Failed to update SCIM retention', 'Error'); - console.error(e); + scimError = 'Failed to update SCIM settings'; + console.error('failed to update SCIM settings', e); + } finally { + isSaving = false; } } @@ -70,7 +85,7 @@ if (domains.success) { const names = (domains.data.rows || []).map((d) => d.name); scimDomainOptions = [ - { value: '', label: '— Disabled —' }, + { value: '', label: '- Disabled -' }, ...names.map((n) => ({ value: n, label: n })) ]; } @@ -79,20 +94,6 @@ } } - async function setScimDomain() { - try { - const res = await api.option.setScimDomain(scimDomain); - if (res.success) { - addToast(scimDomain ? 'SCIM domain updated' : 'SCIM serving disabled', 'Success'); - } else { - addToast(res.error || 'Failed to update SCIM domain', 'Error'); - await refreshScimDomain(); - } - } catch (e) { - addToast('Failed to update SCIM domain', 'Error'); - console.error(e); - } - } {#if !loaded} @@ -104,11 +105,10 @@ Global domain that serves SCIM provisioning. Must be publicly reachable on 443 with a valid certificate; prefer a dedicated domain not used for campaigns.

-
+ SCIM domain Retention (days) +

+ How long deprovisioned recipients are kept (disabled) before being permanently removed. +

+ +
+ Save Changes +
-

- How long deprovisioned recipients are kept (disabled) before being permanently removed. -

{/if} diff --git a/frontend/src/routes/settings/panels/System.svelte b/frontend/src/routes/settings/panels/System.svelte index da9fcec..f8a204b 100644 --- a/frontend/src/routes/settings/panels/System.svelte +++ b/frontend/src/routes/settings/panels/System.svelte @@ -7,7 +7,10 @@ import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsLoading from '$lib/components/SettingsLoading.svelte'; import Button from '$lib/components/Button.svelte'; + import Alert from '$lib/components/Alert.svelte'; import Form from '$lib/components/Form.svelte'; + import FormButton from '$lib/components/FormButton.svelte'; + import FormError from '$lib/components/FormError.svelte'; import TextFieldSelect from '$lib/components/TextFieldSelect.svelte'; const logLevels = ['debug', 'info', 'warn', 'error']; @@ -16,12 +19,15 @@ let loaded = false; let logLevel = ''; let dbLogLevel = ''; + let logError = ''; + let isSavingLog = false; let version = ''; let updateAvailable = false; let isCheckingUpdate = false; let isWipingBrowserCache = false; + let isWipeAlertVisible = false; onMount(async () => { try { @@ -47,14 +53,22 @@ } } - async function setLogLevel() { + // save both log levels together, only applied when the user clicks Save + async function saveLogLevel() { + logError = ''; + isSavingLog = true; try { const res = await api.log.setLevel(logLevel, dbLogLevel); if (!res.success) { - console.error(res); + logError = res.error || 'Failed to update logging'; + return; } + addToast('Logging updated', 'Success'); } catch (err) { + logError = 'Failed to update logging'; console.error(err); + } finally { + isSavingLog = false; } } @@ -109,11 +123,13 @@ const response = await api.reportTemplate.wipeBrowserCache(); if (response.success) { addToast('Browser cache wiped', 'Success'); - } else { - addToast(response.error || 'Failed to wipe browser cache', 'Error'); + return { success: true }; } + addToast(response.error || 'Failed to wipe browser cache', 'Error'); + return { success: false }; } catch (e) { addToast('Failed to wipe browser cache', 'Error'); + return { success: false }; } finally { isWipingBrowserCache = false; } @@ -125,12 +141,11 @@ {:else}
-
+ Application log level @@ -138,9 +153,12 @@ id="dbLogLevel" required bind:value={dbLogLevel} - onSelect={setLogLevel} options={dbLogLevels}>Database log level + +
+ Save Changes +
@@ -154,7 +172,7 @@ size={'large'} backgroundColor="bg-red-600" disabled={isWipingBrowserCache} - on:click={onWipeBrowserCache} + on:click={() => (isWipeAlertVisible = true)} > {isWipingBrowserCache ? 'Wiping...' : 'Wipe Browser Cache'} @@ -203,3 +221,15 @@
{/if} + + +

+ Chromium will be deleted and downloaded again on the next PDF report or remote browser session, + which can take a few minutes. +

+