diff --git a/Overlord-Server/bun.lock b/Overlord-Server/bun.lock index f541303..ac1d12d 100644 --- a/Overlord-Server/bun.lock +++ b/Overlord-Server/bun.lock @@ -5,6 +5,7 @@ "": { "name": "overlord-server", "dependencies": { + "@hotwired/turbo": "^8.0.23", "@msgpack/msgpack": "^3.0.0", "@solana/web3.js": "^1.98.4", "adm-zip": "^0.5.12", @@ -62,6 +63,8 @@ "@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@6.5.2", "", {}, "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q=="], + "@hotwired/turbo": ["@hotwired/turbo@8.0.23", "", {}, "sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], diff --git a/Overlord-Server/package.json b/Overlord-Server/package.json index cf38820..2b44dcc 100644 --- a/Overlord-Server/package.json +++ b/Overlord-Server/package.json @@ -19,6 +19,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@hotwired/turbo": "^8.0.23", "@msgpack/msgpack": "^3.0.0", "@solana/web3.js": "^1.98.4", "adm-zip": "^0.5.12", diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index ddaaa10..a8d35e8 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -2294,6 +2294,7 @@ function showBuildFiles(files, buildId, expiresAt) { const downloadBtn = document.createElement("a"); downloadBtn.href = `/api/build/download/${encodeURIComponent(file.name)}`; + downloadBtn.download = ""; downloadBtn.className = "inline-flex items-center gap-1 px-3 py-1 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm transition-colors"; downloadBtn.innerHTML = ' Download'; diff --git a/Overlord-Server/public/assets/command-palette.js b/Overlord-Server/public/assets/command-palette.js index 710af99..365740a 100644 --- a/Overlord-Server/public/assets/command-palette.js +++ b/Overlord-Server/public/assets/command-palette.js @@ -278,7 +278,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 if (window.Turbo?.visit) window.Turbo.visit(href); else window.location.href = href; close(); } diff --git a/Overlord-Server/public/assets/nav.js b/Overlord-Server/public/assets/nav.js index 02d910a..a9629f9 100644 --- a/Overlord-Server/public/assets/nav.js +++ b/Overlord-Server/public/assets/nav.js @@ -13,11 +13,10 @@ 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"; + setupTurboNavigation, + turboVisit, +} from "./turbo-navigation.js"; const host = document.getElementById("top-nav"); if (host) { @@ -112,11 +111,7 @@ if (host) { if (refs.accountSettingsBtn && !refs.accountSettingsBtn.dataset.boundSettings) { refs.accountSettingsBtn.dataset.boundSettings = "true"; refs.accountSettingsBtn.addEventListener("click", () => { - if (window.overlordSoftNavigate) { - window.overlordSoftNavigate("/settings"); - } else { - window.location.href = "/settings"; - } + turboVisit("/settings"); }); } @@ -160,32 +155,6 @@ 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" }); @@ -243,14 +212,12 @@ if (host) { }); }); - installPageResourceTracker(); - setupSoftNavigation(host, { + setupTurboNavigation({ onPathChange: (path) => { applyActivePath(path); applyBranding(); }, }); - startPageTracking(); } async function applyBranding() { diff --git a/Overlord-Server/public/assets/soft-nav.js b/Overlord-Server/public/assets/soft-nav.js deleted file mode 100644 index 758b31c..0000000 --- a/Overlord-Server/public/assets/soft-nav.js +++ /dev/null @@ -1,357 +0,0 @@ -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; -} diff --git a/Overlord-Server/public/assets/turbo-navigation.js b/Overlord-Server/public/assets/turbo-navigation.js new file mode 100644 index 0000000..5f3d5f4 --- /dev/null +++ b/Overlord-Server/public/assets/turbo-navigation.js @@ -0,0 +1,269 @@ +import { cache, config, visit } from "/vendor/hotwired/turbo.es2017-esm.js"; + +let installed = false; +let pageTrackingEnabled = false; +let visitSequence = 0; +let pendingVisitUrl = ""; + +const tracked = { + events: new Set(), + intervals: new Set(), + sockets: new Set(), + workers: new Set(), +}; + +const original = {}; +const loadedClassicScripts = new Set(); +const persistentIds = [ + "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 cleanupPageResources() { + if (!pageTrackingEnabled) return; + 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, "Turbo 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.getElementById("cmdp-root")?.remove(); +} + +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; + } +} + +function installPageResourceTracker() { + if (original.addEventListener) return; + + 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.defineProperties(window.WebSocket, { + CONNECTING: { value: original.WebSocket.CONNECTING }, + OPEN: { value: original.WebSocket.OPEN }, + CLOSING: { value: original.WebSocket.CLOSING }, + 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 preserveGlobalElements(newBody) { + for (const id of persistentIds) { + const current = document.getElementById(id); + if (!current) continue; + + current.setAttribute("data-turbo-permanent", ""); + let incoming = newBody.querySelector(`#${CSS.escape(id)}`); + if (!incoming) { + incoming = document.createElement(current.tagName.toLowerCase()); + incoming.id = id; + newBody.appendChild(incoming); + } + incoming.setAttribute("data-turbo-permanent", ""); + } +} + +function preserveLayoutClasses(newBody) { + for (const name of document.body.classList) { + if (name.startsWith("sb-") || name === "nav-hidden") { + newBody.classList.add(name); + } + } +} + +function prepareIncomingScripts(newBody) { + visitSequence += 1; + + for (const script of newBody.querySelectorAll("script")) { + const src = script.getAttribute("src"); + if (!src) continue; + + const path = scriptPath(src); + if (path === "/assets/nav.js" || path === "/assets/nav-prelude.js") { + script.setAttribute("data-turbo-eval", "false"); + continue; + } + + if (script.type === "module") { + const url = new URL(src, pendingVisitUrl || window.location.href); + if (url.origin === window.location.origin) { + url.searchParams.set("turboVisit", String(visitSequence)); + script.src = url.href; + } + continue; + } + + const normalized = normalizeUrl(src, pendingVisitUrl || window.location.href); + if (loadedClassicScripts.has(normalized)) { + script.setAttribute("data-turbo-eval", "false"); + } else { + loadedClassicScripts.add(normalized); + } + } +} + +function refreshGlobalEnhancements() { + if (typeof window.addRippleEffect === "function") { + document + .querySelectorAll("button:not(.no-ripple), .button:not(.no-ripple)") + .forEach((button) => window.addRippleEffect(button)); + } +} + +export function setupTurboNavigation({ onPathChange } = {}) { + if (installed) return; + installed = true; + + installPageResourceTracker(); + config.drive.progressBarDelay = 150; + // Existing forms already have explicit fetch handlers. Turbo owns navigation only. + config.forms.mode = "off"; + cache.exemptPageFromCache(); + + document.getElementById("top-nav")?.setAttribute("data-turbo-permanent", ""); + + document.addEventListener("turbo:before-visit", (event) => { + pendingVisitUrl = event.detail.url || ""; + }); + + document.addEventListener("turbo:before-render", (event) => { + const newBody = event.detail.newBody; + if (!newBody?.querySelector("#top-nav")) { + event.preventDefault(); + window.location.assign(pendingVisitUrl || window.location.href); + return; + } + + cleanupPageResources(); + preserveGlobalElements(newBody); + preserveLayoutClasses(newBody); + prepareIncomingScripts(newBody); + pageTrackingEnabled = true; + }); + + document.addEventListener("turbo:load", () => { + cache.exemptPageFromCache(); + pendingVisitUrl = ""; + pageTrackingEnabled = true; + onPathChange?.(window.location.pathname); + refreshGlobalEnhancements(); + window.dispatchEvent(new CustomEvent("overlord:turbo-load", { + detail: { path: window.location.pathname }, + })); + }); + + pageTrackingEnabled = true; +} + +export function turboVisit(url, options) { + visit(url, options); +} diff --git a/Overlord-Server/public/graph.html b/Overlord-Server/public/graph.html index 56a180f..3b0928d 100644 --- a/Overlord-Server/public/graph.html +++ b/Overlord-Server/public/graph.html @@ -15,7 +15,6 @@ -
@@ -64,5 +63,6 @@ + diff --git a/Overlord-Server/public/settings.html b/Overlord-Server/public/settings.html index cfba0ea..77d7c6c 100644 --- a/Overlord-Server/public/settings.html +++ b/Overlord-Server/public/settings.html @@ -1090,7 +1090,7 @@ - + Export diff --git a/Overlord-Server/scripts/vendor.ts b/Overlord-Server/scripts/vendor.ts index a761516..43d77b0 100644 --- a/Overlord-Server/scripts/vendor.ts +++ b/Overlord-Server/scripts/vendor.ts @@ -197,6 +197,14 @@ copyFile( path.join(VENDOR, "cytoscape", "cytoscape.min.js"), ); +/* ── Hotwire Turbo ───────────────────────────────────────────────── */ + +console.log("Copying Hotwire Turbo ..."); +copyFile( + path.join(NM, "@hotwired", "turbo", "dist", "turbo.es2017-esm.js"), + path.join(VENDOR, "hotwired", "turbo.es2017-esm.js"), +); + /* ── highlight.js (bundle core + languages) ──────────────────────── */ console.log("Bundling highlight.js ...");