From f818fbb2347255b10eccae97a35469a8387a2408 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 24 Jan 2026 02:19:46 +0100 Subject: [PATCH] feat: persist notification preferences to server-side settings Notification prefs are now included in the PUT /api/settings payload and stored in ~/.claudeman/settings.json. On page load, server-stored prefs are loaded as fallback when localStorage is empty (e.g. new browser or cleared cache). Co-Authored-By: Claude Opus 4.5 --- src/web/public/app.js | 44 ++++++++++++++++++++++++++++++------------- src/web/server.ts | 2 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index f6bcb934..5502b579 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -388,6 +388,11 @@ class ClaudemanApp { this.setupEventListeners(); // Start system stats polling this.startSystemStatsPolling(); + // Load server-stored settings (async, re-applies visibility after load) + this.loadAppSettingsFromServer().then(() => { + this.applyHeaderVisibilitySettings(); + this.applyMonitorVisibility(); + }); } initTerminal() { @@ -2518,16 +2523,17 @@ class ClaudemanApp { localStorage.setItem('claudeman-app-settings', JSON.stringify(settings)); // Save notification preferences separately + const notifPrefsToSave = { + enabled: document.getElementById('appSettingsNotifEnabled').checked, + browserNotifications: document.getElementById('appSettingsNotifBrowser').checked, + audioAlerts: document.getElementById('appSettingsNotifAudio').checked, + stuckThresholdMs: (parseInt(document.getElementById('appSettingsNotifStuckMins').value) || 10) * 60000, + muteCritical: !document.getElementById('appSettingsNotifCritical').checked, + muteWarning: !document.getElementById('appSettingsNotifWarning').checked, + muteInfo: !document.getElementById('appSettingsNotifInfo').checked, + }; if (this.notificationManager) { - this.notificationManager.preferences = { - enabled: document.getElementById('appSettingsNotifEnabled').checked, - browserNotifications: document.getElementById('appSettingsNotifBrowser').checked, - audioAlerts: document.getElementById('appSettingsNotifAudio').checked, - stuckThresholdMs: (parseInt(document.getElementById('appSettingsNotifStuckMins').value) || 10) * 60000, - muteCritical: !document.getElementById('appSettingsNotifCritical').checked, - muteWarning: !document.getElementById('appSettingsNotifWarning').checked, - muteInfo: !document.getElementById('appSettingsNotifInfo').checked, - }; + this.notificationManager.preferences = notifPrefsToSave; this.notificationManager.savePreferences(); } @@ -2535,12 +2541,12 @@ class ClaudemanApp { this.applyHeaderVisibilitySettings(); this.applyMonitorVisibility(); - // Also save to server + // Save to server (includes notification prefs for cross-browser persistence) try { await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(settings) + body: JSON.stringify({ ...settings, notificationPreferences: notifPrefsToSave }) }); this.showToast('Settings saved', 'success'); } catch (err) { @@ -2630,10 +2636,22 @@ class ClaudemanApp { const res = await fetch('/api/settings'); if (res.ok) { const settings = await res.json(); - // Merge with localStorage (server takes precedence) + // Extract notification prefs before merging app settings + const { notificationPreferences, ...appSettings } = settings; + // Merge app settings with localStorage (server takes precedence) const localSettings = this.loadAppSettingsFromStorage(); - const merged = { ...localSettings, ...settings }; + const merged = { ...localSettings, ...appSettings }; localStorage.setItem('claudeman-app-settings', JSON.stringify(merged)); + + // Apply notification prefs from server if present (only if localStorage has none) + if (notificationPreferences && this.notificationManager) { + const localNotifPrefs = localStorage.getItem('claudeman-notification-prefs'); + if (!localNotifPrefs) { + this.notificationManager.preferences = notificationPreferences; + this.notificationManager.savePreferences(); + } + } + return merged; } } catch (err) { diff --git a/src/web/server.ts b/src/web/server.ts index 89c2f22e..22bf90c2 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -1306,7 +1306,7 @@ export class WebServer extends EventEmitter { }); this.app.put('/api/settings', async (req) => { - const settings = req.body as { defaultClaudeMdPath?: string; defaultWorkingDir?: string }; + const settings = req.body as Record; try { const dir = dirname(settingsPath);