44
55// WolfStack Dashboard — app.js
66
7+ // ─── User Preferences (synced to server) ───
8+
9+ // Keys that should be persisted server-side per user
10+ const _syncedPrefKeys = [
11+ 'wolfstack-theme', 'wolfstack-icon-theme', 'wolfstack-mono-icons', 'wolfstack-mono-preset',
12+ 'wolfstack_sidebar_collapsed', 'wolfstack_map_collapsed',
13+ 'wolfstack_dc_layout', 'wolfstack_dc_compact', 'wolfstack_dc_bg_image',
14+ 'wolfstack_dc_bg_brightness', 'wolfstack_dc_bg_blur',
15+ 'wolfstack_bookmarks', 'wolfstack_view_docker', 'wolfstack_view_lxc', 'wolfstack_view_vm',
16+ ];
17+ let _prefsLoaded = false;
18+
19+ // Load user preferences from server and apply to localStorage.
20+ // If the server has no saved prefs yet, upload current localStorage values
21+ // so existing users don't lose their bookmarks or theme on first sync.
22+ async function loadUserPreferences() {
23+ try {
24+ var resp = await fetch('/api/user/preferences');
25+ if (!resp.ok) return;
26+ var prefs = await resp.json();
27+ if (!prefs || typeof prefs !== 'object') return;
28+
29+ var serverHasData = Object.keys(prefs).length > 0;
30+
31+ if (serverHasData) {
32+ // Server has prefs — apply to localStorage
33+ for (var key of _syncedPrefKeys) {
34+ if (prefs[key] !== undefined && prefs[key] !== null) {
35+ localStorage.setItem(key, typeof prefs[key] === 'string' ? prefs[key] : JSON.stringify(prefs[key]));
36+ }
37+ }
38+ } else {
39+ // Server has nothing — upload current localStorage so existing users keep their settings
40+ var upload = {};
41+ var hasAnything = false;
42+ for (var key of _syncedPrefKeys) {
43+ var val = localStorage.getItem(key);
44+ if (val !== null) {
45+ upload[key] = val;
46+ hasAnything = true;
47+ }
48+ }
49+ if (hasAnything) {
50+ fetch('/api/user/preferences', {
51+ method: 'POST',
52+ headers: { 'Content-Type': 'application/json' },
53+ body: JSON.stringify(upload),
54+ }).catch(function() {});
55+ }
56+ }
57+ _prefsLoaded = true;
58+ } catch (e) { /* silent — localStorage fallback works */ }
59+ }
60+
61+ // Save a single preference to both localStorage and server
62+ function savePref(key, value) {
63+ localStorage.setItem(key, value);
64+ if (_syncedPrefKeys.includes(key)) {
65+ var patch = {};
66+ patch[key] = value;
67+ fetch('/api/user/preferences', {
68+ method: 'PATCH',
69+ headers: { 'Content-Type': 'application/json' },
70+ body: JSON.stringify(patch),
71+ }).catch(function() {});
72+ }
73+ }
74+
75+ // Remove a preference from both localStorage and server
76+ function removePref(key) {
77+ localStorage.removeItem(key);
78+ if (_syncedPrefKeys.includes(key)) {
79+ var patch = {};
80+ patch[key] = null;
81+ fetch('/api/user/preferences', {
82+ method: 'PATCH',
83+ headers: { 'Content-Type': 'application/json' },
84+ body: JSON.stringify(patch),
85+ }).catch(function() {});
86+ }
87+ }
88+
789// ─── State ───
890let currentPage = 'datacenter';
991let currentComponent = null;
@@ -33,7 +115,7 @@ function toggleSidebar() {
33115 // Desktop: toggle collapsed sidebar
34116 document.body.classList.toggle('sidebar-collapsed');
35117 const collapsed = document.body.classList.contains('sidebar-collapsed');
36- localStorage.setItem ('wolfstack_sidebar_collapsed', collapsed ? '1' : '0');
118+ savePref ('wolfstack_sidebar_collapsed', collapsed ? '1' : '0');
37119 }
38120 // Invalidate map size + 3D topology after sidebar transition
39121 setTimeout(() => {
@@ -67,7 +149,7 @@ let mapCollapsed = localStorage.getItem('wolfstack_map_collapsed') === '1'; // d
67149
68150function toggleMapCollapse() {
69151 mapCollapsed = !mapCollapsed;
70- localStorage.setItem ('wolfstack_map_collapsed', mapCollapsed ? '1' : '0');
152+ savePref ('wolfstack_map_collapsed', mapCollapsed ? '1' : '0');
71153 applyMapCollapse();
72154}
73155
@@ -530,7 +612,7 @@ function loadBookmarks() {
530612 catch { return []; }
531613}
532614function saveBookmarks(bookmarks) {
533- localStorage.setItem ('wolfstack_bookmarks', JSON.stringify(bookmarks));
615+ savePref ('wolfstack_bookmarks', JSON.stringify(bookmarks));
534616}
535617function renderBookmarks() {
536618 const el = document.getElementById('bookmarks-list');
@@ -705,7 +787,7 @@ function toggleBgSettingsPanel() {
705787
706788function setDcLayout(layout) {
707789 dcLayout = layout;
708- localStorage.setItem ('wolfstack_dc_layout', layout);
790+ savePref ('wolfstack_dc_layout', layout);
709791 const icon = document.getElementById('dc-layout-icon');
710792 const label = document.getElementById('dc-layout-label');
711793 if (icon) icon.textContent = layout === 'grid' ? '⊞' : '▥';
@@ -718,7 +800,7 @@ function setDcLayout(layout) {
718800
719801function toggleDcCompact(checked) {
720802 dcCompact = checked;
721- localStorage.setItem ('wolfstack_dc_compact', checked ? '1' : '0');
803+ savePref ('wolfstack_dc_compact', checked ? '1' : '0');
722804 renderDatacenterOverview();
723805}
724806
@@ -732,29 +814,29 @@ function handleBgUpload(input) {
732814 const reader = new FileReader();
733815 reader.onload = (e) => {
734816 dcBgImage = e.target.result;
735- localStorage.setItem ('wolfstack_dc_bg_image', dcBgImage);
817+ savePref ('wolfstack_dc_bg_image', dcBgImage);
736818 applyDcBackground();
737819 };
738820 reader.readAsDataURL(file);
739821}
740822
741823function clearBgImage() {
742824 dcBgImage = '';
743- localStorage.removeItem ('wolfstack_dc_bg_image');
825+ removePref ('wolfstack_dc_bg_image');
744826 applyDcBackground();
745827}
746828
747829function updateBgBrightness(val) {
748830 dcBgBrightness = parseInt(val, 10);
749- localStorage.setItem ('wolfstack_dc_bg_brightness', val);
831+ savePref ('wolfstack_dc_bg_brightness', val);
750832 const el = document.getElementById('dc-bg-bright-val');
751833 if (el) el.textContent = val + '%';
752834 applyDcBackground();
753835}
754836
755837function updateBgBlur(val) {
756838 dcBgBlur = parseInt(val, 10);
757- localStorage.setItem ('wolfstack_dc_bg_blur', val);
839+ savePref ('wolfstack_dc_bg_blur', val);
758840 const el = document.getElementById('dc-bg-blur-val');
759841 if (el) el.textContent = val + 'px';
760842 applyDcBackground();
@@ -9836,6 +9918,7 @@ function tomlToggleRaw(component, displayName) {
98369918const POLL_FAST = isMobileView() ? 30000 : 15000;
98379919const POLL_SLOW = isMobileView() ? 120000 : 60000;
98389920let _currentPollSpeed = 'fast';
9921+ loadUserPreferences(); // Load user preferences from server (async, non-blocking)
98399922fetchNodes();
98409923fetchMetricsHistory(); // Initial history load
98419924loadTaskLog(); // Restore task log from localStorage
@@ -11481,7 +11564,7 @@ function getContainerView(type) {
1148111564}
1148211565
1148311566function setContainerView(type, view) {
11484- localStorage.setItem (`wolfstack_view_${type}`, view);
11567+ savePref (`wolfstack_view_${type}`, view);
1148511568 applyContainerView(type);
1148611569 // Re-render the current data
1148711570 if (type === 'docker') loadDockerContainers();
@@ -22270,7 +22353,7 @@ function applyTheme(themeId) {
2227022353 }
2227122354
2227222355 // Save preference
22273- localStorage.setItem ('wolfstack-theme', themeId);
22356+ savePref ('wolfstack-theme', themeId);
2227422357
2227522358 // Swap logo for light/dark backgrounds
2227622359 const lightThemes = ['light', 'arctic'];
@@ -22321,7 +22404,7 @@ function applyIconTheme(themeName) {
2232122404 document.body.appendChild(modal);
2232222405 modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
2232322406 document.getElementById('icon-theme-confirm-btn').onclick = function() {
22324- localStorage.setItem ('wolfstack-icon-theme', themeName);
22407+ savePref ('wolfstack-icon-theme', themeName);
2232522408 location.reload();
2232622409 };
2232722410}
@@ -22331,7 +22414,7 @@ async function initIconTheme() {
2233122414 // Migrate old "candy" key to "candy_emoji"
2233222415 if (currentIconTheme === 'candy') {
2233322416 currentIconTheme = 'candy_emoji';
22334- localStorage.setItem ('wolfstack-icon-theme', 'candy_emoji');
22417+ savePref ('wolfstack-icon-theme', 'candy_emoji');
2233522418 }
2233622419 // Update icon theme selector UI
2233722420 document.querySelectorAll('.icon-theme-card').forEach(card => {
@@ -22532,12 +22615,12 @@ const MONO_PRESETS = [
2253222615function toggleMonoIcons(enabled) {
2253322616 if (enabled) {
2253422617 document.documentElement.setAttribute('data-mono-icons', '');
22535- localStorage.setItem ('wolfstack-mono-icons', '1');
22618+ savePref ('wolfstack-mono-icons', '1');
2253622619 const saved = localStorage.getItem('wolfstack-mono-preset') || 'Gray';
2253722620 applyMonoPreset(saved);
2253822621 } else {
2253922622 document.documentElement.removeAttribute('data-mono-icons');
22540- localStorage.removeItem ('wolfstack-mono-icons');
22623+ removePref ('wolfstack-mono-icons');
2254122624 }
2254222625 const opts = document.getElementById('mono-icons-options');
2254322626 if (opts) opts.style.display = enabled ? '' : 'none';
@@ -22550,7 +22633,7 @@ function applyMonoPreset(name) {
2255022633 root.style.setProperty('--mono-hue', preset.hue + 'deg');
2255122634 root.style.setProperty('--mono-sat', preset.sat);
2255222635 root.style.setProperty('--mono-brightness', preset.brightness);
22553- localStorage.setItem ('wolfstack-mono-preset', preset.name);
22636+ savePref ('wolfstack-mono-preset', preset.name);
2255422637 // Update active state on preset buttons
2255522638 document.querySelectorAll('.mono-preset-btn').forEach(btn => {
2255622639 btn.style.outline = btn.dataset.preset === preset.name ? '2px solid var(--accent)' : 'none';
0 commit comments