// UI controller for unattended-upgrades settings. // Fetches current config, mirrors it into the form, and saves changes. import { getUpdateConfig, saveUpdateConfig } from "./api.js"; import { logUi } from "./diaglog.js"; const TIME_RE = /^(\d{1,2}):(\d{2})$/; const MAX_BANDWIDTH_KBPS = 1_000_000; // 1 GB/s cap to prevent typos const fallback = (val, def) => (val === undefined || val === null ? def : val); let updatesDirty = false; function isValidTime(value) { if (!value) return false; const m = TIME_RE.exec(value.trim()); if (!m) return false; const h = Number(m[1]); const mi = Number(m[2]); return h >= 0 && h < 24 && mi >= 0 && mi < 60; } function normalizeTime(value, def) { return isValidTime(value) ? value.padStart(5, "0") : def; } export function initUpdateSettings({ elements, onAfterSave, overlay = { show: () => {}, hide: () => {} }, toast = null, }) { const { updatesStatus, updatesToggle, scopeSelect, updateTimeInput, upgradeTimeInput, cleanupToggle, bandwidthInput, rebootToggle, rebootTimeInput, rebootWithUsersToggle, saveBtn, msgEl, updatesUnsavedNote, updatesSection, updatesControls, } = elements; let lastConfig = null; let saving = false; let dirty = false; function normalizeConfig(cfg) { if (!cfg) return null; return { enable: cfg.enable !== undefined ? !!cfg.enable : cfg.enabled !== undefined ? !!cfg.enabled : false, scope: cfg.scope || "all", update_time: normalizeTime(cfg.update_time, "04:00"), upgrade_time: normalizeTime(cfg.upgrade_time, "04:30"), cleanup: !!cfg.cleanup, bandwidth_limit_kbps: cfg.bandwidth_limit_kbps === null || cfg.bandwidth_limit_kbps === undefined ? null : Number(cfg.bandwidth_limit_kbps), auto_reboot: !!cfg.auto_reboot, reboot_time: normalizeTime(cfg.reboot_time, "04:30"), reboot_with_users: !!cfg.reboot_with_users, }; } function setStatusChip(enabled) { const on = !!enabled; if (updatesToggle) updatesToggle.checked = on; if (updatesStatus) { updatesStatus.textContent = on ? "On" : "Off"; updatesStatus.classList.toggle("chip-on", on); updatesStatus.classList.toggle("chip-off", !on); } } function setControlsEnabled(on) { const controls = [ scopeSelect, updateTimeInput, upgradeTimeInput, cleanupToggle, bandwidthInput, rebootToggle, rebootTimeInput, rebootWithUsersToggle, ]; controls.forEach((el) => { if (el) el.disabled = !on; }); if (updatesControls) { updatesControls.classList.toggle("is-disabled", !on); } // Reboot sub-controls follow their own toggle if (rebootToggle) { const allowReboot = on && rebootToggle.checked; if (rebootTimeInput) rebootTimeInput.disabled = !allowReboot; if (rebootWithUsersToggle) rebootWithUsersToggle.disabled = !allowReboot; } } function setRebootControlsState(on) { if (rebootTimeInput) rebootTimeInput.disabled = !on; if (rebootWithUsersToggle) rebootWithUsersToggle.disabled = !on; } function showMessage(text, isError = false) { if (!msgEl) return; if (isError) { msgEl.textContent = text || "Something went wrong"; msgEl.classList.add("error"); toast?.(text || "Error", "error"); } else { msgEl.textContent = text || ""; msgEl.classList.remove("error"); } } function currentConfigFromForm() { try { return normalizeConfig(buildPayload()); } catch (e) { return null; } } function setDirty(on) { dirty = !!on; updatesDirty = dirty; if (saveBtn) saveBtn.disabled = !dirty; if (updatesUnsavedNote) updatesUnsavedNote.classList.toggle("hidden", !dirty); } function populateForm(cfg) { lastConfig = normalizeConfig(cfg); setStatusChip(cfg?.enabled); setControlsEnabled(cfg?.enabled); if (scopeSelect) scopeSelect.value = cfg.scope || "all"; if (updateTimeInput) updateTimeInput.value = normalizeTime(cfg.update_time, "04:00"); if (upgradeTimeInput) upgradeTimeInput.value = normalizeTime(cfg.upgrade_time, "04:30"); if (cleanupToggle) cleanupToggle.checked = !!cfg.cleanup; if (bandwidthInput) { bandwidthInput.value = cfg.bandwidth_limit_kbps === null || cfg.bandwidth_limit_kbps === undefined ? "" : cfg.bandwidth_limit_kbps; } if (rebootToggle) rebootToggle.checked = !!cfg.auto_reboot; if (rebootTimeInput) rebootTimeInput.value = normalizeTime(cfg.reboot_time, "04:30"); if (rebootWithUsersToggle) rebootWithUsersToggle.checked = !!cfg.reboot_with_users; setRebootControlsState(rebootToggle?.checked); setDirty(false); } function buildPayload() { const enable = updatesToggle?.checked !== false; const scope = scopeSelect?.value === "security" ? "security" : "all"; const updateTime = normalizeTime(updateTimeInput?.value, "04:00"); const upgradeTime = normalizeTime( upgradeTimeInput?.value || updateTime, "04:30", ); if (!isValidTime(updateTime) || !isValidTime(upgradeTime)) { throw new Error("Time must be HH:MM (24h)."); } const bwRaw = bandwidthInput?.value?.trim(); let bw = null; if (bwRaw) { const n = Number(bwRaw); if (Number.isNaN(n) || n < 0) throw new Error("Bandwidth must be >= 0."); if (n > MAX_BANDWIDTH_KBPS) { throw new Error(`Bandwidth too high (max ${MAX_BANDWIDTH_KBPS.toLocaleString()} KB/s).`); } bw = n === 0 ? null : n; } const autoReboot = !!rebootToggle?.checked; const rebootTime = normalizeTime( rebootTimeInput?.value || upgradeTime, "04:30", ); if (autoReboot && !isValidTime(rebootTime)) { throw new Error("Reboot time must be HH:MM (24h)."); } return { enable: enable !== false, scope, update_time: updateTime, upgrade_time: upgradeTime, cleanup: !!cleanupToggle?.checked, bandwidth_limit_kbps: bw, auto_reboot: autoReboot, reboot_time: rebootTime, reboot_with_users: !!rebootWithUsersToggle?.checked, }; } async function loadConfig() { try { const cfg = await getUpdateConfig(); populateForm(cfg); } catch (e) { console.error("Failed to load update config", e); showMessage("Could not load update settings", true); } } async function persistConfig({ overrideEnable = null } = {}) { if (saving) return; saving = true; showMessage(""); try { const prev = lastConfig ? { ...lastConfig } : null; const payload = buildPayload(); if (overrideEnable !== null) payload.enable = !!overrideEnable; overlay.show?.("Saving updates", "Applying unattended-upgrades settings…"); const cfg = await saveUpdateConfig(payload); populateForm(cfg); showMessage("Update settings saved."); toast?.("Updates saved", "success"); logUi("Update settings saved", "info", { from: prev, to: payload }); onAfterSave?.(); setDirty(false); } catch (e) { console.error(e); if (overrideEnable !== null && lastConfig) { // revert toggle on failure setStatusChip(lastConfig.enabled); setControlsEnabled(lastConfig.enabled); } showMessage(e?.error || e?.message || "Save failed", true); logUi("Update settings save failed", "error", { payload: (() => { try { return buildPayload(); } catch { return null; } })(), reason: e?.error || e?.message, }); } finally { saving = false; overlay.hide?.(); } } updatesToggle?.addEventListener("change", () => { setStatusChip(updatesToggle.checked); setControlsEnabled(updatesToggle.checked); const cfgNow = currentConfigFromForm(); setDirty(!lastConfig || (cfgNow && JSON.stringify(cfgNow) !== JSON.stringify(lastConfig))); }); saveBtn?.addEventListener("click", async () => { await persistConfig(); }); rebootToggle?.addEventListener("change", () => { setRebootControlsState(rebootToggle.checked); const cfgNow = currentConfigFromForm(); setDirty(!lastConfig || (cfgNow && JSON.stringify(cfgNow) !== JSON.stringify(lastConfig))); }); [ scopeSelect, updateTimeInput, upgradeTimeInput, cleanupToggle, bandwidthInput, rebootTimeInput, rebootWithUsersToggle, ] .filter(Boolean) .forEach((el) => { el.addEventListener("input", () => { const cfgNow = currentConfigFromForm(); setDirty(!lastConfig || (cfgNow && JSON.stringify(cfgNow) !== JSON.stringify(lastConfig))); }); el.addEventListener("change", () => { const cfgNow = currentConfigFromForm(); setDirty(!lastConfig || (cfgNow && JSON.stringify(cfgNow) !== JSON.stringify(lastConfig))); }); }); loadConfig(); return { reload: loadConfig, isDirty: () => dirty }; } export const isUpdatesDirty = () => updatesDirty;