2026-03-05 00:04:26 -06:00
|
|
|
import {
|
|
|
|
|
startNotificationClient,
|
|
|
|
|
setNotificationsEnabled,
|
|
|
|
|
getNotificationsEnabled,
|
|
|
|
|
subscribeStatus,
|
|
|
|
|
subscribeUnread,
|
2026-05-19 23:12:02 -05:00
|
|
|
markAllNotificationsRead,
|
2026-03-05 00:04:26 -06:00
|
|
|
} from "./notify-client.js";
|
2026-03-25 09:50:41 -05:00
|
|
|
|
2026-03-05 00:04:26 -06:00
|
|
|
import { mountNav } from "./nav/template.js";
|
|
|
|
|
import { createAdaptiveNavController } from "./nav/layout.js";
|
2026-05-17 22:02:23 -05:00
|
|
|
import { applyUserRoleUI, applyThumbnailWallVisibility } from "./nav/role-ui.js";
|
2026-04-20 18:58:27 -05:00
|
|
|
import { loadPluginNavItems } from "./nav/plugins-loader.js";
|
2026-05-14 18:33:03 -05:00
|
|
|
import { init as initCommandPalette } from "./command-palette.js";
|
2026-06-04 00:04:49 -05:00
|
|
|
import {
|
|
|
|
|
installPageResourceTracker,
|
|
|
|
|
runWithoutPageTracking,
|
|
|
|
|
setupSoftNavigation,
|
|
|
|
|
startPageTracking,
|
|
|
|
|
} from "./soft-nav.js";
|
2026-03-05 00:04:26 -06:00
|
|
|
|
|
|
|
|
const host = document.getElementById("top-nav");
|
|
|
|
|
if (host) {
|
|
|
|
|
const refs = mountNav(host);
|
2026-05-14 18:33:03 -05:00
|
|
|
initCommandPalette();
|
2026-04-19 00:20:35 -05:00
|
|
|
import("./cert-banner.js").then(({ showCertBannerIfNeeded }) => {
|
2026-06-04 00:04:49 -05:00
|
|
|
runWithoutPageTracking(() => showCertBannerIfNeeded(document.getElementById("sb-mobile-bar") || host));
|
2026-04-19 00:20:35 -05:00
|
|
|
});
|
2026-04-17 18:43:16 -05:00
|
|
|
const { applyAdaptiveNavLayout, navHide } = createAdaptiveNavController(host, refs);
|
|
|
|
|
|
|
|
|
|
if (refs.navHideBtn && navHide) {
|
|
|
|
|
refs.navHideBtn.addEventListener("click", () => navHide.setHidden(true));
|
|
|
|
|
}
|
2026-03-05 00:04:26 -06:00
|
|
|
|
|
|
|
|
const path = window.location.pathname;
|
|
|
|
|
const activeMap = {
|
|
|
|
|
"/": "nav-clients",
|
|
|
|
|
"/metrics": "metrics-link",
|
2026-06-04 16:07:42 -05:00
|
|
|
"/graph": "graph-link",
|
2026-05-17 22:02:23 -05:00
|
|
|
"/screenshots": "screenshots-link",
|
2026-03-05 00:04:26 -06:00
|
|
|
"/logs": "logs-link",
|
|
|
|
|
"/scripts": "scripts-link",
|
2026-03-15 16:14:50 -05:00
|
|
|
"/socks5-manager": "socks5-link",
|
2026-03-05 00:04:26 -06:00
|
|
|
"/plugins": "plugins-link",
|
|
|
|
|
"/build": "build-link",
|
2026-04-05 18:18:40 -05:00
|
|
|
"/sol-publish": "sol-publish-link",
|
2026-03-05 00:04:26 -06:00
|
|
|
"/users": "users-link",
|
2026-03-06 20:50:02 -06:00
|
|
|
"/user-client-access": "users-link",
|
2026-03-05 00:04:26 -06:00
|
|
|
"/notifications": "notifications-link",
|
2026-04-08 10:33:22 -05:00
|
|
|
"/file-share": "file-share-link",
|
2026-04-15 17:36:00 +01:00
|
|
|
"/purgatory": "enrollment-link",
|
2026-03-05 00:04:26 -06:00
|
|
|
};
|
2026-04-20 18:58:27 -05:00
|
|
|
|
2026-06-04 00:04:49 -05:00
|
|
|
const applyActivePath = (nextPath = window.location.pathname) => {
|
|
|
|
|
host.querySelectorAll(".nav-active").forEach((el) => el.classList.remove("nav-active"));
|
|
|
|
|
host.querySelectorAll(".sb-group-btn[aria-expanded='true']").forEach((btn) => btn.setAttribute("aria-expanded", "false"));
|
|
|
|
|
host.querySelectorAll(".sb-group-children.sb-group-open").forEach((children) => children.classList.remove("sb-group-open"));
|
|
|
|
|
host.querySelectorAll(".sb-chevron.sb-chevron-open").forEach((chevron) => chevron.classList.remove("sb-chevron-open"));
|
|
|
|
|
refs.accountSettingsBtn?.classList.remove("ring-1", "ring-sky-500/60", "bg-slate-700");
|
|
|
|
|
|
|
|
|
|
const activeId = activeMap[nextPath];
|
2026-04-20 18:58:27 -05:00
|
|
|
if (activeId) {
|
|
|
|
|
const el = document.getElementById(activeId);
|
|
|
|
|
if (el) {
|
|
|
|
|
el.classList.add("nav-active");
|
|
|
|
|
const group = el.closest(".sb-group");
|
|
|
|
|
if (group) {
|
|
|
|
|
const btn = group.querySelector(".sb-group-btn");
|
|
|
|
|
const children = group.querySelector(".sb-group-children");
|
|
|
|
|
const chevron = btn?.querySelector(".sb-chevron");
|
|
|
|
|
if (btn) btn.setAttribute("aria-expanded", "true");
|
|
|
|
|
if (children) children.classList.add("sb-group-open");
|
|
|
|
|
if (chevron) chevron.classList.add("sb-chevron-open");
|
|
|
|
|
}
|
2026-04-15 17:36:00 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-04 00:04:49 -05:00
|
|
|
|
|
|
|
|
if (nextPath === "/settings" && refs.accountSettingsBtn) {
|
|
|
|
|
refs.accountSettingsBtn.classList.add("ring-1", "ring-sky-500/60", "bg-slate-700");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loadPluginNavItems(activeMap).then(() => {
|
|
|
|
|
applyActivePath(path);
|
2026-04-20 18:58:27 -05:00
|
|
|
});
|
|
|
|
|
|
2026-03-05 00:04:26 -06:00
|
|
|
if (refs.logoutBtn && !refs.logoutBtn.dataset.boundLogout) {
|
|
|
|
|
refs.logoutBtn.dataset.boundLogout = "true";
|
|
|
|
|
refs.logoutBtn.addEventListener("click", async () => {
|
|
|
|
|
if (!confirm("Are you sure you want to logout?")) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/logout", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
window.location.href = "/";
|
|
|
|
|
} else {
|
|
|
|
|
alert("Logout failed. Please try again.");
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Logout error:", err);
|
|
|
|
|
alert("Logout failed. Please try again.");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 20:50:02 -06:00
|
|
|
if (refs.accountSettingsBtn && !refs.accountSettingsBtn.dataset.boundSettings) {
|
|
|
|
|
refs.accountSettingsBtn.dataset.boundSettings = "true";
|
|
|
|
|
refs.accountSettingsBtn.addEventListener("click", () => {
|
2026-06-04 00:04:49 -05:00
|
|
|
if (window.overlordSoftNavigate) {
|
|
|
|
|
window.overlordSoftNavigate("/settings");
|
|
|
|
|
} else {
|
|
|
|
|
window.location.href = "/settings";
|
|
|
|
|
}
|
2026-03-06 20:50:02 -06:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 00:04:26 -06:00
|
|
|
const updateToggle = () => {
|
|
|
|
|
const enabled = getNotificationsEnabled();
|
|
|
|
|
if (refs.notifyToggle) {
|
|
|
|
|
refs.notifyToggle.classList.toggle("text-emerald-200", enabled);
|
|
|
|
|
refs.notifyToggle.classList.toggle("border-emerald-500/40", enabled);
|
|
|
|
|
refs.notifyToggle.classList.toggle("text-slate-300", !enabled);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-19 23:12:02 -05:00
|
|
|
let lastNotifyClickTime = 0;
|
|
|
|
|
const DOUBLE_CLICK_WINDOW_MS = 700;
|
|
|
|
|
refs.notifyToggle?.addEventListener("click", (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const isSecondClick = now - lastNotifyClickTime < DOUBLE_CLICK_WINDOW_MS;
|
|
|
|
|
lastNotifyClickTime = isSecondClick ? 0 : now;
|
|
|
|
|
|
2026-03-05 00:04:26 -06:00
|
|
|
const next = !getNotificationsEnabled();
|
|
|
|
|
setNotificationsEnabled(next);
|
|
|
|
|
updateToggle();
|
2026-05-19 23:12:02 -05:00
|
|
|
|
|
|
|
|
if (isSecondClick) {
|
|
|
|
|
markAllNotificationsRead();
|
|
|
|
|
}
|
2026-03-05 00:04:26 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
subscribeUnread((count) => {
|
|
|
|
|
if (!refs.notifyBadge) return;
|
|
|
|
|
refs.notifyBadge.textContent = String(count);
|
|
|
|
|
refs.notifyBadge.classList.toggle("hidden", count <= 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
updateToggle();
|
|
|
|
|
startNotificationClient();
|
|
|
|
|
subscribeStatus((status) => {
|
|
|
|
|
if (status === "connected") {
|
|
|
|
|
// no-op
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-04 00:04:49 -05:00
|
|
|
const prefetchedPages = new Set();
|
|
|
|
|
const prefetchPage = (href) => {
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(href, window.location.href);
|
|
|
|
|
if (url.origin !== window.location.origin) return;
|
|
|
|
|
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
|
|
|
|
const key = `${url.pathname}${url.search}`;
|
|
|
|
|
if (prefetchedPages.has(key)) return;
|
|
|
|
|
prefetchedPages.add(key);
|
|
|
|
|
const link = document.createElement("link");
|
|
|
|
|
link.rel = "prefetch";
|
|
|
|
|
link.as = "document";
|
|
|
|
|
link.href = key;
|
|
|
|
|
document.head.appendChild(link);
|
|
|
|
|
} catch {}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
host.addEventListener("pointerenter", (event) => {
|
|
|
|
|
const link = event.target?.closest?.("a[href]");
|
|
|
|
|
if (link) prefetchPage(link.getAttribute("href"));
|
|
|
|
|
}, true);
|
|
|
|
|
host.addEventListener("focusin", (event) => {
|
|
|
|
|
const link = event.target?.closest?.("a[href]");
|
|
|
|
|
if (link) prefetchPage(link.getAttribute("href"));
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-05 00:04:26 -06:00
|
|
|
async function loadCurrentUser() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/auth/me", { credentials: "include" });
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const user = await res.json();
|
|
|
|
|
applyUserRoleUI(user, refs);
|
2026-05-17 22:02:23 -05:00
|
|
|
applyThumbnailWallVisibility(user);
|
2026-03-05 00:04:26 -06:00
|
|
|
|
2026-03-20 22:41:12 -05:00
|
|
|
if (user.role === "admin" || user.role === "operator") {
|
|
|
|
|
try {
|
|
|
|
|
const statsRes = await fetch("/api/enrollment/stats", { credentials: "include" });
|
|
|
|
|
if (statsRes.ok) {
|
|
|
|
|
const stats = await statsRes.json();
|
|
|
|
|
const badge = refs.enrollmentBadge;
|
|
|
|
|
if (badge) {
|
|
|
|
|
if (stats.pending > 0) {
|
|
|
|
|
badge.textContent = stats.pending;
|
|
|
|
|
badge.classList.remove("hidden");
|
|
|
|
|
} else {
|
|
|
|
|
badge.classList.add("hidden");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 00:04:26 -06:00
|
|
|
applyAdaptiveNavLayout();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to load user:", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (refs.usernameDisplay && refs.roleBadge) {
|
|
|
|
|
loadCurrentUser();
|
|
|
|
|
}
|
2026-04-14 21:56:18 -05:00
|
|
|
|
2026-04-19 00:20:35 -05:00
|
|
|
import("./chat-widget.js").then((chatWidget) => {
|
2026-06-04 00:04:49 -05:00
|
|
|
runWithoutPageTracking(() => {
|
|
|
|
|
chatWidget.init();
|
2026-04-14 21:56:18 -05:00
|
|
|
|
2026-06-04 00:04:49 -05:00
|
|
|
if (chatWidget.isHidden() && refs.navUtility) {
|
|
|
|
|
const restoreBtn = document.createElement("button");
|
|
|
|
|
restoreBtn.id = "chat-restore-btn";
|
|
|
|
|
restoreBtn.className = "inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 text-slate-500 hover:text-slate-300 hover:bg-slate-800 text-xs transition-colors";
|
|
|
|
|
restoreBtn.title = "Show team chat";
|
|
|
|
|
restoreBtn.innerHTML = '<i class="fa-solid fa-comments"></i><span class="sb-text">Chat</span>';
|
|
|
|
|
restoreBtn.addEventListener("click", () => {
|
|
|
|
|
chatWidget.show();
|
|
|
|
|
restoreBtn.remove();
|
|
|
|
|
});
|
|
|
|
|
refs.navUtility.insertBefore(restoreBtn, refs.navUtility.firstChild);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-19 00:20:35 -05:00
|
|
|
});
|
2026-06-04 00:04:49 -05:00
|
|
|
|
|
|
|
|
installPageResourceTracker();
|
|
|
|
|
setupSoftNavigation(host, { onPathChange: applyActivePath });
|
|
|
|
|
startPageTracking();
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|