Loading indicator: - Branded boot splash (Hub logo inside an accent spinner ring, "Starting The Hub") covers the shell while the first services data loads, then fades out once the welcome dashboard has rendered; never blocks longer than 25s and reassures the user after 8s (message about post-reboot delays) - Fire the network and update checks before the first services render so the dashboard cards are current at first paint - Sidebar Update button now adopts the last known update state when built (order-independent), and the welcome dashboard re-renders when the update state changes Icons: - New monochrome g-pulse glyph (activity line) for Systems Operational: welcome card, dialog header, and System Status section — the shield no longer doubles as Security - Tech Support / Security dialog header gets a standard chip; Security shows the shield chip with a plain "Security" title (no emoji), and the shared dialog title now resets correctly when reopening Tech Support after Security
273 lines
12 KiB
JavaScript
273 lines
12 KiB
JavaScript
"use strict";
|
|
|
|
// ── Event listeners ───────────────────────────────────────────────
|
|
|
|
// if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); // moved to sidebar in tiles.js
|
|
if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal);
|
|
if ($btnCheckAgain) $btnCheckAgain.addEventListener("click", function() { openUpdateModal(); });
|
|
if ($updateCloseBtn) $updateCloseBtn.addEventListener("click", closeUpdateModal);
|
|
if ($btnReboot) $btnReboot.addEventListener("click", doReboot);
|
|
if ($btnSave) $btnSave.addEventListener("click", saveErrorReport);
|
|
if ($btnRetryUpdate) $btnRetryUpdate.addEventListener("click", retryUpdateStatus);
|
|
if ($btnRetryRun) $btnRetryRun.addEventListener("click", retryUpdateRun);
|
|
|
|
// Browser timers and requests may be suspended while an RDP session/tab is in
|
|
// the background. Reconcile immediately when the user returns instead of
|
|
// waiting for the next interval.
|
|
window.addEventListener("focus", resumeUpdateStatusAfterInterruption);
|
|
window.addEventListener("online", resumeUpdateStatusAfterInterruption);
|
|
document.addEventListener("visibilitychange", function() {
|
|
if (document.visibilityState === "visible") resumeUpdateStatusAfterInterruption();
|
|
});
|
|
if ($credsCloseBtn) $credsCloseBtn.addEventListener("click", closeCredsModal);
|
|
if ($supportCloseBtn) $supportCloseBtn.addEventListener("click", closeSupportModal);
|
|
|
|
// Logout button
|
|
if ($logoutBtn) $logoutBtn.addEventListener("click", function () {
|
|
fetch("/api/logout", { method: "POST", credentials: "same-origin" })
|
|
.finally(function () { window.location.replace("/login"); });
|
|
});
|
|
|
|
// Rebuild modal
|
|
if ($rebuildClose) $rebuildClose.addEventListener("click", closeRebuildModal);
|
|
if ($rebuildCloseHdr) $rebuildCloseHdr.addEventListener("click", closeRebuildModal);
|
|
if ($rebuildReboot) $rebuildReboot.addEventListener("click", doReboot);
|
|
if ($rebuildSave) $rebuildSave.addEventListener("click", saveRebuildErrorReport);
|
|
if ($rebuildModal) $rebuildModal.addEventListener("click", function(e) { if (e.target === $rebuildModal) closeRebuildModal(); });
|
|
|
|
// Domain setup modal
|
|
if ($domainSetupClose) $domainSetupClose.addEventListener("click", closeDomainSetupModal);
|
|
if ($domainSetupModal) $domainSetupModal.addEventListener("click", function(e) { if (e.target === $domainSetupModal) closeDomainSetupModal(); });
|
|
|
|
// SSL Email modal
|
|
if ($sslEmailClose) $sslEmailClose.addEventListener("click", closeSslEmailModal);
|
|
if ($sslEmailCancel) $sslEmailCancel.addEventListener("click", closeSslEmailModal);
|
|
if ($sslEmailModal) $sslEmailModal.addEventListener("click", function(e) { if (e.target === $sslEmailModal) closeSslEmailModal(); });
|
|
|
|
// Feature confirm modal
|
|
if ($featureConfirmClose) $featureConfirmClose.addEventListener("click", closeFeatureConfirm);
|
|
if ($featureConfirmCancel) $featureConfirmCancel.addEventListener("click", closeFeatureConfirm);
|
|
if ($featureConfirmModal) $featureConfirmModal.addEventListener("click", function(e) { if (e.target === $featureConfirmModal) closeFeatureConfirm(); });
|
|
|
|
if ($modal) $modal.addEventListener("click", function(e) { if (e.target === $modal) closeUpdateModal(); });
|
|
if ($credsModal) $credsModal.addEventListener("click", function(e) { if (e.target === $credsModal) closeCredsModal(); });
|
|
if ($supportModal) $supportModal.addEventListener("click", function(e) { if (e.target === $supportModal) closeSupportModal(); });
|
|
|
|
// Upgrade modal
|
|
if ($upgradeCloseBtn) $upgradeCloseBtn.addEventListener("click", closeUpgradeModal);
|
|
if ($upgradeCancelBtn) $upgradeCancelBtn.addEventListener("click", closeUpgradeModal);
|
|
if ($upgradeModal) $upgradeModal.addEventListener("click", function(e) { if (e.target === $upgradeModal) closeUpgradeModal(); });
|
|
|
|
// Restart confirm dialog
|
|
if ($restartConfirmCancel) $restartConfirmCancel.addEventListener("click", closeRestartConfirmDialog);
|
|
if ($restartConfirmModal) $restartConfirmModal.addEventListener("click", function(e) { if (e.target === $restartConfirmModal) closeRestartConfirmDialog(); });
|
|
if ($restartConfirmModal) $restartConfirmModal.addEventListener("keydown", function(e) { if (e.key === "Escape") closeRestartConfirmDialog(); });
|
|
|
|
// Header Reboot button
|
|
if ($headerRebootBtn) $headerRebootBtn.addEventListener("click", function() { openRestartConfirmDialog(); });
|
|
if ($restartConfirmOk) $restartConfirmOk.addEventListener("click", function() {
|
|
if ($restartConfirmOk.disabled) return;
|
|
$restartConfirmOk.disabled = true;
|
|
closeRestartConfirmDialog();
|
|
doReboot();
|
|
});
|
|
|
|
// Reboot error card buttons
|
|
var $rebootErrorCloseBtn = document.getElementById("reboot-error-close-btn");
|
|
var $rebootErrorRetryBtn = document.getElementById("reboot-error-retry-btn");
|
|
if ($rebootErrorCloseBtn) $rebootErrorCloseBtn.addEventListener("click", function() {
|
|
if ($rebootOverlay) $rebootOverlay.classList.remove("visible");
|
|
});
|
|
if ($rebootErrorRetryBtn) $rebootErrorRetryBtn.addEventListener("click", doReboot);
|
|
|
|
// ── Upgrade modal functions ───────────────────────────────────────
|
|
|
|
function openUpgradeModal() {
|
|
if ($upgradeModal) $upgradeModal.classList.add("open");
|
|
}
|
|
|
|
function closeUpgradeModal() {
|
|
if ($upgradeModal) $upgradeModal.classList.remove("open");
|
|
}
|
|
|
|
// ── Restart confirm dialog functions ─────────────────────────────
|
|
|
|
var _restartDialogOpener = null;
|
|
|
|
function openRestartConfirmDialog() {
|
|
if (!$restartConfirmModal) return;
|
|
_restartDialogOpener = document.activeElement;
|
|
|
|
// Detect conflicting operations
|
|
var isOperationInProgress = !!_updatePollTimer || !!_rebuildPollTimer;
|
|
if ($restartConflictBox) $restartConflictBox.style.display = isOperationInProgress ? "" : "none";
|
|
if ($restartConfirmOk) $restartConfirmOk.disabled = isOperationInProgress;
|
|
|
|
$restartConfirmModal.classList.add("open");
|
|
|
|
// Focus Cancel initially for safety
|
|
var cancelBtn = document.getElementById("restart-confirm-cancel-btn");
|
|
if (cancelBtn) setTimeout(function() { cancelBtn.focus(); }, 50);
|
|
}
|
|
|
|
function closeRestartConfirmDialog() {
|
|
if ($restartConfirmModal) $restartConfirmModal.classList.remove("open");
|
|
// Re-enable confirm button for next open
|
|
if ($restartConfirmOk) $restartConfirmOk.disabled = false;
|
|
// Return focus to the element that opened the dialog
|
|
if (_restartDialogOpener && _restartDialogOpener.focus) {
|
|
try { _restartDialogOpener.focus(); } catch (_) {}
|
|
_restartDialogOpener = null;
|
|
}
|
|
}
|
|
|
|
async function doUpgradeToServer() {
|
|
var confirmBtn = $upgradeConfirmBtn;
|
|
if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = "Upgrading…"; }
|
|
closeUpgradeModal();
|
|
|
|
// Reuse the rebuild modal to show reboot progress
|
|
_rebuildFeatureName = "Server + Desktop";
|
|
_rebuildIsEnabling = true;
|
|
openRebuildModal();
|
|
|
|
try {
|
|
await apiFetch("/api/role/upgrade-to-server", { method: "POST" });
|
|
// Server is rebooting — show message and wait for it to come back
|
|
if ($rebuildStatus) $rebuildStatus.textContent = "Rebooting — the setup wizard will guide you through domain and port configuration…";
|
|
if ($rebuildSpinner) $rebuildSpinner.classList.add("spinning");
|
|
|
|
// Poll until server comes back, then redirect to onboarding
|
|
var pollInterval = setInterval(async function() {
|
|
try {
|
|
await apiFetch("/api/ping");
|
|
clearInterval(pollInterval);
|
|
window.location.href = "/onboarding";
|
|
} catch (_) {
|
|
// Server still down — keep polling
|
|
}
|
|
}, 3000);
|
|
} catch (err) {
|
|
if ($rebuildStatus) $rebuildStatus.textContent = "✗ Upgrade failed: " + err.message;
|
|
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
|
|
if ($rebuildClose) $rebuildClose.disabled = false;
|
|
if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.textContent = "Yes, Upgrade"; }
|
|
}
|
|
}
|
|
|
|
if ($upgradeConfirmBtn) $upgradeConfirmBtn.addEventListener("click", doUpgradeToServer);
|
|
|
|
// ── First-login security banner ───────────────────────────────────
|
|
|
|
function showSecurityBanner() {
|
|
var existing = document.getElementById("security-first-login-banner");
|
|
if (existing) return;
|
|
|
|
var banner = document.createElement("div");
|
|
banner.id = "security-first-login-banner";
|
|
banner.className = "security-first-login-banner";
|
|
banner.innerHTML =
|
|
'<div class="security-banner-content">' +
|
|
'<span class="security-banner-icon">\uD83D\uDEE1</span>' +
|
|
'<span class="security-banner-text">' +
|
|
'<strong>Did someone else set up this machine?</strong> ' +
|
|
'If this computer was pre-configured by another person, go to ' +
|
|
'<strong>Menu \u2192 Security</strong> to reset all passwords and keys. ' +
|
|
'This ensures only you have access.' +
|
|
'</span>' +
|
|
'</div>' +
|
|
'<button class="security-banner-dismiss" id="security-banner-dismiss-btn" title="Dismiss">\u2715</button>';
|
|
|
|
var contentArea = document.querySelector(".content");
|
|
if (contentArea) {
|
|
contentArea.insertAdjacentElement("afterbegin", banner);
|
|
} else {
|
|
document.body.insertAdjacentElement("afterbegin", banner);
|
|
}
|
|
|
|
var dismissBtn = document.getElementById("security-banner-dismiss-btn");
|
|
if (dismissBtn) {
|
|
dismissBtn.addEventListener("click", async function() {
|
|
banner.remove();
|
|
try {
|
|
await apiFetch("/api/security/banner-dismiss", { method: "POST" });
|
|
} catch (_) {
|
|
// Non-fatal
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────
|
|
|
|
async function init() {
|
|
// These lightweight requests are independent. Running them together avoids
|
|
// making the dashboard wait through three serial network round-trips before
|
|
// it can even start loading the service tiles.
|
|
var onboardingStatus = null;
|
|
var bannerData = null;
|
|
var cfg;
|
|
try {
|
|
var startupResults = await Promise.all([
|
|
apiFetch("/api/onboarding/status").catch(function() { return null; }),
|
|
apiFetch("/api/security/banner-status").catch(function() { return null; }),
|
|
apiFetch("/api/config"),
|
|
]);
|
|
onboardingStatus = startupResults[0];
|
|
bannerData = startupResults[1];
|
|
cfg = startupResults[2];
|
|
} catch (_) {
|
|
// If config cannot be loaded, continue with the normal service fallback.
|
|
cfg = null;
|
|
}
|
|
|
|
if (onboardingStatus && !onboardingStatus.complete) {
|
|
window.location.href = "/onboarding";
|
|
return;
|
|
}
|
|
|
|
// Show first-login security banner only for machines that went through onboarding
|
|
// (legacy machines without the onboarding flag will never see this)
|
|
if (bannerData && bannerData.show) {
|
|
showSecurityBanner();
|
|
}
|
|
|
|
try {
|
|
if (!cfg) throw new Error("Hub config unavailable");
|
|
_currentRole = cfg.role || "server_plus_desktop";
|
|
if (cfg.category_order) {
|
|
for (var i = 0; i < cfg.category_order.length; i++) {
|
|
_categoryLabels[cfg.category_order[i][0]] = cfg.category_order[i][1];
|
|
}
|
|
}
|
|
var badge = document.getElementById("role-badge");
|
|
if (badge && cfg.role_label) badge.textContent = cfg.role_label;
|
|
window._roleLabel = cfg.role_label || "";
|
|
|
|
loadNetwork();
|
|
checkUpdates();
|
|
await refreshServices();
|
|
|
|
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
|
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
|
|
|
if (cfg.feature_manager) {
|
|
loadFeatureManager();
|
|
}
|
|
loadAutolaunchToggle();
|
|
} catch (_) {
|
|
loadNetwork();
|
|
checkUpdates();
|
|
await refreshServices();
|
|
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
|
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
|
loadAutolaunchToggle();
|
|
}
|
|
|
|
// If the page was reloaded or the RDP/browser session resumed during an
|
|
// update, reopen the modal from the persisted backend state. This also
|
|
// surfaces a completed update that is waiting for its activation reboot.
|
|
await restoreUpdateModalIfNeeded();
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", init); |