import { accountUiState, createSiwcControls, createSiwcRecovery, normalizeSiwcAccount, prepareSiwcBrowser, siwcErrorFromResponse, siwcErrorText } from "./siwc-controls.js"; import { bindWizardNavigation, readWizardSession } from "./session.js"; import { clearFieldError, setFieldError } from "./ssh-form.js"; import { INITIAL_CHAT_TESTER_FEEDBACK, nextChatTesterFeedback, } from "./chat-tester-feedback.js"; import { initWizardTopbar } from "./topbar.js"; import { createDeviceCodeControls } from "./device-code.js"; import { formatLocalDateTime } from "./time.js"; const CLAUDE_REVIEW_KEYS = ["officialBinaryReviewed", "effectivePolicyReviewed", "nativeAuthAndBillingReviewed", "policyChangesControlled"]; // Docker reports "starting" for about a minute after every install, renewal or Docker // restart, so it reads as a calm state. Only a companion that keeps stopping gets advice. // A status read can find "absent" or "unavailable" instead of a runtime; "unconfirmed" // follows the last check when the runtime has not settled. const CLAUDE_RUNTIME_TEXT = Object.freeze({ running: "Running.", starting: "Starting. The first check takes about a minute.", restarting: "Restarting. The companion keeps stopping.", unhealthy: "Unhealthy. The companion fails its health check.", stopped: "Stopped. The companion container is not running.", absent: "Not found. Relmio no longer finds the companion on this computer.", unavailable: "Unavailable. Relmio could not read the companion's status.", unconfirmed: "Unknown. Relmio could not confirm the companion's state. Check again with Refresh status on the dashboard.", }); // The logs tell an account lock from other causes. A reinstall keeps the login volume and // any lock in it, so the advice is the documented recovery, never a reinstall. const CLAUDE_NOT_RUNNING_ADVICE = "Check its logs with docker logs relmio-claude. If they show account_locked, follow the account-lock recovery guide in the docs only after everything that uses this Claude account has stopped."; const claudeKeepsStopping = (runtime) => runtime === "restarting" || runtime === "unhealthy"; const token = readWizardSession(); const element = (id) => document.getElementById(id); const state = { step: 1, target: "xai-grok-build", dockerAvailable: false, localN8nStackState: null, planId: null, plan: null, installedStack: null, stackAddonPrefill: null, installedTarget: null, installUnconfirmed: false, managing: false, operationBusy: false, operationButton: null, operationControlObserver: null, operationControlStates: [], operationLabel: "", operationProgressStartedAt: 0, operationProgressTimer: null, suppressTargetRefresh: false, n8nContainers: [], n8nDiscoveryLoaded: false, n8nOAuthExists: false, n8nOAuthGeneration: 0, n8nOAuthAttemptId: null, n8nOAuthRetryBlocked: false, n8nOAuthCancellationMessage: "", n8nOAuthIntent: { purpose: "sign-in" }, installedOwner: null, installedSiwcModelLocked: false, catalogLabels: new Map(), assistantSearxngReviewId: null, assistantSearxngReview: null, localModelReview: null, localModelPollTimer: null, localModelGeneration: 0, localModelReady: false, dashboardSnapshot: null, dashboardSnapshotStale: true, dashboardSelectedTarget: null, dashboardBusy: false, dashboardGeneration: 0, dashboardStaleTimer: null, dashboardFocusIdentity: null, dashboardUsage: null, installedUsage: null, installedImages: null, imagesGeneration: 0, imagesTimer: null, claudeStatusTimer: null, claudeStatusGeneration: 0, claudeStatusChecks: 0, installImagesReady: false, chatTester: { activeController: null, conversationId: null, encryptedCredential: null, endpointBaseUrl: null, expiresAt: null, generation: 0, models: [], keyId: null, feedback: { ...INITIAL_CHAT_TESTER_FEEDBACK }, }, }; const DASHBOARD_STATES = Object.freeze([ "checking", "healthy", "stopped", "staged", "partial", "legacy", "unavailable", "stale", "absent", ]); const DASHBOARD_ACTIONS = Object.freeze([ "setup", "resume", "remove", "sign-out-chatgpt", "disable-chatgpt-plan", "inspect-stopped-chatgpt", "sign-in-grok-build", "sign-out-grok-build", "remove-owned-supergrok", "rotate-local-capability", "retry-model", "manage-claude", ]); const DASHBOARD_SERVICE_DEFINITIONS = Object.freeze([ Object.freeze({ target: "codex-chatgpt", label: "Codex (ChatGPT plan)", kind: "endpoint" }), Object.freeze({ target: "codex-chat", label: "Codex Chat adapter", kind: "endpoint" }), Object.freeze({ target: "xai-grok-build", label: "SuperGrok", kind: "endpoint" }), Object.freeze({ target: "local-n8n-stack", label: "Local n8n stack", kind: "n8n-stack" }), Object.freeze({ target: "n8n-openai-oauth", label: "ChatGPT plan sidecar", kind: "n8n-oauth-bridge" }), Object.freeze({ target: "local-n8n-assistant", label: "AI Assistant tools", kind: "n8n-assistant" }), Object.freeze({ target: "n8n-supergrok-oauth", label: "SuperGrok for n8n", kind: "n8n-supergrok" }), Object.freeze({ target: "n8n-claude-oauth", label: "Claude for n8n", kind: "n8n-claude" }), Object.freeze({ target: "n8n-local-model", label: "Local model for n8n", kind: "n8n-local-model" }), ]); const DASHBOARD_PROVIDER_DEFINITIONS = Object.freeze([ Object.freeze({ target: "codex-chatgpt", label: "ChatGPT", authentication: "provider-oauth", readiness: "runtime-owned" }), Object.freeze({ target: "codex-chat", label: "ChatGPT", authentication: "provider-oauth", readiness: "runtime-owned" }), Object.freeze({ target: "xai-grok-build", label: "SuperGrok", authentication: "provider-oauth", readiness: "runtime-owned" }), Object.freeze({ target: "n8n-supergrok-oauth", label: "SuperGrok (n8n)", authentication: "provider-oauth", readiness: "runtime-owned" }), Object.freeze({ target: "n8n-claude-oauth", label: "Claude (n8n)", authentication: "provider-oauth", readiness: "runtime-owned" }), ]); const DASHBOARD_SERVICE_STATES = new Set( DASHBOARD_STATES.filter((value) => !["checking", "stale"].includes(value)), ); const DASHBOARD_ACTION_SET = new Set(DASHBOARD_ACTIONS); const DASHBOARD_ASSISTANT_MODES = new Set([ "disabled", "sandbox", "sandbox-with-searxng", ]); const DASHBOARD_STALE_AFTER_MS = 5 * 60 * 1_000; const messageBox = element("global-message"); const messageText = element("global-message-text"); const errorBox = element("global-error"); const errorText = element("global-error-text"); bindWizardNavigation(element("back-to-vps"), "/", token); bindWizardNavigation(element("setup-another-local"), "/local", token); bindWizardNavigation(element("return-to-vps"), "/", token); bindWizardNavigation(element("local-route-vps-openai"), "/", token); bindWizardNavigation(element("local-route-vps-supergrok"), "/supergrok-vps", token); bindWizardNavigation(element("local-route-vps-model"), "/local-model-vps", token); bindWizardNavigation(element("dashboard-hosting-link"), "/hosting", token); initWizardTopbar({ session: token, isBusy: () => state.operationBusy, loadProjectMeta: () => api("/api/local/project-meta"), }); // Step 1 has two stages so each fits one screen: choose a connection, then // check its settings. The form, its fields and the review submit are shared. function showSetupStage(stage, { focus = true } = {}) { const configure = stage === "configure"; element("choose-stage").hidden = configure; element("configure-stage").hidden = !configure; element("back-to-vps").hidden = configure; element("choose-continue").hidden = configure; element("configure-back").hidden = !configure; element("review-button").hidden = !configure; element("choose-step-caption").textContent = configure ? "Step 1 of 4 · Settings" : "Step 1 of 4 · Choose"; element("choose-title").textContent = configure ? "Check the settings" : "Choose what to set up"; if (focus) element("choose-title").focus({ preventScroll: true }); } element("choose-continue").addEventListener("click", () => { if (state.operationBusy) return; showSetupStage("configure"); if (!element("local-siwc").hidden) siwc.load().catch(showError); }); element("configure-back").addEventListener("click", () => { if (state.operationBusy) return; showSetupStage("choose"); }); element("local-route-current").addEventListener("click", () => { if (state.operationBusy) return; showSetupStage("choose", { focus: false }); document.querySelector('input[name="target"]:checked')?.focus({ preventScroll: true, }); }); function setMessage(text) { messageText.textContent = text; messageBox.hidden = false; } // The status line stays in the page as a live region. An empty one takes no room. function clearMessage() { messageText.textContent = ""; } // Rejected fields point at the visible error through aria-describedby and // lose that link, and aria-invalid, whenever the error is cleared or replaced. const LOCAL_PORT_REJECTION = /^(?:Port is invalid\.|Local endpoint port must|The selected local endpoint port)/u; // True when a failure names a setting the person has to change. function namesRejectedField(error) { return typeof error?.param === "string" || LOCAL_PORT_REJECTION.test(error?.message ?? ""); } function clearFieldErrors() { for (const field of document.querySelectorAll('[aria-describedby~="global-error-text"]')) { clearFieldError(field, "global-error-text"); } } function clearError() { errorText.textContent = ""; element("local-image-build-troubleshooting").hidden = true; element("global-error-recovery").hidden = true; errorBox.hidden = true; clearFieldErrors(); globalThis.relmioGuide?.clearError?.(); } function showError(error, invalidFields = []) { const localImageBuildFailed = error?.message === "Local image build failed."; errorText.textContent = localImageBuildFailed ? "Relmio could not build the local image. Check that Docker is running, has enough disk space, and can pull its base image." : error?.recovery && error.recovery !== "none" ? siwcErrorText(error) : error?.message ?? "Something went wrong."; element("global-error-recovery").hidden = error?.recovery !== "manage-usage"; element("local-image-build-troubleshooting").hidden = !localImageBuildFailed; clearFieldErrors(); const rejected = LOCAL_PORT_REJECTION.test(errorText.textContent) ? [element("local-port")] : invalidFields; for (const field of rejected) setFieldError(field, "global-error-text"); errorBox.hidden = false; errorBox.focus(); globalThis.relmioGuide?.error?.(error); } function stackUsesNgrok() { return element("n8n-stack-public-access").checked; } function updateStackPublicAccess() { const enabled = isN8nStack(state.target) && stackUsesNgrok(); for (const id of ["ngrok-hostname-field", "ngrok-checklist"]) { element(id).hidden = !enabled; } const hostname = element("ngrok-hostname"); hostname.disabled = !enabled; hostname.required = enabled; if (!enabled) hostname.setCustomValidity(""); element("boundary-title").textContent = enabled ? "This computer, plus a public link" : "This computer only"; element("boundary-detail").textContent = enabled ? "n8n stays on this computer. The public ngrok link requires Basic Auth." : "n8n stays on this computer. No public URL is created."; invalidatePlan(); } function validateLocalN8nStackCredentials() { const ngrokAuthtoken = element("ngrok-authtoken"); const basicAuthUsername = element("ngrok-basic-auth-username"); const basicAuthPassword = element("ngrok-basic-auth-password"); const validations = [ { input: ngrokAuthtoken, valid: ngrokAuthtoken.value.length >= 8 && ngrokAuthtoken.value.length <= 512 && !/\s/u.test(ngrokAuthtoken.value) && !/^ngrok(?:\.exe)?\s+config\s+add-authtoken\b/iu.test( ngrokAuthtoken.value, ), message: "Paste only the agent token value from ngrok’s “Your Authtoken” or “Authtokens” page (8–512 characters, no whitespace), not the ngrok config add-authtoken command or an ngrok API key.", }, { input: basicAuthUsername, valid: /^[A-Za-z0-9_-]{1,64}$/u.test(basicAuthUsername.value), message: "Use 1–64 letters, numbers, hyphens, or underscores.", }, { input: basicAuthPassword, valid: basicAuthPassword.value.length >= 12 && basicAuthPassword.value.length <= 512 && !/[\0\r\n:]/u.test(basicAuthPassword.value), message: "Use 12–512 characters without a colon or line break.", }, ]; let firstInvalidInput = null; for (const validation of validations) { validation.input.setCustomValidity(validation.valid ? "" : validation.message); if (!validation.valid && !firstInvalidInput) { firstInvalidInput = validation.input; } } if (firstInvalidInput) { firstInvalidInput.reportValidity(); return false; } return true; } function resetBasicAuthPasswordVisibility() { const input = element("ngrok-basic-auth-password"); const button = element("toggle-ngrok-basic-auth-password"); input.type = "password"; button.textContent = "Show password"; button.setAttribute("aria-pressed", "false"); } const OPERATION_INTERACTIVE_SELECTOR = 'button, input, select, textarea, summary, a[href], [contenteditable]'; const OPERATION_ALLOWED_SELECTOR = '[data-operation-allow="auth"], [data-operation-allow="copy"], [data-operation-allow="stop"]'; const OPERATION_BLOCKED_EVENTS = [ "click", "pointerdown", "keydown", "beforeinput", "input", "change", "submit", ]; function readOperationAttribute(control, name) { if (typeof control.getAttribute === "function") { return control.getAttribute(name); } return control.attributes?.get?.(name) ?? null; } function restoreOperationAttribute(control, name, value) { if (value === null) { if (typeof control.removeAttribute === "function") { control.removeAttribute(name); } else { control.attributes?.delete?.(name); } return; } control.setAttribute?.(name, value); } function operationControlCandidates() { const scope = typeof document === "undefined" ? element("install-panel") : document; return Array.from( scope.querySelectorAll?.(OPERATION_INTERACTIVE_SELECTOR) ?? [], ); } function isOperationAllowedControl(control) { return Boolean(control?.closest?.(".rm-guide-layer button, .rm-guide-layer select, .rm-guide-layer summary, [data-guide-help]") || control?.closest?.(OPERATION_ALLOWED_SELECTOR)); } function lockOperationControl(control) { if (!control || isOperationAllowedControl(control)) return; const existingSnapshot = state.operationControlStates.find( (snapshot) => snapshot.control === control, ); if (existingSnapshot) { if (existingSnapshot.disabled !== null && control.disabled !== true) { control.disabled = true; } if (existingSnapshot.readOnly !== null && control.readOnly !== true) { control.readOnly = true; } if (readOperationAttribute(control, "aria-disabled") !== "true") { control.setAttribute?.("aria-disabled", "true"); } if ( existingSnapshot.disabled === null && readOperationAttribute(control, "tabindex") !== "-1" ) { control.setAttribute?.("tabindex", "-1"); } if ( existingSnapshot.contentEditable !== null && readOperationAttribute(control, "contenteditable") !== "false" ) { control.setAttribute?.("contenteditable", "false"); } return; } const snapshot = { control, disabled: typeof control.disabled === "boolean" ? control.disabled : null, readOnly: typeof control.readOnly === "boolean" ? control.readOnly : null, ariaDisabled: readOperationAttribute(control, "aria-disabled"), tabIndex: readOperationAttribute(control, "tabindex"), contentEditable: readOperationAttribute(control, "contenteditable"), }; state.operationControlStates.push(snapshot); if (snapshot.disabled !== null) control.disabled = true; if (snapshot.readOnly !== null) control.readOnly = true; control.setAttribute?.("aria-disabled", "true"); if (snapshot.disabled === null) control.setAttribute?.("tabindex", "-1"); if (snapshot.contentEditable !== null) { control.setAttribute?.("contenteditable", "false"); } } function lockAddedOperationControls(node) { if (!node || node.nodeType !== 1) return; if (node.matches?.(OPERATION_INTERACTIVE_SELECTOR)) { lockOperationControl(node); } for (const control of node.querySelectorAll?.(OPERATION_INTERACTIVE_SELECTOR) ?? []) { lockOperationControl(control); } } function formatInstallElapsed(elapsedSeconds) { const minutes = String(Math.floor(elapsedSeconds / 60)).padStart(2, "0"); const seconds = String(elapsedSeconds % 60).padStart(2, "0"); return `${minutes}:${seconds}`; } function updateOperationProgress() { const elapsedSeconds = Math.max( 0, Math.floor((Date.now() - state.operationProgressStartedAt) / 1_000), ); const label = state.operationLabel || "Working…"; const labelElement = element("install-progress-phase"); labelElement.textContent = label; element("install-progress-bar").setAttribute("aria-valuetext", label); const elapsedElement = element("install-elapsed"); elapsedElement.textContent = formatInstallElapsed(elapsedSeconds); elapsedElement.dateTime = `PT${elapsedSeconds}S`; } function startOperation(button, label, { markMainBusy = true, progressNote = "Duration varies. Keep this page open until it finishes.", showProgress = true, } = {}) { if (state.operationBusy) return false; state.operationBusy = true; state.operationButton = button ?? null; state.operationControlStates = []; state.operationLabel = label || "Working…"; state.operationProgressStartedAt = Date.now(); if (button) { if (!button.dataset.label) { button.dataset.label = button.textContent.trim(); } button.setAttribute("aria-busy", "true"); button.textContent = state.operationLabel; } for (const control of operationControlCandidates()) { lockOperationControl(control); } if (typeof document !== "undefined") { document.body.dataset.operationBusy = "true"; if (markMainBusy) { element("main-content").setAttribute("aria-busy", "true"); } } const progress = element("operation-progress"); const compatibilityProgress = element("install-progress"); if (showProgress) { element("install-progress-duration-note").textContent = progressNote; progress.hidden = false; compatibilityProgress.hidden = false; updateOperationProgress(); compatibilityProgress.focus?.({ preventScroll: true }); } if ( typeof MutationObserver !== "undefined" && typeof document !== "undefined" && document.body ) { state.operationControlObserver = new MutationObserver((records) => { if (!state.operationBusy) return; for (const record of records) { if (record.type === "attributes") { lockAddedOperationControls(record.target); } for (const node of record.addedNodes ?? []) { lockAddedOperationControls(node); } } }); state.operationControlObserver.observe(document.body, { attributeFilter: ["disabled", "readonly", "href", "contenteditable"], attributes: true, childList: true, subtree: true, }); } if (showProgress) { state.operationProgressTimer = window.setInterval( updateOperationProgress, 1_000, ); } return true; } function stopOperation(button) { if (!state.operationBusy) return false; if (state.operationProgressTimer !== null) { window.clearInterval(state.operationProgressTimer); } state.operationProgressTimer = null; state.operationControlObserver?.disconnect?.(); state.operationControlObserver = null; state.operationBusy = false; for (const snapshot of state.operationControlStates) { if (snapshot.disabled !== null) snapshot.control.disabled = snapshot.disabled; if (snapshot.readOnly !== null) snapshot.control.readOnly = snapshot.readOnly; restoreOperationAttribute( snapshot.control, "aria-disabled", snapshot.ariaDisabled, ); restoreOperationAttribute(snapshot.control, "tabindex", snapshot.tabIndex); restoreOperationAttribute( snapshot.control, "contenteditable", snapshot.contentEditable, ); } state.operationControlStates = []; if (typeof document !== "undefined") { document.body.dataset.operationBusy = "false"; element("main-content").setAttribute("aria-busy", "false"); } const progress = element("operation-progress"); const activeButton = state.operationButton ?? button ?? null; const restoreButtonFocus = typeof document !== "undefined" && Boolean(document.activeElement) && (document.activeElement === progress || progress.contains?.(document.activeElement)); progress.hidden = true; element("install-progress").hidden = true; element("install-progress-phase").textContent = "No operation is running."; element("install-progress-bar").setAttribute( "aria-valuetext", "No operation is running.", ); const elapsedElement = element("install-elapsed"); elapsedElement.textContent = "00:00"; elapsedElement.dateTime = "PT0S"; if (activeButton) { activeButton.setAttribute("aria-busy", "false"); activeButton.textContent = activeButton.dataset.label; if (restoreButtonFocus && !activeButton.disabled && !activeButton.hidden) { activeButton.focus?.({ preventScroll: true }); } } state.operationButton = null; state.operationLabel = ""; state.operationProgressStartedAt = 0; return true; } function setBusy(button, busy, busyText) { return busy ? startOperation(button, busyText) : stopOperation(button); } function setButtonLabel(button, label) { button.textContent = label; button.dataset.label = label; } function startInstallProgress(button) { const panel = element("install-panel"); if (!startOperation(button, "Installing locally…", { progressNote: "Docker may be downloading or building. Keep this page open until it finishes.", })) { return false; } panel.setAttribute("aria-busy", "true"); return true; } function stopInstallProgress(button) { stopOperation(button); element("install-settings-button").disabled = !state.planId || !state.plan || !element("install-confirm").checked; // The operation lock snapshots controls before a successful install renders // its removal panel. Restore the private removal confirmation after that // snapshot so a ready sidecar can be removed only after a new confirmation. if (state.installedTarget === "n8n-supergrok-oauth") { element("remove-supergrok-confirm").checked = false; element("remove-supergrok-confirm").disabled = false; element("remove-supergrok-button").disabled = true; } // The same snapshot would re-enable the installed model select after a // finalization failure or an empty catalog. if (state.installedSiwcModelLocked) element("installed-siwc-model").disabled = true; element("install-panel").setAttribute("aria-busy", "false"); } function blockOperationInteraction(event) { if ( !state.operationBusy || (event.type === "keydown" && event.key === "Tab") || (event.type === "keydown" && event.key === "Escape" && event.target?.closest?.(".rm-guide-layer")) || event.target?.closest?.("#operation-progress") || isOperationAllowedControl(event.target) ) { return; } event.preventDefault?.(); event.stopImmediatePropagation?.(); } function preferredScrollBehavior() { return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth"; } if (typeof document !== "undefined") { for (const eventName of OPERATION_BLOCKED_EVENTS) { document.addEventListener(eventName, blockOperationInteraction, true); } } function showStep(step, { showSetupProgress = !(state.managing && step === 4), installed = false } = {}) { // Management views open on the Ready panel. Any other step starts or continues a setup. if (step !== 4) state.managing = false; state.step = step; document.body.dataset.currentStep = String(step); element("page-title").textContent = state.managing ? "Manage a connection" : "Add a local connection"; element("setup-progress").hidden = !showSetupProgress; element("done-step-caption").hidden = !showSetupProgress; // The success mark confirms an install that just finished, never a management or recovery view. element("success-mark").hidden = !installed || (state.installedTarget === "n8n-local-model" && !state.localModelReady); for (const panel of document.querySelectorAll("[data-step]")) { const active = Number(panel.dataset.step) === step; panel.hidden = !active; if (active) { panel.querySelector("h2")?.focus({ preventScroll: true }); } } for (const marker of document.querySelectorAll("[data-step-marker]")) { const markerStep = Number(marker.dataset.stepMarker); if (showSetupProgress && markerStep < step) { marker.dataset.state = "done"; } else { delete marker.dataset.state; } if (showSetupProgress && markerStep === step) { marker.setAttribute("aria-current", "step"); } else { marker.removeAttribute("aria-current"); } } window.scrollTo({ top: 0, behavior: preferredScrollBehavior() }); } async function api(path, { method = "GET", body } = {}) { if (!token) { throw new Error( "This wizard link is incomplete. Close this tab. For a persistent install, run relmio open. For an NPX run, use npx --yes --ignore-scripts relmio@latest open. For a hosted foreground launcher, return to the active terminal and press Enter to create a fresh private handoff.", ); } let response; try { response = await fetch(path, { method, headers: { "Content-Type": "application/json", "X-Setup-Token": token, }, body: body === undefined ? undefined : JSON.stringify(body), }); } catch { throw new Error( "The local Relmio wizard is not reachable. For a persistent install, run relmio status, then relmio open. For NPX, use npx --yes --ignore-scripts relmio@latest status, then npx --yes --ignore-scripts relmio@latest open. For a hosted foreground launcher, keep its terminal open and restart that launcher if needed.", ); } let result; try { result = await response.json(); } catch { throw new Error("The local wizard returned an unreadable response."); } if (!result || typeof result !== "object" || Array.isArray(result)) { throw new Error("The local wizard returned an unexpected response."); } if (!response.ok) { const error = siwcErrorFromResponse(result, response.status); error.retryablePlan = result.retryablePlan === true; error.retryableNgrokSetup = result.retryableNgrokSetup === true; error.managedPartialStack = result.managedPartialStack === true; error.oauthRetryBlocked = result.retryBlocked === true; throw error; } return result; } const siwc = createSiwcControls({ root: element("local-siwc"), api, onChange(account) { state.n8nOAuthExists = n8nOAuthReadiness(account).ready; invalidatePlan(); state.catalogLabels.clear(); element("installed-siwc-model").replaceChildren(); element("installed-siwc-models").hidden = true; void forgetChatTester({ announce: false }); element("n8n-oauth-status").textContent = n8nOAuthReadiness(account).status; updateReviewAvailability(); }, onLogin(intent) { if (state.operationBusy) return; state.n8nOAuthIntent = intent; element("n8n-oauth-sign-in").click(); }, onError: showError, }); const siwcRecovery = createSiwcRecovery({ root: element("local-siwc-recovery"), api, getTarget({ target, action }) { if (target !== "n8n-openai-oauth" || action !== "resume") return {}; const n8nContainerId = element("n8n-container").value; const dockerNetworkId = element("n8n-network").value; return n8nContainerId && dockerNetworkId ? { n8nContainerId, dockerNetworkId } : {}; }, onReview(result) { state.planId = result.planId; state.plan = result.plan; state.target = result.plan.target; renderPlan(result.plan); element("install-confirm").checked = false; updateLocalReviewApproval(); showStep(2); }, async onResult() { await siwc.load({ welcome: false }); }, onError: showError, }); function dashboardContractError() { return new Error("The local wizard returned an unexpected dashboard response."); } function assertDashboardKeys(value, names) { if (!hasExactKeys(value, names)) throw dashboardContractError(); } function normalizeDashboardVersion(value) { if (value === null) return null; if (typeof value !== "string" || !/^[A-Za-z0-9.+-]{1,64}$/u.test(value)) { throw dashboardContractError(); } return value; } function readDashboardLoopbackPort(value) { const match = /^(?:http|ws):\/\/127\.0\.0\.1:(\d{1,5})(?:\/|$)/u.exec(value); const port = Number(match?.[1]); if (!Number.isInteger(port) || port < 1 || port > 65_535) { throw dashboardContractError(); } return port; } function normalizeDashboardEndpoint(value, target) { if (typeof value !== "string" || value.length > 256) { throw dashboardContractError(); } let parsed; try { parsed = new URL(value); } catch { throw dashboardContractError(); } const expectedProtocol = target === "codex-chatgpt" ? "ws:" : "http:"; const expectedPath = "/"; const port = readDashboardLoopbackPort(value); if ( parsed.protocol !== expectedProtocol || parsed.hostname !== "127.0.0.1" || parsed.pathname !== expectedPath || parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "" || !Number.isInteger(port) || port < 1 || port > 65_535 ) { throw dashboardContractError(); } return value; } function normalizeDashboardPublicUrl(value) { if (typeof value !== "string" || value.length > 256) { throw dashboardContractError(); } let parsed; try { parsed = new URL(value); } catch { throw dashboardContractError(); } if ( parsed.protocol !== "https:" || parsed.port !== "" || parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "" || parsed.username !== "" || parsed.password !== "" || !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/u.test(parsed.hostname) ) { throw dashboardContractError(); } return value; } function normalizeDashboardBooleans(value, names) { assertDashboardKeys(value, names); const normalized = {}; for (const name of names) { if (typeof value[name] !== "boolean") throw dashboardContractError(); normalized[name] = value[name]; } return normalized; } function normalizeInstalledSiwc(snapshot) { const legacy = snapshot.migrationRequired; if (typeof legacy !== "boolean" || typeof snapshot.auth?.configured !== "boolean") { throw dashboardContractError(); } const account = snapshot.auth.account === undefined ? null : normalizeSiwcAccount(snapshot.auth.account); if (legacy ? (snapshot.registrationId !== undefined || account !== null || snapshot.auth.configured) : (typeof snapshot.registrationId !== "string" || !/^[A-Za-z0-9_-]{8,128}$/u.test(snapshot.registrationId) || (account && account.registrationId !== snapshot.registrationId) || snapshot.auth.configured !== (account?.ownership === "owned" && account?.session === "connected" && account?.planPermission === "granted" && account?.planEnabled === true))) throw dashboardContractError(); if (snapshot.migrationState !== undefined && (!legacy || !["incomplete", "prepared", "stopped"].includes(snapshot.migrationState) || snapshot.legacyResourcesPreserved !== true)) throw dashboardContractError(); return { migrationRequired: legacy, ...(legacy ? {} : { registrationId: snapshot.registrationId }), ...(snapshot.migrationState ? { migrationState: snapshot.migrationState, legacyResourcesPreserved: true } : {}), auth: { configured: snapshot.auth.configured, ...(account ? { account } : {}) }, }; } function normalizeEndpointDashboardSnapshot(snapshot, definition) { const chatgpt = ["codex-chatgpt", "codex-chat"].includes(definition.target); assertDashboardKeys(snapshot, chatgpt ? ["target", "endpoint", "auth", "canRotateCredential", "migrationRequired", ...(snapshot.registrationId !== undefined ? ["registrationId"] : []), ...(snapshot.migrationState !== undefined ? ["migrationState", "legacyResourcesPreserved"] : [])] : ["target", "endpoint", "auth", "canRotateCredential"]); assertDashboardKeys(snapshot.auth, ["configured", "disclosure", ...(snapshot.auth.account !== undefined ? ["account"] : [])]); const siwc = chatgpt ? normalizeInstalledSiwc(snapshot) : null; if (snapshot.target !== definition.target || snapshot.auth.disclosure !== "rotate-only" || snapshot.canRotateCredential !== (chatgpt && snapshot.migrationRequired ? false : true) || (!chatgpt && snapshot.auth.configured !== true)) throw dashboardContractError(); return { target: definition.target, endpoint: normalizeDashboardEndpoint(snapshot.endpoint, definition.target), auth: { configured: snapshot.auth.configured, disclosure: "rotate-only", ...(siwc?.auth.account ? { account: siwc.auth.account } : {}) }, ...(siwc ? { migrationRequired: siwc.migrationRequired, ...(siwc.registrationId ? { registrationId: siwc.registrationId } : {}), ...(siwc.migrationState ? { migrationState: siwc.migrationState, legacyResourcesPreserved: true } : {}) } : {}), canRotateCredential: snapshot.canRotateCredential, }; } function isSafeLocalN8nUrl(value) { const match = /^http:\/\/localhost:([1-9][0-9]{0,4})$/u.exec(value); const port = Number(match?.[1]); return port >= 1024 && port <= 65_535; } function normalizeStackDashboardSnapshot(snapshot) { assertDashboardKeys(snapshot, [ "target", "assistantMode", "endpoints", "components", "canResume", "canRemove", ...(["publicAccess", "localUrl", "ngrokPublicUrl", "n8nContainerName", "networkName"] .filter((key) => Object.hasOwn(snapshot, key))), ]); assertDashboardKeys(snapshot.endpoints, [ "n8nLocal", "ngrokPublic", ]); if ( snapshot.target !== "local-n8n-stack" || !DASHBOARD_ASSISTANT_MODES.has(snapshot.assistantMode) || typeof snapshot.canResume !== "boolean" || snapshot.canRemove !== true || (snapshot.publicAccess !== undefined && !["none", "ngrok"].includes(snapshot.publicAccess)) || (snapshot.localUrl !== undefined && !isSafeLocalN8nUrl(snapshot.localUrl)) || (snapshot.n8nContainerName !== undefined && !isSafeDockerDisplayName(snapshot.n8nContainerName)) || (snapshot.networkName !== undefined && !isSafeDockerDisplayName(snapshot.networkName)) || (snapshot.ngrokPublicUrl !== undefined && snapshot.ngrokPublicUrl !== null && normalizeDashboardPublicUrl(snapshot.ngrokPublicUrl) !== snapshot.ngrokPublicUrl) || (snapshot.publicAccess === "none" && (snapshot.ngrokPublicUrl !== null || snapshot.endpoints.ngrokPublic !== null)) || (snapshot.publicAccess === "ngrok" && (typeof snapshot.ngrokPublicUrl !== "string" || snapshot.endpoints.ngrokPublic === null)) ) { throw dashboardContractError(); } return { target: "local-n8n-stack", assistantMode: snapshot.assistantMode, publicAccess: snapshot.publicAccess ?? "ngrok", localUrl: snapshot.localUrl ?? snapshot.endpoints.n8nLocal.replace("127.0.0.1", "localhost").replace(/\/$/u, ""), ngrokPublicUrl: snapshot.ngrokPublicUrl ?? null, n8nContainerName: snapshot.n8nContainerName ?? null, networkName: snapshot.networkName ?? null, endpoints: { // Newer servers send the validated localhost URL in both fields; older ones send a 127.0.0.1 endpoint. n8nLocal: snapshot.localUrl !== undefined && snapshot.endpoints.n8nLocal === snapshot.localUrl ? snapshot.localUrl : normalizeDashboardEndpoint(snapshot.endpoints.n8nLocal, "codex-chat"), ngrokPublic: snapshot.endpoints.ngrokPublic === null ? null : normalizeDashboardPublicUrl(snapshot.endpoints.ngrokPublic), }, components: normalizeDashboardBooleans(snapshot.components, [ "n8n", "ngrok", "codeSandbox", "searxng", ]), canResume: snapshot.canResume, canRemove: true, }; } function normalizeBridgeDashboardSnapshot(snapshot) { assertDashboardKeys(snapshot, ["target", "endpoint", "auth", "canRefreshCredential", "canRemove", "migrationRequired", ...(snapshot.registrationId !== undefined ? ["registrationId"] : []), ...(snapshot.migrationState !== undefined ? ["migrationState", "legacyResourcesPreserved"] : [])]); assertDashboardKeys(snapshot.auth, ["configured", "disclosure", ...(snapshot.auth.account !== undefined ? ["account"] : [])]); if (snapshot.target !== "n8n-openai-oauth" || snapshot.endpoint !== "http://n8n-openai-oauth:10531/v1" || snapshot.auth.disclosure !== "server-managed" || snapshot.canRefreshCredential !== false || snapshot.canRemove !== true) throw dashboardContractError(); const siwc = normalizeInstalledSiwc(snapshot); return { target: "n8n-openai-oauth", endpoint: snapshot.endpoint, auth: { configured: siwc.auth.configured, disclosure: "server-managed", ...(siwc.auth.account ? { account: siwc.auth.account } : {}) }, ...(siwc.registrationId ? { registrationId: siwc.registrationId } : {}), migrationRequired: siwc.migrationRequired, ...(siwc.migrationState ? { migrationState: siwc.migrationState, legacyResourcesPreserved: true } : {}), canRefreshCredential: false, canRemove: true, }; } function normalizeAssistantDashboardSnapshot(snapshot) { assertDashboardKeys(snapshot, ["target", "components", "auth", "canRemove"]); assertDashboardKeys(snapshot.auth, ["sandboxConfigured", "disclosure"]); if ( snapshot.target !== "local-n8n-assistant" || snapshot.auth.sandboxConfigured !== true || snapshot.auth.disclosure !== "one-time" || snapshot.canRemove !== true ) { throw dashboardContractError(); } return { target: "local-n8n-assistant", components: normalizeDashboardBooleans(snapshot.components, [ "codeSandbox", "searxng", ]), auth: { sandboxConfigured: true, disclosure: "one-time" }, canRemove: true, }; } function normalizeN8nSuperGrokDashboardSnapshot(snapshot) { assertDashboardKeys(snapshot, ["target", "endpoint", "auth", "canRemove"]); assertDashboardKeys(snapshot.auth, ["configured", "disclosure"]); if ( snapshot.target !== "n8n-supergrok-oauth" || snapshot.endpoint !== "http://n8n-supergrok:14502/v1" || snapshot.auth.configured !== true || snapshot.auth.disclosure !== "one-time" || snapshot.canRemove !== true ) { throw dashboardContractError(); } return { target: "n8n-supergrok-oauth", endpoint: "http://n8n-supergrok:14502/v1", auth: { configured: true, disclosure: "one-time" }, canRemove: true, }; } const LOCAL_MODEL_IDS = new Set(["qwen3:0.6b", "qwen3:1.7b", "qwen3.5:2b", "qwen3.5:4b", "qwen3.5:9b"]); function normalizeLocalModelDashboardSnapshot(snapshot) { assertDashboardKeys(snapshot, ["target", "endpoint", "model", "canRetry", "canRemove"]); assertDashboardKeys(snapshot.model, ["state", "id", "digest"]); if (snapshot.target !== "n8n-local-model" || snapshot.endpoint !== "http://n8n-local-model:11434/v1" || !["missing", "downloading", "ready", "failed", "partial"].includes(snapshot.model.state) || !LOCAL_MODEL_IDS.has(snapshot.model.id) || (snapshot.model.digest !== null && !/^[a-f0-9]{64}$/u.test(snapshot.model.digest)) || snapshot.canRetry !== ["missing", "failed"].includes(snapshot.model.state) || snapshot.canRemove !== (snapshot.model.state !== "downloading") || (snapshot.model.state === "ready" && snapshot.model.digest === null)) throw dashboardContractError(); return { target: snapshot.target, endpoint: snapshot.endpoint, model: { state: snapshot.model.state, id: snapshot.model.id, digest: snapshot.model.digest }, canRetry: snapshot.canRetry, canRemove: snapshot.canRemove, }; } function normalizeN8nClaudeDashboardSnapshot(snapshot) { assertDashboardKeys(snapshot, ["target", "endpoint", "auth", "canRemove", "models", "expiresAt", "approvalExpired", "runtime"]); assertDashboardKeys(snapshot.auth, ["configured", "disclosure"]); if (snapshot.target !== "n8n-claude-oauth" || snapshot.endpoint !== "http://relmio-claude:14502/v1" || snapshot.auth.configured !== true || snapshot.auth.disclosure !== "one-time" || snapshot.canRemove !== true || !Array.isArray(snapshot.models) || !snapshot.models.length || snapshot.models.length > 8 || new Set(snapshot.models).size !== snapshot.models.length || snapshot.models.some(model => typeof model !== "string" || model.length > 128 || !/^claude-[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(model) || model.includes("fable")) || typeof snapshot.expiresAt !== "string" || !Number.isFinite(Date.parse(snapshot.expiresAt)) || new Date(snapshot.expiresAt).toISOString() !== snapshot.expiresAt || typeof snapshot.approvalExpired !== "boolean" || !["running", "starting", "restarting", "unhealthy", "stopped"].includes(snapshot.runtime)) throw dashboardContractError(); return { ...snapshot, models: [...snapshot.models], auth: { configured: true, disclosure: "one-time" } }; } function normalizeDashboardServiceSnapshot(snapshot, definition) { if (definition.kind === "endpoint") { return normalizeEndpointDashboardSnapshot(snapshot, definition); } if (definition.kind === "n8n-stack") { return normalizeStackDashboardSnapshot(snapshot); } if (definition.kind === "n8n-oauth-bridge") { return normalizeBridgeDashboardSnapshot(snapshot); } if (definition.kind === "n8n-supergrok") { return normalizeN8nSuperGrokDashboardSnapshot(snapshot); } if (definition.kind === "n8n-claude") return normalizeN8nClaudeDashboardSnapshot(snapshot); if (definition.kind === "n8n-local-model") { return normalizeLocalModelDashboardSnapshot(snapshot); } return normalizeAssistantDashboardSnapshot(snapshot); } function expectedDashboardActions(definition, serviceState, snapshot, provider) { if (serviceState === "absent" || serviceState === "staged") return ["setup"]; if (serviceState === "unavailable") return []; if (snapshot === null) return []; const actions = []; if (serviceState === "legacy") return snapshot.migrationRequired === true ? ["setup"] : []; if (serviceState === "partial" && snapshot.migrationState) return []; if (serviceState === "stopped" && snapshot.registrationId && (["codex-chatgpt", "codex-chat"].includes(snapshot.target) || definition.kind === "n8n-oauth-bridge")) actions.push("inspect-stopped-chatgpt"); if ( definition.kind === "n8n-stack" && serviceState === "stopped" && snapshot.canResume === true ) { actions.push("resume"); } if ( definition.kind === "endpoint" && serviceState === "healthy" && snapshot.canRotateCredential === true ) { if (["codex-chatgpt", "codex-chat"].includes(definition.target)) { actions.push("setup"); if (snapshot.auth.account?.ownership === "owned") { actions.push("sign-out-chatgpt"); if (snapshot.auth.account.planEnabled) actions.push("disable-chatgpt-plan"); } } else if (definition.target === "xai-grok-build") { actions.push("sign-in-grok-build", "sign-out-grok-build"); } actions.push("rotate-local-capability"); } if (definition.kind === "n8n-oauth-bridge" && serviceState === "healthy") { actions.push("setup"); if (snapshot.auth.account?.ownership === "owned") { actions.push("sign-out-chatgpt"); if (snapshot.auth.account.planEnabled) actions.push("disable-chatgpt-plan"); } } if (definition.kind === "n8n-local-model" && snapshot.canRetry === true) { actions.push("retry-model"); } if (definition.kind === "n8n-claude") actions.push("manage-claude"); if (definition.kind === "n8n-supergrok" && serviceState === "healthy") { return ["sign-in-grok-build", "sign-out-grok-build", "remove-owned-supergrok"]; } if (snapshot.canRemove === true) actions.push("remove"); return actions; } function normalizeDashboardProvider(value, definition) { assertDashboardKeys(value, ["target", "label", "authentication", "readiness"]); if ( value.target !== definition.target || value.label !== definition.label || value.authentication !== "provider-oauth" || value.readiness !== "runtime-owned" ) { throw dashboardContractError(); } return { ...definition }; } function normalizeDashboardService(service, definition, provider) { assertDashboardKeys(service, [ "target", "label", "kind", "managed", "state", "snapshot", "actions", ...(service.state === "staged" ? ["staging"] : []), ]); if ( service.target !== definition.target || service.label !== definition.label || service.kind !== definition.kind || typeof service.managed !== "boolean" || !DASHBOARD_SERVICE_STATES.has(service.state) || !Array.isArray(service.actions) || service.actions.some((action) => !DASHBOARD_ACTION_SET.has(action)) || new Set(service.actions).size !== service.actions.length ) { throw dashboardContractError(); } if (["absent", "unavailable"].includes(service.state)) { if (service.managed !== false || service.snapshot !== null) { throw dashboardContractError(); } } else if ( service.managed !== true || (service.snapshot === null && !["partial", "staged"].includes(service.state)) ) { throw dashboardContractError(); } if (service.state === "staged" && (!["codex-chatgpt", "codex-chat", "n8n-openai-oauth"].includes(service.target) || service.snapshot !== null || !/^[A-Za-z0-9_-]{8,128}$/u.test(service.staging?.installId ?? "") || !/^[A-Za-z0-9_-]{8,128}$/u.test(service.staging?.registrationId ?? "") || !/^[a-z][a-z0-9-]{0,63}$/u.test(service.staging?.stage ?? ""))) throw dashboardContractError(); const snapshot = service.snapshot === null ? null : normalizeDashboardServiceSnapshot(service.snapshot, definition); const expectedActions = expectedDashboardActions( definition, service.state, snapshot, provider, ); if ( expectedActions.length !== service.actions.length || expectedActions.some((action, index) => action !== service.actions[index]) ) { throw dashboardContractError(); } return { target: definition.target, label: definition.label, kind: definition.kind, managed: service.managed, state: service.state, snapshot, ...(service.state === "staged" ? { staging: service.staging } : {}), actions: [...service.actions], }; } function normalizeDashboardSnapshot(value) { const topLevelNames = [ "schemaVersion", "generatedAt", "docker", "auth", "services", "providers", ...(Object.hasOwn(value ?? {}, "previewMode") ? ["previewMode"] : []), ]; assertDashboardKeys(value, topLevelNames); assertDashboardKeys(value.docker, ["available", "version", "composeVersion"]); assertDashboardKeys(value.auth, ["secretsRevealable"]); if ( value.schemaVersion !== 1 || (Object.hasOwn(value, "previewMode") && value.previewMode !== true) || typeof value.generatedAt !== "string" || value.generatedAt.length > 64 || typeof value.docker.available !== "boolean" || value.auth.secretsRevealable !== false || !Array.isArray(value.services) || value.services.length !== DASHBOARD_SERVICE_DEFINITIONS.length || !Array.isArray(value.providers) || value.providers.length !== DASHBOARD_PROVIDER_DEFINITIONS.length ) { throw dashboardContractError(); } const providers = value.providers.map((provider, index) => normalizeDashboardProvider(provider, DASHBOARD_PROVIDER_DEFINITIONS[index])); const providersByTarget = new Map(providers.map((provider) => [provider.target, provider])); const generatedAt = new Date(value.generatedAt); if ( !Number.isFinite(generatedAt.getTime()) || generatedAt.toISOString() !== value.generatedAt ) { throw dashboardContractError(); } const version = normalizeDashboardVersion(value.docker.version); const composeVersion = normalizeDashboardVersion(value.docker.composeVersion); if ( (value.docker.available && (!version || !composeVersion)) || (!value.docker.available && (version !== null || composeVersion !== null)) ) { throw dashboardContractError(); } return { schemaVersion: 1, generatedAt: value.generatedAt, docker: { available: value.docker.available, version, composeVersion, }, auth: { secretsRevealable: false }, services: value.services.map((service, index) => normalizeDashboardService( service, DASHBOARD_SERVICE_DEFINITIONS[index], providersByTarget.get(service.target), )), providers, ...(value.previewMode === true ? { previewMode: true } : {}), }; } function dashboardStateLabel(serviceState) { return { checking: "Checking", healthy: "Healthy", starting: "Starting", stopped: "Stopped", staged: "Staged; review resume", partial: "Needs recovery", unavailable: "Unavailable", stale: "Stale", absent: "Not configured", }[serviceState] ?? "Unavailable"; } // The dot, badge and word a service shows. The server reports a Claude companion that is // not running, or whose approval lapsed, as "partial". The page names the real reason and // never shows such a companion as healthy. "Starting" is a calm state: Docker reports it // for about a minute after every install, renewal or Docker restart. function dashboardServiceStatus(service) { const claude = service.kind === "n8n-claude" ? service.snapshot : null; if (!claude || (service.state === "healthy" && claude.runtime === "running" && !claude.approvalExpired)) { return { tone: service.state, label: dashboardStateLabel(service.state) }; } if (claude.runtime !== "running") { return { tone: claude.runtime === "starting" && !claude.approvalExpired ? "starting" : "partial", label: `${claude.runtime[0].toUpperCase()}${claude.runtime.slice(1)}`, }; } return { tone: "partial", label: claude.approvalExpired ? "Approval expired" : dashboardStateLabel("partial") }; } function dashboardBoundary(service) { if (service.kind === "endpoint") return "Loopback only"; if (service.kind === "n8n-stack") return service.snapshot?.publicAccess === "none" ? "Loopback only" : "Loopback + authenticated tunnel"; return "Docker network only"; } function dashboardServiceDescription(service) { if (service.state === "absent") return "Choose a connection to install."; if (service.state === "unavailable") { return "Ownership could not be verified. No maintenance actions are available."; } if (service.kind === "n8n-claude" && service.snapshot) { if (service.snapshot.runtime !== "running") { const runtime = service.snapshot.runtime; return claudeKeepsStopping(runtime) ? `${CLAUDE_RUNTIME_TEXT[runtime]} ${CLAUDE_NOT_RUNNING_ADVICE}` : CLAUDE_RUNTIME_TEXT[runtime]; } if (service.snapshot.approvalExpired) return "The approval has expired. Renew it before use."; } if (service.state === "stopped") return "Owned service is present but stopped."; if (service.state === "partial") { if (service.snapshot === null) { return "Relmio found an incomplete install but could not confirm a safe recovery action."; } return "Owned resources need a recovery decision before setup can continue."; } if (service.kind === "n8n-local-model") { if (service.state === "healthy" && service.snapshot?.model.state === "ready") { return "Inference check passed. No provider account is involved. That does not prove every workflow."; } if (service.snapshot?.model.state === "downloading") { return "The model download is still running. Settings are not ready."; } return "The runtime is owned, but model inference is not verified. Review a retry if one is available."; } if (service.kind === "n8n-assistant") { return "Private Assistant tools are present. You still update n8n yourself."; } return "Owned service passed the latest local inventory check."; } function dashboardStateNode(serviceState, className = "dashboard-state-token", label = dashboardStateLabel(serviceState)) { const node = document.createElement("span"); node.className = `rm-badge ${className} state-${serviceState}`; node.textContent = label; return node; } function dashboardStatusDot(serviceState) { const dot = document.createElement("span"); dot.className = `rm-status__dot dashboard-status-dot state-${serviceState}`; dot.setAttribute("aria-hidden", "true"); return dot; } // A date for people: the short local form, with the exact instant kept in the datetime attribute. function localTime(iso) { const time = document.createElement("time"); time.dateTime = iso; time.textContent = formatLocalDateTime(iso); return time; } function isDashboardSnapshotStale(snapshot) { const age = Date.now() - new Date(snapshot.generatedAt).getTime(); return age > DASHBOARD_STALE_AFTER_MS || age < -60_000; } function clearDashboardStaleTimer() { if (state.dashboardStaleTimer !== null) { window.clearTimeout(state.dashboardStaleTimer); state.dashboardStaleTimer = null; } } function scheduleDashboardStaleExpiry(snapshot) { clearDashboardStaleTimer(); if ( document.body.dataset.localView !== "dashboard" || isDashboardSnapshotStale(snapshot) ) { return; } const expiresIn = Math.max( 0, new Date(snapshot.generatedAt).getTime() + DASHBOARD_STALE_AFTER_MS - Date.now() + 1, ); state.dashboardStaleTimer = window.setTimeout(() => { state.dashboardStaleTimer = null; if ( document.body.dataset.localView !== "dashboard" || state.dashboardSnapshot !== snapshot ) { return; } if (isDashboardSnapshotStale(snapshot)) { renderDashboardSnapshot(snapshot, { stale: true }); } else { scheduleDashboardStaleExpiry(snapshot); } }, expiresIn); } function captureDashboardFocusIdentity() { const active = document.activeElement; const service = active?.dataset?.dashboardService; if (!service) return null; return { service, action: active.dataset.dashboardAction ?? null, actionLocation: active.dataset.dashboardActionLocation ?? null, fact: active.dataset.dashboardFact ?? null, }; } function restoreDashboardFocus(identity) { if (!identity) return; const controls = Array.from( document.querySelectorAll("[data-dashboard-service]"), ); const exact = controls.find((control) => { if (control.dataset.dashboardService !== identity.service) return false; if (identity.fact !== null) { return control.dataset.dashboardFact === identity.fact; } if (identity.action === null) { return control.dataset.dashboardControl === "select"; } return control.dataset.dashboardAction === identity.action && control.dataset.dashboardActionLocation === identity.actionLocation; }); const fallback = controls.find((control) => control.dataset.dashboardService === identity.service && control.dataset.dashboardControl === "select"); const target = exact && !exact.disabled && !exact.hidden ? exact : fallback; if (target && !target.disabled && !target.hidden) { target.focus({ preventScroll: true }); } } function appendDashboardFact( container, label, value, { copyLabel = null, service = null, fact = null } = {}, ) { const row = document.createElement("div"); const term = document.createElement("dt"); const detail = document.createElement("dd"); term.textContent = label; if (copyLabel) { detail.className = "dashboard-copyable-fact"; const displayedValue = document.createElement("code"); displayedValue.textContent = value; const button = document.createElement("button"); button.type = "button"; button.className = "rm-icon-button rm-icon-button--outline rm-icon-button--sm dashboard-fact-copy"; button.dataset.copyLabel = copyLabel; button.dataset.dashboardService = service; button.dataset.dashboardFact = fact; const icon = document.createElement("span"); icon.className = "rm-icon rm-icon--copy rm-icon--sm"; icon.setAttribute("aria-hidden", "true"); button.append(icon); button.setAttribute("aria-label", `Copy ${copyLabel}`); button.setAttribute("title", `Copy ${copyLabel}`); button.addEventListener("click", async () => { const error = element("dashboard-error"); const status = element("dashboard-copy-status"); error.hidden = true; error.textContent = ""; status.textContent = ""; try { await copyText(displayedValue.textContent, button); flashCopied(button); status.textContent = `${copyLabel} copied.`; } catch { error.textContent = `Copy failed. Select the ${copyLabel} manually.`; error.hidden = false; error.focus(); } }); detail.append(displayedValue, button); } else if (Array.isArray(value)) { // Text with a