Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions public/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@
hint.textContent = (window.GC.t || {})['settings.smtp.password_set'] || 'Password is set';
hint.style.display = '';
}
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('smtp');
}
}).catch(function(err) {
console.error('Failed to load SMTP settings:', err);
Expand Down Expand Up @@ -457,6 +458,7 @@
if (pwNum) { if (pw.require_number) pwNum.classList.add('on'); else pwNum.classList.remove('on'); }
var pwSpecial = document.getElementById('security-password-special');
if (pwSpecial) { if (pw.require_special) pwSpecial.classList.add('on'); else pwSpecial.classList.remove('on'); }
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('security');
} catch (err) {
console.error('Failed to load security settings:', err);
}
Expand Down Expand Up @@ -569,6 +571,7 @@
if (el2) el2.value = d.retention_activity_days;
var el3 = document.getElementById('data-peer-timeout');
if (el3) el3.value = d.peer_online_timeout;
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('data');
} catch (err) {
console.error('Failed to load data settings:', err);
}
Expand Down Expand Up @@ -621,6 +624,7 @@
if (monEmailToggle) { if (d.emailAlerts) monEmailToggle.classList.add('on'); else monEmailToggle.classList.remove('on'); }
var emailEl = document.getElementById('monitoring-alert-email');
if (emailEl) emailEl.value = d.alertEmail || '';
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('monitoring');
} catch (err) {
console.error('Failed to load monitoring settings:', err);
}
Expand Down Expand Up @@ -675,6 +679,7 @@
var groupEvents = cb.dataset.events.split(',');
cb.checked = groupEvents.some(function(e) { return configuredEvents.includes(e); });
});
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('alerts');
} catch (err) {
console.error('Failed to load alert settings:', err);
}
Expand Down Expand Up @@ -839,6 +844,7 @@
? new Date(d.lastRun).toLocaleString()
: (GC.t['autobackup.last_run_never'] || 'Never');
}
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('autobackup');
} catch (err) {
console.error('Failed to load auto-backup settings:', err);
}
Expand Down Expand Up @@ -983,6 +989,7 @@
if (data.data.enabled) metricsEnabledToggle.classList.add('on');
else metricsEnabledToggle.classList.remove('on');
}
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('metrics');
} catch (err) {
console.error('Failed to load metrics settings:', err);
}
Expand Down Expand Up @@ -1080,14 +1087,17 @@

if (dnsInput) {
api.get('/api/v1/settings/dns').then(function(data) {
if (data.ok) dnsInput.value = data.data.dns || '';
if (data.ok) {
dnsInput.value = data.data.dns || '';
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('dns');
}
}).catch(function() {});

SettingsAutosave.bind({
cluster: 'dns',
fields: [dnsInput],
statusEl: document.getElementById('dns-status'),
valuesById: function () { return { dns: dnsInput.value.trim() }; },
valuesById: function () { return { 'settings-dns-input': dnsInput.value.trim() }; },
save: function () { return api.put('/api/v1/settings/dns', { dns: dnsInput.value.trim() }); },
});
}
Expand All @@ -1100,6 +1110,7 @@
window.api.get('/api/system/auto-update').then(function (d) {
var el = card.querySelector('input[name="au-mode"][value="' + ((d && d.mode) || 'auto') + '"]');
if (el) el.checked = true;
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('auto-update');
}).catch(function () {});
var auRadios = Array.prototype.slice.call(document.querySelectorAll('input[name="au-mode"]'));
if (auRadios.length) {
Expand Down Expand Up @@ -1346,6 +1357,7 @@
linkLocal.checked = nets.some(function (n) { return n.cidr === LINK_LOCAL.cidr; });
customNets = nets.filter(function (n) { return pCidrs.indexOf(n.cidr) < 0 && n.cidr !== LINK_LOCAL.cidr; });
renderCustom();
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('split-tunnel');
} catch {}
}

Expand Down Expand Up @@ -1415,7 +1427,7 @@
cluster: 'gateway-failover',
fields: [sliderEl],
statusEl: document.getElementById('gw-failover-status'),
valuesById: function () { return { gw: sliderEl.value }; },
valuesById: function () { return { 'gw-down-threshold': sliderEl.value }; },
save: function () { return api.put('/api/v1/settings/gateway-failover', { gateway_down_threshold_s: parseInt(sliderEl.value, 10) }); },
});
}
Expand Down Expand Up @@ -1445,6 +1457,7 @@
if (intervalEl) intervalEl.value = cfg.sync_interval_sec || 30;
phInstances = (cfg.instances || []).slice();
renderInstances();
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('pihole');
} catch (err) {
console.error('Failed to load Pi-hole settings:', err);
}
Expand Down Expand Up @@ -1771,6 +1784,7 @@
setToggle(widgetDevice, d.widgets && d.widgets.device);
setToggle(widgetTraffic, d.widgets && d.widgets.traffic);
setToggle(widgetServices, d.widgets && d.widgets.services);
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('portal');
}).catch(function (err) {
console.error('Failed to load portal settings:', err);
});
Expand Down Expand Up @@ -1891,6 +1905,7 @@
if (bodyEl) bodyEl.value = r.data.body || '';
if (redirectEl) redirectEl.value = r.data.redirect_url || '';
syncSettingsBlockVisibility();
if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('route-block');
}
}).catch(function (err) {
console.error('Failed to load route block default:', err);
Expand Down
6 changes: 6 additions & 0 deletions public/js/settingsAutosave.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
var Core = window.SettingsAutosaveCore;
var enqueue = Core.createQueue(); // shared per-cluster serialization
var t = (window.GC && window.GC.t) || {};
var binds = {}; // registry of bound clusters → { resync }
window.SettingsAutosave = { enqueue: enqueue };

function flash(statusEl) {
Expand Down Expand Up @@ -44,6 +45,8 @@
var statusEl = opts.statusEl || null;
var valuesById = opts.valuesById || function () { return {}; };
var snapshot = JSON.stringify(valuesById()); // last successfully persisted state
function resync() { snapshot = JSON.stringify(valuesById()); }
if (cluster) binds[cluster] = { resync: resync };

// Dev guard: warn if any bound field id is absent from valuesById (would never be dirty).
var _boundIds = fields.map(function(f) { return f && f.id; }).filter(Boolean);
Expand Down Expand Up @@ -93,7 +96,10 @@
});
}
});

return { resync: resync };
}

window.SettingsAutosave.bind = bind;
window.SettingsAutosave.resync = function (cluster) { if (binds[cluster] && binds[cluster].resync) binds[cluster].resync(); };
})();
25 changes: 25 additions & 0 deletions tests/settings_autosave_smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,28 @@ test('two concurrent pihole PUTs leave a deterministic (non-corrupted) DB state'
license._overrideForTest({ pihole_integration: false });
}
});

test('resync mechanism: controller exposes resync and keeps a binds registry', async () => {
const ctrl = await supertest(app).get('/js/settingsAutosave.js').expect(200);
assert.match(ctrl.text, /SettingsAutosave\.resync/);
assert.match(ctrl.text, /binds\[/);
assert.match(ctrl.text, /function resync\(\)/);
});

test('settings.js calls SettingsAutosave.resync for all async-populated clusters', async () => {
const js = await supertest(app).get('/js/settings.js').expect(200);
const clusters = ['smtp', 'security', 'data', 'monitoring', 'alerts', 'autobackup', 'metrics', 'dns', 'auto-update', 'split-tunnel', 'pihole', 'portal', 'route-block'];
clusters.forEach(function (c) {
assert.match(js.text, new RegExp("SettingsAutosave\\.resync\\('" + c.replace('-', '\\-') + "'\\)"), 'missing resync for cluster: ' + c);
});
});

test('valuesById keys align with element ids: dns uses settings-dns-input, gateway-failover uses gw-down-threshold', async () => {
const js = await supertest(app).get('/js/settings.js').expect(200);
// dns valuesById must use the element id, not the semantic payload key 'dns'
assert.match(js.text, /'settings-dns-input': dnsInput/);
// gateway-failover valuesById must use the element id
assert.match(js.text, /'gw-down-threshold': sliderEl/);
// old semantic key 'gw' must be gone from valuesById (return { gw: sliderEl... })
assert.doesNotMatch(js.text, /return \{ gw: sliderEl/);
});
Loading