Files
pi-kit/pikit-web/assets/update-settings.js
2025-12-10 18:51:31 -05:00

345 lines
8.6 KiB
JavaScript

// UI controller for unattended-upgrades settings.
// Fetches current config, mirrors it into the form, and saves changes.
import { getUpdateConfig, saveUpdateConfig } from "./api.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;
msgEl.textContent = text || "";
msgEl.classList.toggle("error", isError);
if (text) {
if (toast && msgEl.id !== "updatesMsg") toast(text, isError ? "error" : "success");
setTimeout(() => (msgEl.textContent = ""), 2500);
}
}
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 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");
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);
} 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;