finish up turbo and add stimulus.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import cytoscape from "/vendor/cytoscape/cytoscape.esm.min.mjs";
|
||||
|
||||
const container = document.getElementById("client-graph");
|
||||
const searchInput = document.getElementById("graph-search");
|
||||
const statusSelect = document.getElementById("graph-status");
|
||||
@@ -12,6 +14,7 @@ const detailActions = document.getElementById("graph-detail-actions");
|
||||
let cy = null;
|
||||
let currentGraph = null;
|
||||
let searchTimer = null;
|
||||
let graphPageActive = true;
|
||||
const LARGE_GRAPH_ANIMATION_CLIENTS = 30000;
|
||||
|
||||
const typeColors = {
|
||||
@@ -215,16 +218,12 @@ function selectNode(node) {
|
||||
}
|
||||
|
||||
function renderGraph(graph) {
|
||||
if (!graphPageActive || !container?.isConnected) return;
|
||||
currentGraph = graph;
|
||||
setSummary(graph.summary);
|
||||
|
||||
if (!window.cytoscape) {
|
||||
if (summaryEl) summaryEl.textContent = "Cytoscape failed to load";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cy) {
|
||||
cy = window.cytoscape({
|
||||
cy = cytoscape({
|
||||
container,
|
||||
elements: graphElements(graph),
|
||||
minZoom: 0.18,
|
||||
@@ -305,7 +304,7 @@ function renderGraph(graph) {
|
||||
}
|
||||
|
||||
async function loadGraph() {
|
||||
if (!container) return;
|
||||
if (!graphPageActive || !container) return;
|
||||
if (summaryEl) summaryEl.textContent = "Loading...";
|
||||
const params = new URLSearchParams({
|
||||
limit: "350",
|
||||
@@ -314,10 +313,12 @@ async function loadGraph() {
|
||||
});
|
||||
const res = await fetch(`/api/client-graph?${params}`, { credentials: "include" });
|
||||
if (!res.ok) {
|
||||
if (!graphPageActive) return;
|
||||
if (summaryEl) summaryEl.textContent = `Graph unavailable (${res.status})`;
|
||||
return;
|
||||
}
|
||||
renderGraph(await res.json());
|
||||
const graph = await res.json();
|
||||
if (graphPageActive) renderGraph(graph);
|
||||
}
|
||||
|
||||
function debounceLoad() {
|
||||
@@ -332,10 +333,19 @@ searchInput?.addEventListener("input", debounceLoad);
|
||||
statusSelect?.addEventListener("change", loadGraph);
|
||||
refreshBtn?.addEventListener("click", loadGraph);
|
||||
fitBtn?.addEventListener("click", () => cy?.fit(undefined, 32));
|
||||
window.addEventListener("resize", () => {
|
||||
const handleResize = () => {
|
||||
cy?.resize();
|
||||
cy?.fit(undefined, 32);
|
||||
});
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
window.addEventListener("pagehide", () => {
|
||||
graphPageActive = false;
|
||||
clearTimeout(searchTimer);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
cy?.destroy();
|
||||
cy = null;
|
||||
}, { once: true });
|
||||
|
||||
bindInspectorActions();
|
||||
loadGraph();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Chart from "/vendor/chart.js/chart.esm.js";
|
||||
|
||||
let clientsChart = null;
|
||||
let commandsChart = null;
|
||||
let bandwidthChart = null;
|
||||
@@ -29,6 +31,8 @@ let countryGlobeHoverEntry = null;
|
||||
let countryGlobeSortedFeatures = [];
|
||||
let countryGlobeLastSpinFrameAt = 0;
|
||||
let countryGlobeLastHoverFrameAt = 0;
|
||||
let metricsPollTimer = null;
|
||||
let metricsPageActive = true;
|
||||
|
||||
const GEOJSON_URL = "/vendor/geo-countries/countries.geojson";
|
||||
const MAX_CHART_POINTS = 240;
|
||||
@@ -1806,6 +1810,7 @@ function updateCharts(history, snapshot) {
|
||||
}
|
||||
|
||||
async function fetchMetrics() {
|
||||
if (!metricsPageActive) return;
|
||||
try {
|
||||
const response = await fetch(`/api/metrics?historyLimit=${MAX_CHART_POINTS}`, {
|
||||
credentials: "include",
|
||||
@@ -1821,11 +1826,13 @@ async function fetchMetrics() {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!metricsPageActive) return;
|
||||
updateMetrics(data.snapshot, data.debug);
|
||||
updateCharts(data.history, data.snapshot);
|
||||
|
||||
document.getElementById("status-text").textContent = "Live";
|
||||
} catch (err) {
|
||||
if (!metricsPageActive) return;
|
||||
console.error("Error fetching metrics:", err);
|
||||
document.getElementById("status-text").textContent = "Error";
|
||||
}
|
||||
@@ -1834,12 +1841,14 @@ async function fetchMetrics() {
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const res = await fetch("/api/auth/me", { credentials: "include" });
|
||||
if (!metricsPageActive) return;
|
||||
if (!res.ok) {
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (!metricsPageActive) return;
|
||||
document.getElementById("username-display").textContent = data.username;
|
||||
|
||||
const roleBadge = document.getElementById("role-badge");
|
||||
@@ -1895,16 +1904,18 @@ async function checkAuth() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
async function initMetricsPage() {
|
||||
await checkAuth();
|
||||
if (!metricsPageActive) return;
|
||||
|
||||
initCharts();
|
||||
|
||||
await initCountryMap();
|
||||
if (!metricsPageActive) return;
|
||||
|
||||
await fetchMetrics();
|
||||
|
||||
setInterval(fetchMetrics, METRICS_POLL_INTERVAL_MS);
|
||||
metricsPollTimer = setInterval(fetchMetrics, METRICS_POLL_INTERVAL_MS);
|
||||
|
||||
const logoutBtn = document.getElementById("logout-btn");
|
||||
if (logoutBtn && !logoutBtn.dataset.boundLogout) {
|
||||
@@ -1929,4 +1940,37 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function teardownMetricsPage() {
|
||||
metricsPageActive = false;
|
||||
if (metricsPollTimer !== null) clearInterval(metricsPollTimer);
|
||||
metricsPollTimer = null;
|
||||
if (countryGlobeFrame !== null) cancelAnimationFrame(countryGlobeFrame);
|
||||
countryGlobeFrame = null;
|
||||
countryGlobeResizeObserver?.disconnect();
|
||||
countryGlobeResizeObserver = null;
|
||||
countryGlobeCtx = null;
|
||||
countryGlobeCanvas = null;
|
||||
for (const chart of [
|
||||
clientsChart,
|
||||
commandsChart,
|
||||
bandwidthChart,
|
||||
httpRequestsChart,
|
||||
httpChart,
|
||||
memoryChart,
|
||||
eventLoopChart,
|
||||
sessionsChart,
|
||||
osChart,
|
||||
]) {
|
||||
chart?.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("pagehide", teardownMetricsPage, { once: true });
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initMetricsPage, { once: true });
|
||||
} else {
|
||||
void initMetricsPage();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 "./stimulus/application.js";
|
||||
import {
|
||||
runWithoutPageTracking,
|
||||
setupTurboNavigation,
|
||||
|
||||
@@ -64,7 +64,7 @@ export const NAV_GROUPS = [
|
||||
children: [
|
||||
{ href: "/metrics", label: "Metrics", icon: "fa-chart-line", iconColor: "text-emerald-400", linkId: "metrics-link" },
|
||||
{ href: "/graph", label: "Graph", icon: "fa-diagram-project", iconColor: "text-cyan-400", linkId: "graph-link" },
|
||||
{ href: "/screenshots", label: "Screenshot Wall", icon: "fa-images", iconColor: "text-sky-400", linkId: "screenshots-link", hidden: true },
|
||||
{ href: "/screenshots", label: "Screenshot Wall", icon: "fa-images", iconColor: "text-sky-400", linkId: "screenshots-link", hidden: true, turboPrefetch: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -83,10 +83,11 @@ export const NAV_GROUPS = [
|
||||
|
||||
export function dropdownItem(child) {
|
||||
const hiddenCls = child.hidden ? " hidden" : "";
|
||||
const prefetchAttr = child.turboPrefetch === false ? ' data-turbo-prefetch="false"' : "";
|
||||
const badgeHtml = child.hasBadge
|
||||
? `<span id="enrollment-badge" class="nav-dd-badge hidden"></span>`
|
||||
: "";
|
||||
return `<a href="${child.href}" id="${child.linkId}"
|
||||
return `<a href="${child.href}" id="${child.linkId}"${prefetchAttr}
|
||||
class="nav-dd-item${hiddenCls}" data-link-id="${child.linkId}">
|
||||
<i class="fa-solid ${child.icon} ${child.iconColor} nav-dd-item-icon"></i>
|
||||
<span>${child.label}</span>
|
||||
|
||||
@@ -85,6 +85,8 @@ let defaultWebhookTemplate = "";
|
||||
let defaultTelegramTemplate = "";
|
||||
let webhookTemplateEditor = null;
|
||||
let notificationTable = null;
|
||||
let pageActive = true;
|
||||
const subscriptionCleanups = [];
|
||||
|
||||
function tryFormatJsonText(text) {
|
||||
const source = String(text || "").trim();
|
||||
@@ -336,12 +338,17 @@ function entryMatchesSearch(entry, query) {
|
||||
}
|
||||
|
||||
function applyTableView() {
|
||||
if (!pageActive) return;
|
||||
const q = tableState.search.trim().toLowerCase();
|
||||
const all = Array.from(tableState.entries.values());
|
||||
const filtered = q ? all.filter((entry) => entryMatchesSearch(entry, q)) : all;
|
||||
filtered.sort(compareEntries);
|
||||
const rows = filtered.map(entryToRow);
|
||||
if (notificationTable) notificationTable.replaceData(rows);
|
||||
if (notificationTable && tableEl?.isConnected) {
|
||||
void notificationTable.replaceData(rows).catch((err) => {
|
||||
if (pageActive) console.error("Failed to update notification table:", err);
|
||||
});
|
||||
}
|
||||
|
||||
if (emptyState) {
|
||||
if (all.length === 0) {
|
||||
@@ -894,24 +901,25 @@ function connect() {
|
||||
startNotificationClient();
|
||||
if (panel) markAllNotificationsRead();
|
||||
|
||||
subscribeStatus((status) => {
|
||||
subscriptionCleanups.push(subscribeStatus((status) => {
|
||||
if (status === "connected") setStatus("Connected", "ok");
|
||||
if (status === "error") setStatus("Error", "error");
|
||||
if (status === "disconnected") setStatus("Disconnected", "warn");
|
||||
});
|
||||
}));
|
||||
|
||||
subscribeReady((history) => {
|
||||
subscriptionCleanups.push(subscribeReady((history) => {
|
||||
if (!pageActive) return;
|
||||
clearTable();
|
||||
for (const item of history) {
|
||||
upsertEntry(item, "notification", false);
|
||||
}
|
||||
applyTableView();
|
||||
markAllNotificationsRead();
|
||||
});
|
||||
}));
|
||||
|
||||
subscribeNotifications((item) => addNotification(item, true));
|
||||
subscribeClientEvents((item) => addClientEvent(item));
|
||||
subscribeNotificationsCleared((clientId) => removeEntriesForClient(clientId));
|
||||
subscriptionCleanups.push(subscribeNotifications((item) => addNotification(item, true)));
|
||||
subscriptionCleanups.push(subscribeClientEvents((item) => addClientEvent(item)));
|
||||
subscriptionCleanups.push(subscribeNotificationsCleared((clientId) => removeEntriesForClient(clientId)));
|
||||
}
|
||||
|
||||
const CLIENT_EVENT_BADGE = {
|
||||
@@ -1166,3 +1174,14 @@ wireTableControls();
|
||||
wireClientIdToggle();
|
||||
initRoleUi();
|
||||
connect();
|
||||
|
||||
window.addEventListener("pagehide", () => {
|
||||
pageActive = false;
|
||||
for (const unsubscribe of subscriptionCleanups.splice(0)) unsubscribe();
|
||||
webhookTemplateEditor?.dispose?.();
|
||||
webhookTemplateEditor = null;
|
||||
try {
|
||||
notificationTable?.destroy();
|
||||
} catch {}
|
||||
notificationTable = null;
|
||||
}, { once: true });
|
||||
|
||||
@@ -1858,178 +1858,6 @@ async function wipeOfflineClients() {
|
||||
}
|
||||
}
|
||||
|
||||
const sessionsTableBody = document.getElementById("sessions-table-body");
|
||||
const sessionsMessage = document.getElementById("sessions-message");
|
||||
const refreshSessionsBtn = document.getElementById("refresh-sessions-btn");
|
||||
const removeInactiveSessionsBtn = document.getElementById("remove-inactive-sessions-btn");
|
||||
|
||||
function parseUserAgent(ua) {
|
||||
if (!ua) return "Unknown";
|
||||
if (ua.length > 80) return ua.substring(0, 77) + "...";
|
||||
return ua;
|
||||
}
|
||||
|
||||
function formatRelativeTime(epochSeconds) {
|
||||
if (!epochSeconds) return "—";
|
||||
const diff = Math.floor(Date.now() / 1000) - epochSeconds;
|
||||
if (diff < 60) return "Just now";
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
||||
return `${Math.floor(diff / 86400)}d ago`;
|
||||
}
|
||||
|
||||
function showSessionsMessage(text, type = "ok") {
|
||||
if (!sessionsMessage) return;
|
||||
sessionsMessage.textContent = text;
|
||||
sessionsMessage.classList.remove(
|
||||
"hidden",
|
||||
"text-emerald-200", "border-emerald-700", "bg-emerald-900/30",
|
||||
"text-rose-200", "border-rose-700", "bg-rose-900/30",
|
||||
);
|
||||
if (type === "error") {
|
||||
sessionsMessage.classList.add("text-rose-200", "border-rose-700", "bg-rose-900/30");
|
||||
} else {
|
||||
sessionsMessage.classList.add("text-emerald-200", "border-emerald-700", "bg-emerald-900/30");
|
||||
}
|
||||
setTimeout(() => sessionsMessage.classList.add("hidden"), 5000);
|
||||
}
|
||||
|
||||
async function loadSessions() {
|
||||
if (!sessionsTableBody) return;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/sessions", { credentials: "include" });
|
||||
if (!res.ok) {
|
||||
sessionsTableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="px-3 py-6 text-center text-rose-300">Failed to load sessions</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
||||
|
||||
if (sessions.length === 0) {
|
||||
sessionsTableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="px-3 py-6 text-center text-slate-400">No sessions found</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
sessionsTableBody.innerHTML = sessions
|
||||
.map((s) => {
|
||||
const isExpired = s.expiresAt && s.expiresAt < Math.floor(Date.now() / 1000);
|
||||
const statusLabel = s.revoked
|
||||
? '<span class="text-rose-400">Revoked</span>'
|
||||
: isExpired
|
||||
? '<span class="text-slate-500">Expired</span>'
|
||||
: '<span class="text-emerald-400">Active</span>';
|
||||
const currentBadge = s.current
|
||||
? ' <span class="ml-1 px-1.5 py-0.5 text-[10px] rounded bg-sky-600/30 text-sky-300 border border-sky-500/30">Current</span>'
|
||||
: "";
|
||||
const canRevoke = !s.revoked && !isExpired;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td class="px-3 py-2 font-mono text-xs text-slate-100">${escapeHtml(s.ip || "—")}</td>
|
||||
<td class="px-3 py-2 text-slate-300 text-xs max-w-[200px] truncate" title="${escapeHtml(s.userAgent || "")}">${escapeHtml(parseUserAgent(s.userAgent))}</td>
|
||||
<td class="px-3 py-2 text-slate-400 text-xs">${formatDate(s.createdAt * 1000)}</td>
|
||||
<td class="px-3 py-2 text-slate-400 text-xs">${formatRelativeTime(s.lastActivity)}</td>
|
||||
<td class="px-3 py-2 text-xs">${statusLabel}${currentBadge}</td>
|
||||
<td class="px-3 py-2 text-right">
|
||||
${canRevoke ? `
|
||||
<button
|
||||
type="button"
|
||||
class="revoke-session-btn px-2.5 py-1.5 rounded bg-red-700/80 hover:bg-red-600 text-white text-xs"
|
||||
data-session-id="${escapeHtml(s.id)}"
|
||||
${s.current ? 'data-is-current="true"' : ""}
|
||||
>
|
||||
<i class="fa-solid fa-right-from-bracket mr-1"></i>Revoke
|
||||
</button>
|
||||
` : ""}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
} catch (err) {
|
||||
sessionsTableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="px-3 py-6 text-center text-rose-300">Error loading sessions</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRevokeSessionClick(event) {
|
||||
const button = event.target.closest(".revoke-session-btn");
|
||||
if (!button) return;
|
||||
|
||||
const sessionId = button.dataset.sessionId;
|
||||
const isCurrent = button.dataset.isCurrent === "true";
|
||||
|
||||
if (isCurrent) {
|
||||
if (!confirm("This will revoke your current session and log you out. Continue?")) return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
showSessionsMessage(data.error || "Failed to revoke session", "error");
|
||||
button.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCurrent) {
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
showSessionsMessage("Session revoked successfully");
|
||||
await loadSessions();
|
||||
} catch {
|
||||
showSessionsMessage("Request failed", "error");
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveInactiveSessions() {
|
||||
if (!confirm("Remove all expired and revoked sessions?")) return;
|
||||
|
||||
if (removeInactiveSessionsBtn) removeInactiveSessionsBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/sessions/inactive", {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
showSessionsMessage(data.error || "Failed to remove inactive sessions", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
showSessionsMessage(`Removed ${data.removed || 0} inactive session(s)`);
|
||||
await loadSessions();
|
||||
} catch {
|
||||
showSessionsMessage("Request failed", "error");
|
||||
} finally {
|
||||
if (removeInactiveSessionsBtn) removeInactiveSessionsBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Registration Settings ──────────────────────────────────────────────────
|
||||
|
||||
async function loadRegistrationSettings() {
|
||||
@@ -2844,10 +2672,6 @@ async function init() {
|
||||
}
|
||||
}
|
||||
|
||||
await loadSessions();
|
||||
if (refreshSessionsBtn) refreshSessionsBtn.addEventListener("click", loadSessions);
|
||||
if (removeInactiveSessionsBtn) removeInactiveSessionsBtn.addEventListener("click", handleRemoveInactiveSessions);
|
||||
if (sessionsTableBody) sessionsTableBody.addEventListener("click", handleRevokeSessionClick);
|
||||
} catch (error) {
|
||||
console.error("settings init failed", error);
|
||||
showMessage("Failed to load settings.", "error");
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Application } from "/vendor/hotwired/stimulus.js";
|
||||
import SessionsController from "./controllers/sessions-controller.js";
|
||||
|
||||
const application = Application.start();
|
||||
application.debug = false;
|
||||
application.register("sessions", SessionsController);
|
||||
|
||||
window.OverlordStimulus = application;
|
||||
|
||||
export { application };
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Controller } from "/vendor/hotwired/stimulus.js";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["time"];
|
||||
|
||||
connect() {
|
||||
this.updateTimes();
|
||||
this.clock = window.setInterval(() => this.updateTimes(), 30_000);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
window.clearInterval(this.clock);
|
||||
}
|
||||
|
||||
confirm(event) {
|
||||
const message = event.currentTarget.dataset.confirmMessage;
|
||||
if (message && !window.confirm(message)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
updateTimes() {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
for (const element of this.timeTargets) {
|
||||
const timestamp = Number(element.dataset.timestamp);
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||||
element.textContent = "—";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element.dataset.format === "relative") {
|
||||
const difference = Math.max(0, now - timestamp);
|
||||
element.textContent = difference < 60
|
||||
? "Just now"
|
||||
: difference < 3_600
|
||||
? `${Math.floor(difference / 60)}m ago`
|
||||
: difference < 86_400
|
||||
? `${Math.floor(difference / 3_600)}h ago`
|
||||
: `${Math.floor(difference / 86_400)}d ago`;
|
||||
} else {
|
||||
element.textContent = new Date(timestamp * 1_000).toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ function cleanupPageResources() {
|
||||
pageTrackingEnabled = false;
|
||||
window.dispatchEvent(new Event("pagehide"));
|
||||
|
||||
for (const id of tracked.intervals) original.clearInterval(id);
|
||||
for (const id of tracked.intervals) original.clearInterval.call(window, id);
|
||||
for (const entry of tracked.events) {
|
||||
original.removeEventListener.call(entry.target, entry.type, entry.listener, entry.options);
|
||||
}
|
||||
@@ -225,8 +225,8 @@ export function setupTurboNavigation({ onPathChange } = {}) {
|
||||
|
||||
installPageResourceTracker();
|
||||
config.drive.progressBarDelay = 150;
|
||||
// Existing forms already have explicit fetch handlers. Turbo owns navigation only.
|
||||
config.forms.mode = "off";
|
||||
// Existing forms keep their explicit fetch handlers. New Turbo Frames opt in per form.
|
||||
config.forms.mode = "optin";
|
||||
cache.exemptPageFromCache();
|
||||
|
||||
document.getElementById("top-nav")?.setAttribute("data-turbo-permanent", "");
|
||||
|
||||
Reference in New Issue
Block a user