433 lines
16 KiB
JavaScript
433 lines
16 KiB
JavaScript
// Entry point for the dashboard: wires UI events, pulls status, and initializes
|
|
// feature modules (services, settings, stats).
|
|
import { getStatus, triggerReset } from "./api.js";
|
|
import { initServiceControls } from "./services.js";
|
|
import { placeholderStatus, renderStats } from "./status.js";
|
|
import { initSettings } from "./settings.js";
|
|
import { initUpdateSettings, isUpdatesDirty } from "./update-settings.js";
|
|
import { initReleaseUI } from "./releases.js?v=20251213h";
|
|
import { initDiagUI, logUi } from "./diaglog.js?v=20251213j";
|
|
import { createToastManager } from "./toast.js?v=20251213a";
|
|
import {
|
|
applyTooltips,
|
|
wireModalPairs,
|
|
wireAccordions,
|
|
createBusyOverlay,
|
|
createConfirmModal,
|
|
} from "./ui.js";
|
|
import { createStatusController } from "./status-controller.js";
|
|
|
|
const servicesGrid = document.getElementById("servicesGrid");
|
|
const heroStats = document.getElementById("heroStats");
|
|
const refreshHintMain = document.getElementById("refreshHintMain");
|
|
const refreshHintServices = document.getElementById("refreshHintServices");
|
|
const refreshFlagTop = document.getElementById("refreshFlagTop");
|
|
const themeToggle = document.getElementById("themeToggle");
|
|
const themeToggleIcon = document.getElementById("themeToggleIcon");
|
|
const animToggle = document.getElementById("animToggle");
|
|
|
|
const resetConfirm = document.getElementById("resetConfirm");
|
|
const resetBtn = document.getElementById("resetBtn");
|
|
const updatesToggle = document.getElementById("updatesToggle");
|
|
const updatesStatus = document.getElementById("updatesStatus");
|
|
const updatesFlagTop = document.getElementById("updatesFlagTop");
|
|
const updatesNoteTop = document.getElementById("updatesNoteTop");
|
|
const tempFlagTop = document.getElementById("tempFlagTop");
|
|
const releaseFlagTop = document.getElementById("releaseFlagTop");
|
|
const refreshIntervalInput = document.getElementById("refreshIntervalInput");
|
|
const refreshIntervalSave = document.getElementById("refreshIntervalSave");
|
|
const refreshIntervalMsg = document.getElementById("refreshIntervalMsg");
|
|
const toastPosSelect = document.getElementById("toastPosSelect");
|
|
const toastAnimSelect = document.getElementById("toastAnimSelect");
|
|
const toastSpeedInput = document.getElementById("toastSpeedInput");
|
|
const toastDurationInput = document.getElementById("toastDurationInput");
|
|
const toastTestBtn = document.getElementById("toastTestBtn");
|
|
const fontSelect = document.getElementById("fontSelect");
|
|
const updatesScope = document.getElementById("updatesScope");
|
|
const updateTimeInput = document.getElementById("updateTimeInput");
|
|
const upgradeTimeInput = document.getElementById("upgradeTimeInput");
|
|
const updatesCleanup = document.getElementById("updatesCleanup");
|
|
const updatesBandwidth = document.getElementById("updatesBandwidth");
|
|
const updatesRebootToggle = document.getElementById("updatesRebootToggle");
|
|
const updatesRebootTime = document.getElementById("updatesRebootTime");
|
|
const updatesRebootUsers = document.getElementById("updatesRebootUsers");
|
|
const updatesSaveBtn = document.getElementById("updatesSaveBtn");
|
|
const updatesMsg = document.getElementById("updatesMsg");
|
|
const updatesUnsavedNote = document.getElementById("updatesUnsavedNote");
|
|
const updatesSection = document.getElementById("updatesSection");
|
|
const svcName = document.getElementById("svcName");
|
|
const svcPort = document.getElementById("svcPort");
|
|
const svcPath = document.getElementById("svcPath");
|
|
const svcAddBtn = document.getElementById("svcAddBtn");
|
|
const svcMsg = document.getElementById("svcMsg");
|
|
const svcScheme = document.getElementById("svcScheme");
|
|
const svcNotice = document.getElementById("svcNotice");
|
|
const svcNoticeLink = document.getElementById("svcNoticeLink");
|
|
const svcSelfSigned = document.getElementById("svcSelfSigned");
|
|
const svcSelfSignedLabel = document.querySelector("label[for='svcSelfSigned']") || null;
|
|
const addServiceModal = document.getElementById("addServiceModal");
|
|
const addSvcClose = document.getElementById("addSvcClose");
|
|
const addServiceOpen = document.getElementById("addServiceOpen");
|
|
const menuModal = document.getElementById("menuModal");
|
|
const menuTitle = document.getElementById("menuTitle");
|
|
const menuSubtitle = document.getElementById("menuSubtitle");
|
|
const menuRename = document.getElementById("menuRename");
|
|
const menuPort = document.getElementById("menuPort");
|
|
const menuPath = document.getElementById("menuPath");
|
|
const menuScheme = document.getElementById("menuScheme");
|
|
const menuNotice = document.getElementById("menuNotice");
|
|
const menuNoticeLink = document.getElementById("menuNoticeLink");
|
|
const menuSelfSigned = document.getElementById("menuSelfSigned");
|
|
const menuSaveBtn = document.getElementById("menuSaveBtn");
|
|
const menuCancelBtn = document.getElementById("menuCancelBtn");
|
|
const menuRemoveBtn = document.getElementById("menuRemoveBtn");
|
|
const menuMsg = document.getElementById("menuMsg");
|
|
const menuClose = document.getElementById("menuClose");
|
|
|
|
const advBtn = document.getElementById("advBtn");
|
|
const advModal = document.getElementById("advModal");
|
|
const advClose = document.getElementById("advClose");
|
|
|
|
const helpBtn = document.getElementById("helpBtn");
|
|
const helpModal = document.getElementById("helpModal");
|
|
const helpClose = document.getElementById("helpClose");
|
|
const aboutBtn = document.getElementById("aboutBtn");
|
|
const aboutModal = document.getElementById("aboutModal");
|
|
const aboutClose = document.getElementById("aboutClose");
|
|
const busyOverlay = document.getElementById("busyOverlay");
|
|
const busyTitle = document.getElementById("busyTitle");
|
|
const busyText = document.getElementById("busyText");
|
|
const toastContainer = document.getElementById("toastContainer");
|
|
const readyOverlay = document.getElementById("readyOverlay");
|
|
const confirmModal = document.getElementById("confirmModal");
|
|
const confirmTitle = document.getElementById("confirmTitle");
|
|
const confirmBody = document.getElementById("confirmBody");
|
|
const confirmOk = document.getElementById("confirmOk");
|
|
const confirmCancel = document.getElementById("confirmCancel");
|
|
const changelogModal = document.getElementById("changelogModal");
|
|
const changelogTitle = document.getElementById("changelogTitle");
|
|
const changelogBody = document.getElementById("changelogBody");
|
|
const changelogClose = document.getElementById("changelogClose");
|
|
const diagEnableToggle = document.getElementById("diagEnableToggle");
|
|
const diagDebugToggle = document.getElementById("diagDebugToggle");
|
|
const diagRefreshBtn = document.getElementById("diagRefreshBtn");
|
|
const diagClearBtn = document.getElementById("diagClearBtn");
|
|
const diagCopyBtn = document.getElementById("diagCopyBtn");
|
|
const diagDownloadBtn = document.getElementById("diagDownloadBtn");
|
|
const diagLogBox = document.getElementById("diagLogBox");
|
|
const diagStatus = document.getElementById("diagStatus");
|
|
const diagLogBtn = document.getElementById("diagLogBtn");
|
|
const diagModal = document.getElementById("diagModal");
|
|
const diagClose = document.getElementById("diagClose");
|
|
const diagStatusModal = document.getElementById("diagStatusModal");
|
|
|
|
const toastController = createToastManager({
|
|
container: toastContainer,
|
|
posSelect: toastPosSelect,
|
|
animSelect: toastAnimSelect,
|
|
speedInput: toastSpeedInput,
|
|
durationInput: toastDurationInput,
|
|
fontSelect,
|
|
testBtn: toastTestBtn,
|
|
});
|
|
const showToast = toastController.showToast;
|
|
let releaseUI = null;
|
|
const { showBusy, hideBusy } = createBusyOverlay({
|
|
overlay: busyOverlay,
|
|
titleEl: busyTitle,
|
|
textEl: busyText,
|
|
});
|
|
const confirmAction = createConfirmModal({
|
|
modal: confirmModal,
|
|
titleEl: confirmTitle,
|
|
bodyEl: confirmBody,
|
|
okBtn: confirmOk,
|
|
cancelBtn: confirmCancel,
|
|
});
|
|
const collapseAccordions = () => document.querySelectorAll(".accordion").forEach((a) => a.classList.remove("open"));
|
|
|
|
const statusController = createStatusController({
|
|
heroStats,
|
|
servicesGrid,
|
|
updatesFlagTop,
|
|
updatesNoteTop,
|
|
tempFlagTop,
|
|
readyOverlay,
|
|
logUi,
|
|
getStatus,
|
|
isUpdatesDirty,
|
|
setUpdatesUI,
|
|
updatesFlagEl: setUpdatesFlag,
|
|
releaseUIGetter: () => releaseUI,
|
|
onReadyWait: () => setTimeout(() => statusController.loadStatus(), 3000),
|
|
});
|
|
const { loadStatus } = statusController;
|
|
|
|
function wireDialogs() {
|
|
wireModalPairs([
|
|
{ openBtn: aboutBtn, modal: aboutModal, closeBtn: aboutClose },
|
|
{ openBtn: helpBtn, modal: helpModal, closeBtn: helpClose },
|
|
]);
|
|
// Settings modal keeps custom accordion collapse on close
|
|
advBtn?.addEventListener("click", () => {
|
|
if (window.__pikitTest?.forceServiceFormVisible) {
|
|
// For tests: avoid opening any modal; just ensure form controls are visible
|
|
addServiceModal?.classList.add("hidden");
|
|
addServiceModal?.setAttribute("style", "display:none;");
|
|
window.__pikitTest.forceServiceFormVisible();
|
|
return;
|
|
}
|
|
advModal.classList.remove("hidden");
|
|
});
|
|
advClose?.addEventListener("click", () => {
|
|
advModal.classList.add("hidden");
|
|
collapseAccordions();
|
|
});
|
|
menuClose.onclick = () => menuModal.classList.add("hidden");
|
|
addServiceOpen?.addEventListener("click", openAddService);
|
|
addSvcClose?.addEventListener("click", () => addServiceModal?.classList.add("hidden"));
|
|
addServiceModal?.addEventListener("click", (e) => {
|
|
if (e.target === addServiceModal) addServiceModal.classList.add("hidden");
|
|
});
|
|
}
|
|
|
|
// Testing hook
|
|
if (typeof window !== "undefined") {
|
|
window.__pikitTest = window.__pikitTest || {};
|
|
window.__pikitTest.showBusy = showBusy;
|
|
window.__pikitTest.hideBusy = hideBusy;
|
|
window.__pikitTest.exposeServiceForm = () => {
|
|
if (!addServiceModal) return;
|
|
const card = addServiceModal.querySelector(".modal-card");
|
|
if (!card) return;
|
|
addServiceModal.classList.add("hidden"); // keep overlay out of the way
|
|
card.style.position = "static";
|
|
card.style.background = "transparent";
|
|
card.style.boxShadow = "none";
|
|
card.style.border = "none";
|
|
card.style.padding = "0";
|
|
card.style.margin = "12px auto";
|
|
card.style.maxWidth = "720px";
|
|
// Move the form inline so Playwright can see it without the overlay
|
|
document.body.appendChild(card);
|
|
};
|
|
}
|
|
|
|
const TOOLTIP_MAP = {
|
|
updatesFlagTop: "Auto updates status; configure in Settings → Automatic updates.",
|
|
refreshFlagTop: "Auto-refresh interval; change in Settings → Refresh interval.",
|
|
tempFlagTop: "CPU temperature status; see details in the hero stats below.",
|
|
releaseFlagTop: "Pi-Kit release status; open Settings → Updates to install.",
|
|
themeToggle: "Toggle light or dark theme",
|
|
helpBtn: "Open quick help",
|
|
advBtn: "Open settings",
|
|
animToggle: "Enable or disable dashboard animations",
|
|
refreshIntervalInput: "Seconds between automatic refreshes (5-120)",
|
|
refreshIntervalSave: "Save refresh interval",
|
|
svcName: "Display name for the service card",
|
|
svcPort: "Port number the service listens on",
|
|
svcPath: "Optional path like /admin",
|
|
svcScheme: "Choose HTTP or HTTPS link",
|
|
svcSelfSigned: "Mark service as using a self-signed certificate",
|
|
svcNotice: "Optional note shown on the service card",
|
|
svcNoticeLink: "Optional link for more info about the service",
|
|
svcAddBtn: "Add the service to the dashboard",
|
|
updatesToggle: "Turn unattended upgrades on or off",
|
|
updatesScope: "Select security-only or all updates",
|
|
updateTimeInput: "Time to download updates (24h)",
|
|
upgradeTimeInput: "Time to install updates (24h)",
|
|
updatesCleanup: "Remove unused dependencies after upgrades",
|
|
updatesBandwidth: "Limit download bandwidth in KB/s (0 = unlimited)",
|
|
updatesRebootToggle: "Auto-reboot if required by updates",
|
|
updatesRebootTime: "Scheduled reboot time when auto-reboot is on",
|
|
updatesRebootUsers: "Allow reboot even if users are logged in",
|
|
updatesSaveBtn: "Save unattended-upgrades settings",
|
|
resetConfirm: "Type YES to enable factory reset",
|
|
resetBtn: "Factory reset this Pi-Kit",
|
|
menuRename: "Change the service display name",
|
|
menuPort: "Change the service port",
|
|
menuPath: "Optional service path",
|
|
menuScheme: "Switch between HTTP and HTTPS",
|
|
menuSelfSigned: "Mark the service as self-signed",
|
|
menuNotice: "Edit the notice text shown on the card",
|
|
menuNoticeLink: "Optional link for the notice",
|
|
menuSaveBtn: "Save service changes",
|
|
menuCancelBtn: "Cancel changes",
|
|
menuRemoveBtn: "Remove this service",
|
|
};
|
|
|
|
// Clamp name inputs to 30 chars
|
|
[svcName, menuRename].forEach((el) => {
|
|
if (!el) return;
|
|
el.setAttribute("maxlength", "32");
|
|
el.addEventListener("input", () => {
|
|
if (el.value.length > 32) el.value = el.value.slice(0, 32);
|
|
});
|
|
});
|
|
|
|
function setUpdatesUI(enabled) {
|
|
const on = !!enabled;
|
|
updatesToggle.checked = on;
|
|
updatesStatus.textContent = on ? "On" : "Off";
|
|
updatesStatus.classList.toggle("chip-on", on);
|
|
updatesStatus.classList.toggle("chip-off", !on);
|
|
}
|
|
|
|
function setUpdatesFlag(enabled) {
|
|
if (!updatesFlagTop) return;
|
|
const labelOn = "System updates: On";
|
|
const labelOff = "System updates: Off";
|
|
updatesFlagTop.textContent =
|
|
enabled === true ? labelOn : enabled === false ? labelOff : "System updates";
|
|
updatesFlagTop.className = "status-chip quiet chip-system";
|
|
if (enabled === false) updatesFlagTop.classList.add("chip-off");
|
|
}
|
|
|
|
function wireResetAndUpdates() {
|
|
resetBtn.onclick = async () => {
|
|
resetBtn.disabled = true;
|
|
try {
|
|
await triggerReset(resetConfirm.value.trim());
|
|
alert("Resetting now. The device will reboot.");
|
|
} catch (e) {
|
|
alert(e.error || "Reset failed");
|
|
} finally {
|
|
resetBtn.disabled = false;
|
|
}
|
|
};
|
|
|
|
resetConfirm.addEventListener("input", () => {
|
|
resetBtn.disabled = resetConfirm.value.trim() !== "YES";
|
|
});
|
|
}
|
|
|
|
function openAddService() {
|
|
if (addServiceModal) addServiceModal.classList.remove("hidden");
|
|
document.getElementById("svcName")?.focus();
|
|
}
|
|
if (typeof window !== "undefined") {
|
|
window.__pikitOpenAddService = openAddService;
|
|
}
|
|
|
|
function main() {
|
|
applyTooltips(TOOLTIP_MAP);
|
|
// Test convenience: ensure service form elements are visible when hook is set
|
|
if (window.__pikitTest?.forceServiceFormVisible) {
|
|
window.__pikitTest.forceServiceFormVisible();
|
|
window.__pikitTest.exposeServiceForm?.();
|
|
}
|
|
wireDialogs();
|
|
wireResetAndUpdates();
|
|
wireAccordions({
|
|
forceOpen: typeof window !== "undefined" && window.__pikitTest?.forceAccordionsOpen,
|
|
});
|
|
releaseUI = initReleaseUI({
|
|
showToast,
|
|
showBusy,
|
|
hideBusy,
|
|
confirmAction,
|
|
logUi,
|
|
});
|
|
|
|
initServiceControls({
|
|
gridEl: servicesGrid,
|
|
onChange: loadStatus,
|
|
overlay: { show: showBusy, hide: hideBusy },
|
|
toast: showToast,
|
|
openAddService,
|
|
menu: {
|
|
modal: menuModal,
|
|
title: menuTitle,
|
|
subtitle: menuSubtitle,
|
|
renameInput: menuRename,
|
|
portInput: menuPort,
|
|
pathInput: menuPath,
|
|
schemeSelect: menuScheme,
|
|
saveBtn: menuSaveBtn,
|
|
cancelBtn: menuCancelBtn,
|
|
removeBtn: menuRemoveBtn,
|
|
msg: menuMsg,
|
|
noticeInput: menuNotice,
|
|
noticeLinkInput: menuNoticeLink,
|
|
selfSignedInput: menuSelfSigned,
|
|
},
|
|
addForm: {
|
|
nameInput: svcName,
|
|
portInput: svcPort,
|
|
pathInput: svcPath,
|
|
schemeSelect: svcScheme,
|
|
addBtn: svcAddBtn,
|
|
msg: svcMsg,
|
|
noticeInput: svcNotice,
|
|
noticeLinkInput: svcNoticeLink,
|
|
selfSignedInput: svcSelfSigned,
|
|
},
|
|
});
|
|
|
|
initSettings({
|
|
refreshHintMain,
|
|
refreshHintServices,
|
|
refreshFlagTop,
|
|
refreshIntervalInput,
|
|
refreshIntervalSave,
|
|
refreshIntervalMsg,
|
|
themeToggle,
|
|
themeToggleIcon,
|
|
animToggle,
|
|
onTick: loadStatus,
|
|
toast: showToast,
|
|
onThemeToggle: () => {
|
|
document.body.classList.add("theming");
|
|
setTimeout(() => document.body.classList.remove("theming"), 300);
|
|
},
|
|
});
|
|
|
|
// Diagnostics
|
|
initDiagUI({
|
|
elements: {
|
|
enableToggle: diagEnableToggle,
|
|
debugToggle: diagDebugToggle,
|
|
refreshBtn: diagRefreshBtn,
|
|
clearBtn: diagClearBtn,
|
|
copyBtn: diagCopyBtn,
|
|
downloadBtn: diagDownloadBtn,
|
|
logBox: diagLogBox,
|
|
statusEl: diagStatusModal || diagStatus,
|
|
logButton: diagLogBtn,
|
|
modal: diagModal,
|
|
modalClose: diagClose,
|
|
},
|
|
toast: showToast,
|
|
}).catch((e) => {
|
|
console.error("Diag init failed", e);
|
|
});
|
|
|
|
initUpdateSettings({
|
|
elements: {
|
|
updatesStatus,
|
|
updatesToggle,
|
|
scopeSelect: updatesScope,
|
|
updateTimeInput,
|
|
upgradeTimeInput,
|
|
cleanupToggle: updatesCleanup,
|
|
bandwidthInput: updatesBandwidth,
|
|
rebootToggle: updatesRebootToggle,
|
|
rebootTimeInput: updatesRebootTime,
|
|
rebootWithUsersToggle: updatesRebootUsers,
|
|
saveBtn: updatesSaveBtn,
|
|
msgEl: updatesMsg,
|
|
updatesUnsavedNote,
|
|
updatesSection,
|
|
},
|
|
onAfterSave: loadStatus,
|
|
overlay: { show: showBusy, hide: hideBusy },
|
|
toast: showToast,
|
|
});
|
|
|
|
// initial paint
|
|
renderStats(heroStats, placeholderStatus);
|
|
loadStatus();
|
|
}
|
|
|
|
main();
|