(function () { const root = document.getElementById("qortal-qapps-root"); if (!root) { return; } let qapps = []; const normalizeQappIconMode = function (value) { const normalized = String(value || "") .trim() .toLowerCase(); if (normalized === "custom" || normalized === "name") { return normalized; } return "auto"; }; try { const parsed = JSON.parse(root.dataset.qapps || "[]"); if (Array.isArray(parsed)) { qapps = parsed .filter(function (entry) { return ( entry && typeof entry === "object" && typeof entry.address === "string" && entry.address.trim() !== "" ); }) .map(function (entry) { return { name: typeof entry.name === "string" ? entry.name.trim() : "", address: String(entry.address || "").trim(), description: typeof entry.description === "string" ? entry.description.trim() : "", iconMode: normalizeQappIconMode(entry.iconMode), iconUrl: typeof entry.iconUrl === "string" ? entry.iconUrl.trim() : "", iconFromName: typeof entry.iconFromName === "string" ? entry.iconFromName.trim() : typeof entry.iconSourceName === "string" ? entry.iconSourceName.trim() : "", }; }); } } catch (error) { qapps = []; } const enabled = root.dataset.qappsEnabled === "1"; const browserEnabled = root.dataset.qappsBrowserEnabled === "1"; const browserAddress = root.dataset.qappsBrowserAddress || ""; const urlParams = new URLSearchParams(window.location.search); const embedMode = root.dataset.embedMode === "1" || urlParams.get("embed") === "1"; const embedChromeless = embedMode && (urlParams.get("chromeless") === "1" || urlParams.get("embed_chrome") === "0"); const embedLockQapp = embedMode && urlParams.get("lock_qapp") === "1"; const embedRequestedQapp = normalizeBrowserAddress( decodeURIComponent(urlParams.get("qapp") || urlParams.get("app") || "") ); const debugFromQuery = urlParams.get("debug") === "1" || urlParams.get("qapps_debug") === "1"; const debugFromStorage = window.localStorage && window.localStorage.getItem("qortal_qapps_debug") === "1"; const debugEnabled = root.dataset.qappsDebugEnabled === "1" || debugFromQuery || debugFromStorage; const nodeUrl = root.dataset.qortalNodeUrl || ""; const gatewayUrl = root.dataset.qortalGatewayUrl || ""; const gatewayProxyTemplate = root.dataset.gatewayProxyUrl || ""; const viewerRendererConfigured = Boolean(nodeUrl || gatewayUrl); const qappsRequestUrl = root.dataset.qappsRequestUrl || ""; const qappsApproveUrl = root.dataset.qappsApproveUrl || ""; const qappsPermissionsUrl = root.dataset.qappsPermissionsUrl || ""; const qappsUnlockUrl = root.dataset.qappsUnlockUrl || ""; const qappsUnlockStatusUrl = root.dataset.qappsUnlockStatusUrl || ""; const advancedUnlockSettingsEnabled = root.dataset.qappsAdvancedUnlockSettingsEnabled === "1"; const defaultApprovalMode = (function () { if (!advancedUnlockSettingsEnabled) { return "app_always"; } const rawMode = String(root.dataset.qappsDefaultApprovalMode || "").trim(); if (rawMode === "session") { return "type_minutes"; } if (rawMode === "scope") { return "type_always"; } if (rawMode === "app") { return "app_always"; } if ( rawMode === "type_minutes" || rawMode === "type_always" || rawMode === "app_always" ) { return rawMode; } return "once"; })(); const defaultApprovalTempMinutes = (function () { const parsed = Number( String(root.dataset.qappsDefaultApprovalTempMinutes || "120").trim() ); if (!Number.isFinite(parsed)) { return 120; } const value = Math.floor(parsed); const allowed = [60, 120, 180, 360, 720, 1440, 7200]; return allowed.includes(value) ? value : 120; })(); const defaultApprovalUnlock10Min = advancedUnlockSettingsEnabled && root.dataset.qappsDefaultApprovalUnlock10Min === "1"; const defaultUnlockSession20Min = advancedUnlockSettingsEnabled && root.dataset.qappsDefaultUnlockSession20Min === "1"; const userMappingsUrl = root.dataset.userMappingsUrl || ""; const nextcloudPublicUrl = root.dataset.nextcloudPublicUrl || ""; const settingsPath = root.dataset.settingsPath || "/settings/user/qortal_integration"; const sovereignMode = root.dataset.sovereignMode === "full_qortal" ? "full_qortal" : "powered"; const isPoweredMode = sovereignMode === "powered"; const effectiveDebugEnabled = !isPoweredMode && debugEnabled; const modeCopy = isPoweredMode ? { title: "NuQloud Apps", help: "Launch approved NuQloud Apps from inside Nextcloud.", emptyNote: "App access is not enabled yet. Ask an admin to enable it.", browserTitle: "Full NuQloud Browser", browserNote: "Open the full NuQloud browser for unrestricted app access.", warningNote: "No linked wallet is available for your account yet. You can browse apps, but authenticated actions will not work until you create or attach a wallet.", settingsButton: "Open Integration Settings", nameRequiredTitle: "Distributed Identity Setup Needed", nameRequiredContinue: "Continue in View-Only Mode", listTitle: "Available NuQloud Apps", listNote: "Launch approved NuQloud Apps from this list, or use Full Browser mode for unrestricted navigation.", listEmpty: "No apps available.", nodeMissingAlert: "Node or gateway URL is not configured. Ask an admin to configure rendering.", launchDisabledTitle: "Node or gateway URL is not configured.", browserName: "NuQloud Browser", requestEndpointMissing: "Apps request endpoint is not configured", approvalTitle: advancedUnlockSettingsEnabled ? "Authorize Request" : "Continue in NuQloud App", approvalNote: advancedUnlockSettingsEnabled ? "Choose how this request should be handled. Broker/API session auth is automatic while you are logged into Nextcloud." : "Continue with this app action. Enter your account password only if your account is currently locked.", unlockTitle: advancedUnlockSettingsEnabled ? "Unlock Wallet" : "Unlock to Continue", unlockNote: advancedUnlockSettingsEnabled ? "Your wallet is locked. Enter your wallet password to continue." : "Enter your account password to continue in this app.", loadingLabel: "Loading from decentralized network...", loadingFromQdn: "Loading from decentralized network...", accountLabel: "NuQloud decentralized account", unitLabel: "publish credits", costLabel: "125 publish credits", createAccountStep: "Use the 'Create Account' button on the dashboard.", fundingStep: "Request initial publish credits from your admins or CHD, or purchase publish credits on the dashboard.", nameStep: "Use 125 publish credits to register your distributed identity name on the network.", prereqOutro: "Then you will have full access to distributed authentication, communications, backups, and data features.", mobileSwitchNote: 'Open "{name}" here? This will replace your current app in this view.', mobileLeaveNote: 'Leave your current app and open "{name}" here? Tap OK to open anyway or Cancel to stay.', } : { title: "Q-Apps", help: "Launch approved Q-Apps from inside Nextcloud.", emptyNote: "Q-Apps access is not enabled yet. Ask an admin to enable it.", browserTitle: "Full Qortal Browser", browserNote: "Open the full Qortal browser for unrestricted Q-App access.", warningNote: "No Qortal account is linked to your Nextcloud user yet. You can browse Q-Apps, but authentication-required actions will not work until you create or attach a Qortal account.", settingsButton: "Open Qortal Settings", nameRequiredTitle: "Distributed Identity Setup Needed", nameRequiredContinue: "Continue in View-Only Mode", listTitle: "Available Q-Apps", listNote: "Launch approved Q-Apps from this list, or use Full Browser mode for unrestricted navigation.", listEmpty: "No Q-Apps available.", nodeMissingAlert: "Qortal node or gateway URL is not configured. Ask an admin to configure rendering.", launchDisabledTitle: "Qortal node or gateway URL is not configured.", browserName: "Qortal Browser", requestEndpointMissing: "Q-Apps request endpoint is not configured", approvalTitle: advancedUnlockSettingsEnabled ? "Authorize Qortal Request" : "Continue in Q-App", approvalNote: advancedUnlockSettingsEnabled ? "Choose how this Qortal request should be handled. Broker/API session auth is automatic while you are logged into Nextcloud." : "Continue with this Q-App action. Enter your wallet password only if your wallet is currently locked.", unlockTitle: advancedUnlockSettingsEnabled ? "Unlock Qortal Wallet" : "Unlock to Continue", unlockNote: advancedUnlockSettingsEnabled ? "Your wallet is locked. Enter your wallet password to continue." : "Enter your wallet password to continue in this app.", loadingLabel: "Loading from QDN...", loadingFromQdn: "Loading from QDN...", accountLabel: "Qortal account", unitLabel: "QORT", costLabel: "1.25 QORT", createAccountStep: "Use the 'Create Account' button on the dashboard.", fundingStep: "Request initial QORT from your admins/CHD, or purchase publish credits/QORT on the dashboard.", nameStep: "Use 1.25 QORT to register your distributed identity name on the network.", prereqOutro: "Then you will have full access to distributed authentication, communications, backups, and QDN data features.", mobileSwitchNote: 'Open "{name}" here? This will replace your current app in this view.', mobileLeaveNote: 'Leave your current app and open "{name}" here? Tap OK to open anyway or Cancel to stay.', }; const emptyCard = document.getElementById("qortal-qapps-empty"); const titleEl = document.getElementById("qortal-qapps-title"); const helpEl = document.getElementById("qortal-qapps-help"); const emptyNoteEl = document.getElementById("qortal-qapps-empty-note"); const browserCard = document.getElementById("qortal-qapps-browser"); const browserTitleEl = document.getElementById("qortal-qapps-browser-title"); const browserNoteEl = document.getElementById("qortal-qapps-browser-note"); const browserAddressEl = document.getElementById( "qortal-qapps-browser-address" ); const browserButton = document.getElementById("qortal-qapps-open-browser"); const listCard = document.getElementById("qortal-qapps-list"); const listTitleEl = document.getElementById("qortal-qapps-list-title"); const listNoteEl = document.getElementById("qortal-qapps-list-note"); const galleryEl = document.getElementById("qortal-qapps-gallery"); const listBody = document.getElementById("qortal-qapps-body"); const warningCard = document.getElementById("qortal-qapps-warning"); const warningNoteEl = document.getElementById("qortal-qapps-warning-note"); const settingsLink = document.getElementById("qortal-qapps-settings-link"); const viewerCard = document.getElementById("qortal-qapps-viewer"); const viewerAddress = document.getElementById("qortal-qapps-viewer-address"); const viewerAddressInputWrap = root.querySelector( ".qortal-viewer-address-input" ); const viewerAddressInput = document.getElementById( "qortal-qapps-viewer-address-input" ); const viewerAddressGo = document.getElementById( "qortal-qapps-viewer-address-go" ); const viewerNodeStats = document.getElementById( "qortal-qapps-viewer-node-stats" ); const viewerPeersPill = document.getElementById( "qortal-qapps-viewer-peers-pill" ); const viewerPeersValue = document.getElementById( "qortal-qapps-viewer-peers-value" ); const viewerDataPeersPill = document.getElementById( "qortal-qapps-viewer-data-peers-pill" ); const viewerDataPeersValue = document.getElementById( "qortal-qapps-viewer-data-peers-value" ); const viewerLoadingProgress = document.getElementById( "qortal-qapps-loading-progress" ); const viewerLoadingOverlay = document.getElementById( "qortal-qapps-loading-overlay" ); const viewerLoadingStatus = document.getElementById( "qortal-qapps-loading-status" ); const viewerLoadingMeta = document.getElementById( "qortal-qapps-loading-meta" ); const viewerLoadingProgressBar = document.getElementById( "qortal-qapps-loading-progress-bar" ); const viewerFrame = document.getElementById("qortal-qapps-frame"); const viewerFrameWrap = document.getElementById("qortal-qapps-frame-wrap"); const viewerClose = document.getElementById("qortal-qapps-close"); const viewerOpenExternal = document.getElementById( "qortal-qapps-open-external" ); const viewerBack = document.getElementById("qortal-qapps-back"); const viewerReload = document.getElementById("qortal-qapps-reload"); const viewerMobileRefresh = document.getElementById("qortal-qapps-mobile-refresh"); const mobileMenuToggle = document.getElementById("qortal-qapps-mobile-menu"); const viewerMenuToggle = document.getElementById("qortal-qapps-menu"); const pageHeader = root.querySelector(".qortal-page-header"); const debugCard = document.getElementById("qortal-qapps-debug"); const debugResizeHandle = document.getElementById( "qortal-qapps-debug-resize-handle" ); const debugBody = document.getElementById("qortal-qapps-debug-body"); const debugCopy = document.getElementById("qortal-qapps-debug-copy"); const debugClear = document.getElementById("qortal-qapps-debug-clear"); const debugFloat = document.getElementById("qortal-qapps-debug-float"); const debugMinimize = document.getElementById("qortal-qapps-debug-minimize"); const approvalModal = document.getElementById("qortal-qapps-approval"); const approvalTitleEl = document.getElementById( "qortal-qapps-approval-title" ); const approvalNoteEl = document.getElementById("qortal-qapps-approval-note"); const approvalAppEl = document.getElementById("qortal-approval-app"); const approvalActionEl = document.getElementById("qortal-approval-action"); const approvalTempMinutesEl = document.getElementById( "qortal-approval-temp-minutes" ); const approvalPassword = document.getElementById("qortal-approval-password"); const approvalTtl = document.getElementById("qortal-approval-ttl"); const approvalPolicyStatusEl = document.getElementById( "qortal-approval-policy-status" ); const approvalWalletStatusEl = document.getElementById( "qortal-approval-wallet-status" ); const approvalUnlockFieldsEl = document.getElementById( "qortal-approval-unlock-fields" ); const approvalConfirm = document.getElementById("qortal-approval-confirm"); const approvalCancel = document.getElementById("qortal-approval-cancel"); const approvalError = document.getElementById("qortal-approval-error"); const unlockModal = document.getElementById("qortal-qapps-unlock"); const unlockTitleEl = document.getElementById("qortal-qapps-unlock-title"); const unlockNoteEl = document.getElementById("qortal-qapps-unlock-note"); const unlockAppEl = document.getElementById("qortal-unlock-app"); const unlockActionEl = document.getElementById("qortal-unlock-action"); const unlockPassword = document.getElementById("qortal-unlock-password"); const unlockSession = document.getElementById("qortal-unlock-session"); const unlockConfirm = document.getElementById("qortal-unlock-confirm"); const unlockCancel = document.getElementById("qortal-unlock-cancel"); const unlockError = document.getElementById("qortal-unlock-error"); const nameRequiredModal = document.getElementById( "qortal-qapps-name-required" ); const nameRequiredTitleEl = document.getElementById( "qortal-qapps-name-required-title" ); const nameRequiredNote1El = document.getElementById( "qortal-qapps-name-required-note-1" ); const nameRequiredStepsEl = document.getElementById( "qortal-qapps-name-required-steps" ); const nameRequiredStep1El = document.getElementById( "qortal-qapps-name-required-step-1" ); const nameRequiredStep2El = document.getElementById( "qortal-qapps-name-required-step-2" ); const nameRequiredStep3El = document.getElementById( "qortal-qapps-name-required-step-3" ); const nameRequiredNote2El = document.getElementById( "qortal-qapps-name-required-note-2" ); const nameRequiredDashboardLink = document.getElementById( "qortal-qapps-name-required-dashboard" ); const nameRequiredContinue = document.getElementById( "qortal-qapps-name-required-continue" ); const nameRequiredCancel = document.getElementById( "qortal-qapps-name-required-cancel" ); const switchModal = document.getElementById("qortal-qapps-switch-modal"); const switchModalNoteEl = document.getElementById( "qortal-qapps-switch-modal-note" ); const switchModalOpenHereButton = document.getElementById( "qortal-qapps-switch-open-here" ); const switchModalOpenNewTabButton = document.getElementById( "qortal-qapps-switch-open-new-tab" ); const switchModalCancelButton = document.getElementById( "qortal-qapps-switch-cancel" ); const debugPanelDockStorageKey = "qortal_qapps_debug_dock"; const debugPanelHeightStorageKey = "qortal_qapps_debug_height"; const debugPanelDefaultHeight = 280; const debugPanelMinHeight = 160; const debugPanelCompactRatio = 0.25; const debugPanelPeekHeight = 18; let debugPanelExpandedHeight = debugPanelDefaultHeight; let debugPanelResizeObserver = null; let debugDockHoverSuppressed = false; if (embedMode) { root.classList.add("qortal-embed-mode"); if (embedChromeless) { root.classList.add("qortal-embed-chromeless"); } } const debugEntries = []; const debugLimit = 200; const qdnLoadingRetryDelayMs = 4000; const qdnLoadingReloadFallbackDelayMs = 7000; const qdnLoadingRetryMaxAttempts = 240; const qdnLoadingMarkers = [ "files are being retrieved from the qortal data network", "this page will refresh automatically when the content becomes available", "initializing", "initialising", ]; const qdnStatusTitles = { NOT_STARTED: "Preparing resource...", PUBLISHED: "Published, download not started", DOWNLOADING: isPoweredMode ? "Downloading from decentralized network..." : "Downloading from QDN...", DOWNLOADED: "Downloaded, preparing content...", BUILDING: "Building content...", MISSING_DATA: "Missing data, waiting for peers...", BUILD_FAILED: "Build failed. Retrying...", NOT_PUBLISHED: "Resource not found", UNSUPPORTED: "Unsupported request", BLOCKED: "Content is blocked", }; const qdnStatusNumericMap = { 1: "PUBLISHED", 2: "NOT_PUBLISHED", 3: "DOWNLOADING", 4: "DOWNLOADED", 5: "BUILDING", 6: "READY", 7: "MISSING_DATA", 8: "BUILD_FAILED", 9: "UNSUPPORTED", 10: "BLOCKED", }; const mobileMenuClass = "qortal-mobile-menu-open"; const viewerOpenClass = "qortal-viewer-open"; const approvalScopeAliases = { PUBLISH_MULTIPLE_QDN_RESOURCES: "PUBLISH_QDN_RESOURCE", }; let currentWalletId = ""; let currentWalletAddress = ""; let currentQappAddress = ""; let currentViewerAddress = ""; let approvalContext = null; let activeApprovalPolicyMode = defaultApprovalMode; const walletLockState = { state: "unknown", expiresAt: null, }; const temporaryApprovalStorageKey = "qortal_qapps_temp_rules_v1"; const temporaryApprovalRules = loadTemporaryApprovalRules(); let qdnLoadingRetryTimer = null; let qdnLoadingRetryAttempts = 0; let qdnLoadingStatusFailures = 0; let qdnLoadingLastSnapshot = ""; let currentQdnResourceTarget = null; let viewerNodeStatusPollTimer = null; let viewerNodeStatusLastFetchAt = 0; let switchModalResolver = null; const nameLookupCacheTtlMs = 60 * 1000; let nameLookupCache = { address: "", hasName: null, checkedAt: 0, }; let balanceLookupCache = { address: "", balanceQort: null, checkedAt: 0, }; function applyModeCopy() { if (titleEl) { titleEl.textContent = modeCopy.title; } if (helpEl) { helpEl.textContent = modeCopy.help; } if (emptyNoteEl) { emptyNoteEl.textContent = modeCopy.emptyNote; } if (browserTitleEl) { browserTitleEl.textContent = modeCopy.browserTitle; } if (browserNoteEl) { browserNoteEl.textContent = modeCopy.browserNote; } if (warningNoteEl) { warningNoteEl.textContent = modeCopy.warningNote; } if (settingsLink) { settingsLink.textContent = modeCopy.settingsButton; } if (nameRequiredTitleEl) { nameRequiredTitleEl.textContent = modeCopy.nameRequiredTitle; } if (nameRequiredContinue) { nameRequiredContinue.textContent = modeCopy.nameRequiredContinue; } if (listTitleEl) { listTitleEl.textContent = modeCopy.listTitle; } if (listNoteEl) { listNoteEl.textContent = modeCopy.listNote; } if (approvalTitleEl) { approvalTitleEl.textContent = modeCopy.approvalTitle; } if (approvalNoteEl) { approvalNoteEl.textContent = modeCopy.approvalNote; } if (unlockTitleEl) { unlockTitleEl.textContent = modeCopy.unlockTitle; } if (unlockNoteEl) { unlockNoteEl.textContent = modeCopy.unlockNote; } root.setAttribute("data-mode-active", sovereignMode); } function approvalConfirmLabel(showUnlockFields) { if (advancedUnlockSettingsEnabled) { return "Approve"; } return showUnlockFields ? "Unlock and Continue" : "Continue"; } function approvalBusyLabel(showUnlockFields) { if (advancedUnlockSettingsEnabled) { return "Approving..."; } return showUnlockFields ? "Unlocking..." : "Continuing..."; } function unlockConfirmLabel() { return "Unlock"; } function clearQdnLoadingProgress() { if (viewerLoadingProgress) { viewerLoadingProgress.textContent = ""; viewerLoadingProgress.classList.add("qortal-hidden"); } if (viewerLoadingStatus) { viewerLoadingStatus.textContent = ""; } if (viewerLoadingMeta) { viewerLoadingMeta.textContent = ""; } if (viewerLoadingProgressBar) { viewerLoadingProgressBar.style.width = "0%"; } if (viewerLoadingOverlay) { viewerLoadingOverlay.classList.add("qortal-hidden"); } } function setViewerNodeStatPillState(element, state) { if (!element) { return; } element.classList.remove( "qortal-node-stat-ok", "qortal-node-stat-warn", "qortal-node-stat-error" ); if (state === "ok") { element.classList.add("qortal-node-stat-ok"); } else if (state === "warn") { element.classList.add("qortal-node-stat-warn"); } else if (state === "error") { element.classList.add("qortal-node-stat-error"); } } function setViewerNodeStatusDisplay(peers, dataPeers, unavailable) { if (!viewerNodeStats) { return; } if (viewerPeersValue) { viewerPeersValue.textContent = unavailable ? "!" : peers !== null ? String(peers) : "—"; } if (viewerDataPeersValue) { viewerDataPeersValue.textContent = unavailable ? "!" : dataPeers !== null ? String(dataPeers) : "—"; } if (unavailable) { setViewerNodeStatPillState(viewerPeersPill, "error"); setViewerNodeStatPillState(viewerDataPeersPill, "error"); return; } setViewerNodeStatPillState( viewerPeersPill, peers === null ? null : peers < 3 ? "error" : "ok" ); setViewerNodeStatPillState( viewerDataPeersPill, dataPeers === null ? null : dataPeers < 3 ? "error" : "ok" ); } function findNodeStatusField(source, keys, depth) { if (!source || typeof source !== "object") { return null; } if (!Array.isArray(keys) || keys.length === 0) { return null; } const limit = typeof depth === "number" && depth > 0 ? depth : 3; const queue = [{ value: source, level: 0 }]; const seen = new Set(); while (queue.length > 0) { const next = queue.shift(); if (!next || !next.value || typeof next.value !== "object") { continue; } if (seen.has(next.value)) { continue; } seen.add(next.value); for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; if (Object.prototype.hasOwnProperty.call(next.value, key)) { return next.value[key]; } } if (next.level >= limit) { continue; } Object.keys(next.value).forEach(function (childKey) { const child = next.value[childKey]; if (child && typeof child === "object") { queue.push({ value: child, level: next.level + 1 }); } }); } return null; } function parseNodeStatusInt(value) { const parsed = coerceFiniteNumber(value); if (parsed === null) { return null; } return Math.max(0, Math.floor(parsed)); } function stopViewerNodeStatusPolling() { if (viewerNodeStatusPollTimer) { window.clearInterval(viewerNodeStatusPollTimer); viewerNodeStatusPollTimer = null; } } async function refreshViewerNodeStatus(force) { if (!viewerCard || viewerCard.classList.contains("qortal-hidden")) { return; } const now = Date.now(); if ( !force && viewerNodeStatusLastFetchAt && now - viewerNodeStatusLastFetchAt < 8000 ) { return; } viewerNodeStatusLastFetchAt = now; try { const result = await postBrokerQortalRequest("GET_NODE_STATUS", {}); const peers = parseNodeStatusInt( findNodeStatusField( result, [ "peers", "peerCount", "numberOfConnections", "connectedPeers", "connections", ], 4 ) ); const dataPeers = parseNodeStatusInt( findNodeStatusField( result, [ "numberOfDataConnections", "dataPeers", "dataPeerCount", "numberOfDataPeers", "connectedDataPeers", ], 4 ) ); setViewerNodeStatusDisplay(peers, dataPeers, false); } catch (_error) { setViewerNodeStatusDisplay(null, null, true); } } function startViewerNodeStatusPolling() { if (!viewerNodeStats) { return; } stopViewerNodeStatusPolling(); refreshViewerNodeStatus(true).catch(function () { // ignored }); viewerNodeStatusPollTimer = window.setInterval(function () { refreshViewerNodeStatus(false).catch(function () { // ignored }); }, 20000); } function coerceFiniteNumber(value) { if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string" && value.trim() !== "") { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return null; } function getQdnStatusLabel(status, description) { if (typeof description === "string" && description.trim() !== "") { return description.trim(); } const normalizedStatus = String(status || "") .trim() .toUpperCase(); if (normalizedStatus && qdnStatusTitles[normalizedStatus]) { return qdnStatusTitles[normalizedStatus]; } return modeCopy.loadingFromQdn; } function isTerminalQdnStatus(status) { const normalizedStatus = String(status || "") .trim() .toUpperCase(); return ( normalizedStatus === "NOT_PUBLISHED" || normalizedStatus === "UNSUPPORTED" || normalizedStatus === "BLOCKED" ); } function updateQdnLoadingProgress( status, percentLoaded, localChunkCount, totalChunkCount, description ) { const normalizedStatus = String(status || "").toUpperCase(); if (normalizedStatus === "READY") { clearQdnLoadingProgress(); return; } const localCount = coerceFiniteNumber(localChunkCount); const totalCount = coerceFiniteNumber(totalChunkCount); // Some QDN status responses expose per-chunk percent (0..100) without aggregate totals, // which causes visible 0/100 flicker while multiple chunks download. In that case, // prefer an overall "loading/chunks" indicator instead of a percent bar. const shouldUsePercent = normalizedStatus === "READY" || (totalCount !== null && totalCount > 0); const boundedPercent = shouldUsePercent ? coerceFiniteNumber(percentLoaded) : null; const bounded = boundedPercent !== null ? Math.max(0, Math.min(100, boundedPercent)) : null; if (!viewerLoadingProgress) { return; } let toolbarLabel = modeCopy.loadingLabel; if (bounded !== null) { toolbarLabel = "Loading " + bounded.toFixed(1) + "%"; } if (localCount !== null && String(toolbarLabel).indexOf("%") === -1) { toolbarLabel += " (" + localCount + " chunks)"; } viewerLoadingProgress.textContent = toolbarLabel; viewerLoadingProgress.classList.remove("qortal-hidden"); if (viewerLoadingOverlay) { viewerLoadingOverlay.classList.remove("qortal-hidden"); } if (viewerLoadingStatus) { const statusLabel = getQdnStatusLabel(normalizedStatus, description); viewerLoadingStatus.textContent = bounded !== null ? statusLabel + " " + bounded.toFixed(1) + "%" : statusLabel; } if (viewerLoadingProgressBar) { viewerLoadingProgressBar.style.width = (bounded !== null ? bounded : 0) + "%"; } if (viewerLoadingMeta) { const local = localCount; const total = totalCount; if (local !== null && total !== null && total > 0) { viewerLoadingMeta.textContent = "Chunks " + local + " / " + total; } else if (local !== null) { viewerLoadingMeta.textContent = local + " chunks downloaded"; } else { viewerLoadingMeta.textContent = ""; } } } function clearQdnLoadingRetry(reason) { if (qdnLoadingRetryTimer) { window.clearTimeout(qdnLoadingRetryTimer); qdnLoadingRetryTimer = null; logDebug("info", "QDN loading auto-retry stopped", { reason: reason || "cleared", attempts: qdnLoadingRetryAttempts, }); } qdnLoadingStatusFailures = 0; qdnLoadingLastSnapshot = ""; clearQdnLoadingProgress(); } function safeDecodeURIComponent(value) { if (typeof value !== "string") { return ""; } try { return decodeURIComponent(value); } catch (_error) { return value; } } function stripQueryAndHash(value) { return String(value || "") .split("#")[0] .split("?")[0]; } function parseQdnResourceTarget(address) { const normalized = normalizeBrowserAddress(address || ""); if (!normalized || !normalized.toLowerCase().startsWith("qortal://")) { return null; } const rawPath = stripQueryAndHash( normalized.slice("qortal://".length) ).replace(/^\/+/, ""); if (!rawPath) { return null; } const segments = rawPath .split("/") .filter(function (segment) { return Boolean(segment); }) .map(safeDecodeURIComponent); if (segments.length < 2) { return null; } const service = String(segments[0] || "") .trim() .toUpperCase(); const name = String(segments[1] || "").trim(); if (!service || !name) { return null; } const identifierCandidate = segments.length > 2 ? String(segments[2] || "").trim() : ""; return { service, name, identifierCandidate, }; } function buildQdnStatusUrls(target) { if (!target || !target.service || !target.name) { return []; } const encodeSegmentsLocally = !gatewayProxyTemplate; const paths = []; const urls = []; const pushPath = function (segments) { const encoded = segments .filter(function (segment) { return typeof segment === "string" && segment.trim() !== ""; }) .map(function (segment) { const value = segment.trim(); return encodeSegmentsLocally ? encodeURIComponent(value) : value; }) .join("/"); if (!encoded || paths.includes(encoded)) { return; } paths.push(encoded); }; const pushUrl = function (baseUrl, withBuildParam) { if (!baseUrl) { return; } const built = withBuildParam ? baseUrl + (baseUrl.indexOf("?") === -1 ? "?" : "&") + "build=true" : baseUrl; if (!urls.includes(built)) { urls.push(built); } }; const base = [ "arbitrary", "resource", "status", target.service, target.name, ]; if (target.identifierCandidate) { pushPath(base.concat([target.identifierCandidate])); } pushPath(base); paths.forEach(function (path) { const baseUrl = openGatewayPath(path); // Prefer build=true (progress/status from loading flow), but fall back to plain status. pushUrl(baseUrl, true); pushUrl(baseUrl, false); }); return urls.filter(function (url) { return Boolean(url); }); } function isLikelyStatus404(result) { if (!result) { return false; } if (result.status !== 404) { return false; } const body = result.json; if (!body || typeof body !== "object") { return true; } const message = (typeof body.message === "string" ? body.message : "") || (typeof body.error === "string" ? body.error : ""); return message.toLowerCase().includes("not found") || message === ""; } async function probeQdnResourceProperties(target) { if (!target || !target.service || !target.name) { return { ok: false }; } const identifier = target.identifierCandidate || "default"; const path = `arbitrary/resource/properties/${target.service}/${target.name}/${identifier}`; const baseUrl = openGatewayPath(path); if (!baseUrl) { return { ok: false }; } try { const result = await fetchJsonWithTimeout( baseUrl + (baseUrl.indexOf("?") === -1 ? "?" : "&") + "build=true", 4500 ); if (!result.ok) { return { ok: false }; } // properties endpoint is mostly used to trigger/build; return generic downloading state. return { ok: true, status: "BUILDING", description: "Initializing and building content...", percentLoaded: null, localChunkCount: null, totalChunkCount: null, url: baseUrl, }; } catch (_error) { return { ok: false }; } } async function probeQdnResourceStatus(target) { const urls = buildQdnStatusUrls(target); let sawStatus404 = false; let terminalMissingResult = null; for (let i = 0; i < urls.length; i += 1) { const url = urls[i]; try { const result = await fetchJsonWithTimeout(url, 4500); if (!result.ok || !result.json) { if (isLikelyStatus404(result)) { sawStatus404 = true; } continue; } const parsed = parseQdnStatusPayload(result.json); if (!parsed) { continue; } const normalizedStatus = String(parsed.status || "").toUpperCase(); if ( normalizedStatus === "NOT_PUBLISHED" || normalizedStatus === "UNSUPPORTED" || normalizedStatus === "BLOCKED" ) { // Deep links inside a Q-App can look like an "identifier" on first probe candidate. // Keep trying alternate status URLs (especially without identifier) before concluding missing. if (!terminalMissingResult) { terminalMissingResult = { ok: true, status: normalizedStatus, description: parsed.description || "", percentLoaded: coerceFiniteNumber(parsed.percentLoaded), localChunkCount: coerceFiniteNumber(parsed.localChunkCount), totalChunkCount: coerceFiniteNumber(parsed.totalChunkCount), url, }; } continue; } return { ok: true, status: normalizedStatus, description: parsed.description || "", percentLoaded: coerceFiniteNumber(parsed.percentLoaded), localChunkCount: coerceFiniteNumber(parsed.localChunkCount), totalChunkCount: coerceFiniteNumber(parsed.totalChunkCount), url, }; } catch (_error) { // Try the next candidate URL. } } if (terminalMissingResult) { return terminalMissingResult; } if (sawStatus404) { logDebug( "warn", "QDN status endpoint returned 404 variants; falling back to properties probe", { service: target.service, name: target.name, identifier: target.identifierCandidate || "", } ); const propertiesResult = await probeQdnResourceProperties(target); if (propertiesResult.ok) { return propertiesResult; } } return { ok: false }; } function parseQdnStatusCandidate(candidate) { if (!candidate || typeof candidate !== "object") { return null; } const nestedStatus = candidate.status && typeof candidate.status === "object" ? candidate.status : null; let statusCode = ""; const rawCandidates = [ candidate.id, candidate.status, nestedStatus ? nestedStatus.id : null, nestedStatus ? nestedStatus.status : null, nestedStatus ? nestedStatus.name : null, ]; for (let i = 0; i < rawCandidates.length; i += 1) { const value = rawCandidates[i]; if (typeof value === "string" && value.trim() !== "") { statusCode = value.trim().toUpperCase(); break; } } if (!statusCode) { const numericValue = coerceFiniteNumber(candidate.status) || (nestedStatus ? coerceFiniteNumber(nestedStatus.value) : null); if ( numericValue !== null && qdnStatusNumericMap[Math.floor(numericValue)] ) { statusCode = qdnStatusNumericMap[Math.floor(numericValue)]; } } if (!statusCode) { return null; } let localChunkCount = coerceFiniteNumber(candidate.localChunkCount); if (localChunkCount === null && nestedStatus) { localChunkCount = coerceFiniteNumber(nestedStatus.localChunkCount); } let totalChunkCount = coerceFiniteNumber(candidate.totalChunkCount); if (totalChunkCount === null && nestedStatus) { totalChunkCount = coerceFiniteNumber(nestedStatus.totalChunkCount); } let percentLoaded = coerceFiniteNumber(candidate.percentLoaded); if (percentLoaded === null && nestedStatus) { percentLoaded = coerceFiniteNumber(nestedStatus.percentLoaded); } if ( percentLoaded === null && localChunkCount !== null && totalChunkCount !== null && totalChunkCount > 0 ) { percentLoaded = (localChunkCount / totalChunkCount) * 100; } return { status: statusCode, description: (typeof candidate.description === "string" && candidate.description.trim() !== "" ? candidate.description.trim() : "") || (nestedStatus && typeof nestedStatus.description === "string" ? nestedStatus.description.trim() : "") || (typeof candidate.title === "string" ? candidate.title.trim() : ""), percentLoaded, localChunkCount, totalChunkCount, }; } function parseQdnStatusPayload(payload) { const visited = new Set(); const queue = [payload]; let depth = 0; while (queue.length > 0 && depth < 16) { const candidate = queue.shift(); depth += 1; if (!candidate || typeof candidate !== "object") { continue; } if (visited.has(candidate)) { continue; } visited.add(candidate); const parsed = parseQdnStatusCandidate(candidate); if (parsed) { return parsed; } if (candidate.data && typeof candidate.data === "object") { queue.push(candidate.data); } if (candidate.result && typeof candidate.result === "object") { queue.push(candidate.result); } if (candidate.status && typeof candidate.status === "object") { queue.push(candidate.status); } } return null; } async function fetchJsonWithTimeout(url, timeoutMs) { const canAbort = typeof window.AbortController === "function"; const controller = canAbort ? new window.AbortController() : null; let timeoutId = null; if (controller && timeoutMs > 0) { timeoutId = window.setTimeout(function () { controller.abort(); }, timeoutMs); } try { const response = await fetch(url, { method: "GET", signal: controller ? controller.signal : undefined, }); const rawText = await response.text(); let json = null; if (rawText) { try { json = JSON.parse(rawText); } catch (_error) { json = null; } } return { ok: response.ok, status: response.status, json, }; } finally { if (timeoutId) { window.clearTimeout(timeoutId); } } } function detectQdnLoadingInterstitial() { if (!viewerFrame) { return { inspectable: false, detected: false }; } let doc = null; try { doc = viewerFrame.contentDocument; } catch (_error) { return { inspectable: false, detected: false }; } if (!doc || !doc.body) { return { inspectable: true, detected: false }; } const bodyText = ( doc.body.innerText || doc.body.textContent || "" ).toLowerCase(); const titleText = String(doc.title || "").toLowerCase(); const combinedText = bodyText + "\n" + titleText; const detected = qdnLoadingMarkers.some(function (marker) { return combinedText.includes(marker); }); return { inspectable: true, detected }; } function reloadViewerFrame() { if (!viewerFrame) { return false; } try { if (viewerFrame.contentWindow) { viewerFrame.contentWindow.location.reload(); return true; } } catch (_error) { // fallback below } try { if (viewerFrame.src) { viewerFrame.src = viewerFrame.src; return true; } } catch (_error) { return false; } return false; } async function runQdnLoadingRetryTick() { if (qdnLoadingRetryAttempts >= qdnLoadingRetryMaxAttempts) { logDebug("warn", "QDN loading auto-retry max attempts reached", { attempts: qdnLoadingRetryAttempts, }); return; } if (!viewerCard || viewerCard.classList.contains("qortal-hidden")) { clearQdnLoadingRetry("viewer_hidden"); return; } const detection = detectQdnLoadingInterstitial(); if (!detection.inspectable) { clearQdnLoadingRetry("cross_origin_or_uninspectable"); return; } let prefetchedStatusResult = null; if (!detection.detected && currentQdnResourceTarget) { prefetchedStatusResult = await probeQdnResourceStatus( currentQdnResourceTarget ); if (prefetchedStatusResult.ok) { if (isTerminalQdnStatus(prefetchedStatusResult.status)) { clearQdnLoadingRetry("content_loaded_terminal_status"); qdnLoadingRetryAttempts = 0; return; } updateQdnLoadingProgress( prefetchedStatusResult.status, prefetchedStatusResult.percentLoaded, prefetchedStatusResult.localChunkCount, prefetchedStatusResult.totalChunkCount, prefetchedStatusResult.description ); if (prefetchedStatusResult.status === "READY") { clearQdnLoadingRetry("content_loaded"); qdnLoadingRetryAttempts = 0; return; } } else { clearQdnLoadingRetry("content_loaded"); qdnLoadingRetryAttempts = 0; return; } } if (!detection.detected && !prefetchedStatusResult) { clearQdnLoadingRetry("content_loaded"); qdnLoadingRetryAttempts = 0; return; } updateQdnLoadingProgress("DOWNLOADING", null, null, null, ""); qdnLoadingRetryAttempts += 1; let shouldReload = true; let delayMs = qdnLoadingRetryDelayMs; if (currentQdnResourceTarget) { const statusResult = prefetchedStatusResult || (await probeQdnResourceStatus(currentQdnResourceTarget)); if (statusResult.ok) { qdnLoadingStatusFailures = 0; const statusSnapshot = statusResult.status + ":" + (statusResult.percentLoaded !== null ? statusResult.percentLoaded : "") + ":" + (statusResult.localChunkCount !== null ? statusResult.localChunkCount : ""); if ( statusSnapshot !== qdnLoadingLastSnapshot || qdnLoadingRetryAttempts % 8 === 0 ) { logDebug("info", "QDN loading status", { attempt: qdnLoadingRetryAttempts, status: statusResult.status, percentLoaded: statusResult.percentLoaded, localChunkCount: statusResult.localChunkCount, totalChunkCount: statusResult.totalChunkCount, }); qdnLoadingLastSnapshot = statusSnapshot; } updateQdnLoadingProgress( statusResult.status, statusResult.percentLoaded, statusResult.localChunkCount, statusResult.totalChunkCount, statusResult.description ); if (statusResult.status === "READY") { if (detection.detected) { logDebug( "info", "QDN resource ready while loading interstitial is visible, refreshing viewer", { attempt: qdnLoadingRetryAttempts, } ); reloadViewerFrame(); } else { clearQdnLoadingRetry("content_loaded_ready"); qdnLoadingRetryAttempts = 0; } return; } shouldReload = false; } else { qdnLoadingStatusFailures += 1; delayMs = qdnLoadingReloadFallbackDelayMs; if (qdnLoadingStatusFailures <= 3) { shouldReload = false; logDebug( "warn", "QDN status probe failed, waiting before fallback reload", { attempt: qdnLoadingRetryAttempts, failures: qdnLoadingStatusFailures, } ); } } } if (shouldReload) { logDebug("info", "QDN loading detected, retrying load", { attempt: qdnLoadingRetryAttempts, delayMs, mode: currentQdnResourceTarget ? "fallback_reload" : "reload", }); reloadViewerFrame(); } scheduleQdnLoadingRetry(delayMs); } function scheduleQdnLoadingRetry(delayOverrideMs) { if (qdnLoadingRetryTimer) { return; } if (qdnLoadingRetryAttempts >= qdnLoadingRetryMaxAttempts) { logDebug("warn", "QDN loading auto-retry max attempts reached", { attempts: qdnLoadingRetryAttempts, }); return; } const nextDelay = typeof delayOverrideMs === "number" && Number.isFinite(delayOverrideMs) ? Math.max(1000, Math.floor(delayOverrideMs)) : qdnLoadingRetryDelayMs; qdnLoadingRetryTimer = window.setTimeout(function () { qdnLoadingRetryTimer = null; runQdnLoadingRetryTick().catch(function (error) { logDebug("warn", "QDN loading retry tick failed", { error: error && error.message ? error.message : String(error), }); scheduleQdnLoadingRetry(qdnLoadingReloadFallbackDelayMs); }); }, nextDelay); } function fallbackApprovalScope(requestType) { return approvalScopeAliases[requestType] || requestType || ""; } function normalizeApprovalScope(scope, requestType) { const candidate = typeof scope === "string" ? scope.trim() : ""; if (!candidate) { return fallbackApprovalScope(requestType); } if ( requestType && approvalScopeAliases[requestType] && candidate === requestType ) { return approvalScopeAliases[requestType]; } return candidate; } const spendingRequestTypes = new Set([ "SIGN_TRANSACTION", "REGISTER_NAME", "UPDATE_NAME", "SELL_NAME", "CANCEL_SELL_NAME", "BUY_NAME", "SEND_COIN", "SEND_COIN_MULTIPLE", "DEPLOY_AT", "CREATE_POLL", "VOTE_ON_POLL", "CREATE_TRADE_BUY_ORDER", "CANCEL_TRADE_BUY_ORDER", "CREATE_ASSET_ORDER", "CANCEL_ASSET_ORDER", "TRANSFER_ASSET", "PUBLISH_QDN_RESOURCE", "PUBLISH_MULTIPLE_QDN_RESOURCES", "ADD_LIST_ITEMS", "DELETE_LIST_ITEM", ]); function isLikelySpendingRequestType(requestType) { const normalized = String(requestType || "") .trim() .toUpperCase(); if (!normalized) { return false; } if (spendingRequestTypes.has(normalized)) { return true; } if (normalized.startsWith("PUBLISH_") || normalized.startsWith("SEND_")) { return true; } if (normalized.includes("TRADE") || normalized.includes("BUY_ORDER")) { return true; } return false; } function isHighRiskConfirmationRequired(error, json) { if (error === "high_risk_confirmation_required") { return true; } return Boolean( json && typeof json.error === "string" && json.error === "high_risk_confirmation_required" ); } function highRiskConfirmationMessage(requestType, details) { const info = details && typeof details === "object" ? details : {}; const lines = [ "NuQloud blocked this request for an extra safety check.", "", "Action: " + String(info.requestType || requestType || "Q-App request"), ]; if (info.reason) { lines.push("Reason: " + String(info.reason)); } if (info.amount) { lines.push("Amount: " + String(info.amount)); } if (info.recipient) { lines.push("Recipient: " + String(info.recipient)); } if (info.txCount) { lines.push("Estimated transactions: " + String(info.txCount)); } if (info.feeCredits) { lines.push( "Estimated publish fees: " + String(info.feeCredits) + " credits" + (info.feeQort ? " (" + String(info.feeQort) + " QORT)" : "") ); } lines.push("", "Continue only if you expected this request."); return lines.join("\n"); } async function postQappsRequestWithHighRiskConfirmation( requestType, payload, headers ) { let workingPayload = Object.assign({}, payload || {}); const postOnce = async function () { const response = await fetchWithRetry( qappsRequestUrl, { method: "POST", headers, body: JSON.stringify({ requestType, payload: workingPayload, }), }, 2, 250 ); const json = await response.json(); return { response, json }; }; let outcome = await postOnce(); if ( !outcome.response.ok || outcome.json.ok === false || outcome.json.error ) { const errorMessage = outcome.json.error || "qortal_request_failed"; if (isHighRiskConfirmationRequired(errorMessage, outcome.json)) { const details = outcome.json && outcome.json.details ? outcome.json.details : {}; const token = details && typeof details.confirmationToken === "string" ? details.confirmationToken : ""; if (!token) { return { response: outcome.response, json: { error: "high_risk_confirmation_unavailable", details }, payload: workingPayload, }; } const accepted = window.confirm( highRiskConfirmationMessage(requestType, details) ); if (!accepted) { return { response: outcome.response, json: { error: "high_risk_confirmation_cancelled", details }, payload: workingPayload, }; } workingPayload = Object.assign({}, workingPayload, { highRiskConfirmationToken: token, }); outcome = await postOnce(); } } return { response: outcome.response, json: outcome.json, payload: workingPayload, }; } function extractSignedTransactionPayload(value, depth) { const currentDepth = typeof depth === "number" ? depth : 0; if (currentDepth > 6 || value === null || value === undefined) { return ""; } if (typeof value === "string") { const trimmed = value.trim(); if (!trimmed) { return ""; } if ( (trimmed[0] === "{" || trimmed[0] === "[") && trimmed.length <= 500000 ) { try { const parsed = JSON.parse(trimmed); const parsedValue = extractSignedTransactionPayload( parsed, currentDepth + 1 ); if (parsedValue) { return parsedValue; } } catch (_error) { // Ignore JSON parse failure and treat as plain payload. } } return trimmed; } if (Array.isArray(value)) { for (let i = 0; i < value.length; i += 1) { const candidate = extractSignedTransactionPayload( value[i], currentDepth + 1 ); if (candidate) { return candidate; } } return ""; } if (typeof value !== "object") { return ""; } const candidateKeys = [ "signedTransaction", "signedTransactionBytes", "transactionBytes", "signedBytes", "bytes", "data", "result", "payload", "transaction", ]; for (let i = 0; i < candidateKeys.length; i += 1) { const key = candidateKeys[i]; if (!Object.prototype.hasOwnProperty.call(value, key)) { continue; } const candidate = extractSignedTransactionPayload( value[key], currentDepth + 1 ); if (candidate) { return candidate; } } for (const key in value) { if (!Object.prototype.hasOwnProperty.call(value, key)) { continue; } const nested = extractSignedTransactionPayload( value[key], currentDepth + 1 ); if (nested) { return nested; } } return ""; } function normalizeQappsResultPayload(requestType, resultPayload) { if ( requestType === "GET_PRIMARY_NAME" && typeof resultPayload !== "string" ) { if (Array.isArray(resultPayload)) { return resultPayload.length > 0 ? resultPayload[0] : ""; } if (resultPayload && typeof resultPayload === "object") { return ( resultPayload.name || resultPayload.primaryName || (resultPayload.data && resultPayload.data.name) || "" ); } if (resultPayload === null || resultPayload === undefined) { return ""; } } if (requestType === "SIGN_TRANSACTION") { const signedPayload = extractSignedTransactionPayload(resultPayload, 0); if (signedPayload) { return signedPayload; } } return resultPayload; } function sanitizeDebugValue(value) { if (!value || typeof value !== "object") { return value; } if (Array.isArray(value)) { return value.map(sanitizeDebugValue); } const clone = {}; for (const key in value) { if (!Object.prototype.hasOwnProperty.call(value, key)) { continue; } const lower = key.toLowerCase(); if ( lower.includes("seed") || lower.includes("password") || lower.includes("secret") || lower.includes("backup") ) { clone[key] = "[redacted]"; continue; } clone[key] = sanitizeDebugValue(value[key]); } return clone; } function logDebug(level, message, details) { if (!effectiveDebugEnabled || !debugBody) { return; } const timestamp = new Date().toISOString(); const entry = { time: timestamp, level, message, details: sanitizeDebugValue(details), }; debugEntries.push(entry); if (debugEntries.length > debugLimit) { debugEntries.shift(); } const lines = debugEntries.map(function (item) { const suffix = item.details !== undefined ? " " + JSON.stringify(item.details) : ""; const level = item.level.toUpperCase(); return "[" + item.time + "] " + level + ": " + item.message + suffix; }); debugBody.textContent = lines.join("\n"); debugBody.scrollTop = debugBody.scrollHeight; } function getViewportHeight() { return ( (window.visualViewport && window.visualViewport.height) || window.innerHeight || document.documentElement.clientHeight || 0 ); } function clampDebugPanelHeight(value) { const numeric = Math.round(Number(value)); if (!Number.isFinite(numeric)) { return debugPanelDefaultHeight; } return Math.max(debugPanelMinHeight, numeric); } function readStoredDebugPanelHeight() { if (!window.localStorage) { return debugPanelExpandedHeight; } const parsed = Number( window.localStorage.getItem(debugPanelHeightStorageKey) ); return clampDebugPanelHeight(parsed); } function applyDebugPanelSizing() { if (!debugCard) { return; } const compactHeight = Math.max( 120, Math.round(debugPanelExpandedHeight * debugPanelCompactRatio) ); debugCard.style.setProperty( "--qortal-debug-panel-height", debugPanelExpandedHeight + "px" ); debugCard.style.setProperty( "--qortal-debug-panel-compact-height", compactHeight + "px" ); debugCard.style.setProperty( "--qortal-debug-panel-peek-height", debugPanelPeekHeight + "px" ); } function setDebugPanelExpandedHeight(nextHeight, persist) { debugPanelExpandedHeight = clampDebugPanelHeight(nextHeight); applyDebugPanelSizing(); if (persist && window.localStorage) { window.localStorage.setItem( debugPanelHeightStorageKey, String(debugPanelExpandedHeight) ); } } function setDebugDockHidden(hidden, persist) { if (!debugCard) { return; } debugCard.classList.toggle("qortal-debug-docked-hidden", Boolean(hidden)); if (!hidden) { releaseDebugDockHoverSuppression(); } if (persist && window.localStorage) { window.localStorage.setItem(debugPanelDockStorageKey, hidden ? "1" : "0"); } updateDebugPanelControls(); } function suppressDebugDockHover() { if (!debugCard) { return; } debugDockHoverSuppressed = true; debugCard.classList.add("qortal-debug-dock-suppressed"); } function releaseDebugDockHoverSuppression() { if (!debugCard) { return; } debugDockHoverSuppressed = false; debugCard.classList.remove("qortal-debug-dock-suppressed"); } function syncDebugPanelState() { if (!debugCard || !effectiveDebugEnabled) { return; } setDebugPanelExpandedHeight(readStoredDebugPanelHeight(), false); setDebugDockHidden( window.localStorage && window.localStorage.getItem(debugPanelDockStorageKey) === "1", false ); updateDebugPanelControls(); } function updateDebugPanelControls() { if (!debugCard) { return; } if (debugMinimize) { const isMinimized = debugCard.classList.contains( "qortal-debug-minimized" ); const minimizeLabel = isMinimized ? "Expand debug panel" : "Minimize debug panel"; debugMinimize.textContent = "−"; debugMinimize.setAttribute("title", minimizeLabel); debugMinimize.setAttribute("aria-label", minimizeLabel); debugMinimize.setAttribute("aria-pressed", isMinimized ? "true" : "false"); } if (debugFloat) { const isDockHidden = debugCard.classList.contains( "qortal-debug-docked-hidden" ); const dockLabel = isDockHidden ? "Keep debug panel open" : "Auto-hide debug panel"; debugFloat.textContent = isDockHidden ? "⇧" : "⤓"; debugFloat.setAttribute("title", dockLabel); debugFloat.setAttribute("aria-label", dockLabel); debugFloat.setAttribute("aria-pressed", isDockHidden ? "true" : "false"); } } if (debugFromQuery && window.localStorage) { window.localStorage.setItem("qortal_qapps_debug", "1"); } function detectTheme() { const docEl = document.documentElement; const body = document.body; const dataTheme = (docEl && docEl.dataset ? docEl.dataset.theme : "") || (body && body.dataset ? body.dataset.theme : ""); if (dataTheme) { return dataTheme; } const classList = docEl && docEl.classList ? docEl.classList : body && body.classList ? body.classList : null; if (classList) { if ( classList.contains("theme-dark") || classList.contains("dark") || classList.contains("theme--dark") || classList.contains("nc-dark") ) { return "dark"; } if ( classList.contains("theme-light") || classList.contains("light") || classList.contains("theme--light") || classList.contains("nc-light") ) { return "light"; } } if ( window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ) { return "dark"; } return "light"; } function detectLanguage() { return ( (document.documentElement && document.documentElement.lang) || navigator.language || "" ); } function appendThemeParams(url) { if (!url) { return url; } try { const u = new URL(url, window.location.origin); if (!u.searchParams.has("theme")) { u.searchParams.set("theme", detectTheme()); } const lang = detectLanguage(); if (lang && !u.searchParams.has("lang")) { u.searchParams.set("lang", lang); } return u.toString(); } catch (error) { return url; } } function encodeGatewayPath(path) { return path .split("/") .map(function (segment) { return encodeURIComponent(segment); }) .join("/"); } function buildProxyUrl(path) { if (!gatewayProxyTemplate) { return ""; } if (gatewayProxyTemplate.includes("__PATH__")) { return gatewayProxyTemplate.replace("__PATH__", encodeGatewayPath(path)); } return gatewayProxyTemplate + encodeURIComponent(path); } function isUpstreamRootPath(path) { const normalized = String(path || "").replace(/^\/+/, ""); return /^(render|arbitrary|names|addresses|transactions|blocks|crosschain|groups|chat|lists|assets|at|admin|peers|utils|apps|payments|polls|stats|bootstrap|developer|websockets)(\/|$)/i.test( normalized ); } function toUpstreamProxyPath(path, preferRender) { const normalized = String(path || "").replace(/^\/+/, ""); if (!normalized) { return ""; } if (preferRender && nodeUrl && !isUpstreamRootPath(normalized)) { return "render/" + normalized; } return normalized; } function resolveGatewayAddress(address, ensureTrailingSlash) { if (!address) { return ""; } let path = address.trim(); let fromQortalScheme = false; let suffix = ""; if (/^https?:\/\//i.test(path)) { return appendThemeParams(path); } if (path.toLowerCase().startsWith("qortal://")) { fromQortalScheme = true; path = path.slice("qortal://".length); } if (fromQortalScheme) { const queryIndex = path.indexOf("?"); const hashIndex = path.indexOf("#"); let splitIndex = -1; if (queryIndex >= 0 && hashIndex >= 0) { splitIndex = Math.min(queryIndex, hashIndex); } else if (queryIndex >= 0) { splitIndex = queryIndex; } else if (hashIndex >= 0) { splitIndex = hashIndex; } if (splitIndex >= 0) { suffix = path.slice(splitIndex); path = path.slice(0, splitIndex); } } path = path.replace(/^\/+/, ""); if (ensureTrailingSlash && path && !path.endsWith("/")) { let shouldAppendSlash = true; if (fromQortalScheme) { const basePath = stripQueryAndHash(path); const segments = basePath.split("/").filter(function (segment) { return Boolean(segment); }); // Preserve SPA/app sub-routes like qortal://APP/Q-Assets/assets (no forced trailing slash). // Only force a slash for root-style QDN addresses (service + name only). if (segments.length > 2) { shouldAppendSlash = false; } } if (shouldAppendSlash) { path += "/"; } } const upstreamPath = toUpstreamProxyPath(path, true); if (gatewayProxyTemplate) { if (!viewerRendererConfigured) { return ""; } return appendThemeParams(buildProxyUrl(upstreamPath) + suffix); } const directBase = nodeUrl ? nodeUrl.replace(/\/$/, "") : gatewayUrl.replace(/\/$/, ""); if (!directBase) { return ""; } const base = directBase.replace(/\/$/, ""); return appendThemeParams(base + "/" + upstreamPath + suffix); } function normalizeBrowserAddress(address) { const raw = String(address || "").trim(); if (!raw) { return ""; } if (!raw.toLowerCase().startsWith("qortal://")) { return raw; } const splitIndex = (function () { const queryIndex = raw.indexOf("?"); const hashIndex = raw.indexOf("#"); if (queryIndex === -1) { return hashIndex; } if (hashIndex === -1) { return queryIndex; } return Math.min(queryIndex, hashIndex); })(); const base = splitIndex >= 0 ? raw.slice(0, splitIndex) : raw; const suffix = splitIndex >= 0 ? raw.slice(splitIndex) : ""; const afterScheme = base.slice("qortal://".length).replace(/^\/+/, ""); if (!afterScheme) { return raw; } // Browser convenience: qortal://Name -> qortal://WEBSITE/Name if (!afterScheme.includes("/")) { return "qortal://WEBSITE/" + afterScheme; } const segments = afterScheme .split("/") .filter(function (segment) { return Boolean(segment); }) .map(safeDecodeURIComponent); let changed = true; while (changed) { changed = false; if ( segments.length >= 5 && String(segments[2] || "").toLowerCase() === "apps" && String(segments[3] || "").toLowerCase() === "qortal_integration" && String(segments[4] || "").toLowerCase() === "gateway" ) { segments.splice(2, 3); changed = true; continue; } if ( segments.length >= 5 && String(segments[2] || "").toLowerCase() === "render" && String(segments[3] || "") === String(segments[0] || "") && String(segments[4] || "") === String(segments[1] || "") ) { segments.splice(2, 3); changed = true; continue; } } return "qortal://" + segments.join("/") + suffix; } function detectCloudTheme() { const docEl = document.documentElement; const body = document.body; const dataTheme = (docEl && docEl.dataset ? docEl.dataset.theme : "") || (body && body.dataset ? body.dataset.theme : ""); if (dataTheme) { return String(dataTheme).toLowerCase().includes("dark") ? "dark" : "light"; } const classList = docEl && docEl.classList ? docEl.classList : body && body.classList ? body.classList : null; if (classList) { if ( classList.contains("theme-dark") || classList.contains("dark") || classList.contains("theme--dark") || classList.contains("nc-dark") ) { return "dark"; } if ( classList.contains("theme-light") || classList.contains("light") || classList.contains("theme--light") || classList.contains("nc-light") ) { return "light"; } } if ( window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ) { return "dark"; } return "light"; } function detectCloudLanguage() { return ( (document.documentElement && document.documentElement.lang) || navigator.language || "" ); } function applyEmbedThemeParams(address) { const normalized = normalizeBrowserAddress(address || ""); if (!normalized.toLowerCase().startsWith("qortal://")) { return normalized; } const hashParts = normalized.split("#"); const withoutHash = hashParts[0] || ""; const hash = hashParts.length > 1 ? "#" + hashParts.slice(1).join("#") : ""; const queryIndex = withoutHash.indexOf("?"); const base = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash; const query = queryIndex >= 0 ? withoutHash.slice(queryIndex + 1) : ""; const params = new URLSearchParams(query); params.set("theme", detectCloudTheme()); const lang = detectCloudLanguage(); if (lang) { params.set("lang", lang); } const nextQuery = params.toString(); return base + (nextQuery ? "?" + nextQuery : "") + hash; } function extractQappTargetKey(address) { const normalized = normalizeBrowserAddress(address || ""); if (!normalized.toLowerCase().startsWith("qortal://")) { return ""; } const withoutScheme = normalized.slice("qortal://".length); const withoutQuery = withoutScheme.split("#")[0].split("?")[0]; const segments = withoutQuery.split("/").filter(function (segment) { return Boolean(segment); }); if (segments.length < 2) { return ""; } return ( String(segments[0]).toUpperCase() + "/" + String(segments[1]).toUpperCase() ); } function isEmbedLockedToDifferentQapp(address) { if (!embedLockQapp || !embedRequestedQapp) { return false; } const lockedKey = extractQappTargetKey(embedRequestedQapp); const candidateKey = extractQappTargetKey(address); if (!lockedKey || !candidateKey) { return false; } return lockedKey !== candidateKey; } function splitPathAndSuffix(value) { const raw = String(value || "").trim(); if (!raw) { return { path: "", suffix: "" }; } const queryIndex = raw.indexOf("?"); const hashIndex = raw.indexOf("#"); let splitIndex = -1; if (queryIndex >= 0 && hashIndex >= 0) { splitIndex = Math.min(queryIndex, hashIndex); } else if (queryIndex >= 0) { splitIndex = queryIndex; } else if (hashIndex >= 0) { splitIndex = hashIndex; } if (splitIndex < 0) { return { path: raw, suffix: "" }; } return { path: raw.slice(0, splitIndex), suffix: raw.slice(splitIndex), }; } function parseCurrentQappBaseAddress() { const source = normalizeBrowserAddress( currentViewerAddress || currentQappAddress || "" ); if (!source || !source.toLowerCase().startsWith("qortal://")) { return null; } const rawPath = stripQueryAndHash(source.slice("qortal://".length)).replace( /^\/+/, "" ); if (!rawPath) { return null; } const segments = rawPath .split("/") .filter(function (segment) { return Boolean(segment); }) .map(safeDecodeURIComponent); if (segments.length < 2) { return null; } return { service: String(segments[0] || "") .trim() .toUpperCase(), name: String(segments[1] || "").trim(), }; } function resolveRelativeAddressForCurrentQapp(value) { const split = splitPathAndSuffix(value); let path = String(split.path || "").trim(); if ( !path || /^https?:\/\//i.test(path) || path.toLowerCase().startsWith("qortal://") ) { return ""; } path = path.replace(/^\/+/, ""); if (!path) { return ""; } const gatewayPrefix = "apps/qortal_integration/gateway/"; const lowerPath = path.toLowerCase(); if (lowerPath.startsWith(gatewayPrefix)) { path = path.slice(gatewayPrefix.length); } else if (lowerPath.startsWith("qortal_integration/gateway/")) { path = path.slice("qortal_integration/gateway/".length); } else { const embeddedGatewayIndex = lowerPath.indexOf("/" + gatewayPrefix); if (embeddedGatewayIndex >= 0) { path = path.slice(embeddedGatewayIndex + gatewayPrefix.length + 1); } } if (!path) { return ""; } if (path.toLowerCase().startsWith("render/")) { return normalizeBrowserAddress( "qortal://" + path.slice("render/".length) + split.suffix ); } const segments = path .split("/") .filter(function (segment) { return Boolean(segment); }) .map(safeDecodeURIComponent); if (segments.length < 1) { return ""; } if ( segments.length >= 2 && /^[A-Z0-9_]+$/.test(String(segments[0] || "")) ) { return normalizeBrowserAddress( "qortal://" + segments.join("/") + split.suffix ); } const base = parseCurrentQappBaseAddress(); if (!base || !base.service || !base.name) { return ""; } return normalizeBrowserAddress( "qortal://" + base.service + "/" + base.name + "/" + segments.join("/") + split.suffix ); } function deriveSafeReloadAddress(address) { const normalized = normalizeBrowserAddress(address || ""); if (!normalized || !normalized.toLowerCase().startsWith("qortal://")) { return normalized; } const rawPath = stripQueryAndHash( normalized.slice("qortal://".length) ).replace(/^\/+/, ""); if (!rawPath) { return normalized; } const segments = rawPath .split("/") .filter(function (segment) { return Boolean(segment); }) .map(safeDecodeURIComponent); if (segments.length < 2) { return normalized; } const service = String(segments[0] || "") .trim() .toUpperCase(); const name = String(segments[1] || "").trim(); if (!service || !name) { return normalized; } const suffix = splitPathAndSuffix(normalized).suffix || ""; if (service === "APP" || service === "WEBSITE") { return "qortal://" + service + "/" + name + suffix; } return "qortal://" + segments.join("/") + suffix; } function buildReloadSequence(address) { const normalized = normalizeBrowserAddress(address || ""); if (!normalized) { return null; } const split = splitPathAndSuffix(normalized); const fullAddress = normalizeBrowserAddress(split.path + (split.suffix || "")); const baseAddress = deriveSafeReloadAddress(split.path); const finalAddress = deriveSafeReloadAddress(fullAddress); const needsTwoStep = Boolean(split.suffix) && Boolean(baseAddress) && baseAddress !== finalAddress; return { fullAddress, baseAddress: baseAddress || finalAddress || fullAddress, finalAddress: finalAddress || fullAddress, needsTwoStep, }; } let pendingReloadFinalAddress = ""; let pendingReloadFallbackAddress = ""; let pendingReloadTimeout = null; let launchRetryAddress = ""; let launchRetryCount = 0; const launchRetryMax = 1; function clearPendingReloadSequence() { pendingReloadFinalAddress = ""; pendingReloadFallbackAddress = ""; if (pendingReloadTimeout) { window.clearTimeout(pendingReloadTimeout); pendingReloadTimeout = null; } } function startViewerReloadSequence(address) { const sequence = buildReloadSequence(address); if (!sequence) { return false; } clearPendingReloadSequence(); if (!sequence.needsTwoStep) { openAddress(sequence.finalAddress, modeCopy.browserName); return true; } pendingReloadFinalAddress = sequence.finalAddress; pendingReloadFallbackAddress = sequence.baseAddress; openAddress(sequence.baseAddress, modeCopy.browserName); pendingReloadTimeout = window.setTimeout(function () { if (!pendingReloadFinalAddress) { return; } const finalTarget = pendingReloadFinalAddress; clearPendingReloadSequence(); openAddress(finalTarget, modeCopy.browserName); }, 300); return true; } function isGatewayProxyViewerUrl(url) { const value = String(url || "").toLowerCase(); return ( value.indexOf("/apps/qortal_integration/gateway/") !== -1 || value.indexOf("/index.php/apps/qortal_integration/gateway/") !== -1 ); } function scheduleOneTimeLaunchRetry(reason) { const candidate = normalizeBrowserAddress( currentViewerAddress || currentQappAddress || "" ); if (!candidate) { return false; } if (launchRetryAddress !== candidate) { launchRetryAddress = candidate; launchRetryCount = 0; } if (launchRetryCount >= launchRetryMax) { return false; } launchRetryCount += 1; logDebug("warn", "Retrying initial Q-App load once", { reason: reason || "initial_load_failure", address: candidate, retryCount: launchRetryCount, }); window.setTimeout(function () { openAddress(candidate, modeCopy.browserName); }, 500); return true; } function setViewerAddressDisplay(address) { const value = String(address || "").trim(); currentViewerAddress = value; if (viewerAddress) { viewerAddress.textContent = value; } if (viewerAddressInput) { viewerAddressInput.value = value; } } function parseQortalAddressFromViewerUrl(url) { if (!url) { return ""; } try { const parsed = new URL(url, window.location.origin); const gatewayMarker = "/apps/qortal_integration/gateway/"; const markerIndex = parsed.pathname.indexOf(gatewayMarker); if (markerIndex < 0) { return ""; } let upstreamPath = parsed.pathname .slice(markerIndex + gatewayMarker.length) .replace(/^\/+/, ""); if (!upstreamPath) { return ""; } if (upstreamPath.toLowerCase().startsWith("render/")) { upstreamPath = upstreamPath.slice("render/".length); } else { const resolvedRelative = resolveRelativeAddressForCurrentQapp( "/" + upstreamPath + parsed.search + parsed.hash ); if (resolvedRelative) { return resolvedRelative; } } const decodedPath = upstreamPath .split("/") .map(function (segment) { return safeDecodeURIComponent(segment); }) .join("/"); return normalizeBrowserAddress( "qortal://" + decodedPath + parsed.search + parsed.hash ); } catch (_error) { return ""; } } function getNavigationAddressFromPayload(payload) { if (!payload || typeof payload !== "object") { return ""; } const candidates = [ payload.qortalLink, payload.address, payload.url, payload.href, payload.path, payload.value, ]; for (let i = 0; i < candidates.length; i += 1) { const candidate = candidates[i]; if (typeof candidate !== "string" || candidate.trim() === "") { continue; } const trimmed = candidate.trim(); if (trimmed.toLowerCase().startsWith("qortal://")) { return normalizeBrowserAddress(trimmed); } if (trimmed.startsWith("/")) { const parsedFromViewer = parseQortalAddressFromViewerUrl( window.location.origin + trimmed ); if (parsedFromViewer) { return parsedFromViewer; } const resolvedRelative = resolveRelativeAddressForCurrentQapp(trimmed); if (resolvedRelative) { return resolvedRelative; } return normalizeBrowserAddress( "qortal://" + trimmed.replace(/^\/+/, "") ); } if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) { const parsed = parseQortalAddressFromViewerUrl(trimmed); if (parsed) { return parsed; } } else { const resolvedRelative = resolveRelativeAddressForCurrentQapp(trimmed); if (resolvedRelative) { return resolvedRelative; } return normalizeBrowserAddress( "qortal://" + trimmed.replace(/^\/+/, "") ); } } return ""; } function syncViewerAddressFromFrame() { if (!viewerFrame || !viewerFrame.contentWindow) { return ""; } try { const href = viewerFrame.contentWindow.location.href; const parsed = parseQortalAddressFromViewerUrl(href); if (parsed) { setViewerAddressDisplay(parsed); return parsed; } } catch (_error) { // ignore cross-origin access failures } return ""; } function routeQortalProtocolTarget(rawTarget, label) { const normalizedTarget = normalizeBrowserAddress( String(rawTarget || "").trim() ); if ( !normalizedTarget || !normalizedTarget.toLowerCase().startsWith("qortal://") ) { return false; } openAddress(normalizedTarget, label || modeCopy.browserName); return true; } function installViewerNavigationInterceptors() { if (!viewerFrame || !viewerFrame.contentWindow) { return; } const frameWindow = viewerFrame.contentWindow; let frameDocument = null; try { frameDocument = frameWindow.document; } catch (_error) { return; } if (!frameDocument) { return; } if (!frameWindow.__qortalNativeOpenInterceptorInstalled) { const originalOpen = typeof frameWindow.open === "function" ? frameWindow.open.bind(frameWindow) : null; frameWindow.open = function (url, target, features) { if (routeQortalProtocolTarget(url, modeCopy.browserName)) { logDebug("info", "Iframe window.open routed to integrated viewer", { url: String(url || ""), target: typeof target === "string" ? target : "", }); return frameWindow; } if (originalOpen) { return originalOpen(url, target, features); } return null; }; frameWindow.__qortalNativeOpenInterceptorInstalled = true; } if (frameDocument.__qortalNativeLinkInterceptorInstalled) { return; } frameDocument.addEventListener( "click", function (event) { const target = event.target instanceof Element ? event.target.closest("a[href]") : null; if (!target) { return; } const href = target.getAttribute("href") || ""; if ( !href || !String(href).trim().toLowerCase().startsWith("qortal://") ) { return; } event.preventDefault(); event.stopPropagation(); routeQortalProtocolTarget( href, target.textContent || modeCopy.browserName ); }, true ); frameDocument.__qortalNativeLinkInterceptorInstalled = true; } function copyText(value) { const text = String(value || "").trim(); if (!text) { return Promise.resolve(false); } if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard .writeText(text) .then(function () { return true; }) .catch(function () { return false; }); } return Promise.resolve(false); } function getDashboardRegisterUrl() { return buildDashboardHighlightUrl(["register-name"]); } function buildDashboardHighlightUrl(highlights) { const base = ( nextcloudPublicUrl || window.location.origin + (typeof OC !== "undefined" && OC.webroot ? OC.webroot : "") ).replace(/\/$/, ""); const validHighlights = Array.isArray(highlights) ? highlights .map(function (entry) { return String(entry || "") .trim() .toLowerCase(); }) .filter(function (entry) { return entry !== ""; }) : []; const query = validHighlights.length > 0 ? "?highlight=" + encodeURIComponent(validHighlights.join(",")) : ""; return base + "/apps/qortal_integration/account" + query; } function extractBalanceAmount(balance) { const pickAmount = function (value) { if (value === undefined || value === null || value === "") { return undefined; } if (typeof value === "number" || typeof value === "string") { return value; } if (!value || typeof value !== "object") { return undefined; } return ( value.balance ?? value.confirmedBalance ?? value.confirmed ?? value.availableBalance ?? value.amount ?? value.value ?? undefined ); }; if (Array.isArray(balance)) { for (let i = 0; i < balance.length; i += 1) { const amount = pickAmount(balance[i]); if (amount !== undefined) { return amount; } } return undefined; } return pickAmount(balance); } function parseBalanceQort(value) { const amount = extractBalanceAmount(value); if (amount === undefined || amount === null || amount === "") { return null; } const parsed = Number(amount); return Number.isFinite(parsed) ? parsed : null; } async function fetchBalanceQort(address) { const normalizedAddress = String(address || "").trim(); if (!normalizedAddress) { return null; } const now = Date.now(); if ( balanceLookupCache.address === normalizedAddress && typeof balanceLookupCache.balanceQort === "number" && now - balanceLookupCache.checkedAt <= nameLookupCacheTtlMs ) { return balanceLookupCache.balanceQort; } let balanceQort = null; try { balanceQort = parseBalanceQort( await postBrokerQortalRequest("GET_WALLET_BALANCE", { coin: "QORT", address: normalizedAddress, }) ); } catch (_error) { balanceQort = null; } if (balanceQort === null) { try { balanceQort = parseBalanceQort( await postBrokerQortalRequest("GET_BALANCE", { address: normalizedAddress, }) ); } catch (_error) { balanceQort = null; } } balanceLookupCache = { address: normalizedAddress, balanceQort: balanceQort, checkedAt: now, }; return balanceQort; } function buildIdentityPrerequisiteState( hasWallet, hasEnoughBalance, hasName ) { if (!hasWallet) { return { title: modeCopy.nameRequiredTitle, intro: "You have not yet created your " + modeCopy.accountLabel + ". To use distributed authentication, communications, and data inside apps, you must create your account first.", steps: [ modeCopy.createAccountStep, modeCopy.fundingStep, modeCopy.nameStep, ], outro: modeCopy.prereqOutro, highlights: ["create-account"], }; } if (!hasName && hasEnoughBalance === false) { return { title: modeCopy.nameRequiredTitle, intro: "Your account is created, but you do not yet have enough " + modeCopy.unitLabel + " to register a distributed identity name.", steps: [modeCopy.fundingStep, modeCopy.nameStep], outro: modeCopy.prereqOutro, highlights: ["balance", "purchases", "register-name"], }; } if (!hasName) { return { title: modeCopy.nameRequiredTitle, intro: "Your account is created, but you do not yet have a registered distributed identity name.", steps: [ "Open the dashboard and go to the name registration section.", modeCopy.nameStep, ], outro: modeCopy.prereqOutro, highlights: ["register-name"], }; } return null; } function isTransientFetchError(error) { const message = error && error.message ? String(error.message) : String(error || ""); const normalized = message.toLowerCase(); return ( normalized.includes("failed to fetch") || normalized.includes("networkerror") ); } function sleep(ms) { return new Promise(function (resolve) { window.setTimeout(resolve, ms); }); } async function fetchWithRetry(url, options, attempts, delayMs) { const maxAttempts = Number.isFinite(attempts) ? Math.max(1, Math.floor(attempts)) : 1; const retryDelay = Number.isFinite(delayMs) ? Math.max(0, Math.floor(delayMs)) : 0; let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { return await fetch(url, options); } catch (error) { lastError = error; if (!isTransientFetchError(error) || attempt >= maxAttempts) { throw error; } await sleep(retryDelay); } } throw lastError || new Error("fetch_failed"); } async function postBrokerQortalRequest(requestType, payload) { if (!qappsRequestUrl) { throw new Error(modeCopy.requestEndpointMissing); } const headers = { "Content-Type": "application/json" }; if (typeof OC !== "undefined" && OC.requestToken) { headers.requesttoken = OC.requestToken; } const outcome = await postQappsRequestWithHighRiskConfirmation( requestType, payload || {}, headers ); const response = outcome.response; const json = outcome.json; if (!response.ok || !json || json.ok === false || json.error) { throw new Error((json && json.error) || "qortal_request_failed"); } return json.data !== undefined ? json.data : json; } function parsePrimaryNameResult(resultPayload) { if (typeof resultPayload === "string") { return resultPayload.trim(); } if (Array.isArray(resultPayload)) { return resultPayload.length > 0 ? parsePrimaryNameResult(resultPayload[0]) : ""; } if (resultPayload && typeof resultPayload === "object") { return String( resultPayload.name || resultPayload.primaryName || (resultPayload.data && resultPayload.data.name) || "" ).trim(); } return ""; } async function hasRegisteredName(address) { const normalizedAddress = String(address || "").trim(); if (!normalizedAddress) { return true; } const now = Date.now(); if ( nameLookupCache.address === normalizedAddress && typeof nameLookupCache.hasName === "boolean" && now - nameLookupCache.checkedAt <= nameLookupCacheTtlMs ) { return nameLookupCache.hasName; } let hasName = false; try { const primaryName = parsePrimaryNameResult( await postBrokerQortalRequest("GET_PRIMARY_NAME", { address: normalizedAddress, }) ); if (primaryName) { hasName = true; } else { const namesResult = await postBrokerQortalRequest("GET_ACCOUNT_NAMES", { address: normalizedAddress, }); if (Array.isArray(namesResult)) { hasName = namesResult.some(function (entry) { if (typeof entry === "string") { return entry.trim() !== ""; } if (entry && typeof entry === "object") { return String(entry.name || "").trim() !== ""; } return false; }); } } } catch (error) { logDebug("warn", "Registered-name check failed; allowing access", { address: normalizedAddress, error: error && error.message ? error.message : String(error), }); hasName = true; } nameLookupCache = { address: normalizedAddress, hasName, checkedAt: now, }; return hasName; } function showRegisteredNameRequiredModal(state) { return new Promise(function (resolve) { if ( !nameRequiredModal || !nameRequiredDashboardLink || !nameRequiredContinue || !nameRequiredCancel ) { resolve("continue"); return; } const promptState = state && typeof state === "object" ? state : null; const highlights = promptState && Array.isArray(promptState.highlights) ? promptState.highlights : ["register-name"]; const dashboardUrl = buildDashboardHighlightUrl(highlights); if (nameRequiredTitleEl) { nameRequiredTitleEl.textContent = promptState && promptState.title ? promptState.title : modeCopy.nameRequiredTitle; } if (nameRequiredNote1El) { nameRequiredNote1El.textContent = promptState && promptState.intro ? promptState.intro : ""; } if (nameRequiredStep1El) { nameRequiredStep1El.textContent = promptState && promptState.steps && promptState.steps[0] ? promptState.steps[0] : ""; nameRequiredStep1El.classList.toggle( "qortal-hidden", !nameRequiredStep1El.textContent ); } if (nameRequiredStep2El) { nameRequiredStep2El.textContent = promptState && promptState.steps && promptState.steps[1] ? promptState.steps[1] : ""; nameRequiredStep2El.classList.toggle( "qortal-hidden", !nameRequiredStep2El.textContent ); } if (nameRequiredStep3El) { nameRequiredStep3El.textContent = promptState && promptState.steps && promptState.steps[2] ? promptState.steps[2] : ""; nameRequiredStep3El.classList.toggle( "qortal-hidden", !nameRequiredStep3El.textContent ); } if (nameRequiredStepsEl) { nameRequiredStepsEl.classList.toggle( "qortal-hidden", !promptState || !Array.isArray(promptState.steps) || promptState.steps.length === 0 ); } if (nameRequiredNote2El) { nameRequiredNote2El.textContent = promptState && promptState.outro ? promptState.outro : ""; } nameRequiredDashboardLink.href = dashboardUrl; nameRequiredModal.classList.remove("qortal-hidden"); const cleanup = function () { nameRequiredDashboardLink.onclick = null; nameRequiredContinue.onclick = null; nameRequiredCancel.onclick = null; nameRequiredModal.classList.add("qortal-hidden"); }; nameRequiredDashboardLink.onclick = function (event) { if (event) { event.preventDefault(); } cleanup(); resolve("dashboard"); }; nameRequiredContinue.onclick = function () { cleanup(); resolve("continue"); }; nameRequiredCancel.onclick = function () { cleanup(); resolve("cancel"); }; }); } async function ensureRegisteredNameBeforeOpening() { let effectiveAddress = String(currentWalletAddress || "").trim(); if (!effectiveAddress && currentWalletId && userMappingsUrl) { await refreshUserMapping(); effectiveAddress = String(currentWalletAddress || "").trim(); } if (!effectiveAddress) { const decisionWithoutAccount = await showRegisteredNameRequiredModal( buildIdentityPrerequisiteState(false, false, false) ); if (decisionWithoutAccount === "dashboard") { window.location.href = buildDashboardHighlightUrl(["create-account"]); return false; } return decisionWithoutAccount === "continue"; } const hasName = await hasRegisteredName(effectiveAddress); if (hasName) { return true; } const balanceQort = await fetchBalanceQort(effectiveAddress); const hasEnoughBalance = typeof balanceQort === "number" ? balanceQort >= 1.25 : null; const decision = await showRegisteredNameRequiredModal( buildIdentityPrerequisiteState(true, hasEnoughBalance, false) ); if (decision === "dashboard") { const highlights = hasEnoughBalance === false ? ["balance", "purchases", "register-name"] : ["register-name"]; window.location.href = buildDashboardHighlightUrl(highlights); return false; } return decision === "continue"; } function setMobileMenuOpen(open, shouldResize) { const isOpen = Boolean(open); root.classList.toggle(mobileMenuClass, isOpen); if (mobileMenuToggle) { mobileMenuToggle.setAttribute("aria-expanded", isOpen ? "true" : "false"); } if (viewerMenuToggle) { viewerMenuToggle.setAttribute("aria-expanded", isOpen ? "true" : "false"); } if (shouldResize) { resizeViewerFrame(); } } function setViewerOpen(open) { const isOpen = Boolean(open); root.classList.toggle(viewerOpenClass, isOpen); if (pageHeader) { pageHeader.style.display = isOpen ? "none" : ""; } if (isOpen) { startViewerNodeStatusPolling(); } else { stopViewerNodeStatusPolling(); clearPendingReloadSequence(); } } function extractAppNameFromQortalAddress(address) { const raw = String(address || "").trim(); if (!raw.toLowerCase().startsWith("qortal://")) { return ""; } const trimmed = raw.slice("qortal://".length).replace(/^\/+/, ""); if (!trimmed) { return ""; } const parts = trimmed.split("/").filter(Boolean); if (!parts.length) { return ""; } const first = String(parts[0] || "").toUpperCase(); if ((first === "APP" || first === "WEBSITE") && parts.length > 1) { return String(parts[1] || "").trim(); } return String(parts[0] || "").trim(); } function resolveQappIcon(entry) { if (!entry || typeof entry !== "object") { return ""; } const iconMode = normalizeQappIconMode(entry.iconMode); const iconUrl = String(entry.iconUrl || "").trim(); if (iconMode === "custom" && iconUrl) { if (iconUrl.toLowerCase().startsWith("qortal://")) { const iconPath = iconUrl.slice("qortal://".length).replace(/^\/+/, ""); const proxied = openGatewayPath(iconPath); return proxied || ""; } return iconUrl; } const appName = iconMode === "name" ? String(entry.iconFromName || "").trim() : extractAppNameFromQortalAddress(entry.address || ""); if (!appName) { return ""; } const thumbPath = "arbitrary/THUMBNAIL/" + appName + "/qortal_avatar"; return openGatewayPath(thumbPath); } function isViewerShowingApp() { return Boolean( viewerCard && !viewerCard.classList.contains("qortal-hidden") && currentQappAddress ); } function shouldPromptAppSwitch(targetAddress) { if (!isViewerShowingApp()) { return false; } const normalizedTarget = normalizeBrowserAddress(targetAddress || ""); const normalizedCurrent = normalizeBrowserAddress( currentViewerAddress || currentQappAddress || "" ); if (!normalizedTarget || !normalizedCurrent) { return false; } const toIdentity = function (value) { const raw = String(value || "") .trim() .replace(/^qortal:\/\//i, "") .replace(/^\/+/, ""); const segments = raw.split(/[/?#]/)[0].split("/").filter(Boolean); if (segments.length < 2) { return raw.toLowerCase(); } return ( String(segments[0] || "") + "/" + String(segments[1] || "") ).toLowerCase(); }; if (toIdentity(normalizedTarget) === toIdentity(normalizedCurrent)) { return false; } return normalizedTarget.toLowerCase() !== normalizedCurrent.toLowerCase(); } function closeSwitchModal(choice) { if (switchModal) { switchModal.classList.add("qortal-hidden"); } if (switchModalResolver) { const resolve = switchModalResolver; switchModalResolver = null; resolve(choice || "cancel"); } } function isMobileSwitchPromptMode() { const viewportWidth = (window.visualViewport && window.visualViewport.width) || window.innerWidth || document.documentElement.clientWidth || 0; return Boolean(viewportWidth && viewportWidth <= 780); } function openSwitchModal(targetAddress, targetName, mode) { if ( !switchModal || !switchModalOpenHereButton || !switchModalOpenNewTabButton || !switchModalCancelButton ) { return Promise.resolve("same"); } if (switchModalResolver) { const priorResolve = switchModalResolver; switchModalResolver = null; priorResolve("cancel"); } if (switchModalNoteEl) { const displayName = String( targetName || extractAppNameFromQortalAddress(targetAddress) || targetAddress || "selected destination" ); if (mode === 'leave') { switchModalNoteEl.textContent = 'Opening "' + displayName + '" here will close your current app in this tab.'; } else { switchModalNoteEl.textContent = 'Opening "' + displayName + '" here will replace your current app in this tab.'; } } if (isMobileSwitchPromptMode()) { const displayName = String( targetName || extractAppNameFromQortalAddress(targetAddress) || targetAddress || "selected destination" ); const template = mode === "leave" ? modeCopy.mobileLeaveNote : modeCopy.mobileSwitchNote; const message = String(template || "") .replace("{name}", displayName) .trim(); setMobileMenuOpen(false, false); return Promise.resolve(window.confirm(message) ? "same" : "cancel"); } switchModal.classList.remove("qortal-hidden"); return new Promise(function (resolve) { switchModalResolver = resolve; }); } function buildQappsPageUrl(address) { const normalizedAddress = normalizeBrowserAddress(address || ""); if (!normalizedAddress) { return ""; } const url = new URL(window.location.href); url.searchParams.set("qapp", normalizedAddress); return url.toString(); } async function openAddress(address, name) { if (!address || !viewerCard || !viewerFrame || !viewerAddress) { return; } const allowedToOpen = await ensureRegisteredNameBeforeOpening(); if (!allowedToOpen) { return; } const relativeResolvedAddress = resolveRelativeAddressForCurrentQapp(address); let normalizedAddress = normalizeBrowserAddress( relativeResolvedAddress || address ); if (embedMode) { normalizedAddress = applyEmbedThemeParams(normalizedAddress); } if (isEmbedLockedToDifferentQapp(normalizedAddress)) { const pageUrl = buildQappsPageUrl(normalizedAddress); if (pageUrl) { window.open(pageUrl, "_blank", "noopener"); } return; } const resolved = resolveGatewayAddress(normalizedAddress, true); if (!resolved) { window.alert(modeCopy.nodeMissingAlert); return; } currentQappAddress = normalizedAddress; currentQdnResourceTarget = parseQdnResourceTarget(normalizedAddress); logDebug("info", "Opening Q-App", { address: normalizedAddress, resolved }); if (launchRetryAddress !== normalizedAddress) { launchRetryAddress = normalizedAddress; launchRetryCount = 0; } clearQdnLoadingRetry("open_address"); qdnLoadingRetryAttempts = 0; qdnLoadingStatusFailures = 0; qdnLoadingLastSnapshot = ""; updateQdnLoadingProgress("NOT_STARTED", 0, null, null, ""); setViewerAddressDisplay(normalizedAddress); if (viewerAddressInputWrap) { viewerAddressInputWrap.classList.remove("qortal-hidden"); } if (viewerAddress) { viewerAddress.classList.remove("qortal-hidden"); } viewerCard.classList.remove("qortal-hidden"); // Keep the viewer visible before assigning src to avoid lazy/hidden first-load edge cases. try { viewerFrame.setAttribute("loading", "eager"); } catch (_error) { // Ignore unsupported attribute updates. } viewerFrame.src = resolved; setViewerOpen(true); setMobileMenuOpen(false, false); if (debugCard && effectiveDebugEnabled) { syncDebugPanelState(); } resizeViewerFrame(); if (viewerOpenExternal) { viewerOpenExternal.textContent = "Copy link"; viewerOpenExternal.onclick = async function () { const fromFrame = syncViewerAddressFromFrame(); const linkToCopy = fromFrame || currentViewerAddress || currentQappAddress || normalizedAddress; const copied = await copyText(linkToCopy); viewerOpenExternal.textContent = copied ? "Copied" : "Copy failed"; window.setTimeout(function () { viewerOpenExternal.textContent = "Copy link"; }, 1200); }; } if (viewerClose) { viewerClose.onclick = function () { clearQdnLoadingRetry("viewer_closed"); qdnLoadingRetryAttempts = 0; currentQdnResourceTarget = null; clearPendingReloadSequence(); setViewerOpen(false); setMobileMenuOpen(false, false); viewerFrame.src = "about:blank"; viewerCard.classList.add("qortal-hidden"); }; } if (viewerBack) { viewerBack.onclick = function () { try { if (viewerFrame.contentWindow) { viewerFrame.contentWindow.history.back(); } } catch (_err) { // ignore cross-origin issues } }; } const handleViewerReload = function () { const preferredAddress = currentViewerAddress || currentQappAddress || ""; if ( preferredAddress && preferredAddress.toLowerCase().startsWith("qortal://") ) { logDebug("info", "Reload routed through safe sequence", { currentViewerAddress: currentViewerAddress || "", currentQappAddress: currentQappAddress || "", reloadAddress: preferredAddress, }); startViewerReloadSequence(preferredAddress); return; } try { if (viewerFrame.contentWindow) { viewerFrame.contentWindow.location.reload(); } } catch (_err) { // ignore cross-origin issues } }; if (viewerReload) { viewerReload.onclick = handleViewerReload; } if (viewerMobileRefresh) { viewerMobileRefresh.onclick = handleViewerReload; } } async function openAddressWithSwitchPrompt(address, name) { if (!address) { return; } const normalizedTarget = normalizeBrowserAddress(address); if (!shouldPromptAppSwitch(normalizedTarget)) { await openAddress(normalizedTarget, name); return; } const choice = await openSwitchModal(normalizedTarget, name); if (choice === "newtab") { const pageUrl = buildQappsPageUrl(normalizedTarget); if (pageUrl) { window.open(pageUrl, "_blank", "noopener"); } return; } if (choice === "same") { await openAddress(normalizedTarget, name); } } function resizeViewerFrame() { if (!viewerFrameWrap) { return; } const rect = viewerFrameWrap.getBoundingClientRect(); const rootRect = root.getBoundingClientRect(); const viewportHeight = getViewportHeight(); const viewportWidth = (window.visualViewport && window.visualViewport.width) || window.innerWidth || document.documentElement.clientWidth || 0; const isMobile = viewportWidth && viewportWidth <= 780; if (!isMobile && root.classList.contains(mobileMenuClass)) { setMobileMenuOpen(false, false); } const safeAreaBottom = isMobile ? Math.max(0, window.innerHeight - viewportHeight) + 2 : 8; const padding = isMobile ? safeAreaBottom : 8; const debugHeight = debugCard && !debugCard.classList.contains("qortal-hidden") ? Math.ceil(debugCard.getBoundingClientRect().height) : 0; let nextHeight = Math.floor( viewportHeight - rect.top - padding - debugHeight ); if ( root.classList.contains(viewerOpenClass) && Number.isFinite(rootRect.bottom) ) { const containerPadding = isMobile ? 4 : 8; const containerBoundHeight = Math.floor( rootRect.bottom - rect.top - containerPadding - debugHeight ); if (Number.isFinite(containerBoundHeight) && containerBoundHeight > 0) { nextHeight = Math.min(nextHeight, containerBoundHeight); } } if (isMobile) { nextHeight = Math.min( nextHeight, Math.floor(viewportHeight - rect.top - safeAreaBottom - debugHeight) ); } nextHeight = Math.max(0, nextHeight); viewerFrameWrap.style.height = nextHeight + "px"; } function renderList() { if (galleryEl) { galleryEl.innerHTML = ""; } if (!listBody) { return; } listBody.innerHTML = ""; if (!enabled || qapps.length === 0) { listBody.innerHTML = '