Add dashboard UI updates and settings modal
This commit is contained in:
344
pikit-web/assets/update-settings.js
Normal file
344
pikit-web/assets/update-settings.js
Normal file
@@ -0,0 +1,344 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user