fixed dashboard cards, add fuse js, fix load times, fix csp in scripts, fix remote desktop not working first try.
This commit is contained in:
@@ -268,6 +268,7 @@ function close() {
|
||||
function navigate(href, newTab = false) {
|
||||
pushRecent(href);
|
||||
if (newTab) window.open(href, "_blank", "noopener");
|
||||
else if (window.overlordSoftNavigate) window.overlordSoftNavigate(href);
|
||||
else window.location.href = href;
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,18 @@ const DEFAULT_GRID_LAYOUT = {
|
||||
};
|
||||
let statsGrid = null;
|
||||
|
||||
function teardownDashboardStats() {
|
||||
if (metricsTimer) clearInterval(metricsTimer);
|
||||
metricsTimer = null;
|
||||
onlineChart?.destroy?.();
|
||||
osChart?.destroy?.();
|
||||
onlineChart = null;
|
||||
osChart = null;
|
||||
statsGrid?.destroy?.(false);
|
||||
statsGrid = null;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
const palette = {
|
||||
text: "#cbd5e1",
|
||||
muted: "#94a3b8",
|
||||
@@ -629,6 +641,8 @@ async function fetchDashboardMetrics() {
|
||||
export function initDashboardStats() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
window.removeEventListener("pagehide", teardownDashboardStats);
|
||||
window.addEventListener("pagehide", teardownDashboardStats);
|
||||
if (!makeCharts()) {
|
||||
setTimeout(() => {
|
||||
if (makeCharts()) applyHeightMode(getSavedHeight());
|
||||
|
||||
@@ -14,6 +14,29 @@ let dashboardWsConnected = false;
|
||||
let wsReconnectTimer = null;
|
||||
const manuallyDisconnecting = new Set();
|
||||
|
||||
function stopDashboardRealtime() {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
clearTimeout(wsReconnectTimer);
|
||||
wsReconnectTimer = null;
|
||||
if (dashboardWs) {
|
||||
try {
|
||||
dashboardWs.onopen = null;
|
||||
dashboardWs.onmessage = null;
|
||||
dashboardWs.onerror = null;
|
||||
dashboardWs.onclose = null;
|
||||
dashboardWs.close(1000, "page hidden");
|
||||
} catch {}
|
||||
}
|
||||
dashboardWs = null;
|
||||
dashboardWsConnected = false;
|
||||
}
|
||||
|
||||
function bindDashboardPagehideCleanup() {
|
||||
window.removeEventListener("pagehide", stopDashboardRealtime);
|
||||
window.addEventListener("pagehide", stopDashboardRealtime);
|
||||
}
|
||||
|
||||
function moveClientCardImmediately(msg) {
|
||||
const clientId = typeof msg?.clientId === "string" ? msg.clientId : "";
|
||||
if (!clientId) return;
|
||||
@@ -243,6 +266,7 @@ function adjustPollingForWs() {
|
||||
}
|
||||
|
||||
export function startAutoRefresh(intervalMs = POLL_INTERVAL_MS) {
|
||||
bindDashboardPagehideCleanup();
|
||||
connectDashboardWs();
|
||||
const effectiveInterval = dashboardWsConnected
|
||||
? FALLBACK_POLL_MS
|
||||
|
||||
@@ -1828,6 +1828,35 @@ mark.search-current {
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
background: rgba(2, 6, 23, 0.92);
|
||||
border-bottom: 1px solid rgba(30, 41, 59, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
#top-nav:empty::before {
|
||||
content: "Overlord";
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
html.nav-pre-sidebar #top-nav:empty::before {
|
||||
align-items: flex-start;
|
||||
height: auto;
|
||||
padding: 18px 14px 14px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
html.nav-pre-sidebar.nav-pre-collapsed #top-nav:empty::before {
|
||||
content: "O";
|
||||
justify-content: center;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
#top-nav nav a {
|
||||
@@ -4515,7 +4544,10 @@ body.sb-ready #top-nav {
|
||||
}
|
||||
|
||||
.dashboard-stats-content {
|
||||
height: calc(var(--dashboard-stats-height) + 12px);
|
||||
min-height: calc(var(--dashboard-stats-height) + 12px);
|
||||
contain: layout paint;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-stats-shell.is-collapsed .dashboard-stats-content,
|
||||
|
||||
@@ -12,13 +12,19 @@ import { createAdaptiveNavController } from "./nav/layout.js";
|
||||
import { applyUserRoleUI, applyThumbnailWallVisibility } from "./nav/role-ui.js";
|
||||
import { loadPluginNavItems } from "./nav/plugins-loader.js";
|
||||
import { init as initCommandPalette } from "./command-palette.js";
|
||||
import {
|
||||
installPageResourceTracker,
|
||||
runWithoutPageTracking,
|
||||
setupSoftNavigation,
|
||||
startPageTracking,
|
||||
} from "./soft-nav.js";
|
||||
|
||||
const host = document.getElementById("top-nav");
|
||||
if (host) {
|
||||
const refs = mountNav(host);
|
||||
initCommandPalette();
|
||||
import("./cert-banner.js").then(({ showCertBannerIfNeeded }) => {
|
||||
showCertBannerIfNeeded(document.getElementById("sb-mobile-bar") || host);
|
||||
runWithoutPageTracking(() => showCertBannerIfNeeded(document.getElementById("sb-mobile-bar") || host));
|
||||
});
|
||||
const { applyAdaptiveNavLayout, navHide } = createAdaptiveNavController(host, refs);
|
||||
|
||||
@@ -44,13 +50,18 @@ if (host) {
|
||||
"/purgatory": "enrollment-link",
|
||||
};
|
||||
|
||||
loadPluginNavItems(activeMap).then(() => {
|
||||
const activeId = activeMap[path];
|
||||
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];
|
||||
if (activeId) {
|
||||
const el = document.getElementById(activeId);
|
||||
if (el) {
|
||||
el.classList.add("nav-active");
|
||||
// Also expand the parent sidebar group if applicable
|
||||
const group = el.closest(".sb-group");
|
||||
if (group) {
|
||||
const btn = group.querySelector(".sb-group-btn");
|
||||
@@ -62,6 +73,14 @@ if (host) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextPath === "/settings" && refs.accountSettingsBtn) {
|
||||
refs.accountSettingsBtn.classList.add("ring-1", "ring-sky-500/60", "bg-slate-700");
|
||||
}
|
||||
};
|
||||
|
||||
loadPluginNavItems(activeMap).then(() => {
|
||||
applyActivePath(path);
|
||||
});
|
||||
|
||||
if (refs.logoutBtn && !refs.logoutBtn.dataset.boundLogout) {
|
||||
@@ -91,14 +110,14 @@ if (host) {
|
||||
if (refs.accountSettingsBtn && !refs.accountSettingsBtn.dataset.boundSettings) {
|
||||
refs.accountSettingsBtn.dataset.boundSettings = "true";
|
||||
refs.accountSettingsBtn.addEventListener("click", () => {
|
||||
window.location.href = "/settings";
|
||||
if (window.overlordSoftNavigate) {
|
||||
window.overlordSoftNavigate("/settings");
|
||||
} else {
|
||||
window.location.href = "/settings";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (path === "/settings" && refs.accountSettingsBtn) {
|
||||
refs.accountSettingsBtn.classList.add("ring-1", "ring-sky-500/60", "bg-slate-700");
|
||||
}
|
||||
|
||||
const updateToggle = () => {
|
||||
const enabled = getNotificationsEnabled();
|
||||
if (refs.notifyToggle) {
|
||||
@@ -139,6 +158,32 @@ if (host) {
|
||||
}
|
||||
});
|
||||
|
||||
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"));
|
||||
});
|
||||
|
||||
async function loadCurrentUser() {
|
||||
try {
|
||||
const res = await fetch("/api/auth/me", { credentials: "include" });
|
||||
@@ -178,19 +223,25 @@ if (host) {
|
||||
}
|
||||
|
||||
import("./chat-widget.js").then((chatWidget) => {
|
||||
chatWidget.init();
|
||||
runWithoutPageTracking(() => {
|
||||
chatWidget.init();
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
installPageResourceTracker();
|
||||
setupSoftNavigation(host, { onPathChange: applyActivePath });
|
||||
startPageTracking();
|
||||
}
|
||||
|
||||
@@ -111,6 +111,15 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
let clientOs = "";
|
||||
let clientIsAdmin = false;
|
||||
let firewallWarningAcked = false;
|
||||
let firstFrameLogged = false;
|
||||
|
||||
function rdDebug(label, data = {}) {
|
||||
try {
|
||||
console.debug(`rd: ${label} ${JSON.stringify(data)}`);
|
||||
} catch {
|
||||
console.debug(`rd: ${label}`, data);
|
||||
}
|
||||
}
|
||||
|
||||
function resetH264RuntimeState() {
|
||||
h264TimestampUs = 0;
|
||||
@@ -730,6 +739,14 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
|
||||
function handleStatus(msg) {
|
||||
if (!msg || msg.type !== "status" || !msg.status) return;
|
||||
rdDebug("status", {
|
||||
status: msg.status,
|
||||
reason: msg.reason || "",
|
||||
desiredStreaming,
|
||||
streamState,
|
||||
lastFrameAgeMs: lastFrameAt ? Math.round(performance.now() - lastFrameAt) : null,
|
||||
sessionId: msg.sessionId || "",
|
||||
});
|
||||
if (msg.status === "offline") {
|
||||
scheduleOffline(msg.reason);
|
||||
return;
|
||||
@@ -880,10 +897,26 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
return;
|
||||
}
|
||||
if (ws.readyState !== WebSocket.OPEN) {
|
||||
rdDebug("send skipped websocket not open", {
|
||||
type,
|
||||
readyState: ws.readyState,
|
||||
streamState,
|
||||
desiredStreaming,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const msg = { type, ...payload };
|
||||
console.debug("rd: send", msg);
|
||||
rdDebug("send", {
|
||||
msg,
|
||||
readyState: ws.readyState,
|
||||
streamState,
|
||||
desiredStreaming,
|
||||
display: displaySelect?.value ?? "",
|
||||
quality: qualitySlider?.value ?? "",
|
||||
resolution: resolutionSelect?.value ?? "",
|
||||
transport: getWebrtcMode(),
|
||||
lastFrameAgeMs: lastFrameAt ? Math.round(performance.now() - lastFrameAt) : null,
|
||||
});
|
||||
ws.send(encodeMsgpack(msg));
|
||||
}
|
||||
|
||||
@@ -1021,6 +1054,13 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
const res = await fetch("/api/clients");
|
||||
const data = await res.json();
|
||||
const client = data.items.find((c) => c.id === activeClientId);
|
||||
rdDebug("client info", {
|
||||
found: !!client,
|
||||
monitors: client?.monitors,
|
||||
monitorInfo: client?.monitorInfo,
|
||||
os: client?.os || "",
|
||||
isAdmin: !!client?.isAdmin,
|
||||
});
|
||||
if (client) {
|
||||
clientLabel.textContent = `${client.host || client.id} (${client.os || ""})`;
|
||||
clientOs = (client.os || "").toLowerCase();
|
||||
@@ -1115,6 +1155,19 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
|
||||
startBtn.addEventListener("click", function () {
|
||||
const mode = getWebrtcMode();
|
||||
rdDebug("start click", {
|
||||
mode,
|
||||
wsReadyState: ws.readyState,
|
||||
streamState,
|
||||
desiredStreaming,
|
||||
display: displaySelect?.value ?? "",
|
||||
quality: qualitySlider?.value ?? "",
|
||||
resolution: resolutionSelect?.value ?? "",
|
||||
prefersH264,
|
||||
duplication: !!duplicationCtrl?.checked,
|
||||
clientOs,
|
||||
clientIsAdmin,
|
||||
});
|
||||
if (needsWebrtcFirewallWarning(mode)) {
|
||||
if (!confirm("This agent is not elevated. Starting WebRTC will trigger a Windows Defender Firewall prompt on the target machine.\n\nContinue?")) {
|
||||
return;
|
||||
@@ -1132,6 +1185,7 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
pushResolution();
|
||||
desiredStreaming = true;
|
||||
lastFrameAt = 0;
|
||||
firstFrameLogged = false;
|
||||
resetH264SessionState();
|
||||
setStreamState("starting", "Starting stream");
|
||||
if (mode === "relayed") {
|
||||
@@ -1267,6 +1321,15 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
function markFrameReceived() {
|
||||
lastFrameAt = performance.now();
|
||||
clearOfflineTimer();
|
||||
if (!firstFrameLogged) {
|
||||
firstFrameLogged = true;
|
||||
rdDebug("first frame received", {
|
||||
streamState,
|
||||
desiredStreaming,
|
||||
canvas: { width: canvas.width, height: canvas.height },
|
||||
transport: getWebrtcMode(),
|
||||
});
|
||||
}
|
||||
if (streamState !== "streaming" && desiredStreaming) {
|
||||
setStreamState("streaming", "Streaming");
|
||||
}
|
||||
@@ -1700,6 +1763,11 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
});
|
||||
|
||||
ws.addEventListener("open", function () {
|
||||
rdDebug("ws open", {
|
||||
url: ws.url,
|
||||
readyState: ws.readyState,
|
||||
clientId,
|
||||
});
|
||||
if (qualitySlider) {
|
||||
pushQuality(qualitySlider.value);
|
||||
}
|
||||
@@ -1718,7 +1786,14 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
});
|
||||
});
|
||||
|
||||
ws.addEventListener("close", function () {
|
||||
ws.addEventListener("close", function (event) {
|
||||
rdDebug("ws close", {
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean,
|
||||
streamState,
|
||||
desiredStreaming,
|
||||
});
|
||||
desiredStreaming = false;
|
||||
disconnectAudio();
|
||||
destroyVideoDecoder();
|
||||
@@ -1729,7 +1804,8 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
setStreamState("disconnected", "Disconnected");
|
||||
});
|
||||
|
||||
ws.addEventListener("error", function () {
|
||||
ws.addEventListener("error", function (event) {
|
||||
console.warn("rd: ws error", event);
|
||||
destroyVideoDecoder();
|
||||
stopAllWebrtc();
|
||||
setStreamState("error", "WebSocket error");
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
let installedTracker = false;
|
||||
let pageTrackingEnabled = false;
|
||||
let navSeq = 0;
|
||||
|
||||
const tracked = {
|
||||
events: new Set(),
|
||||
intervals: new Set(),
|
||||
sockets: new Set(),
|
||||
workers: new Set(),
|
||||
};
|
||||
|
||||
const original = {};
|
||||
const loadedClassicScripts = new Set();
|
||||
const persistentIds = new Set([
|
||||
"top-nav",
|
||||
"sb-mobile-bar",
|
||||
"sb-backdrop",
|
||||
"nav-reveal-btn",
|
||||
"chat-bubble",
|
||||
"chat-panel",
|
||||
"cert-trust-banner",
|
||||
]);
|
||||
|
||||
function normalizeUrl(value, base = window.location.href) {
|
||||
try {
|
||||
return new URL(value, base).href;
|
||||
} catch {
|
||||
return value || "";
|
||||
}
|
||||
}
|
||||
|
||||
function scriptPath(value) {
|
||||
try {
|
||||
return new URL(value, window.location.href).pathname;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSkipScript(script) {
|
||||
const path = scriptPath(script.getAttribute("src") || "");
|
||||
return path === "/assets/nav.js" || path === "/assets/nav-prelude.js";
|
||||
}
|
||||
|
||||
function isPersistentNode(node) {
|
||||
return node instanceof Element && persistentIds.has(node.id);
|
||||
}
|
||||
|
||||
function cleanupTrackedResources() {
|
||||
pageTrackingEnabled = false;
|
||||
window.dispatchEvent(new Event("pagehide"));
|
||||
|
||||
for (const id of tracked.intervals) original.clearInterval(id);
|
||||
for (const entry of tracked.events) {
|
||||
original.removeEventListener.call(entry.target, entry.type, entry.listener, entry.options);
|
||||
}
|
||||
for (const socket of tracked.sockets) {
|
||||
try {
|
||||
socket.onopen = null;
|
||||
socket.onmessage = null;
|
||||
socket.onerror = null;
|
||||
socket.onclose = null;
|
||||
socket.close(1000, "soft navigation");
|
||||
} catch {}
|
||||
}
|
||||
for (const worker of tracked.workers) {
|
||||
try {
|
||||
worker.terminate();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
tracked.events.clear();
|
||||
tracked.intervals.clear();
|
||||
tracked.sockets.clear();
|
||||
tracked.workers.clear();
|
||||
|
||||
document.querySelectorAll("script[data-soft-nav-script]").forEach((script) => script.remove());
|
||||
document.getElementById("cmdp-root")?.remove();
|
||||
pageTrackingEnabled = true;
|
||||
}
|
||||
|
||||
export function runWithoutPageTracking(fn) {
|
||||
const previous = pageTrackingEnabled;
|
||||
pageTrackingEnabled = false;
|
||||
try {
|
||||
const result = fn();
|
||||
if (result && typeof result.then === "function") {
|
||||
return result.finally(() => {
|
||||
pageTrackingEnabled = previous;
|
||||
});
|
||||
}
|
||||
pageTrackingEnabled = previous;
|
||||
return result;
|
||||
} catch (err) {
|
||||
pageTrackingEnabled = previous;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function startPageTracking() {
|
||||
pageTrackingEnabled = true;
|
||||
}
|
||||
|
||||
export function installPageResourceTracker() {
|
||||
if (installedTracker) return;
|
||||
installedTracker = true;
|
||||
|
||||
document.querySelectorAll("script[src]").forEach((script) => {
|
||||
const src = script.getAttribute("src");
|
||||
if (src && script.type !== "module") loadedClassicScripts.add(normalizeUrl(src));
|
||||
});
|
||||
|
||||
original.addEventListener = EventTarget.prototype.addEventListener;
|
||||
original.removeEventListener = EventTarget.prototype.removeEventListener;
|
||||
original.setInterval = window.setInterval;
|
||||
original.clearInterval = window.clearInterval;
|
||||
original.WebSocket = window.WebSocket;
|
||||
original.Worker = window.Worker;
|
||||
|
||||
EventTarget.prototype.addEventListener = function (type, listener, options) {
|
||||
if (pageTrackingEnabled && listener) {
|
||||
tracked.events.add({ target: this, type, listener, options });
|
||||
}
|
||||
return original.addEventListener.call(this, type, listener, options);
|
||||
};
|
||||
|
||||
EventTarget.prototype.removeEventListener = function (type, listener, options) {
|
||||
for (const entry of tracked.events) {
|
||||
if (entry.target === this && entry.type === type && entry.listener === listener) {
|
||||
tracked.events.delete(entry);
|
||||
}
|
||||
}
|
||||
return original.removeEventListener.call(this, type, listener, options);
|
||||
};
|
||||
|
||||
window.setInterval = function (...args) {
|
||||
const id = original.setInterval.apply(window, args);
|
||||
if (pageTrackingEnabled) tracked.intervals.add(id);
|
||||
return id;
|
||||
};
|
||||
|
||||
window.clearInterval = function (id) {
|
||||
tracked.intervals.delete(id);
|
||||
return original.clearInterval.call(window, id);
|
||||
};
|
||||
|
||||
if (typeof original.WebSocket === "function") {
|
||||
window.WebSocket = function (...args) {
|
||||
const socket = new original.WebSocket(...args);
|
||||
if (pageTrackingEnabled) tracked.sockets.add(socket);
|
||||
return socket;
|
||||
};
|
||||
window.WebSocket.prototype = original.WebSocket.prototype;
|
||||
Object.defineProperty(window.WebSocket, "CONNECTING", { value: original.WebSocket.CONNECTING });
|
||||
Object.defineProperty(window.WebSocket, "OPEN", { value: original.WebSocket.OPEN });
|
||||
Object.defineProperty(window.WebSocket, "CLOSING", { value: original.WebSocket.CLOSING });
|
||||
Object.defineProperty(window.WebSocket, "CLOSED", { value: original.WebSocket.CLOSED });
|
||||
}
|
||||
|
||||
if (typeof original.Worker === "function") {
|
||||
window.Worker = function (...args) {
|
||||
const worker = new original.Worker(...args);
|
||||
if (pageTrackingEnabled) tracked.workers.add(worker);
|
||||
return worker;
|
||||
};
|
||||
window.Worker.prototype = original.Worker.prototype;
|
||||
}
|
||||
}
|
||||
|
||||
function mergeHead(nextDoc, baseUrl) {
|
||||
document.title = nextDoc.title || document.title;
|
||||
|
||||
const currentHtmlClasses = Array.from(document.documentElement.classList).filter((name) => (
|
||||
name.startsWith("nav-pre") || name === "dark"
|
||||
));
|
||||
document.documentElement.className = nextDoc.documentElement.className || "";
|
||||
currentHtmlClasses.forEach((name) => document.documentElement.classList.add(name));
|
||||
|
||||
nextDoc.head.querySelectorAll("link").forEach((link) => {
|
||||
const href = link.getAttribute("href");
|
||||
const rel = link.getAttribute("rel") || "";
|
||||
if (!href) return;
|
||||
const normalized = normalizeUrl(href, baseUrl);
|
||||
const exists = Array.from(document.head.querySelectorAll("link")).some((existing) => (
|
||||
(existing.getAttribute("rel") || "") === rel
|
||||
&& normalizeUrl(existing.getAttribute("href") || "") === normalized
|
||||
));
|
||||
if (!exists) {
|
||||
const clone = link.cloneNode(true);
|
||||
clone.setAttribute("href", normalized);
|
||||
document.head.appendChild(clone);
|
||||
}
|
||||
});
|
||||
|
||||
nextDoc.head.querySelectorAll("style").forEach((style) => {
|
||||
const text = style.textContent || "";
|
||||
const exists = Array.from(document.head.querySelectorAll("style[data-soft-nav-style]"))
|
||||
.some((existing) => existing.textContent === text);
|
||||
if (!exists) {
|
||||
const clone = style.cloneNode(true);
|
||||
clone.dataset.softNavStyle = "true";
|
||||
document.head.appendChild(clone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function replaceBody(nextDoc) {
|
||||
const preserved = Array.from(document.body.children).filter(isPersistentNode);
|
||||
const nav = document.getElementById("top-nav");
|
||||
const anchor = document.getElementById("cert-trust-banner")
|
||||
|| document.getElementById("sb-mobile-bar")
|
||||
|| nav;
|
||||
const preservedClasses = Array.from(document.body.classList).filter((name) => (
|
||||
name.startsWith("sb-") || name === "nav-hidden"
|
||||
));
|
||||
|
||||
Array.from(document.body.children).forEach((node) => {
|
||||
if (!preserved.includes(node)) node.remove();
|
||||
});
|
||||
|
||||
document.body.className = nextDoc.body.className || "";
|
||||
preservedClasses.forEach((name) => document.body.classList.add(name));
|
||||
|
||||
let insertAfter = anchor;
|
||||
Array.from(nextDoc.body.children).forEach((node) => {
|
||||
if (node.matches?.("script") || node.id === "top-nav") return;
|
||||
const clone = document.importNode(node, true);
|
||||
if (insertAfter?.parentNode) {
|
||||
insertAfter.parentNode.insertBefore(clone, insertAfter.nextSibling);
|
||||
insertAfter = clone;
|
||||
} else {
|
||||
document.body.appendChild(clone);
|
||||
insertAfter = clone;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function appendScript(script, baseUrl) {
|
||||
if (shouldSkipScript(script)) return Promise.resolve();
|
||||
|
||||
const src = script.getAttribute("src");
|
||||
const type = script.getAttribute("type") || "";
|
||||
if (src && type !== "module") {
|
||||
const normalized = normalizeUrl(src, baseUrl);
|
||||
if (loadedClassicScripts.has(normalized)) return Promise.resolve();
|
||||
loadedClassicScripts.add(normalized);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const next = document.createElement("script");
|
||||
for (const { name, value } of script.attributes) {
|
||||
if (name !== "src") next.setAttribute(name, value);
|
||||
}
|
||||
next.dataset.softNavScript = "true";
|
||||
next.onload = () => resolve();
|
||||
next.onerror = () => reject(new Error(`Failed to load ${src || "inline script"}`));
|
||||
|
||||
if (src) {
|
||||
const url = new URL(src, baseUrl);
|
||||
if (type === "module" && url.origin === window.location.origin) {
|
||||
url.searchParams.set("softNav", String(navSeq));
|
||||
}
|
||||
next.src = url.href;
|
||||
} else {
|
||||
next.textContent = script.textContent || "";
|
||||
}
|
||||
|
||||
document.body.appendChild(next);
|
||||
if (!src) resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async function runPageScripts(nextDoc, baseUrl) {
|
||||
const scripts = Array.from(nextDoc.querySelectorAll("script"));
|
||||
for (const script of scripts) {
|
||||
await appendScript(script, baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshGlobalEnhancements() {
|
||||
if (typeof window.addRippleEffect === "function") {
|
||||
document
|
||||
.querySelectorAll("button:not(.no-ripple), .button:not(.no-ripple)")
|
||||
.forEach((button) => window.addRippleEffect(button));
|
||||
}
|
||||
}
|
||||
|
||||
function isSoftNavigationCandidate(url, link) {
|
||||
if (url.origin !== window.location.origin) return false;
|
||||
if (link?.target && link.target !== "_self") return false;
|
||||
if (link?.hasAttribute("download")) return false;
|
||||
if (url.pathname.startsWith("/api/")) return false;
|
||||
if (url.pathname.includes(".")) return false;
|
||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setupSoftNavigation(host, { onPathChange } = {}) {
|
||||
if (!host || host.dataset.softNavReady === "true") return;
|
||||
host.dataset.softNavReady = "true";
|
||||
|
||||
async function navigateTo(href, { replace = false } = {}) {
|
||||
const url = new URL(href, window.location.href);
|
||||
if (!isSoftNavigationCandidate(url)) {
|
||||
window.location.href = url.href;
|
||||
return;
|
||||
}
|
||||
|
||||
navSeq += 1;
|
||||
document.body.classList.add("soft-nav-loading");
|
||||
|
||||
try {
|
||||
const res = await fetch(url.href, { credentials: "include" });
|
||||
if (!res.ok) throw new Error(`Navigation failed: ${res.status}`);
|
||||
const html = await res.text();
|
||||
const nextDoc = new DOMParser().parseFromString(html, "text/html");
|
||||
if (!nextDoc.getElementById("top-nav")) {
|
||||
window.location.href = url.href;
|
||||
return;
|
||||
}
|
||||
|
||||
cleanupTrackedResources();
|
||||
mergeHead(nextDoc, url.href);
|
||||
replaceBody(nextDoc);
|
||||
window.scrollTo(0, 0);
|
||||
if (replace) history.replaceState({}, "", url.href);
|
||||
else history.pushState({}, "", url.href);
|
||||
onPathChange?.(url.pathname);
|
||||
await runPageScripts(nextDoc, url.href);
|
||||
refreshGlobalEnhancements();
|
||||
window.dispatchEvent(new CustomEvent("overlord:soft-nav", { detail: { path: url.pathname } }));
|
||||
} catch (err) {
|
||||
console.error("Soft navigation failed:", err);
|
||||
window.location.href = url.href;
|
||||
} finally {
|
||||
document.body.classList.remove("soft-nav-loading");
|
||||
}
|
||||
}
|
||||
|
||||
host.addEventListener("click", (event) => {
|
||||
const link = event.target?.closest?.("a[href]");
|
||||
if (!link) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;
|
||||
|
||||
const url = new URL(link.getAttribute("href"), window.location.href);
|
||||
if (!isSoftNavigationCandidate(url, link)) return;
|
||||
|
||||
event.preventDefault();
|
||||
navigateTo(url.href);
|
||||
}, true);
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
navigateTo(window.location.href, { replace: true });
|
||||
});
|
||||
|
||||
window.overlordSoftNavigate = navigateTo;
|
||||
}
|
||||
Reference in New Issue
Block a user