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.
-
-
- 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}
-
@@ -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.
+
+