(function () { const root = document.getElementById("qortal-account-root"); if (!root) { return; } const debugEnabled = root.dataset.debugEnabled === "1"; const requestUrl = root.dataset.qappsRequestUrl || ""; const gatewayProxyTemplate = root.dataset.gatewayProxyUrl || ""; const nodeBalanceUrl = root.dataset.nodeBalanceUrl || ""; const mappingsUrl = root.dataset.userMappingsUrl || ""; const qappsApproveUrl = root.dataset.qappsApproveUrl || ""; const qappsUnlockUrl = root.dataset.qappsUnlockUrl || ""; const qappsUnlockStatusUrl = root.dataset.qappsUnlockStatusUrl || ""; 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 userCreateWalletUrl = root.dataset.userCreateWalletUrl || ""; const userBackupWalletUrl = root.dataset.userBackupWalletUrl || ""; const userRequestInitialQortUrl = root.dataset.userRequestInitialQortUrl || ""; const userActivityNotificationsConfigUrl = root.dataset.userActivityNotificationsConfigUrl || ""; const userActivityNotificationsSaveUrl = root.dataset.userActivityNotificationsSaveUrl || ""; const userActivityNotificationsTestUrl = root.dataset.userActivityNotificationsTestUrl || ""; const userActivityNotificationsTestTrackedUrl = root.dataset.userActivityNotificationsTestTrackedUrl || ""; const userActivityNotificationsReadCheckpointUrl = root.dataset.userActivityNotificationsReadCheckpointUrl || ""; const initialQortRequestsUrl = root.dataset.initialQortRequestsUrl || ""; const initialQortActionUrl = root.dataset.initialQortActionUrl || ""; const billingCheckoutUrl = root.dataset.billingCheckoutUrl || ""; const billingUserCheckoutUrl = root.dataset.billingUserCheckoutUrl || ""; const billingCatalogUrl = root.dataset.billingCatalogUrl || ""; const billingSupportUrl = root.dataset.billingSupportUrl || ""; const billingSyncUrl = root.dataset.billingSyncUrl || ""; const billingBootstrapUrl = root.dataset.billingBootstrapUrl || ""; const userChdAnnouncementsUrl = root.dataset.userChdAnnouncementsUrl || ""; const invitesUrl = root.dataset.invitesUrl || ""; const createInviteUrl = root.dataset.createInviteUrl || ""; const revokeInviteUrl = root.dataset.revokeInviteUrl || ""; const sendInviteEmailUrl = root.dataset.sendInviteEmailUrl || ""; const settingsPath = root.dataset.settingsPath || "/settings/user/qortal_integration"; const adminSettingsPath = root.dataset.adminSettingsPath || "/settings/admin/qortal_integration"; const customPwaInstallUrl = root.dataset.customPwaInstallUrl || ""; const advancedUnlockSettingsEnabled = root.dataset.qappsAdvancedUnlockSettingsEnabled === "1"; const nextcloudPublicUrl = root.dataset.nextcloudPublicUrl || ""; const chdAdminDashboardEnabled = root.dataset.chdAdminDashboardEnabled === "1"; const chdAdminPath = root.dataset.chdAdminPath || "/apps/chd_admin/admin"; // Future: this can be expanded to instance-level billing roles. const isAdmin = root.dataset.isAdmin === "1"; const currentUserId = root.dataset.currentUserId || ""; const currentUserEmail = root.dataset.currentUserEmail || ""; const currentUserDisplayName = root.dataset.currentUserDisplayName || ""; const cloudBrandName = root.dataset.cloudBrandName || ""; const qappsNamesRaw = root.dataset.qappsNames || "[]"; const qappsCardsRaw = root.dataset.qappsCards || "[]"; const qappsEnabled = root.dataset.qappsEnabled === "1"; const userPublishingSettingEnabled = root.dataset.userPublishingEnabled === "1"; const qappsUrl = root.dataset.qappsUrl || ""; const scBillingEnabled = root.dataset.scBillingEnabled === "1"; const scBillingManagedByChd = root.dataset.scBillingManagedByChd === "1"; const initialPackageTier = String(root.dataset.scPackageTier || "free_core").trim() || "free_core"; const initialMonthlyCredits = Number.parseInt( String(root.dataset.scMonthlyCredits || "0").trim(), 10 ); const initialChdInstanceId = String( root.dataset.scChdInstanceId || "" ).trim(); const sovereignMode = root.dataset.sovereignMode === "full_qortal" ? "full_qortal" : "powered"; const isPoweredMode = sovereignMode === "powered"; const effectiveDebugEnabled = !isPoweredMode && debugEnabled; const canUserPublish = !isPoweredMode || userPublishingSettingEnabled; const ADMIN_QUEUE_MIN_QORT = 5; const highlightTargets = String( new URLSearchParams(window.location.search).get("highlight") || "" ) .split(",") .map(function (entry) { return entry.trim().toLowerCase(); }) .filter(function (entry) { return entry !== ""; }); const pendingHighlights = new Set(highlightTargets); const modeCopy = isPoweredMode ? { dashboardTitle: "NuQloud Dashboard", dashboardHelp: "Your Global NuQloud account hub for decentralized identity, distributed authentication, and portable data access.", onboardingTitle: "Create Your Global NuQloud Account + Identity", onboardingNote: "Create your global, decentralized, and externally accessible NuQloud account and identity used to publish encrypted data and communications.", addressLabel: "NuQloud Address", balanceLabel: "Publish Credits", requestButton: "Request Initial Credits", requestButtonFromAdmins: "Request Credits from Admins", requestButtonInstallGrant: "Request New Install Credit Grant", requestHelp: "Credits are required to publish data and use advanced NuQloud features.", requestHelpFromAdmins: "Request publishing credits from your cloud admins.", requestHelpInstallGrant: "CHD offers an initial credit grant upon manual approval for approved new installations.", balanceModeNote: "Publish credits can be purchased in the Purchases section below.", appGalleryTitle: "NuQloud Apps", appGalleryNote: "NuQloud Apps run on decentralized infrastructure and continue working beyond any single server.", purchasesTitle: "Purchases", purchasesNote: "Purchases provide additional features, added redundancy guarantees, publish credits, and other available options linked to this instance and account.", purchasesSubscriptionsTitle: "Subscription Packages", purchasesOneTimeTitle: "Add-ons", purchasesCheckoutOpened: "Redirecting to checkout…", purchasesBillingDisabled: "Billing is hidden on the dashboard by the admin for this instance.", purchasesConnectorDisabled: "CHD-managed billing is not enabled for this instance.", purchasesAdminOnly: "Purchases are available to cloud admins only.", purchasesEndpointMissing: "Billing checkout endpoint is not configured.", chdAnnouncementsTitle: "Global Announcements", chdAnnouncementsNote: "Global announcements are published by CHD, founders of NuQloud. Announcements here are published to QDN, the distributed network that powers NuQloud publications.", chdAnnouncementsLoading: "Loading announcements…", chdAnnouncementsLoadFailed: "Failed to load announcements.", chdAnnouncementsUnavailable: "No announcements available.", chdAnnouncementsEndpointMissing: "Announcements endpoint is not configured.", activityNotificationsTitle: "NuQloud Decentralized App Notifications", activityNotificationsNote: "Notifications here are for the off-network distributed apps powered by Qortal and available via NuQloud. These applications are resilient against outages, hacks, and censorship. By default, notifications for Q-Mail messages are enabled when Q-Mail or Qmails is available on the instance, and other notifications may be configured. The groups section lets you configure group announcements and threads for any NuQloud-Qortal group your distributed account is part of.", userRequestsTitle: "Initial Credits Requests", userRequestsTitleFromAdmins: "Request Credits from Admins", userRequestsTitleInstallGrant: "Request New Install Credit Grant", namesTitle: "Registered Names", sendAmountLabel: "Amount (Credits)", sendButtonLabel: "Send Credits", sendNote: "Use whole credits only. Sending consumes a 1 credit network fee.", sendPlaceholder: "10", settingsButton: "Integration Settings", copyAddressStatus: "Address copied", qappsTileAriaSuffix: " - Open App", qappsTileDefaultDescription: "Open this app.", qappsRequestMissing: "Apps request URL is not configured", txUnit: "credits", registerMinimumAmount: "125", txUnknownAmount: "unknown amount", transactionsNoData: "No activity loaded yet.", transactionsLinkWallet: "Link an account to load activity.", transactionsLoading: "Loading activity…", transactionsNone: "No confirmed activity found.", transactionsNewAccount: "New account, no transactions yet.", transactionsShowing: "Showing {count} activity item(s).", transactionsFailed: "Failed to load activity.", unconfirmedTransactionsLoading: "Checking unconfirmed transactions…", unconfirmedTransactionsNone: "No active unconfirmed transactions.", unconfirmedTransactionsFailed: "Failed to load unconfirmed transactions.", registerTitleReady: "Register a new name", registerTitleNeedsBalance: "Requires at least 125 credits to register a name", registerCostNote: "You need at least 125 publish credits to register a name.", registerTitlePending: "Name registration pending confirmation.", registerSuccessTitle: "Name Registration Successful!", registerSuccessNote: "Name registrations, like any transaction on the distributed network, must confirm. Confirmations usually take place within 65-75 seconds. You will not see your name until the transaction has confirmed.", registerAdvancedOutputShow: "Show Advanced Output", registerAdvancedOutputHide: "Hide Advanced Output", requestDetailsBase: "On this cloud, publishing credits may be used for decentralized publishing and secure data operations.", requestDetailsWithAppsPrefix: "On this cloud, publishing credits are used in: ", requestDetailsInstallGrant: "CHD has offered an initial credit grant upon manual approval for approved new installations. If you would like to request an initial grant, click the button.", requestDetailsTail: " Published data remains available beyond any single cloud instance. You can export your backup and import it into other compatible interfaces to access your data across devices.", wholeCreditOnlyError: "Amount must be a whole number of credits", sendPendingLabel: "sending credits…", sendSuccessLabel: "credits transfer sent", sendSuccessNotice: "Sent {amount} {unit} to {recipient}.", initialGrantSending: "Sending initial credits…", initialGrantSent: "Initial credits sent", initialGrantFailed: "Failed to send initial credits", initialGrantDenyConfirm: "Deny this initial credits request?", adminOpsNote: "Publishing operations summary for this cloud instance.", downloadBackupButton: "NuQloud Authentication File", downloadBackupModalTitle: "NuQloud Authentication File", downloadBackupAction: "Download Authentication File", backupModalNote: "This authentication file works in both NuQloud and Qortal. It uses encrypted decentralized authentication powered by Qortal, so you can use it to access your NuQloud account on a new device or browser, or directly in Qortal Hub. The password is the one you created when you made your account, and it only changes if you choose to re-encrypt it with the checkbox below. A password you will remember matters more than one that is complex. We recommend saving a copy to NuQloud Files and keeping additional copies on your devices. Early multi-signature recovery options are being tested now, and can be enabled from your NuQloud integration settings.", saveBackupToFilesButton: "Save to NuQloud", saveBackupToFilesSuccess: "Authentication file saved to your NuQloud Files.", saveBackupToFilesFailed: "Failed to save authentication file to your NuQloud Files.", addPasswordToPasswordsButton: "Open Passwords App", backupSettingsButton: "Open NuQloud Integration Settings", passwordsAppOpened: "Password copied. Passwords app opened in a new tab.", passwordsAppUnavailable: "Unable to open the Passwords app on this cloud.", quickSendTitle: "Quick Send", quickSendNote: "Enter the recipient name or address and the amount to send. The default network fee will be used.", quickSendLabel: "Quick Send", quickSendButtonLabel: "Send Credits", lowBalanceCta: "Purchase Publish Credits Now", installButtonTitle: "Install NuQloud App", installStatusButtonTitle: "NuQloud App Status", refreshButtonTitle: "refresh dashboard", settingsButtonTitle: "integration settings", purchasesLoading: "Loading pricing data from NuQloud…", purchasesFallbackNotice: "NuQloud packages were not able to load properly.", purchasesLiveReady: "Live pricing data loaded from NuQloud.", purchasesSupportNote: "NuQloud packages were not able to load properly. Contact NuQloud support below and we will help resolve it.", purchasesSupportEmailLabel: "Signed-in email", purchasesSupportMessageLabel: "Message to NuQloud support", purchasesSupportMessagePlaceholder: "Describe what happened and what you were trying to purchase.", purchasesSupportSend: "Send to NuQloud Support", purchasesSupportManual: "Email issues@NuQloud.cloud", purchasesSupportSent: "Support request sent to NuQloud.", purchasesSupportFailed: "Unable to send support request automatically. Please email issues@NuQloud.cloud.", purchasesCatalogLiveRequired: "NuQloud packages must load from the connector before checkout can open.", } : { dashboardTitle: "Qortal Dashboard", dashboardHelp: "Your decentralized account hub, powered by Qortal.", onboardingTitle: "Create Your Qortal Account", onboardingNote: "This creates your account on the next generation distributed internet, powered by Qortal.", addressLabel: "Qortal Address", balanceLabel: "QORT Balance", requestButton: "Request Initial QORT", requestButtonFromAdmins: "Request QORT from Admins", requestButtonInstallGrant: "Request Initial QORT", requestHelp: "QORT is required to publish data and use advanced decentralized features (1 publish credit = 0.01 QORT).", requestHelpFromAdmins: "Request QORT from your cloud admins.", requestHelpInstallGrant: "QORT is required to publish data and use advanced decentralized features.", balanceModeNote: "1 publish credit = 0.01 QORT. Publish-credit packages are available in the Purchases section below.", appGalleryTitle: "Q-Apps", appGalleryNote: "Q-Apps are completely decentralized, cannot be hacked, shut down, censored, or taken down. They do not rely upon the internet at all. Access is possible both inside and outside this cloud server. Powered By Qortal.", purchasesTitle: "Purchases", purchasesNote: "Choose monthly packages or add-ons. Checkout links directly to this installation.", purchasesSubscriptionsTitle: "Subscription Packages", purchasesOneTimeTitle: "Add-ons", purchasesCheckoutOpened: "Redirecting to checkout…", purchasesBillingDisabled: "Billing is hidden on the dashboard by the admin for this instance.", purchasesConnectorDisabled: "Managed billing connector is not enabled for this instance.", purchasesAdminOnly: "Purchases are available to cloud admins only.", purchasesEndpointMissing: "Billing checkout endpoint is not configured.", chdAnnouncementsTitle: "CHD Announcements", chdAnnouncementsNote: "Announcements published by CHD to QDN.", chdAnnouncementsLoading: "Loading announcements…", chdAnnouncementsLoadFailed: "Failed to load announcements.", chdAnnouncementsUnavailable: "No announcements available.", chdAnnouncementsEndpointMissing: "Announcements endpoint is not configured.", activityNotificationsTitle: "Activity + Notifications", activityNotificationsNote: "Track QDN resource prefixes for any Q-App (Q-Mail, Q-Tube, Quitter, Q-Blog, Q-Share, Q-Shop, or custom apps) and relay matched activity into Nextcloud notifications or email.", userRequestsTitle: "Initial QORT Requests", userRequestsTitleFromAdmins: "Request QORT from Admins", userRequestsTitleInstallGrant: "Initial QORT Requests", namesTitle: "Registered Names", sendAmountLabel: "Amount (QORT)", sendButtonLabel: "Send QORT", sendNote: "Enter amount in QORT. Network fee is 0.01 QORT.", sendPlaceholder: "0.10000000", settingsButton: "Advanced Settings", copyAddressStatus: "Qortal address copied", qappsTileAriaSuffix: " - Open Q-App", qappsTileDefaultDescription: "Open this Q-App.", qappsRequestMissing: "Q-Apps request URL is not configured", txUnit: "QORT", registerMinimumAmount: "1.25", txUnknownAmount: "unknown amount", transactionsNoData: "No transactions loaded yet.", transactionsLinkWallet: "Link an account to load transactions.", transactionsLoading: "Loading transactions…", transactionsNone: "No confirmed transactions found.", transactionsNewAccount: "New account, no transactions yet.", transactionsShowing: "Showing {count} transaction(s).", transactionsFailed: "Failed to load transactions.", unconfirmedTransactionsLoading: "Checking unconfirmed transactions…", unconfirmedTransactionsNone: "No active unconfirmed transactions.", unconfirmedTransactionsFailed: "Failed to load unconfirmed transactions.", registerTitleReady: "Register a new Qortal name", registerTitleNeedsBalance: "Requires at least 1.25 QORT to register a name", registerCostNote: "You need at least 1.25 QORT to register a name (1 publish credit = 0.01 QORT).", registerTitlePending: "Name registration pending confirmation.", registerSuccessTitle: "Name Registration Successful!", registerSuccessNote: "Name registrations, like any transaction on the distributed network, must confirm. Confirmations usually take place within 65-75 seconds. You will not see your name until the transaction has confirmed.", registerAdvancedOutputShow: "Show Advanced Output", registerAdvancedOutputHide: "Hide Advanced Output", requestDetailsBase: "On this cloud, QORT powers decentralized publishing and secure data operations (1 publish credit = 0.01 QORT).", requestDetailsWithAppsPrefix: "On this cloud, QORT is used in: ", requestDetailsInstallGrant: "On this cloud, QORT may be used for decentralized publishing and secure data operations.", requestDetailsTail: " QDN is a distributed network where published data remains available beyond any single cloud instance. You can export your Qortal backup and import it in other Qortal interfaces to access your data across devices. QORT is the currency that powers this network. Publish-credit packages can be purchased in the Purchases section below.", wholeCreditOnlyError: "Amount must be a positive number", sendPendingLabel: "sending…", sendSuccessLabel: "transfer sent", sendSuccessNotice: "Sent {amount} {unit} to {recipient}.", initialGrantSending: "Sending initial QORT…", initialGrantSent: "Initial QORT sent", initialGrantFailed: "Failed to send initial QORT", initialGrantDenyConfirm: "Deny this initial QORT request?", adminOpsNote: "Publishing operations summary for this cloud instance.", downloadBackupButton: "Download Backup File", downloadBackupModalTitle: "Download Encrypted Backup", downloadBackupAction: "Download Backup", backupModalNote: "Select a linked account, enter the current backup password, and download a fresh encrypted backup file.", saveBackupToFilesButton: "Save to Nextcloud Files", saveBackupToFilesSuccess: "Backup file saved to your Nextcloud Files.", saveBackupToFilesFailed: "Failed to save backup file to your Nextcloud Files.", addPasswordToPasswordsButton: "Open Passwords App", backupSettingsButton: "Open Qortal Settings", passwordsAppOpened: "Password copied. Passwords app opened in a new tab.", passwordsAppUnavailable: "Unable to open the Passwords app on this cloud.", quickSendTitle: "Quick Send", quickSendNote: "Enter the recipient name or address and the amount to send. The default network fee will be used.", quickSendLabel: "Quick Send", quickSendButtonLabel: "Send QORT", lowBalanceCta: "Purchase Publish Credits Now", installButtonTitle: "Install Qortal App", installStatusButtonTitle: "Qortal App Status", refreshButtonTitle: "refresh dashboard", settingsButtonTitle: "integration settings", purchasesLoading: "Loading pricing data from NuQloud…", purchasesFallbackNotice: "Billing packages were not able to load properly.", purchasesLiveReady: "Live pricing data loaded from NuQloud.", purchasesSupportNote: "Billing packages were not able to load properly. Contact NuQloud support below and we will help resolve it.", purchasesSupportEmailLabel: "Signed-in email", purchasesSupportMessageLabel: "Message to NuQloud support", purchasesSupportMessagePlaceholder: "Describe what happened and what you were trying to purchase.", purchasesSupportSend: "Send to NuQloud Support", purchasesSupportManual: "Email issues@NuQloud.cloud", purchasesSupportSent: "Support request sent to NuQloud.", purchasesSupportFailed: "Unable to send support request automatically. Please email issues@NuQloud.cloud.", purchasesCatalogLiveRequired: "Billing packages must load from the connector before checkout can open.", }; const publishTxTypeLabel = isPoweredMode ? "DATA_PUBLISH" : "QDN_PUBLISH"; const QORT_PER_CREDIT = 0.01; const INITIAL_GRANT_QORT = 2; 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: "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 chdQdnRetryDelayMs = 4000; const chdQdnReloadFallbackDelayMs = 7000; const chdQdnRetryMaxAttempts = 240; let chdQdnRetryTimer = null; let chdQdnRetryAttempts = 0; let chdQdnStatusFailures = 0; let chdQdnLastSnapshot = ""; let chdQdnResourceTarget = null; const onboardingCard = document.getElementById("qortal-account-onboarding"); const walletPane = document.getElementById("qortal-account-wallet-pane"); const settingsLink = document.getElementById("qortal-account-settings-link"); const backupSettingsLink = document.getElementById( "qortal-account-backup-settings-link" ); const installPwaLink = document.getElementById( "qortal-account-install-pwa-link" ); const installPwaLabelEl = document.getElementById( "qortal-account-install-pwa-label" ); const installPwaSrLabelEl = document.getElementById( "qortal-account-install-pwa-sr-label" ); const pageTitleEl = document.getElementById("qortal-account-title"); const pageHelpEl = document.getElementById("qortal-account-help"); const onboardingTitleEl = document.getElementById( "qortal-account-onboarding-title" ); const onboardingNoteEl = document.getElementById( "qortal-account-onboarding-note" ); const createPasswordEl = document.getElementById( "qortal-account-create-password" ); const createKdfEl = document.getElementById("qortal-account-create-kdf"); const createButton = document.getElementById("qortal-account-create-button"); const createResultEl = document.getElementById( "qortal-account-create-result" ); const addressLabelEl = document.getElementById( "qortal-account-address-label" ); const addressEl = document.getElementById("qortal-account-address"); const balanceLabelEl = document.getElementById( "qortal-account-balance-label" ); const balanceEl = document.getElementById("qortal-account-balance"); const balanceCardEl = document.getElementById("qortal-account-balance-card"); const primaryCardEl = document.getElementById("qortal-account-primary-card"); const primaryNameEl = document.getElementById("qortal-account-primary-name"); const namesEl = document.getElementById("qortal-account-names"); const namesEmptyEl = document.getElementById("qortal-account-names-empty"); const copyAddressButton = document.getElementById( "qortal-account-copy-address" ); const requestQortWrapEl = document.getElementById( "qortal-account-request-wrap" ); const requestQortButton = document.getElementById( "qortal-account-request-qort" ); const requestQortHelp = document.getElementById( "qortal-account-request-help" ); const balanceModeNoteEl = document.getElementById( "qortal-account-balance-mode-note" ); const requestQortResultEl = document.getElementById( "qortal-account-request-result" ); const requestFeedbackEl = document.getElementById( "qortal-account-request-feedback" ); const publishingDisabledCardEl = document.getElementById( "qortal-account-publishing-disabled" ); const requestDetailsEl = document.getElementById( "qortal-account-request-details" ); const appGalleryCardEl = document.getElementById( "qortal-account-app-gallery-card" ); const appGalleryTitleEl = document.getElementById( "qortal-account-app-gallery-title" ); const appGalleryNoteEl = document.getElementById( "qortal-account-app-gallery-note" ); const appGalleryBodyEl = document.getElementById( "qortal-account-app-gallery-body" ); const appGalleryToggleButton = document.getElementById( "qortal-account-app-gallery-toggle" ); const appGalleryEl = document.getElementById("qortal-account-app-gallery"); const userRequestsCardEl = document.getElementById( "qortal-account-user-requests-card" ); const userRequestsBodyEl = document.getElementById( "qortal-account-user-requests-body" ); const userRequestsTitleEl = document.getElementById( "qortal-account-user-requests-title" ); const userRequestsToggleButton = document.getElementById( "qortal-account-user-requests-toggle" ); const userRequestsListEl = document.getElementById( "qortal-account-user-requests-list" ); const userRequestsEmptyEl = document.getElementById( "qortal-account-user-requests-empty" ); const adminRequestsCardEl = document.getElementById( "qortal-account-admin-requests-card" ); const adminRequestsBodyEl = document.getElementById( "qortal-account-admin-requests-body" ); const adminRequestsToggleButton = document.getElementById( "qortal-account-admin-requests-toggle" ); const adminRequestsListEl = document.getElementById( "qortal-account-admin-requests-list" ); const adminRequestsEmptyEl = document.getElementById( "qortal-account-admin-requests-empty" ); const adminRequestsPaginationEl = document.getElementById( "qortal-account-admin-requests-pagination" ); const adminRequestsPrevButton = document.getElementById( "qortal-account-admin-requests-prev" ); const adminRequestsNextButton = document.getElementById( "qortal-account-admin-requests-next" ); const adminRequestsPageEl = document.getElementById( "qortal-account-admin-requests-page" ); const adminOpsCardEl = document.getElementById( "qortal-account-admin-ops-card" ); const adminOpsBodyEl = document.getElementById( "qortal-account-admin-ops-body" ); const adminOpsToggleButton = document.getElementById( "qortal-account-admin-ops-toggle" ); const adminOpsNoteEl = document.getElementById( "qortal-account-admin-ops-note" ); const adminOpsPendingEl = document.getElementById( "qortal-account-admin-ops-pending" ); const adminOpsSentEl = document.getElementById( "qortal-account-admin-ops-sent" ); const adminOpsDeniedEl = document.getElementById( "qortal-account-admin-ops-denied" ); const adminOpsUsersEl = document.getElementById( "qortal-account-admin-ops-users" ); const adminOpsTotalSentEl = document.getElementById( "qortal-account-admin-ops-total-sent" ); const inviteManagementCardEl = document.getElementById( "qortal-account-invite-management-card" ); const inviteManagementTitleEl = document.getElementById( "qortal-account-invite-management-title" ); const inviteManagementBodyEl = document.getElementById( "qortal-account-invite-management-body" ); const inviteManagementToggleButton = document.getElementById( "qortal-account-invite-management-toggle" ); const inviteManagementNoteEl = document.getElementById( "qortal-account-invite-management-note" ); const inviteRecipientEmailEl = document.getElementById( "qortal-account-invite-recipient-email" ); const inviteExpiryHoursEl = document.getElementById( "qortal-account-invite-expiry-hours" ); const inviteSendEmailOnCreateEl = document.getElementById( "qortal-account-invite-send-email-on-create" ); const inviteCreateButtonEl = document.getElementById( "qortal-account-invite-create" ); const inviteRefreshButtonEl = document.getElementById( "qortal-account-invite-refresh" ); const inviteCreatedDetailsEl = document.getElementById( "qortal-account-invite-created-details" ); const inviteCreatedTokenEl = document.getElementById( "qortal-account-invite-created-token" ); const inviteCreatedLinkEl = document.getElementById( "qortal-account-invite-created-link" ); const inviteCreatedEmailStatusEl = document.getElementById( "qortal-account-invite-created-email-status" ); const inviteCreateResultEl = document.getElementById( "qortal-account-invite-create-result" ); const invitesTableBodyEl = document.getElementById( "qortal-account-invites-body" ); const adminSettingsLink = document.getElementById( "qortal-account-admin-settings-link" ); const chdAdminLink = document.getElementById("qortal-account-chd-admin-link"); const purchasesCardEl = document.getElementById( "qortal-account-purchases-card" ); const purchasesBodyEl = document.getElementById( "qortal-account-purchases-body" ); const purchasesToggleButton = document.getElementById( "qortal-account-purchases-toggle" ); const purchasesTitleEl = document.getElementById( "qortal-account-purchases-title" ); const purchasesNoteEl = document.getElementById( "qortal-account-purchases-note" ); const purchasesStatusEl = document.getElementById( "qortal-account-purchases-status" ); const purchasesLoadingEl = document.getElementById( "qortal-account-purchases-loading" ); const purchasesLoadingLabelEl = document.getElementById( "qortal-account-purchases-loading-label" ); const purchasesSupportEl = document.getElementById( "qortal-account-purchases-support" ); const purchasesSupportNoteEl = document.getElementById( "qortal-account-purchases-support-note" ); const purchasesSupportEmailEl = document.getElementById( "qortal-account-purchases-support-email" ); const purchasesSupportMessageEl = document.getElementById( "qortal-account-purchases-support-message" ); const purchasesSupportStatusEl = document.getElementById( "qortal-account-purchases-support-status" ); const purchasesSupportSendButton = document.getElementById( "qortal-account-purchases-support-send" ); const purchasesSupportMailtoLink = document.getElementById( "qortal-account-purchases-support-mailto" ); const purchasesSubscriptionsTitleEl = document.getElementById( "qortal-account-purchases-subscriptions-title" ); const purchasesOneTimeTitleEl = document.getElementById( "qortal-account-purchases-onetime-title" ); const purchasesSubscriptionsSectionEl = document.getElementById( "qortal-account-purchases-subscriptions-section" ); const purchasesOneTimeSectionEl = document.getElementById( "qortal-account-purchases-onetime-section" ); const subscriptionCardsEl = document.getElementById( "qortal-account-subscription-cards" ); const oneTimeCardsEl = document.getElementById( "qortal-account-onetime-cards" ); const chdAnnouncementsCardEl = document.getElementById( "qortal-account-chd-announcements-card" ); const chdAnnouncementsTitleEl = document.getElementById( "qortal-account-chd-announcements-title" ); const chdAnnouncementsNoteEl = document.getElementById( "qortal-account-chd-announcements-note" ); const chdAnnouncementsBodyEl = document.getElementById( "qortal-account-chd-announcements-body" ); const chdAnnouncementsToggleButton = document.getElementById( "qortal-account-chd-announcements-toggle" ); const chdAnnouncementsRefreshButton = document.getElementById( "qortal-account-chd-announcements-refresh" ); const chdAnnouncementsOpenLink = document.getElementById( "qortal-account-chd-announcements-open" ); const chdAnnouncementsStatusEl = document.getElementById( "qortal-account-chd-announcements-status" ); const chdAnnouncementsFrameEl = document.getElementById( "qortal-account-chd-announcements-frame" ); const chdAnnouncementsLoadingOverlayEl = document.getElementById( "qortal-account-chd-announcements-loading-overlay" ); const chdAnnouncementsLoadingStatusEl = document.getElementById( "qortal-account-chd-announcements-loading-status" ); const chdAnnouncementsLoadingMetaEl = document.getElementById( "qortal-account-chd-announcements-loading-meta" ); const chdAnnouncementsLoadingProgressBarEl = document.getElementById( "qortal-account-chd-announcements-loading-progress-bar" ); const namesBodyEl = document.getElementById("qortal-account-names-body"); const namesTitleEl = document.getElementById("qortal-account-names-title"); const namesToggleButton = document.getElementById( "qortal-account-names-toggle" ); const activityNotificationsCardEl = document.getElementById( "qortal-account-activity-notifications-card" ); const activityNotificationsTitleEl = document.getElementById( "qortal-account-activity-notifications-title" ); const activityNotificationsNoteEl = document.getElementById( "qortal-account-activity-notifications-note" ); const activityNotificationsBodyEl = document.getElementById( "qortal-account-activity-notifications-body" ); const activityNotificationsToggleButton = document.getElementById( "qortal-account-activity-notifications-toggle" ); const activityNotificationsEnabledEl = document.getElementById( "qortal-account-activity-enabled" ); const activityNotificationsChannelNextcloudEl = document.getElementById( "qortal-account-activity-channel-nextcloud" ); const activityNotificationsChannelEmailEl = document.getElementById( "qortal-account-activity-channel-email" ); const activityNotificationsAutoDefaultsEl = document.getElementById( "qortal-account-activity-defaults-auto" ); const activityNotificationsDefaultsToggleButton = document.getElementById( "qortal-account-activity-defaults-toggle" ); const activityNotificationsDefaultsBodyEl = document.getElementById( "qortal-account-activity-defaults-body" ); const activityNotificationsDefaultsListEl = document.getElementById( "qortal-account-activity-defaults-list" ); const activityNotificationsDefaultsEmptyEl = document.getElementById( "qortal-account-activity-defaults-empty" ); const activityNotificationsNewIdentifierEl = document.getElementById( "qortal-account-activity-new-identifier" ); const activityNotificationsNewLabelEl = document.getElementById( "qortal-account-activity-new-label" ); const activityNotificationsNewKindEl = document.getElementById( "qortal-account-activity-new-kind" ); const activityNotificationsNewSourceEl = document.getElementById( "qortal-account-activity-new-source" ); const activityNotificationsNewNamesEl = document.getElementById( "qortal-account-activity-new-names" ); const activityNotificationsGroupsSelectEl = document.getElementById( "qortal-account-activity-groups-select" ); const activityNotificationsRefreshGroupsButton = document.getElementById( "qortal-account-activity-refresh-groups" ); const activityNotificationsAddGroupThreadsButton = document.getElementById( "qortal-account-activity-add-group-threads" ); const activityNotificationsAddGroupAnnouncementsButton = document.getElementById("qortal-account-activity-add-group-announcements"); const activityNotificationsAddButton = document.getElementById( "qortal-account-activity-add-identifier" ); const activityNotificationsSaveButton = document.getElementById( "qortal-account-activity-save" ); const activityNotificationsTestButton = document.getElementById( "qortal-account-activity-test" ); const activityNotificationsTestTrackedButton = document.getElementById( "qortal-account-activity-test-tracked" ); const activityNotificationsMarkReadAllButton = document.getElementById( "qortal-account-activity-mark-read-all" ); const activityNotificationsTrackedListEl = document.getElementById( "qortal-account-activity-tracked-list" ); const activityNotificationsTrackedEmptyEl = document.getElementById( "qortal-account-activity-tracked-empty" ); const activityNotificationsDebugAuditCardEl = document.getElementById( "qortal-account-activity-debug-audit-card" ); const activityNotificationsDebugAuditEl = document.getElementById( "qortal-account-activity-debug-audit" ); const activityNotificationsStatusEl = document.getElementById( "qortal-account-activity-status" ); const registerNameButton = document.getElementById( "qortal-account-register-name-button" ); const downloadBackupButton = document.getElementById( "qortal-account-download-backup" ); const downloadBackupLabelEl = document.getElementById( "qortal-account-download-backup-label" ); const backupModal = document.getElementById("qortal-account-backup-modal"); const backupModalTitleEl = document.getElementById( "qortal-account-backup-modal-title" ); const backupModalNoteEl = document.getElementById( "qortal-account-backup-modal-note" ); const backupWalletSelectEl = document.getElementById( "qortal-account-backup-wallet-select" ); const backupPasswordEl = document.getElementById( "qortal-account-backup-password" ); const backupReencryptToggleEl = document.getElementById( "qortal-account-backup-reencrypt-toggle" ); const backupReencryptFieldsEl = document.getElementById( "qortal-account-backup-reencrypt-fields" ); const backupNewPasswordEl = document.getElementById( "qortal-account-backup-new-password" ); const backupNewPasswordConfirmEl = document.getElementById( "qortal-account-backup-new-password-confirm" ); const backupModalErrorEl = document.getElementById( "qortal-account-backup-modal-error" ); const backupModalStatusEl = document.getElementById( "qortal-account-backup-modal-status" ); const backupSaveFileButton = document.getElementById( "qortal-account-backup-save-file" ); const backupSavePasswordButton = document.getElementById( "qortal-account-backup-save-password" ); const backupModalConfirmButton = document.getElementById( "qortal-account-backup-confirm" ); const backupModalCancelButton = document.getElementById( "qortal-account-backup-cancel" ); const refreshButton = document.getElementById("qortal-account-refresh"); const walletActionsCardEl = document.getElementById( "qortal-account-wallet-actions-card" ); const advancedToggleButton = document.getElementById( "qortal-account-advanced-toggle" ); const advancedBody = document.getElementById("qortal-account-advanced-body"); const sendRecipientEl = document.getElementById( "qortal-account-send-recipient" ); const sendAmountLabelEl = document.getElementById( "qortal-account-send-amount-label" ); const sendAmountEl = document.getElementById("qortal-account-send-amount"); const sendNoteEl = document.getElementById("qortal-account-send-note"); const sendFeeEl = document.getElementById("qortal-account-send-fee"); const sendFeeEditButton = document.getElementById( "qortal-account-send-fee-edit-button" ); const sendButton = document.getElementById("qortal-account-send-button"); const sendButtonLabelEl = document.getElementById( "qortal-account-send-button-label" ); const sendResultEl = document.getElementById("qortal-account-send-result"); const quickSendButton = document.getElementById( "qortal-account-balance-quick-send" ); const quickSendModal = document.getElementById( "qortal-account-quick-send-modal" ); const quickSendTitleEl = document.getElementById( "qortal-account-quick-send-title" ); const quickSendNoteEl = document.getElementById( "qortal-account-quick-send-note" ); const quickSendRecipientLabelEl = document.getElementById( "qortal-account-quick-send-recipient-label" ); const quickSendRecipientEl = document.getElementById( "qortal-account-quick-send-recipient" ); const quickSendAmountLabelEl = document.getElementById( "qortal-account-quick-send-amount-label" ); const quickSendAmountEl = document.getElementById( "qortal-account-quick-send-amount" ); const quickSendErrorEl = document.getElementById( "qortal-account-quick-send-error" ); const quickSendConfirmButton = document.getElementById( "qortal-account-quick-send-confirm" ); const quickSendCancelButton = document.getElementById( "qortal-account-quick-send-cancel" ); const lowBalanceWrapEl = document.getElementById( "qortal-account-low-balance-wrap" ); const purchaseCreditsNowButton = document.getElementById( "qortal-account-purchase-credits-now" ); const transactionsRefreshButton = document.getElementById( "qortal-account-transactions-refresh" ); const transactionsStatusEl = document.getElementById( "qortal-account-transactions-status" ); const transactionsPendingWrapEl = document.getElementById( "qortal-account-transactions-pending-wrap" ); const transactionsShowNewButton = document.getElementById( "qortal-account-transactions-show-new" ); const transactionsPendingNoteEl = document.getElementById( "qortal-account-transactions-pending-note" ); const transactionsEmptyEl = document.getElementById( "qortal-account-transactions-empty" ); const transactionsListEl = document.getElementById( "qortal-account-transactions-list" ); const transactionsLoadMoreWrapEl = document.getElementById( "qortal-account-transactions-load-more-wrap" ); const transactionsLoadMoreButton = document.getElementById( "qortal-account-transactions-load-more" ); const transactionsLoadMoreNoteEl = document.getElementById( "qortal-account-transactions-load-more-note" ); const unconfirmedTransactionsCardEl = document.getElementById( "qortal-account-unconfirmed-transactions" ); const unconfirmedTransactionsRefreshButton = document.getElementById( "qortal-account-unconfirmed-refresh" ); const unconfirmedTransactionsStatusEl = document.getElementById( "qortal-account-unconfirmed-status" ); const unconfirmedTransactionsListEl = document.getElementById( "qortal-account-unconfirmed-list" ); const nodeStatusHeightEl = document.getElementById( "qortal-node-status-height" ); const nodeStatusSyncEl = document.getElementById("qortal-node-status-sync"); const nodeStatusSynchronizingEl = document.getElementById( "qortal-node-status-syncing" ); const nodeStatusPeersEl = document.getElementById("qortal-node-status-peers"); const nodeStatusDataPeersEl = document.getElementById( "qortal-node-status-datapeers" ); const nodeStatusHeightCardEl = document.getElementById( "qortal-node-status-height-card" ); const nodeStatusSyncCardEl = document.getElementById( "qortal-node-status-sync-card" ); const nodeStatusSynchronizingCardEl = document.getElementById( "qortal-node-status-syncing-card" ); const nodeStatusPeersCardEl = document.getElementById( "qortal-node-status-peers-card" ); const nodeStatusDataPeersCardEl = document.getElementById( "qortal-node-status-datapeers-card" ); const testButton = document.getElementById("qortal-account-test"); const liveToggleButton = document.getElementById( "qortal-account-toggle-live" ); const lastRefreshEl = document.getElementById("qortal-account-last-refresh"); const statusEl = document.getElementById("qortal-account-status"); const walletIdEl = document.getElementById("qortal-account-wallet-id"); const publicKeyEl = document.getElementById("qortal-account-public-key"); const balanceRawEl = document.getElementById("qortal-account-balance-raw"); const detailsEl = document.getElementById("qortal-account-details"); const registerModal = document.getElementById( "qortal-account-register-modal" ); const registerCostNoteEl = document.getElementById( "qortal-account-register-cost-note" ); const registerNameInputEl = document.getElementById( "qortal-account-register-name-input" ); const registerErrorEl = document.getElementById( "qortal-account-register-error" ); const registerSuccessEl = document.getElementById( "qortal-account-register-success" ); const registerSuccessNoteEl = document.getElementById( "qortal-account-register-success-note" ); const registerSubmitActionsEl = document.getElementById( "qortal-account-register-submit-actions" ); const registerSuccessActionsEl = document.getElementById( "qortal-account-register-success-actions" ); const registerOutputToggleButton = document.getElementById( "qortal-account-register-output-toggle" ); const registerOkButton = document.getElementById( "qortal-account-register-ok" ); const registerResultEl = document.getElementById( "qortal-account-register-result" ); const registerConfirmButton = document.getElementById( "qortal-account-register-confirm" ); const registerCancelButton = document.getElementById( "qortal-account-register-cancel" ); const approvalModal = document.getElementById("qortal-account-approval"); const approvalRequestEl = document.getElementById( "qortal-account-approval-request" ); const approvalWalletStatusEl = document.getElementById( "qortal-account-approval-wallet-status" ); const approvalUnlockFieldsEl = document.getElementById( "qortal-account-approval-unlock-fields" ); const approvalPasswordEl = document.getElementById( "qortal-account-approval-password" ); const approvalTtlEl = document.getElementById("qortal-account-approval-ttl"); const approvalErrorEl = document.getElementById( "qortal-account-approval-error" ); const approvalConfirmButton = document.getElementById( "qortal-account-approval-confirm" ); const approvalCancelButton = document.getElementById( "qortal-account-approval-cancel" ); const refreshIntervalMs = 15_000; const unconfirmedRefreshIntervalMs = 10_000; const transactionPageSize = 100; let intervalId = null; let unconfirmedIntervalId = null; let inFlight = false; let liveEnabled = true; let unconfirmedChatCollapsed = true; let latestRenderedConfirmedTxMarker = ""; let pendingConfirmedTxCount = 0; let confirmedTransactionsOffset = 0; let confirmedTransactionsHasMore = false; let confirmedTransactionsLoadingMore = false; let renderedConfirmedTxMarkers = new Set(); let walletLockState = { known: false, unlocked: null, expiresAtMs: null, }; let activeWalletContext = { hasWallet: false, mapping: null, wallet: null, address: "", balance: null, primaryName: "", names: [], accountDetails: null, }; let availableWalletMappings = []; let pendingNameRegistration = false; let pendingRegisteredName = ""; let hasPendingInitialGrantRequest = false; let hasUserRequestHistory = false; const ADMIN_REQUESTS_PAGE_SIZE = 10; let adminRequestsOffset = 0; const txRecipientLabelCache = new Map(); let currentPackageTier = initialPackageTier; let currentMonthlyCredits = Number.isFinite(initialMonthlyCredits) ? initialMonthlyCredits : 0; let currentChdInstanceId = initialChdInstanceId; let activityNotificationsConfig = null; let activityNotificationsLoaded = false; let activityNotificationsDebugDismissAudit = []; let activityNotificationsDebugLastPoll = null; let activityNotificationsAvailableDefaults = []; let activityNotificationsGroupsOptions = []; let activityNotificationsGroupsRetryTimer = null; const billingConnectorEnabled = scBillingEnabled && scBillingManagedByChd; const billingAutoSyncIntervalMs = 180000; const billingCatalogRetryDelayMs = 800; const billingCatalogMaxAttempts = 3; let billingAutoSyncInFlight = false; let lastBillingAutoSyncAtMs = 0; let purchasesCatalogSource = "defaults"; let purchasesCatalogReady = !billingConnectorEnabled || !billingCatalogUrl; let purchasesCatalogLoadState = purchasesCatalogReady ? "idle" : "loading"; let purchasesSubscriptionsLiveReady = !billingConnectorEnabled || !billingCatalogUrl; let purchasesOneTimeLiveReady = !billingConnectorEnabled || !billingCatalogUrl; let cloudAppInstalled = false; const passwordsDraftStorageKey = "nuqloud_passwords_app_draft"; let subscriptionPackages = []; let oneTimePurchases = []; let qappsNames = []; let qappsCards = []; try { const parsedQappsNames = JSON.parse(qappsNamesRaw); if (Array.isArray(parsedQappsNames)) { qappsNames = parsedQappsNames .map(function (entry) { return typeof entry === "string" ? entry.trim() : ""; }) .filter(function (entry) { return entry !== ""; }); } } catch (_error) { qappsNames = []; } try { const parsedQappsCards = JSON.parse(qappsCardsRaw); if (Array.isArray(parsedQappsCards)) { qappsCards = parsedQappsCards .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: entry.address.trim(), description: typeof entry.description === "string" ? entry.description.trim() : "", iconMode: typeof entry.iconMode === "string" && entry.iconMode.trim() !== "" ? entry.iconMode.trim().toLowerCase() : "auto", iconUrl: typeof entry.iconUrl === "string" ? entry.iconUrl.trim() : "", iconFromName: typeof entry.iconFromName === "string" ? entry.iconFromName.trim() : typeof entry.iconSourceName === "string" ? entry.iconSourceName.trim() : "", }; }); qappsCards = qappsCards.map(function (entry) { if (entry.iconMode !== "custom" && entry.iconMode !== "name") { entry.iconMode = "auto"; } return entry; }); } } catch (_error) { qappsCards = []; } if (effectiveDebugEnabled) { const debugCards = root.querySelectorAll(".qortal-debug-only"); debugCards.forEach(function (el) { el.classList.remove("qortal-hidden"); }); } function setStatus(message, isError) { if (!statusEl) { return; } statusEl.textContent = message; statusEl.classList.toggle("qortal-error", Boolean(isError)); } function notifyDashboard(message, isError) { const text = String(message || "").trim(); if (!text) { return; } if ( typeof OC !== "undefined" && OC.Notification && typeof OC.Notification.showTemporary === "function" ) { OC.Notification.showTemporary(text); return; } if (isError) { console.error("[QortalDashboard]", text); return; } console.log("[QortalDashboard]", text); } function buildSendSuccessNotice(recipient, amount, unit) { const template = String( modeCopy.sendSuccessNotice || "Sent {amount} {unit} to {recipient}." ); return template .replace("{amount}", String(amount || "").trim() || "0") .replace("{unit}", String(unit || modeCopy.txUnit || "").trim()) .replace("{recipient}", String(recipient || "").trim() || "recipient"); } function updateLastRefresh() { if (lastRefreshEl) { lastRefreshEl.textContent = new Date().toLocaleTimeString(); } } function getNextcloudBaseUrl() { return ( nextcloudPublicUrl || window.location.origin + (typeof OC !== "undefined" && OC.webroot ? OC.webroot : "") ).replace(/\/$/, ""); } function toTitleCaseWord(word) { const value = String(word || "").trim(); if (!value) { return ""; } if (/^qortal$/i.test(value)) { return "Qortal"; } if (/^nuqloud$/i.test(value)) { return "NuQloud"; } return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase(); } function normalizeCloudBrandLabel(value) { const raw = String(value || "").trim(); if (!raw) { return ""; } const firstSegment = raw.split(/\s+(?:\-|\||:)\s+/)[0] || raw; const cleaned = firstSegment .replace(/\s+dashboard$/i, "") .replace(/\s+account$/i, "") .trim(); if (!cleaned) { return ""; } if (!/\s/.test(cleaned) && /^[a-z0-9.-]+$/i.test(cleaned)) { const hostBits = cleaned.split(".").filter(function (part) { return ( part !== "" && !/^(www|com|net|org|cloud|io|app|co|dev|local|localhost)$/i.test(part) ); }); if (hostBits.length > 0) { return toTitleCaseWord(hostBits[0]); } } return cleaned; } function resolveCloudAppBrandName() { const explicitBrand = normalizeCloudBrandLabel(cloudBrandName); if (explicitBrand) { return explicitBrand; } const documentBrand = normalizeCloudBrandLabel(document.title); if (documentBrand) { return documentBrand; } let host = ""; if (nextcloudPublicUrl) { try { host = new URL(nextcloudPublicUrl).hostname || ""; } catch (_error) { host = ""; } } if (!host && window.location && window.location.hostname) { host = window.location.hostname; } const hostBrand = normalizeCloudBrandLabel(host); if (hostBrand) { return hostBrand; } return isPoweredMode ? "NuQloud" : "Qortal"; } function setSettingsLink() { const base = getNextcloudBaseUrl(); const href = base + settingsPath; if (settingsLink) { settingsLink.href = href; settingsLink.title = modeCopy.settingsButtonTitle || modeCopy.settingsButton; settingsLink.setAttribute( "aria-label", modeCopy.settingsButtonTitle || modeCopy.settingsButton ); } if (backupSettingsLink) { backupSettingsLink.href = href; } } function detectInstalledCloudApp() { if ( window.matchMedia && window.matchMedia("(display-mode: standalone)").matches ) { return true; } return Boolean(window.navigator && window.navigator.standalone); } function updateInstallPwaVisibility() { if (!installPwaLink) { return; } installPwaLink.classList.toggle("qortal-hidden", !customPwaInstallUrl); } function getInstallPwaButtonLabel() { const appBrandName = resolveCloudAppBrandName(); if (cloudAppInstalled) { return appBrandName + " App Status"; } return "Install " + appBrandName + " App"; } function setInstallPwaLink() { if (!installPwaLink) { return; } const base = getNextcloudBaseUrl(); if (/^https?:\/\//i.test(customPwaInstallUrl)) { installPwaLink.href = customPwaInstallUrl; } else { installPwaLink.href = base + customPwaInstallUrl; } installPwaLink.target = "_blank"; installPwaLink.rel = "noopener"; const label = getInstallPwaButtonLabel(); installPwaLink.classList.toggle( "qortal-icon-button--install", !cloudAppInstalled ); installPwaLink.classList.toggle( "qortal-icon-button--status", cloudAppInstalled ); installPwaLink.title = label; installPwaLink.setAttribute("aria-label", label); if (installPwaSrLabelEl) { installPwaSrLabelEl.textContent = label; } if (installPwaLabelEl) { installPwaLabelEl.textContent = label; } updateInstallPwaVisibility(); } function setAdminSettingsLink() { if (!adminSettingsLink) { return; } const base = getNextcloudBaseUrl(); adminSettingsLink.href = base + adminSettingsPath; } function setChdAdminLink() { if (!chdAdminLink) { return; } if (!isAdmin || !chdAdminDashboardEnabled) { chdAdminLink.classList.add("qortal-hidden"); return; } const base = getNextcloudBaseUrl(); chdAdminLink.href = /^https?:\/\//i.test(chdAdminPath) ? chdAdminPath : base + chdAdminPath; chdAdminLink.classList.remove("qortal-hidden"); } function applyModeCopy() { if (pageTitleEl) { pageTitleEl.textContent = modeCopy.dashboardTitle; } if (pageHelpEl) { pageHelpEl.textContent = modeCopy.dashboardHelp; } if (onboardingTitleEl) { onboardingTitleEl.textContent = modeCopy.onboardingTitle; } if (onboardingNoteEl) { onboardingNoteEl.textContent = modeCopy.onboardingNote; } if (addressLabelEl) { addressLabelEl.textContent = modeCopy.addressLabel; } if (balanceLabelEl) { balanceLabelEl.textContent = modeCopy.balanceLabel; } if (downloadBackupLabelEl) { downloadBackupLabelEl.textContent = modeCopy.downloadBackupButton; } if (downloadBackupButton) { downloadBackupButton.title = modeCopy.downloadBackupButton; } if (backupModalTitleEl) { backupModalTitleEl.textContent = modeCopy.downloadBackupModalTitle; } if (backupModalNoteEl) { backupModalNoteEl.textContent = modeCopy.backupModalNote; } if (backupModalConfirmButton) { backupModalConfirmButton.textContent = modeCopy.downloadBackupAction; } if (backupSaveFileButton) { backupSaveFileButton.textContent = modeCopy.saveBackupToFilesButton; } if (backupSavePasswordButton) { backupSavePasswordButton.textContent = modeCopy.addPasswordToPasswordsButton; } if (backupSettingsLink) { backupSettingsLink.textContent = modeCopy.backupSettingsButton; backupSettingsLink.title = modeCopy.backupSettingsButton; backupSettingsLink.setAttribute( "aria-label", modeCopy.backupSettingsButton ); } if (requestQortButton) { requestQortButton.textContent = modeCopy.requestButton; } if (requestQortHelp) { requestQortHelp.textContent = modeCopy.requestHelp; } if (balanceModeNoteEl) { const note = String(modeCopy.balanceModeNote || "").trim(); balanceModeNoteEl.textContent = note; balanceModeNoteEl.classList.toggle("qortal-hidden", note === ""); } if (registerCostNoteEl) { registerCostNoteEl.textContent = modeCopy.registerCostNote || ""; } if (appGalleryTitleEl) { appGalleryTitleEl.textContent = modeCopy.appGalleryTitle; } if (appGalleryNoteEl) { appGalleryNoteEl.textContent = modeCopy.appGalleryNote; } if (purchasesTitleEl) { purchasesTitleEl.textContent = modeCopy.purchasesTitle; } if (purchasesLoadingLabelEl) { purchasesLoadingLabelEl.textContent = modeCopy.purchasesLoading; } if (purchasesSubscriptionsTitleEl) { purchasesSubscriptionsTitleEl.textContent = modeCopy.purchasesSubscriptionsTitle; } if (purchasesOneTimeTitleEl) { purchasesOneTimeTitleEl.textContent = modeCopy.purchasesOneTimeTitle; } if (userRequestsTitleEl) { userRequestsTitleEl.textContent = modeCopy.userRequestsTitle; } if (chdAnnouncementsTitleEl) { chdAnnouncementsTitleEl.textContent = modeCopy.chdAnnouncementsTitle; } if (chdAnnouncementsNoteEl) { chdAnnouncementsNoteEl.textContent = modeCopy.chdAnnouncementsNote; } if (namesTitleEl) { namesTitleEl.textContent = modeCopy.namesTitle; } if (activityNotificationsTitleEl) { activityNotificationsTitleEl.textContent = modeCopy.activityNotificationsTitle; } if (activityNotificationsNoteEl) { activityNotificationsNoteEl.textContent = modeCopy.activityNotificationsNote; } if (quickSendTitleEl) { quickSendTitleEl.textContent = modeCopy.quickSendTitle; } if (quickSendNoteEl) { quickSendNoteEl.textContent = modeCopy.quickSendNote; } if (quickSendRecipientLabelEl) { quickSendRecipientLabelEl.textContent = "Recipient (address or name)"; } if (quickSendAmountLabelEl) { quickSendAmountLabelEl.textContent = modeCopy.sendAmountLabel; } if (quickSendAmountEl) { quickSendAmountEl.placeholder = modeCopy.sendPlaceholder || ""; quickSendAmountEl.step = isPoweredMode ? "1" : "0.00000001"; quickSendAmountEl.min = isPoweredMode ? "1" : "0.00000001"; } if (quickSendButton) { quickSendButton.title = modeCopy.quickSendLabel; quickSendButton.setAttribute("aria-label", modeCopy.quickSendLabel); } if (quickSendConfirmButton) { quickSendConfirmButton.textContent = modeCopy.quickSendButtonLabel; } if (purchaseCreditsNowButton) { purchaseCreditsNowButton.textContent = modeCopy.lowBalanceCta; } setInstallPwaLink(); if (refreshButton) { refreshButton.title = modeCopy.refreshButtonTitle; refreshButton.setAttribute("aria-label", modeCopy.refreshButtonTitle); } if (settingsLink) { settingsLink.title = modeCopy.settingsButtonTitle || modeCopy.settingsButton; settingsLink.setAttribute( "aria-label", modeCopy.settingsButtonTitle || modeCopy.settingsButton ); } if (sendAmountLabelEl) { sendAmountLabelEl.textContent = modeCopy.sendAmountLabel; } if (sendAmountEl) { sendAmountEl.placeholder = modeCopy.sendPlaceholder; if (isPoweredMode) { sendAmountEl.min = "1"; sendAmountEl.step = "1"; } else { sendAmountEl.min = "0.00000001"; sendAmountEl.step = "0.00000001"; } } if (sendButtonLabelEl) { sendButtonLabelEl.textContent = modeCopy.sendButtonLabel; } if (sendNoteEl) { sendNoteEl.textContent = modeCopy.sendNote; } if (adminOpsNoteEl) { adminOpsNoteEl.textContent = modeCopy.adminOpsNote; } if (inviteManagementTitleEl) { inviteManagementTitleEl.textContent = isPoweredMode ? "NuQloud Invite Tokens" : "Invite Tokens"; } if (inviteManagementNoteEl) { inviteManagementNoteEl.textContent = "Create invite links for new users. The email address is saved as the invite comment for tracking. You can optionally send the invite email now, or send it later from the invite table."; } root.setAttribute("data-mode-active", sovereignMode); } function getBalanceQort(value) { const numeric = parseBalanceNumber(value); return numeric === null ? null : numeric; } function isAdminQueueEligible(context) { if (!isAdmin) { return false; } const source = context && typeof context === "object" ? context : activeWalletContext; const balanceQort = getBalanceQort(source && source.balance); if (balanceQort === null) { return false; } return balanceQort > ADMIN_QUEUE_MIN_QORT; } function isInstallGrantRequestMode(context) { return isPoweredMode && isAdmin && !isAdminQueueEligible(context); } function shouldShowUserRequestsSection(context) { if (isAdmin && !isAdminQueueEligible(context)) { return hasUserRequestHistory || hasPendingInitialGrantRequest; } if (!canUserPublish) { return false; } return !isAdmin && (hasUserRequestHistory || hasPendingInitialGrantRequest); } function shouldShowAdminQueueSection(context) { return isAdmin && isAdminQueueEligible(context); } function applyRequestCopy(context) { const showUserRequests = shouldShowUserRequestsSection(context); const installGrantMode = isInstallGrantRequestMode(context); if (userRequestsTitleEl) { if (installGrantMode) { userRequestsTitleEl.textContent = modeCopy.userRequestsTitleInstallGrant || modeCopy.userRequestsTitle; } else if (showUserRequests) { userRequestsTitleEl.textContent = modeCopy.userRequestsTitleFromAdmins || modeCopy.userRequestsTitle; } else { userRequestsTitleEl.textContent = modeCopy.userRequestsTitle; } } if (requestQortButton) { if (installGrantMode) { requestQortButton.textContent = modeCopy.requestButtonInstallGrant || modeCopy.requestButton; } else if (showUserRequests) { requestQortButton.textContent = modeCopy.requestButtonFromAdmins || modeCopy.requestButton; } else { requestQortButton.textContent = modeCopy.requestButton; } } if (requestQortHelp) { if (installGrantMode) { requestQortHelp.textContent = modeCopy.requestHelpInstallGrant || modeCopy.requestHelp; } else if (showUserRequests) { requestQortHelp.textContent = modeCopy.requestHelpFromAdmins || modeCopy.requestHelp; } else { requestQortHelp.textContent = modeCopy.requestHelp; } } updateRequestDetailsText(context); } function applyUserPublishingVisibility(context) { const publishingDisabled = isPoweredMode && !canUserPublish && !isAdmin; const showAdminOps = shouldShowAdminQueueSection(context); const showUserRequests = shouldShowUserRequestsSection(context); showCard(publishingDisabledCardEl, publishingDisabled); showCard(userRequestsCardEl, showUserRequests); showCard(adminRequestsCardEl, showAdminOps); showCard(adminOpsCardEl, showAdminOps); showCard(inviteManagementCardEl, isAdmin); if (showUserRequests && !hasPendingInitialGrantRequest) { setSectionCollapsed( userRequestsToggleButton, userRequestsBodyEl, "user_requests" ); } showCard(walletActionsCardEl, canUserPublish || isAdmin); if (requestQortWrapEl && (!canUserPublish || showAdminOps)) { requestQortWrapEl.classList.add("qortal-hidden"); } if (registerNameButton && !canUserPublish && !isAdmin) { registerNameButton.classList.add("qortal-hidden"); registerNameButton.disabled = true; registerNameButton.title = "User publishing is disabled by admin"; } if (!canUserPublish && !isAdmin && advancedBody) { advancedBody.classList.add("qortal-hidden"); } if (advancedToggleButton && !canUserPublish && !isAdmin) { advancedToggleButton.classList.add("qortal-hidden"); } showCard(purchasesCardEl, isAdmin || billingUserCheckoutUrl !== ""); applyRequestCopy(context); } function applyPurchasesCopy() { let noteText = modeCopy.purchasesNote; if (purchasesNoteEl) { if (!scBillingEnabled) { noteText = modeCopy.purchasesBillingDisabled; } else if (!scBillingManagedByChd) { noteText = modeCopy.purchasesConnectorDisabled; } if (effectiveDebugEnabled) { noteText += " [Catalog source: " + purchasesCatalogSource + "]"; } purchasesNoteEl.textContent = noteText; } if (purchasesSupportNoteEl) { purchasesSupportNoteEl.textContent = modeCopy.purchasesSupportNote || modeCopy.purchasesFallbackNotice; } if (purchasesSupportMessageEl) { purchasesSupportMessageEl.placeholder = modeCopy.purchasesSupportMessagePlaceholder || ""; } if (purchasesSupportSendButton) { purchasesSupportSendButton.textContent = modeCopy.purchasesSupportSend || "Send to NuQloud Support"; } if (purchasesSupportMailtoLink) { purchasesSupportMailtoLink.textContent = modeCopy.purchasesSupportManual || "Email issues@NuQloud.cloud"; } if (purchasesSupportEmailEl) { if (currentUserEmail) { purchasesSupportEmailEl.textContent = (modeCopy.purchasesSupportEmailLabel || "Signed-in email") + ": " + currentUserEmail; purchasesSupportEmailEl.classList.remove("qortal-hidden"); } else { purchasesSupportEmailEl.classList.add("qortal-hidden"); } } } function hasLivePurchasesCatalog() { return purchasesCatalogReady && purchasesCatalogSource === "live"; } function hasLiveSubscriptionCatalog() { return purchasesCatalogReady && purchasesSubscriptionsLiveReady; } function hasLiveOneTimeCatalog() { return purchasesCatalogReady && purchasesOneTimeLiveReady; } function buildPurchasesSupportMailtoHref() { const subject = encodeURIComponent( "NuQloud package load issue" + (currentChdInstanceId ? " - " + currentChdInstanceId : "") ); const bodyLines = []; if (currentUserEmail) { bodyLines.push("Signed-in email: " + currentUserEmail); } if (currentChdInstanceId) { bodyLines.push("Instance ID: " + currentChdInstanceId); } bodyLines.push("Catalog source: " + purchasesCatalogSource); bodyLines.push("Catalog status: " + purchasesCatalogLoadState); bodyLines.push(""); bodyLines.push("Describe what happened:"); return ( "mailto:issues@NuQloud.cloud?subject=" + subject + "&body=" + encodeURIComponent(bodyLines.join("\n")) ); } function setPurchasesSupportStatus(message, isError) { if (!purchasesSupportStatusEl) { return; } const text = String(message || "").trim(); purchasesSupportStatusEl.textContent = text; purchasesSupportStatusEl.classList.toggle("qortal-hidden", text === ""); purchasesSupportStatusEl.classList.toggle("qortal-error", Boolean(isError)); } function updatePurchasesSupportUi() { if (purchasesSupportMailtoLink) { purchasesSupportMailtoLink.href = buildPurchasesSupportMailtoHref(); } if (!purchasesSupportEl) { return; } const showSupport = billingConnectorEnabled && purchasesCatalogLoadState === "fallback"; purchasesSupportEl.classList.toggle("qortal-hidden", !showSupport); } function updatePurchasesLoadUi() { const isLoading = purchasesCatalogLoadState === "loading"; const showLiveSubscriptions = hasLiveSubscriptionCatalog(); const showLiveOneTime = hasLiveOneTimeCatalog(); if (purchasesLoadingEl) { purchasesLoadingEl.classList.toggle("qortal-hidden", !isLoading); } if (purchasesStatusEl) { purchasesStatusEl.classList.toggle( "qortal-hidden", purchasesCatalogLoadState === "idle" || purchasesCatalogLoadState === "loading" ); } if (purchasesSubscriptionsSectionEl) { purchasesSubscriptionsSectionEl.classList.toggle( "qortal-hidden", !showLiveSubscriptions ); } if (purchasesOneTimeSectionEl) { purchasesOneTimeSectionEl.classList.toggle( "qortal-hidden", !showLiveOneTime ); } updatePurchasesSupportUi(); } function setPurchasesLoadState(state, message, isError) { purchasesCatalogLoadState = state; if (purchasesStatusEl) { purchasesStatusEl.textContent = message || ""; purchasesStatusEl.classList.toggle("qortal-error", Boolean(isError)); } if (state !== "fallback") { setPurchasesSupportStatus("", false); } if (purchasesLoadingLabelEl && state === "loading") { purchasesLoadingLabelEl.textContent = message || modeCopy.purchasesLoading; } updatePurchasesLoadUi(); } function setPurchasesCatalogSource(source) { const normalized = String(source || "") .trim() .toLowerCase(); purchasesCatalogSource = normalized === "live" ? "live" : "defaults"; applyPurchasesCopy(); } function getSectionStorageKey(storageKey) { return ( "qortal_account_section_" + String(storageKey || "") .trim() .toLowerCase() ); } function setSectionExpanded(button, body, expanded) { if (!button || !body) { return; } const isExpanded = Boolean(expanded); body.classList.toggle("qortal-hidden", !isExpanded); button.textContent = isExpanded ? "Hide" : "Show"; button.setAttribute("aria-expanded", isExpanded ? "true" : "false"); } function setSectionCollapsed(button, body, storageKey) { setSectionExpanded(button, body, false); try { if (window.localStorage) { window.localStorage.setItem(getSectionStorageKey(storageKey), "hide"); } } catch (_error) { // ignore localStorage write errors } } function initializeSectionToggle(button, body, storageKey, defaultExpanded) { if (!button || !body) { return; } let expanded = Boolean(defaultExpanded); try { const stored = window.localStorage ? window.localStorage.getItem(getSectionStorageKey(storageKey)) : null; if (stored === "show") { expanded = true; } if (stored === "hide") { expanded = false; } } catch (_error) { // ignore localStorage read errors } setSectionExpanded(button, body, expanded); button.addEventListener("click", function () { const nextExpanded = body.classList.contains("qortal-hidden"); setSectionExpanded(button, body, nextExpanded); try { if (window.localStorage) { window.localStorage.setItem( getSectionStorageKey(storageKey), nextExpanded ? "show" : "hide" ); } } catch (_error) { // ignore localStorage write errors } }); } function getTierRank(tier) { switch (String(tier || "").trim()) { case "replication_basic": return 1; case "replication_standard": return 2; case "replication_enterprise": return 3; case "nuqloud_starter": return 1; case "nuqloud_advanced": return 2; case "nuqloud_pro": return 3; case "nuqloud_team_starter": return 4; case "nuqloud_team_advanced": return 5; case "nuqloud_team_pro": return 6; case "nuqloud_branded": return 7; case "nuqloud_branded_pro": return 8; case "nuqloud_branded_enterprise": return 9; case "free_core": default: return 0; } } function normalizeTierKey(tier) { const normalized = String(tier || "") .trim() .toLowerCase(); switch (normalized) { case "replication_basic": return "nuqloud_starter"; case "replication_standard": return "nuqloud_advanced"; case "replication_enterprise": return "nuqloud_pro"; case "nuqloud_team": return "nuqloud_team_starter"; case "free_core": case "nuqloud_starter": case "nuqloud_advanced": case "nuqloud_pro": case "nuqloud_team_starter": case "nuqloud_team_advanced": case "nuqloud_team_pro": case "nuqloud_branded": case "nuqloud_branded_pro": case "nuqloud_branded_enterprise": return normalized; default: return ""; } } function deriveTierFromEntry(code, tier, name) { const normalizedTier = normalizeTierKey(tier); if (normalizedTier !== "") { return normalizedTier; } const codeKey = String(code || "") .trim() .toLowerCase(); if (normalizeTierKey(codeKey) !== "") { return codeKey; } const nameKey = String(name || "") .trim() .toLowerCase(); if (nameKey.indexOf("free core") !== -1) { return "free_core"; } if (nameKey.indexOf("replication basic") !== -1) { return "nuqloud_starter"; } if (nameKey.indexOf("replication standard") !== -1) { return "nuqloud_advanced"; } if (nameKey.indexOf("replication enterprise") !== -1) { return "nuqloud_pro"; } if (nameKey.indexOf("nuqloud starter") !== -1) { return "nuqloud_starter"; } if (nameKey.indexOf("nuqloud advanced") !== -1) { return "nuqloud_advanced"; } if (nameKey.indexOf("nuqloud pro") !== -1) { return "nuqloud_pro"; } if (nameKey.indexOf("team starter") !== -1) { return "nuqloud_team_starter"; } if (nameKey.indexOf("team advanced") !== -1) { return "nuqloud_team_advanced"; } if (nameKey.indexOf("team pro") !== -1) { return "nuqloud_team_pro"; } if (nameKey.indexOf("branded enterprise") !== -1) { return "nuqloud_branded_enterprise"; } if (nameKey.indexOf("branded pro") !== -1) { return "nuqloud_branded_pro"; } if (nameKey.indexOf("branded") !== -1) { return "nuqloud_branded"; } return ""; } function enrichSubscriptionCatalogEntry(entry) { const normalized = Object.assign({}, entry); normalized.tier = deriveTierFromEntry( normalized.code, normalized.tier, normalized.name ); normalized.name = String(normalized.name || "").trim(); normalized.price = String(normalized.price || "").trim(); normalized.summary = String(normalized.summary || "").trim(); normalized.features = Array.isArray(normalized.features) ? normalized.features.slice() : []; return normalized; } function enrichAddonCatalogEntry(entry) { const normalized = Object.assign({}, entry); normalized.name = String(normalized.name || "").trim(); normalized.summary = String(normalized.summary || "").trim(); normalized.actionLabel = String(normalized.actionLabel || "").trim() || "Buy add-on"; normalized.price = String(normalized.price || "").trim(); normalized.codeByTier = normalized.codeByTier && typeof normalized.codeByTier === "object" ? Object.assign({}, normalized.codeByTier) : undefined; normalized.priceByTier = normalized.priceByTier && typeof normalized.priceByTier === "object" ? Object.assign({}, normalized.priceByTier) : undefined; if (!normalized.purchaseInterval) { normalized.purchaseInterval = isMonthlyAddonItem(normalized) ? "monthly" : "one_time"; } return normalized; } function compareSubscriptionEntries(a, b) { const rankDelta = getTierRank(a.tier) - getTierRank(b.tier); if (rankDelta !== 0) { return rankDelta; } return String(a.name || "").localeCompare(String(b.name || ""), undefined, { sensitivity: "base", }); } function compareAddonEntries(a, b) { const monthlyA = isMonthlyAddonItem(a) ? 1 : 0; const monthlyB = isMonthlyAddonItem(b) ? 1 : 0; if (monthlyA !== monthlyB) { return monthlyA - monthlyB; } return String(a.name || "").localeCompare(String(b.name || ""), undefined, { sensitivity: "base", }); } function getAddonTierKeyFromCode(code) { const normalized = String(code || "") .trim() .toLowerCase(); if (!normalized) { return ""; } const isCapacityAddon = normalized.includes("capacity_") || normalized.includes("capacity-"); const hasMonthlyMarker = normalized.includes("_monthly") || normalized.includes("-monthly"); if ( !isCapacityAddon || (!hasMonthlyMarker && !normalized.startsWith("capacity-")) ) { return ""; } if (/_enterprise$/.test(normalized)) { return "nuqloud_pro"; } if (/_standard$/.test(normalized)) { return "nuqloud_advanced"; } if (/_basic$/.test(normalized)) { return "nuqloud_starter"; } return ""; } function isMonthlyAddonItem(item) { if (!item || typeof item !== "object") { return false; } const interval = String(item.purchaseInterval || "") .trim() .toLowerCase(); const type = String(item.purchaseType || "") .trim() .toLowerCase(); const code = String(item.code || "") .trim() .toLowerCase(); return ( interval === "monthly" || type === "monthly_addon" || code.indexOf("_monthly") !== -1 || code.indexOf("-monthly") !== -1 || code.indexOf("capacity_") === 0 || code.indexOf("capacity-") === 0 || code.indexOf("storage-") === 0 || code.indexOf("storage") === 0 || code.indexOf("capacity") !== -1 ); } function monthlyAddonMatchesActiveTier(item, activeTier) { if (!isMonthlyAddonItem(item)) { return true; } if (item.codeByTier && typeof item.codeByTier === "object") { // Grouped tier-aware entry: keep one card and let existing codeByTier resolution handle the active tier. return true; } const targetTier = getAddonTierKeyFromCode(item.code); if (!targetTier) { return true; } const normalizedActiveTier = normalizeTierKey(activeTier || "free_core") || "free_core"; if (targetTier === "nuqloud_starter") { return ( normalizedActiveTier === "free_core" || normalizedActiveTier === "nuqloud_starter" ); } return normalizedActiveTier === targetTier; } function isExistingClientInstance() { if (currentChdInstanceId !== "") { return true; } if (currentPackageTier !== "free_core") { return true; } return Number.isFinite(currentMonthlyCredits) && currentMonthlyCredits > 0; } function parseIntegerSetting(value, fallbackValue) { const parsed = Number.parseInt( String(value === undefined || value === null ? "" : value).trim(), 10 ); return Number.isFinite(parsed) ? parsed : fallbackValue; } function applyBillingSettingsFromPayload(payload) { const data = payload && typeof payload === "object" ? payload : null; const settings = data && data.settings && typeof data.settings === "object" ? data.settings : null; if (!settings) { return false; } let changed = false; const packageTier = String(settings.scPackageTier || "").trim(); if (packageTier !== "" && packageTier !== currentPackageTier) { currentPackageTier = packageTier; changed = true; } const monthlyCredits = parseIntegerSetting( settings.scMonthlyCredits, currentMonthlyCredits ); if (monthlyCredits !== currentMonthlyCredits) { currentMonthlyCredits = monthlyCredits; changed = true; } const chdInstanceId = String(settings.scChdInstanceId || "").trim(); if (chdInstanceId !== currentChdInstanceId) { currentChdInstanceId = chdInstanceId; changed = true; } return changed; } function shouldRunBillingAutoSync(context) { if (!isAdmin || !billingConnectorEnabled) { return false; } if (!billingSyncUrl && !billingBootstrapUrl) { return false; } if (billingAutoSyncInFlight) { return false; } if (context === "init" || context === "manual" || context === "test") { return true; } return Date.now() - lastBillingAutoSyncAtMs >= billingAutoSyncIntervalMs; } async function refreshBillingConnectorState(context) { if (!shouldRunBillingAutoSync(context)) { return; } const endpoint = currentChdInstanceId !== "" ? billingSyncUrl : billingBootstrapUrl; if (!endpoint) { return; } billingAutoSyncInFlight = true; lastBillingAutoSyncAtMs = Date.now(); try { const payload = await requestJson(endpoint, { method: "POST", headers: getCsrfHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), body: "", }); const billingSettingsChanged = applyBillingSettingsFromPayload(payload); if (billingSettingsChanged) { renderPurchases(); } await refreshBillingCatalog(context); } catch (error) { if (context === "manual" || context === "test") { setStatus(error.message || "billing_sync_failed", true); } } finally { billingAutoSyncInFlight = false; } } async function openBillingCheckout(checkoutOptions) { const options = checkoutOptions && typeof checkoutOptions === "object" ? checkoutOptions : {}; const requestedScope = String(options.billingScope || "") .trim() .toLowerCase(); const billingScope = requestedScope === "user" ? "user" : "instance"; const checkoutEndpoint = billingScope === "user" ? billingUserCheckoutUrl : billingCheckoutUrl; if (billingScope === "instance" && !isAdmin) { throw new Error(modeCopy.purchasesAdminOnly); } if (!checkoutEndpoint) { throw new Error(modeCopy.purchasesEndpointMissing); } if (!scBillingEnabled) { throw new Error(modeCopy.purchasesBillingDisabled); } if (!scBillingManagedByChd) { throw new Error(modeCopy.purchasesConnectorDisabled); } const purchaseMeta = options.purchaseMeta && typeof options.purchaseMeta === "object" ? options.purchaseMeta : null; if (purchaseMeta) { const purchaseCategory = String(purchaseMeta.purchaseCategory || "") .trim() .toLowerCase(); const hasRequiredLiveCatalog = purchaseCategory === "package" ? hasLiveSubscriptionCatalog() : hasLiveOneTimeCatalog(); if (!hasRequiredLiveCatalog) { throw new Error( modeCopy.purchasesCatalogLiveRequired || "Packages must load from the connector before checkout can open." ); } } const purchaseType = String( purchaseMeta && purchaseMeta.purchaseType ? purchaseMeta.purchaseType : "" ) .trim() .toLowerCase(); const purchaseCategory = String( purchaseMeta && purchaseMeta.purchaseCategory ? purchaseMeta.purchaseCategory : "" ) .trim() .toLowerCase(); const purchaseCode = String( purchaseMeta && purchaseMeta.purchaseCode ? purchaseMeta.purchaseCode : "" ).trim(); let selectedTier = String(options.packageTier || "").trim() || currentPackageTier || "free_core"; if ( purchaseCode && (purchaseType === "subscription" || purchaseCategory === "package") ) { selectedTier = purchaseCode; } const body = new URLSearchParams({ packageTier: selectedTier, }); if (purchaseMeta) { [ "checkoutType", "purchaseType", "purchaseCategory", "purchaseCode", "productId", "purchaseProductId", "oneTimeCode", "oneTimeQuantity", "quantity", ].forEach(function (key) { const value = purchaseMeta[key]; if (value === undefined || value === null) { return; } const normalized = String(value).trim(); if (normalized === "") { return; } body.set(key, normalized); }); } if (billingScope === "user") { body.set("billingScope", "user"); body.set("billingSubjectType", "nextcloud_user"); if (currentUserId) { body.set("billingSubjectNextcloudUserId", currentUserId); } if (currentUserEmail) { body.set("billingContactEmail", currentUserEmail); } if (currentUserDisplayName) { body.set("billingContactName", currentUserDisplayName); } const qortalAddress = activeWalletContext && activeWalletContext.address ? String(activeWalletContext.address).trim() : ""; if (qortalAddress) { body.set("qortalAddress", qortalAddress); } } setStatus("opening checkout…", false); const payload = await requestJson(checkoutEndpoint, { method: "POST", headers: getCsrfHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), body: body.toString(), }); const checkoutUrl = payload && typeof payload.checkoutUrl === "string" ? payload.checkoutUrl.trim() : ""; if (!checkoutUrl) { throw new Error("Checkout URL is missing from connector response"); } setStatus(modeCopy.purchasesCheckoutOpened, false); if (window.location && typeof window.location.assign === "function") { window.location.assign(checkoutUrl); return; } window.location.href = checkoutUrl; } function renderSubscriptionPurchaseCards() { if (!subscriptionCardsEl) { return; } subscriptionCardsEl.innerHTML = ""; const activeTier = String(currentPackageTier || "free_core").trim() || "free_core"; const activeRank = getTierRank(activeTier); const canCheckout = billingConnectorEnabled && billingUserCheckoutUrl !== "" && hasLiveSubscriptionCatalog(); subscriptionPackages.forEach(function (pkg) { const card = document.createElement("article"); card.className = "qortal-purchase-card qortal-purchase-card--subscription"; if (pkg.tier === activeTier) { card.classList.add("qortal-purchase-current"); } const tierPill = document.createElement("span"); tierPill.className = "qortal-purchase-pill"; tierPill.textContent = pkg.tier === activeTier ? "Current Plan" : "Subscription"; card.appendChild(tierPill); const heading = document.createElement("h5"); heading.textContent = pkg.name; card.appendChild(heading); const price = document.createElement("p"); price.className = "qortal-purchase-price"; price.textContent = pkg.price; card.appendChild(price); const summary = document.createElement("p"); summary.className = "qortal-purchase-meta"; summary.textContent = pkg.summary; card.appendChild(summary); const featureList = document.createElement("ul"); featureList.className = "qortal-purchase-features"; pkg.features.forEach(function (feature) { const item = document.createElement("li"); item.textContent = feature; featureList.appendChild(item); }); card.appendChild(featureList); const actions = document.createElement("div"); actions.className = "qortal-purchase-actions"; const checkoutButton = document.createElement("button"); checkoutButton.className = "button"; if (pkg.tier === activeTier) { checkoutButton.textContent = "Current plan"; checkoutButton.disabled = true; } else if (getTierRank(pkg.tier) > activeRank) { checkoutButton.textContent = "Upgrade"; } else { checkoutButton.textContent = "Switch plan"; } if (!canCheckout) { checkoutButton.disabled = true; } checkoutButton.addEventListener("click", async function () { try { currentPackageTier = pkg.tier; renderPurchases(); await openBillingCheckout({ billingScope: "user", packageTier: pkg.tier, purchaseMeta: { checkoutType: "package", purchaseType: "subscription", purchaseCategory: "package", purchaseCode: pkg.code, productId: pkg.productId, purchaseProductId: pkg.productId, }, }); } catch (error) { setStatus(error.message || "Failed to open package checkout", true); } }); actions.appendChild(checkoutButton); card.appendChild(actions); subscriptionCardsEl.appendChild(card); }); } function renderOneTimePurchaseCards() { if (!oneTimeCardsEl) { return; } oneTimeCardsEl.innerHTML = ""; const canCheckout = billingConnectorEnabled && ((isAdmin && billingCheckoutUrl !== "") || billingUserCheckoutUrl !== "") && hasLiveOneTimeCatalog(); const activeTier = String(currentPackageTier || "free_core").trim() || "free_core"; function resolveTierValue(baseValue, byTierMap) { if (!byTierMap || typeof byTierMap !== "object") { return baseValue; } const tierValue = byTierMap[activeTier]; if (typeof tierValue === "string" && tierValue.trim() !== "") { return tierValue.trim(); } if ( typeof byTierMap.free_core === "string" && byTierMap.free_core.trim() !== "" ) { return byTierMap.free_core.trim(); } return baseValue; } const seenOneTimeKeys = new Set(); oneTimePurchases .filter(function (item) { return monthlyAddonMatchesActiveTier(item, activeTier); }) .forEach(function (item) { const resolvedCode = resolveTierValue( String(item.code || "").trim(), item.codeByTier ); const resolvedPrice = resolveTierValue( String(item.price || "").trim(), item.priceByTier ); const dedupeKey = [ String(resolvedCode || "") .trim() .toLowerCase(), String(item.purchaseInterval || "") .trim() .toLowerCase(), String(item.category || "") .trim() .toLowerCase(), String(item.name || "") .trim() .toLowerCase(), ].join("|"); if (seenOneTimeKeys.has(dedupeKey)) { return; } seenOneTimeKeys.add(dedupeKey); const card = document.createElement("article"); card.className = "qortal-purchase-card qortal-purchase-card--onetime"; const tierPill = document.createElement("span"); tierPill.className = "qortal-purchase-pill"; tierPill.textContent = isMonthlyAddonItem(item) ? "Monthly add-on" : "One-time purchase"; card.appendChild(tierPill); const heading = document.createElement("h5"); heading.textContent = item.name; card.appendChild(heading); const price = document.createElement("p"); price.className = "qortal-purchase-price"; price.textContent = resolvedPrice; card.appendChild(price); const summary = document.createElement("p"); summary.className = "qortal-purchase-meta"; summary.textContent = item.summary; card.appendChild(summary); const actions = document.createElement("div"); actions.className = "qortal-purchase-actions"; const checkoutButton = document.createElement("button"); checkoutButton.className = "button"; checkoutButton.textContent = item.actionLabel || "Buy add-on"; if (!canCheckout) { checkoutButton.disabled = true; } checkoutButton.addEventListener("click", async function () { try { const checkoutItem = await resolveLiveAddonForCheckout(item); const checkoutCode = String( checkoutItem.code || resolvedCode ).trim(); const purchaseCategory = String( checkoutItem.category || "one_time" ).trim(); const quantity = String( checkoutItem.quantity || item.quantity || "1" ); const purchaseType = isMonthlyAddonItem(checkoutItem) ? "monthly_addon" : "one_time"; const purchaseMeta = { checkoutType: "one_time", purchaseType: purchaseType, purchaseCategory: purchaseCategory, purchaseCode: checkoutCode, productId: checkoutItem.productId, purchaseProductId: checkoutItem.productId, oneTimeCode: checkoutCode, oneTimeQuantity: quantity, quantity: quantity, }; await openBillingCheckout({ billingScope: "user", packageTier: currentPackageTier || "free_core", purchaseMeta: purchaseMeta, }); } catch (error) { setStatus(error.message || "Failed to open add-on checkout", true); } }); actions.appendChild(checkoutButton); card.appendChild(actions); oneTimeCardsEl.appendChild(card); }); } function renderPurchases() { updatePurchasesLoadUi(); if (!purchasesCatalogReady) { return; } renderSubscriptionPurchaseCards(); renderOneTimePurchaseCards(); } function findMatchingLiveAddonEntry(candidate) { if (!candidate || typeof candidate !== "object") { return null; } const candidateCode = String(candidate.code || "") .trim() .toLowerCase(); const candidateProductId = String(candidate.productId || "").trim(); const candidateName = String(candidate.name || "") .trim() .toLowerCase(); const candidateInterval = String(candidate.purchaseInterval || "") .trim() .toLowerCase(); const candidateCategory = String(candidate.category || "") .trim() .toLowerCase(); for (let i = 0; i < oneTimePurchases.length; i += 1) { const entry = oneTimePurchases[i]; if (!entry || typeof entry !== "object") { continue; } const entryProductId = String(entry.productId || "").trim(); if ( candidateProductId && entryProductId && entryProductId === candidateProductId ) { return entry; } const entryCode = String(entry.code || "") .trim() .toLowerCase(); const entryName = String(entry.name || "") .trim() .toLowerCase(); const entryInterval = String(entry.purchaseInterval || "") .trim() .toLowerCase(); const entryCategory = String(entry.category || "") .trim() .toLowerCase(); if ( candidateCode && entryCode === candidateCode && entryInterval === candidateInterval && entryCategory === candidateCategory ) { return entry; } if ( !candidateCode && candidateName && entryName === candidateName && entryInterval === candidateInterval && entryCategory === candidateCategory ) { return entry; } } return null; } async function resolveLiveAddonForCheckout(candidate) { if (hasLiveOneTimeCatalog()) { return findMatchingLiveAddonEntry(candidate) || candidate; } await refreshBillingCatalog("manual"); if (!hasLiveOneTimeCatalog()) { throw new Error( "Live billing catalog unavailable. Refresh the dashboard and retry checkout." ); } const matched = findMatchingLiveAddonEntry(candidate); if (matched) { return matched; } throw new Error( "Unable to match the selected add-on against the live connector catalog. Refresh and retry." ); } function normalizeCatalogList(raw) { if (!Array.isArray(raw)) { return []; } return raw .filter(function (entry) { return entry && typeof entry === "object"; }) .map(function (entry) { function firstNonEmptyValue(keys) { for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; const value = entry[key]; if (value === undefined || value === null) { continue; } const normalizedValue = String(value).trim(); if (normalizedValue !== "") { return normalizedValue; } } return ""; } const normalized = Object.assign({}, entry); normalized.code = firstNonEmptyValue([ "code", "purchaseCode", "slug", "sku", "id", ]); normalized.productId = firstNonEmptyValue([ "productId", "product_id", "id", ]); normalized.name = firstNonEmptyValue(["name", "title", "label"]); normalized.price = firstNonEmptyValue([ "price", "priceLabel", "displayPrice", "amount", ]); normalized.summary = firstNonEmptyValue([ "summary", "description", "details", ]); normalized.tier = deriveTierFromEntry( entry.code, entry.tier, normalized.name ); normalized.category = firstNonEmptyValue([ "category", "purchaseCategory", "purchase_category", "group", ]); normalized.purchaseType = firstNonEmptyValue([ "purchaseType", "purchase_type", "type", "kind", ]); normalized.purchaseInterval = firstNonEmptyValue([ "purchaseInterval", "purchase_interval", "interval", "billingCycle", "cycle", ]); normalized.actionLabel = firstNonEmptyValue([ "actionLabel", "buttonLabel", "cta", "action", ]); normalized.quantity = String(entry.quantity || "1").trim(); normalized.features = Array.isArray(entry.features) ? entry.features .map(function (feature) { return String(feature || "").trim(); }) .filter(function (feature) { return feature !== ""; }) : []; normalized.codeByTier = entry.codeByTier && typeof entry.codeByTier === "object" ? entry.codeByTier : undefined; normalized.priceByTier = entry.priceByTier && typeof entry.priceByTier === "object" ? entry.priceByTier : undefined; const creditsCandidate = Number.parseInt( String(entry.credits || entry.purchaseCredits || "").trim(), 10 ); if (Number.isFinite(creditsCandidate) && creditsCandidate > 0) { normalized.credits = creditsCandidate; } return normalized; }) .filter(function (entry) { return entry.code !== ""; }); } function applyBillingCatalogPayload(payload) { const payloadData = payload && payload.data && typeof payload.data === "object" ? payload.data : null; const catalog = payload && payload.catalog && typeof payload.catalog === "object" ? payload.catalog : payloadData && payloadData.catalog && typeof payloadData.catalog === "object" ? payloadData.catalog : payloadData && payloadData.subscriptions && Array.isArray(payloadData.subscriptions) ? payloadData : payload; const subscriptions = normalizeCatalogList( catalog && catalog.subscriptions ); const addons = normalizeCatalogList( catalog && (catalog.addons || catalog.oneTime) ); if (subscriptions.length === 0 && addons.length === 0) { subscriptionPackages = []; oneTimePurchases = []; purchasesSubscriptionsLiveReady = false; purchasesOneTimeLiveReady = false; setPurchasesCatalogSource("defaults"); return { hasLiveSubscriptions: false, hasLiveAddons: false, }; } if (subscriptions.length > 0) { subscriptionPackages = subscriptions .map(enrichSubscriptionCatalogEntry) .sort(compareSubscriptionEntries); purchasesSubscriptionsLiveReady = true; } else { subscriptionPackages = []; purchasesSubscriptionsLiveReady = false; } if (addons.length > 0) { oneTimePurchases = addons .map(enrichAddonCatalogEntry) .sort(compareAddonEntries); purchasesOneTimeLiveReady = true; } else { oneTimePurchases = []; purchasesOneTimeLiveReady = false; } setPurchasesCatalogSource( purchasesSubscriptionsLiveReady && purchasesOneTimeLiveReady ? "live" : "defaults" ); return { hasLiveSubscriptions: purchasesSubscriptionsLiveReady, hasLiveAddons: purchasesOneTimeLiveReady, }; } async function refreshBillingCatalog(context) { if (!billingConnectorEnabled || !billingCatalogUrl) { purchasesCatalogReady = true; setPurchasesLoadState("idle", "", false); renderPurchases(); return; } purchasesCatalogReady = false; setPurchasesLoadState("loading", modeCopy.purchasesLoading, false); for (let attempt = 1; attempt <= billingCatalogMaxAttempts; attempt += 1) { try { const payload = await requestJson(billingCatalogUrl, { method: "GET", headers: getCsrfHeaders(), }); const catalogState = applyBillingCatalogPayload(payload); purchasesCatalogReady = true; if (catalogState.hasLiveSubscriptions && catalogState.hasLiveAddons) { setPurchasesLoadState("ready", "", false); } else { setPurchasesLoadState( "fallback", modeCopy.purchasesFallbackNotice, true ); } renderPurchases(); return; } catch (error) { if (attempt >= billingCatalogMaxAttempts) { subscriptionPackages = []; oneTimePurchases = []; purchasesSubscriptionsLiveReady = false; purchasesOneTimeLiveReady = false; setPurchasesCatalogSource("defaults"); purchasesCatalogReady = true; setPurchasesLoadState( "fallback", modeCopy.purchasesFallbackNotice, true ); renderPurchases(); if (context === "manual" || context === "test") { setStatus(error.message || "failed to load billing catalog", true); } return; } await new Promise(function (resolve) { window.setTimeout(resolve, billingCatalogRetryDelayMs); }); } } } async function sendPurchasesSupportRequest() { if (!purchasesSupportMessageEl) { return; } const message = String(purchasesSupportMessageEl.value || "").trim(); if (!message) { setPurchasesSupportStatus( "Enter a short message for NuQloud support first.", true ); return; } if (!billingSupportUrl) { setPurchasesSupportStatus(modeCopy.purchasesSupportFailed, true); return; } const body = new URLSearchParams({ message: message, catalogSource: purchasesCatalogSource, catalogStatus: purchasesCatalogLoadState, pageUrl: window.location.href, }); if (purchasesSupportSendButton) { purchasesSupportSendButton.disabled = true; } setPurchasesSupportStatus("Sending support request…", false); try { await requestJson(billingSupportUrl, { method: "POST", headers: getCsrfHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), body: body.toString(), }); purchasesSupportMessageEl.value = ""; setPurchasesSupportStatus(modeCopy.purchasesSupportSent, false); setStatus(modeCopy.purchasesSupportSent, false); } catch (error) { const fallbackMessage = error && error.message ? error.message : modeCopy.purchasesSupportFailed; setPurchasesSupportStatus( fallbackMessage || modeCopy.purchasesSupportFailed, true ); } finally { if (purchasesSupportSendButton) { purchasesSupportSendButton.disabled = false; } } } function loadChdAnnouncementsQapp(forceRefresh) { if (!chdAnnouncementsFrameEl || !chdAnnouncementsStatusEl) { return; } const sourceUrl = buildEmbeddedQappsUrl("qortal://CHDC", forceRefresh); if (!sourceUrl) { chdAnnouncementsStatusEl.textContent = "Q-Apps URL is not configured."; return; } clearChdQdnRetry("new_load"); chdQdnRetryAttempts = 0; chdQdnResourceTarget = parseChdQdnResourceTarget("qortal://CHDC"); chdAnnouncementsStatusEl.textContent = "Loading CHDC announcements app…"; updateChdLoadingProgress("DOWNLOADING", null, null, null, ""); chdAnnouncementsFrameEl.src = sourceUrl; } function buildEmbeddedQappsUrl(address, forceRefresh) { if (!qappsUrl) { return ""; } const normalizedAddress = normalizeBrowserAddress(address || ""); if (!normalizedAddress) { return ""; } try { const parsed = new URL(qappsUrl, window.location.origin); parsed.searchParams.set("qapp", normalizedAddress); parsed.searchParams.set("embed", "1"); parsed.searchParams.set("chromeless", "1"); parsed.searchParams.set("lock_qapp", "1"); if (forceRefresh) { parsed.searchParams.set("t", String(Date.now())); } return appendThemeParams(parsed.toString()); } catch (_error) { return ""; } } function normalizeBrowserAddress(address) { const raw = String(address || "").trim(); if (!raw.toLowerCase().startsWith("qortal://")) { return raw; } const trimmed = raw.slice("qortal://".length).replace(/^\/+/, ""); if (!trimmed) { return raw; } if (!trimmed.includes("/")) { return "qortal://WEBSITE/" + trimmed; } return "qortal://" + trimmed; } function qortalAddressToRenderPath(address) { const normalized = normalizeBrowserAddress(address); if (!normalized.toLowerCase().startsWith("qortal://")) { return ""; } const trimmed = normalized.slice("qortal://".length).replace(/^\/+/, ""); if (!trimmed || trimmed.split("/").filter(Boolean).length < 2) { return ""; } return "render/" + trimmed; } 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 stripQueryAndHash(value) { return String(value || "") .split("#")[0] .split("?")[0]; } 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 "Loading from QDN..."; } function isTerminalQdnStatus(status) { const normalizedStatus = String(status || "") .trim() .toUpperCase(); return ( normalizedStatus === "NOT_PUBLISHED" || normalizedStatus === "UNSUPPORTED" || normalizedStatus === "BLOCKED" ); } function clearChdLoadingProgress() { if (chdAnnouncementsLoadingStatusEl) { chdAnnouncementsLoadingStatusEl.textContent = ""; } if (chdAnnouncementsLoadingMetaEl) { chdAnnouncementsLoadingMetaEl.textContent = ""; } if (chdAnnouncementsLoadingProgressBarEl) { chdAnnouncementsLoadingProgressBarEl.style.width = "0%"; } if (chdAnnouncementsLoadingOverlayEl) { chdAnnouncementsLoadingOverlayEl.classList.add("qortal-hidden"); } } function updateChdLoadingProgress( status, percentLoaded, localChunkCount, totalChunkCount, description ) { const normalizedStatus = String(status || "").toUpperCase(); if (normalizedStatus === "READY") { clearChdLoadingProgress(); return; } const localCount = coerceFiniteNumber(localChunkCount); const totalCount = coerceFiniteNumber(totalChunkCount); 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 (chdAnnouncementsLoadingOverlayEl) { chdAnnouncementsLoadingOverlayEl.classList.remove("qortal-hidden"); } if (chdAnnouncementsLoadingStatusEl) { const label = getQdnStatusLabel(normalizedStatus, description); chdAnnouncementsLoadingStatusEl.textContent = bounded !== null ? label + " " + bounded.toFixed(1) + "%" : label; } if (chdAnnouncementsLoadingProgressBarEl) { chdAnnouncementsLoadingProgressBarEl.style.width = (bounded !== null ? bounded : 0) + "%"; } if (chdAnnouncementsLoadingMetaEl) { if (localCount !== null && totalCount !== null && totalCount > 0) { chdAnnouncementsLoadingMetaEl.textContent = "Chunks " + localCount + " / " + totalCount; } else if (localCount !== null) { chdAnnouncementsLoadingMetaEl.textContent = localCount + " chunks downloaded"; } else { chdAnnouncementsLoadingMetaEl.textContent = ""; } } } 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 parseChdQdnResourceTarget(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); }); if (segments.length < 2) { return null; } return { service: String(segments[0] || "") .trim() .toUpperCase(), name: String(segments[1] || "").trim(), identifierCandidate: segments.length > 2 ? String(segments[2] || "").trim() : "", }; } function buildChdQdnStatusUrls(target) { if (!target || !target.service || !target.name) { return []; } const paths = []; const urls = []; const pushPath = function (segments) { const joined = segments .filter(function (segment) { return typeof segment === "string" && segment.trim() !== ""; }) .map(function (segment) { return segment.trim(); }) .join("/"); if (!joined || paths.includes(joined)) { return; } paths.push(joined); }; 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 = appendThemeParams(buildGatewayProxyUrl(path)); if (!baseUrl) { return; } const withBuild = baseUrl + (baseUrl.indexOf("?") === -1 ? "?" : "&") + "build=true"; if (!urls.includes(withBuild)) { urls.push(withBuild); } if (!urls.includes(baseUrl)) { urls.push(baseUrl); } }); return urls; } async function probeChdQdnResourceStatus(target) { const urls = buildChdQdnStatusUrls(target); for (let i = 0; i < urls.length; i += 1) { try { const result = await fetchJsonWithTimeout(urls[i], 4500); if (!result.ok || !result.json) { continue; } const parsed = parseQdnStatusPayload(result.json); if (!parsed) { continue; } return { ok: true, status: String(parsed.status || "").toUpperCase(), description: parsed.description || "", percentLoaded: coerceFiniteNumber(parsed.percentLoaded), localChunkCount: coerceFiniteNumber(parsed.localChunkCount), totalChunkCount: coerceFiniteNumber(parsed.totalChunkCount), }; } catch (_error) { // try next candidate } } return { ok: false }; } function detectChdLoadingInterstitial() { if (!chdAnnouncementsFrameEl) { return { inspectable: false, detected: false }; } let doc = null; try { doc = chdAnnouncementsFrameEl.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 reloadChdAnnouncementsFrame() { if (!chdAnnouncementsFrameEl) { return false; } try { if (chdAnnouncementsFrameEl.contentWindow) { chdAnnouncementsFrameEl.contentWindow.location.reload(); return true; } } catch (_error) { // fallback below } try { if (chdAnnouncementsFrameEl.src) { chdAnnouncementsFrameEl.src = chdAnnouncementsFrameEl.src; return true; } } catch (_error) { return false; } return false; } function clearChdQdnRetry(reason) { if (chdQdnRetryTimer) { window.clearTimeout(chdQdnRetryTimer); chdQdnRetryTimer = null; } chdQdnStatusFailures = 0; chdQdnLastSnapshot = ""; clearChdLoadingProgress(); if (reason === "content_loaded" && chdAnnouncementsStatusEl) { chdAnnouncementsStatusEl.textContent = "CHDC announcements app loaded."; } } function scheduleChdQdnRetry(delayOverrideMs) { if (chdQdnRetryTimer) { return; } if (chdQdnRetryAttempts >= chdQdnRetryMaxAttempts) { return; } const nextDelay = typeof delayOverrideMs === "number" && Number.isFinite(delayOverrideMs) ? Math.max(1000, Math.floor(delayOverrideMs)) : chdQdnRetryDelayMs; chdQdnRetryTimer = window.setTimeout(function () { chdQdnRetryTimer = null; runChdQdnRetryTick().catch(function () { scheduleChdQdnRetry(chdQdnReloadFallbackDelayMs); }); }, nextDelay); } async function runChdQdnRetryTick() { if (chdQdnRetryAttempts >= chdQdnRetryMaxAttempts) { return; } const detection = detectChdLoadingInterstitial(); if (!detection.inspectable) { clearChdQdnRetry("cross_origin_or_uninspectable"); return; } chdQdnRetryAttempts += 1; let shouldReload = true; let delayMs = chdQdnRetryDelayMs; if (chdQdnResourceTarget) { const statusResult = await probeChdQdnResourceStatus( chdQdnResourceTarget ); if (statusResult.ok) { chdQdnStatusFailures = 0; const statusSnapshot = statusResult.status + ":" + (statusResult.percentLoaded !== null ? statusResult.percentLoaded : "") + ":" + (statusResult.localChunkCount !== null ? statusResult.localChunkCount : ""); if (statusSnapshot !== chdQdnLastSnapshot) { chdQdnLastSnapshot = statusSnapshot; } updateChdLoadingProgress( statusResult.status, statusResult.percentLoaded, statusResult.localChunkCount, statusResult.totalChunkCount, statusResult.description ); if ( statusResult.status === "READY" || isTerminalQdnStatus(statusResult.status) ) { if (detection.detected && statusResult.status === "READY") { reloadChdAnnouncementsFrame(); } else { clearChdQdnRetry("content_loaded"); chdQdnRetryAttempts = 0; } return; } shouldReload = false; } else { chdQdnStatusFailures += 1; delayMs = chdQdnReloadFallbackDelayMs; if (chdQdnStatusFailures <= 3) { shouldReload = false; } } } if (shouldReload) { reloadChdAnnouncementsFrame(); } scheduleChdQdnRetry(delayMs); } function applyPendingHighlights() { if (!pendingHighlights.size) { return; } const targets = []; const actions = []; if (pendingHighlights.has("create-account")) { if ( onboardingCard && !onboardingCard.classList.contains("qortal-hidden") ) { targets.push(onboardingCard); } if (createButton && !createButton.disabled) { actions.push(createButton); } } if (pendingHighlights.has("balance")) { if ( balanceCardEl && walletPane && !walletPane.classList.contains("qortal-hidden") ) { targets.push(balanceCardEl); } if ( requestQortButton && requestQortWrapEl && !requestQortWrapEl.classList.contains("qortal-hidden") ) { actions.push(requestQortButton); } } if (pendingHighlights.has("purchases")) { if ( purchasesCardEl && walletPane && !walletPane.classList.contains("qortal-hidden") && !purchasesCardEl.classList.contains("qortal-hidden") ) { targets.push(purchasesCardEl); } if (purchasesToggleButton && !purchasesToggleButton.disabled) { actions.push(purchasesToggleButton); } } if (pendingHighlights.has("register-name")) { if ( primaryCardEl && walletPane && !walletPane.classList.contains("qortal-hidden") ) { targets.push(primaryCardEl); } if (registerNameButton) { actions.push(registerNameButton); } } if (!targets.length && !actions.length) { return; } const firstTarget = targets[0] || actions[0]; targets.forEach(function (element) { element.classList.add("qortal-highlight-target"); }); actions.forEach(function (element) { element.classList.add("qortal-highlight-action"); }); if (firstTarget && typeof firstTarget.scrollIntoView === "function") { firstTarget.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest", }); } window.setTimeout(function () { targets.forEach(function (element) { element.classList.remove("qortal-highlight-target"); }); actions.forEach(function (element) { element.classList.remove("qortal-highlight-action"); }); }, 9000); pendingHighlights.clear(); } function highlightDashboardElements(targets, actions) { const safeTargets = Array.isArray(targets) ? targets.filter(function (element) { return Boolean(element); }) : []; const safeActions = Array.isArray(actions) ? actions.filter(function (element) { return Boolean(element); }) : []; if (!safeTargets.length && !safeActions.length) { return; } const firstTarget = safeTargets[0] || safeActions[0]; safeTargets.forEach(function (element) { element.classList.add("qortal-highlight-target"); }); safeActions.forEach(function (element) { element.classList.add("qortal-highlight-action"); }); if (firstTarget && typeof firstTarget.scrollIntoView === "function") { firstTarget.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest", }); } window.setTimeout(function () { safeTargets.forEach(function (element) { element.classList.remove("qortal-highlight-target"); }); safeActions.forEach(function (element) { element.classList.remove("qortal-highlight-action"); }); }, 9000); } function openPurchasesWithHighlight(preferOneTime) { if ( purchasesToggleButton && purchasesBodyEl && purchasesBodyEl.classList.contains("qortal-hidden") ) { purchasesToggleButton.click(); } const targets = []; const actions = []; if ( preferOneTime && purchasesOneTimeSectionEl && !purchasesOneTimeSectionEl.classList.contains("qortal-hidden") ) { targets.push(purchasesOneTimeSectionEl); const oneTimeAction = purchasesOneTimeSectionEl.querySelector( ".qortal-purchase-actions .button" ); if (oneTimeAction) { actions.push(oneTimeAction); } } else if ( purchasesCardEl && !purchasesCardEl.classList.contains("qortal-hidden") ) { targets.push(purchasesCardEl); } highlightDashboardElements(targets, actions); } function formatValue(value) { if (value === undefined || value === null || value === "") { return "—"; } if (typeof value === "string") { return value; } return JSON.stringify(value); } function formatBalance(value) { if (value === undefined || value === null || value === "") { return "—"; } const num = Number(value); if (Number.isFinite(num)) { if (isPoweredMode) { return formatCreditsAmount(num / QORT_PER_CREDIT); } return num.toFixed(8); } return String(value); } function formatCreditsAmount(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) { return "0"; } const rounded = Math.round(numeric * 10000) / 10000; if (Math.abs(rounded - Math.round(rounded)) < 0.00000001) { return String(Math.round(rounded)); } return rounded.toFixed(2).replace(/\.?0+$/, ""); } function qortToCredits(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) { return 0; } return numeric / QORT_PER_CREDIT; } function creditsToQort(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) { return 0; } return Math.round(numeric * QORT_PER_CREDIT * 100000000) / 100000000; } function parseBalanceNumber(value) { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; } function shouldShowInitialQortRequest(value) { const numeric = parseBalanceNumber(value); if (numeric === null) { return false; } return numeric <= 0; } function updateBalanceVisualState(value) { if (!balanceCardEl) { return; } if (lowBalanceWrapEl) { lowBalanceWrapEl.classList.add("qortal-hidden"); } balanceCardEl.classList.remove( "qortal-wallet-summary__item--balance-good", "qortal-wallet-summary__item--balance-warn", "qortal-wallet-summary__item--balance-bad" ); const numeric = parseBalanceNumber(value); if (numeric === null) { return; } if (numeric <= 0) { balanceCardEl.classList.add("qortal-wallet-summary__item--balance-bad"); return; } if (numeric >= 1.25) { balanceCardEl.classList.add("qortal-wallet-summary__item--balance-good"); return; } balanceCardEl.classList.add("qortal-wallet-summary__item--balance-warn"); if ( lowBalanceWrapEl && canUserPublish && purchasesCardEl && !purchasesCardEl.classList.contains("qortal-hidden") ) { lowBalanceWrapEl.classList.remove("qortal-hidden"); } } function setFeeEditable(isEditable) { if (!sendFeeEl) { return; } sendFeeEl.readOnly = !isEditable; if (!isEditable) { sendFeeEl.value = "0.01"; } if (sendFeeEditButton) { sendFeeEditButton.textContent = isEditable ? "Lock Fee" : "Edit Fee"; } } function showCard(card, visible) { if (!card) { return; } card.classList.toggle("qortal-hidden", !visible); } function setCreateResult(message, isError) { if (!createResultEl) { return; } createResultEl.textContent = message || ""; createResultEl.classList.toggle("qortal-error", Boolean(isError)); } function setRequestQortResult(payload, isError) { if (!requestQortResultEl) { return; } requestQortResultEl.textContent = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2); requestQortResultEl.classList.toggle("qortal-error", Boolean(isError)); } function setRequestFeedback(message, isError) { if (!requestFeedbackEl) { return; } requestFeedbackEl.textContent = message || ""; requestFeedbackEl.classList.toggle("qortal-hidden", !message); requestFeedbackEl.classList.toggle("qortal-error", Boolean(isError)); } function formatTimestamp(value) { if (!value) { return ""; } try { return new Date(value).toLocaleString(); } catch (_error) { return String(value); } } function isValidEmail(value) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || "").trim()); } function buildInviteLink(inviteToken, inviteEmail) { const token = String(inviteToken || "").trim(); if (!token) { return ""; } const baseUrl = getNextcloudBaseUrl(); if (!baseUrl) { return ""; } const query = new URLSearchParams(); query.set("invite_token", token); const email = String(inviteEmail || "").trim(); if (isValidEmail(email)) { query.set("invite_email", email); } return ( baseUrl.replace(/\/$/, "") + "/apps/qortal_integration/invite?" + query.toString() ); } async function copyTextToClipboard(value) { const text = String(value || "").trim(); if (!text) { throw new Error("Nothing to copy"); } try { await navigator.clipboard.writeText(text); return true; } catch (_error) { const input = document.createElement("input"); input.value = text; document.body.appendChild(input); input.select(); const copied = document.execCommand("copy"); input.remove(); if (!copied) { throw new Error("Unable to copy invite link"); } return true; } } function inviteStatus(invite) { if (!invite) { return ""; } if (invite.revokedAt) { return "revoked"; } if (invite.usedAt) { return "used"; } if (invite.expiresAt && new Date(invite.expiresAt).getTime() < Date.now()) { return "expired"; } return "active"; } function inviteStatusLabel(status) { return status.charAt(0).toUpperCase() + status.slice(1); } function setInviteCreateResult(message, isError) { if (!inviteCreateResultEl) { return; } inviteCreateResultEl.textContent = message || ""; inviteCreateResultEl.classList.toggle("qortal-error", Boolean(isError)); } function renderInviteTableMessage(message) { if (!invitesTableBodyEl) { return; } invitesTableBodyEl.innerHTML = ""; const row = document.createElement("tr"); const cell = document.createElement("td"); cell.colSpan = 6; cell.textContent = message || ""; row.appendChild(cell); invitesTableBodyEl.appendChild(row); } function setInviteCreatedDetails(invite, inviteLink, emailResult, recipientEmail) { const hasInvite = Boolean(invite && invite.token); if (inviteCreatedDetailsEl) { inviteCreatedDetailsEl.classList.toggle("qortal-hidden", !hasInvite); } if (inviteCreatedTokenEl) { inviteCreatedTokenEl.textContent = hasInvite ? String(invite.token || "") : ""; } if (inviteCreatedLinkEl) { const link = hasInvite ? String(inviteLink || buildInviteLink(invite.token, recipientEmail) || "") : ""; inviteCreatedLinkEl.textContent = link; inviteCreatedLinkEl.href = link || "#"; inviteCreatedLinkEl.target = "_blank"; inviteCreatedLinkEl.rel = "noopener"; } if (inviteCreatedEmailStatusEl) { let statusText = ""; if (recipientEmail) { if (emailResult && emailResult.sent) { statusText = "Invite email sent to " + recipientEmail + "."; } else if (emailResult && emailResult.error) { statusText = "Invite email failed for " + recipientEmail + ": " + String(emailResult.error || ""); } else { statusText = "Invite email was not sent automatically. You can send it later from the invite table."; } } inviteCreatedEmailStatusEl.textContent = statusText; inviteCreatedEmailStatusEl.classList.toggle( "qortal-hidden", statusText === "" ); inviteCreatedEmailStatusEl.classList.toggle( "qortal-error", Boolean(emailResult && emailResult.sent === false) ); } } function assertInviteResponse(payload, fallbackError) { if (!payload || payload.ok !== true) { throw new Error( (payload && typeof payload.error === "string" && payload.error) || fallbackError ); } return payload; } function renderInvites(payload) { if (!invitesTableBodyEl) { return; } const invites = payload && payload.ok && payload.data && Array.isArray(payload.data.invites) ? payload.data.invites : []; invitesTableBodyEl.innerHTML = ""; if (invites.length === 0) { renderInviteTableMessage("No invites generated yet."); return; } invites.forEach(function (invite) { const row = document.createElement("tr"); row.className = "qortal-invite-row"; const tokenCell = document.createElement("td"); tokenCell.className = "qortal-invite-token-cell"; const tokenCode = document.createElement("code"); tokenCode.className = "qortal-invite-token"; tokenCode.textContent = invite.token || ""; tokenCell.appendChild(tokenCode); const commentCell = document.createElement("td"); commentCell.className = "qortal-invite-comment"; commentCell.textContent = invite.comment || "—"; const statusCell = document.createElement("td"); const status = inviteStatus(invite); const statusBadge = document.createElement("span"); statusBadge.className = "qortal-invite-status qortal-invite-status--" + status; statusBadge.textContent = inviteStatusLabel(status); statusCell.appendChild(statusBadge); const expiresCell = document.createElement("td"); expiresCell.className = "qortal-invite-date"; expiresCell.textContent = formatTimestamp(invite.expiresAt); const usedByCell = document.createElement("td"); usedByCell.className = "qortal-invite-used-by"; const usedText = invite.usedByQortalAddress || invite.usedByNextcloudUserId || ""; if (usedText) { const usedCode = document.createElement("code"); usedCode.textContent = usedText; usedByCell.appendChild(usedCode); } else { usedByCell.textContent = "—"; } const actionCell = document.createElement("td"); actionCell.className = "qortal-invite-actions"; const inviteEmail = String(invite.comment || "").trim(); const inviteLink = String(invite.inviteLink || "").trim() || buildInviteLink(invite.token, inviteEmail); const canSendInviteEmail = !invite.revokedAt && !invite.usedAt && !(invite.expiresAt && new Date(invite.expiresAt).getTime() < Date.now()) && isValidEmail(inviteEmail); const copyButton = document.createElement("button"); copyButton.className = "button qortal-invite-action"; copyButton.type = "button"; copyButton.textContent = "Copy Link"; copyButton.disabled = !inviteLink; copyButton.addEventListener("click", async function () { try { await copyTextToClipboard(inviteLink); setStatus("Invite link copied.", false); notifyDashboard("Invite link copied.", false); } catch (error) { const message = error.message || "Failed to copy invite link"; setStatus(message, true); notifyDashboard(message, true); } }); actionCell.appendChild(copyButton); const sendButton = document.createElement("button"); sendButton.className = "button qortal-invite-action"; sendButton.type = "button"; sendButton.textContent = "Send E-Mail"; sendButton.disabled = !canSendInviteEmail; sendButton.addEventListener("click", async function () { try { const payload = await sendInviteEmail(String(invite.token || "")); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; const emailResult = data && typeof data.email === "object" ? data.email : null; const recipient = inviteEmail || "recipient"; if (emailResult && emailResult.sent) { const message = "Invite email sent to " + recipient + "."; setStatus(message, false); notifyDashboard(message, false); } else { const errorMessage = (emailResult && emailResult.error) || "Invite email could not be sent."; setStatus(errorMessage, true); notifyDashboard(errorMessage, true); } await refreshInvites({ showLoading: false }); } catch (error) { const message = error.message || "Failed to send invite email"; setStatus(message, true); notifyDashboard(message, true); } }); actionCell.appendChild(sendButton); const revokeButton = document.createElement("button"); revokeButton.className = "button qortal-invite-action qortal-invite-action--danger"; revokeButton.type = "button"; revokeButton.textContent = "Revoke"; revokeButton.disabled = Boolean(invite.revokedAt || invite.usedAt); revokeButton.addEventListener("click", async function () { try { await revokeInvite(String(invite.token || "")); setStatus("Invite revoked.", false); notifyDashboard("Invite revoked.", false); } catch (error) { const message = error.message || "Failed to revoke invite"; setStatus(message, true); notifyDashboard(message, true); } }); actionCell.appendChild(revokeButton); row.appendChild(tokenCell); row.appendChild(commentCell); row.appendChild(statusCell); row.appendChild(expiresCell); row.appendChild(usedByCell); row.appendChild(actionCell); invitesTableBodyEl.appendChild(row); }); } async function refreshInvites(options) { const refreshOptions = options && typeof options === "object" ? options : {}; if (!invitesUrl || !invitesTableBodyEl) { return null; } const showLoading = refreshOptions.showLoading !== false; const hasRenderedRows = invitesTableBodyEl.children.length > 0; if (showLoading || !hasRenderedRows) { renderInviteTableMessage("Loading invites…"); } try { const payload = await requestJson(invitesUrl, { method: "GET", headers: getCsrfHeaders(), }); assertInviteResponse(payload, "Failed to load invites"); renderInvites(payload); return payload; } catch (error) { const message = error.message || "Failed to load invites"; if (showLoading || !hasRenderedRows) { renderInviteTableMessage(message); } else { setStatus(message, true); } return null; } } async function createInvite() { if (!createInviteUrl) { throw new Error("Invite creation endpoint is not configured"); } if (!inviteRecipientEmailEl) { throw new Error("Recipient email field is not available"); } const recipientEmail = inviteRecipientEmailEl.value.trim(); if (!recipientEmail) { throw new Error("Recipient email is required"); } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipientEmail)) { throw new Error("Recipient email is invalid"); } const expiryValue = inviteExpiryHoursEl ? inviteExpiryHoursEl.value : ""; const expiryHours = expiryValue ? Number(expiryValue) : null; if ( expiryHours !== null && (!Number.isFinite(expiryHours) || expiryHours <= 0) ) { throw new Error("Expiry hours must be a positive number"); } setInviteCreateResult("Creating invite…", false); renderInviteTableMessage("Loading invites…"); const body = new URLSearchParams(); if (expiryHours !== null) { body.set("expiresInHours", String(Math.floor(expiryHours))); } body.set("recipientEmail", recipientEmail); if (inviteSendEmailOnCreateEl && inviteSendEmailOnCreateEl.checked) { body.set("sendEmail", "1"); } const payload = await requestJson(createInviteUrl, { method: "POST", headers: getCsrfHeaders("application/x-www-form-urlencoded; charset=UTF-8"), body, }); assertInviteResponse(payload, "Failed to create invite"); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; const invite = data && typeof data.invite === "object" ? data.invite : null; const inviteToken = invite && invite.token ? String(invite.token) : ""; const inviteLink = String(data.inviteLink || "").trim() || buildInviteLink(inviteToken, recipientEmail); const emailResult = data && typeof data.email === "object" ? data.email : null; const sendEmailRequested = Boolean(inviteSendEmailOnCreateEl && inviteSendEmailOnCreateEl.checked); const emailSent = Boolean(emailResult && emailResult.sent); const emailFailed = sendEmailRequested ? !emailSent : false; const successMessage = sendEmailRequested ? emailFailed ? "Invite created, but email delivery failed." : "Invite created and email sent." : "Invite created. Email was not sent automatically."; setInviteCreateResult(successMessage, false); setInviteCreatedDetails(invite, inviteLink, emailResult, recipientEmail); setStatus(successMessage, emailFailed); notifyDashboard(successMessage, emailFailed); await refreshInvites({ showLoading: false }); return payload; } async function revokeInvite(token) { if (!revokeInviteUrl) { throw new Error("Invite revoke endpoint is not configured"); } const cleanedToken = String(token || "").trim(); if (!cleanedToken) { throw new Error("token is required"); } const body = new URLSearchParams({ token: cleanedToken }); const payload = await requestJson(revokeInviteUrl, { method: "POST", headers: getCsrfHeaders("application/x-www-form-urlencoded; charset=UTF-8"), body, }); assertInviteResponse(payload, "Failed to revoke invite"); await refreshInvites({ showLoading: false }); return payload; } async function sendInviteEmail(token) { if (!sendInviteEmailUrl) { throw new Error("Invite email endpoint is not configured"); } const cleanedToken = String(token || "").trim(); if (!cleanedToken) { throw new Error("token is required"); } const body = new URLSearchParams({ token: cleanedToken }); const payload = await requestJson(sendInviteEmailUrl, { method: "POST", headers: getCsrfHeaders("application/x-www-form-urlencoded; charset=UTF-8"), body, }); assertInviteResponse(payload, "Failed to send invite email"); return payload; } function updateRequestDetailsText(context) { if (!requestDetailsEl) { return; } if (isInstallGrantRequestMode(context)) { requestDetailsEl.textContent = modeCopy.requestDetailsInstallGrant || modeCopy.requestDetailsBase; return; } const uniqueNames = Array.from(new Set(qappsNames)); let appsDescription = modeCopy.requestDetailsBase; if (uniqueNames.length > 0) { appsDescription = modeCopy.requestDetailsWithAppsPrefix + uniqueNames.join(", ") + "."; } requestDetailsEl.textContent = appsDescription + modeCopy.requestDetailsTail; } 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 === 0) { 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 resolveQappIconUrl(card) { if (!card || typeof card !== "object") { return ""; } if (card.iconMode === "custom" && card.iconUrl) { if (card.iconUrl.toLowerCase().startsWith("qortal://")) { const proxyPath = card.iconUrl .slice("qortal://".length) .replace(/^\/+/, ""); if (proxyPath) { return buildGatewayProxyUrl(proxyPath); } } return card.iconUrl; } const iconName = (function () { if (card.iconMode === "name") { return String(card.iconFromName || "").trim(); } return extractAppNameFromQortalAddress(card.address); })(); const appName = iconName; if (!appName) { return ""; } return buildGatewayProxyUrl( "arbitrary/THUMBNAIL/" + appName + "/qortal_avatar" ); } function buildQappLaunchUrl(address) { const base = String(qappsUrl || "").trim(); if (!base) { return ""; } const separator = base.indexOf("?") === -1 ? "?" : "&"; return ( base + separator + "qapp=" + encodeURIComponent(String(address || "").trim()) ); } function renderQappsGallery() { if (!appGalleryCardEl || !appGalleryEl) { return; } appGalleryEl.innerHTML = ""; if ( !qappsEnabled || !Array.isArray(qappsCards) || qappsCards.length === 0 ) { appGalleryCardEl.classList.add("qortal-hidden"); return; } const fragment = document.createDocumentFragment(); qappsCards.forEach(function (card) { const launchUrl = buildQappLaunchUrl(card.address); if (!launchUrl) { return; } const tile = document.createElement("a"); tile.className = "qortal-app-card"; tile.href = launchUrl; tile.title = card.name || card.address; tile.setAttribute( "aria-label", (card.name || card.address) + modeCopy.qappsTileAriaSuffix ); const iconUrl = resolveQappIconUrl(card); if (iconUrl) { const image = document.createElement("img"); image.className = "qortal-app-card__image"; image.loading = "lazy"; image.alt = card.name || "Q-App"; image.src = iconUrl; image.onerror = function () { image.remove(); if (!tile.querySelector(".qortal-app-card__fallback")) { const fallback = document.createElement("div"); fallback.className = "qortal-app-card__fallback"; fallback.textContent = card.name || card.address; tile.appendChild(fallback); } }; tile.appendChild(image); } else { const fallback = document.createElement("div"); fallback.className = "qortal-app-card__fallback"; fallback.textContent = card.name || card.address; tile.appendChild(fallback); } const title = document.createElement("div"); title.className = "qortal-app-card__title"; title.textContent = card.name || card.address; tile.appendChild(title); const description = document.createElement("div"); description.className = "qortal-app-card__desc"; description.textContent = card.description || modeCopy.qappsTileDefaultDescription; tile.appendChild(description); fragment.appendChild(tile); }); appGalleryEl.appendChild(fragment); appGalleryCardEl.classList.toggle( "qortal-hidden", appGalleryEl.children.length === 0 ); } function setRegisterError(message) { if (!registerErrorEl) { return; } registerErrorEl.textContent = message || ""; registerErrorEl.classList.toggle("qortal-hidden", !message); } function setRegisterResult(payload, isError) { if (!registerResultEl) { return; } registerResultEl.textContent = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2); registerResultEl.classList.toggle("qortal-error", Boolean(isError)); } function resetRegisterModalState() { setRegisterError(""); if (registerSuccessEl) { registerSuccessEl.classList.add("qortal-hidden"); registerSuccessEl.textContent = modeCopy.registerSuccessTitle; } if (registerSuccessNoteEl) { registerSuccessNoteEl.classList.add("qortal-hidden"); registerSuccessNoteEl.textContent = modeCopy.registerSuccessNote; } if (registerSubmitActionsEl) { registerSubmitActionsEl.classList.remove("qortal-hidden"); } if (registerSuccessActionsEl) { registerSuccessActionsEl.classList.add("qortal-hidden"); } if (registerOutputToggleButton) { registerOutputToggleButton.textContent = modeCopy.registerAdvancedOutputShow; } if (registerResultEl) { registerResultEl.classList.add("qortal-hidden"); setRegisterResult("No registration submitted yet.", false); } if (registerNameInputEl) { registerNameInputEl.disabled = false; } } function showRegisterSuccess(name, resultPayload) { if (registerSuccessEl) { registerSuccessEl.classList.remove("qortal-hidden"); registerSuccessEl.textContent = modeCopy.registerSuccessTitle; } if (registerSuccessNoteEl) { registerSuccessNoteEl.classList.remove("qortal-hidden"); registerSuccessNoteEl.textContent = modeCopy.registerSuccessNote; } if (registerSubmitActionsEl) { registerSubmitActionsEl.classList.add("qortal-hidden"); } if (registerSuccessActionsEl) { registerSuccessActionsEl.classList.remove("qortal-hidden"); } if (registerNameInputEl) { registerNameInputEl.disabled = true; } if (registerOutputToggleButton) { registerOutputToggleButton.textContent = modeCopy.registerAdvancedOutputShow; } if (registerResultEl) { registerResultEl.classList.add("qortal-hidden"); } setRegisterResult( { ok: true, name: name, result: resultPayload, }, false ); } function toggleRegisterAdvancedOutput() { if (!registerResultEl || !registerOutputToggleButton) { return; } const shouldShow = registerResultEl.classList.contains("qortal-hidden"); registerResultEl.classList.toggle("qortal-hidden", !shouldShow); registerOutputToggleButton.textContent = shouldShow ? modeCopy.registerAdvancedOutputHide : modeCopy.registerAdvancedOutputShow; } function getCsrfHeaders(contentType) { const headers = {}; if (contentType) { headers["Content-Type"] = contentType; } if (typeof OC !== "undefined" && OC.requestToken) { headers.requesttoken = OC.requestToken; } return headers; } async function requestJson(url, options) { const response = await fetch(url, options); const payload = await response.json(); if (!response.ok) { throw new Error(payload.error || "Request failed"); } if (payload && payload.ok === false) { throw new Error(payload.error || "Request failed"); } return payload; } function setActivityNotificationsStatus(payload, isError) { if (!activityNotificationsStatusEl) { return; } activityNotificationsStatusEl.textContent = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2); activityNotificationsStatusEl.classList.toggle( "qortal-error", Boolean(isError) ); } function renderActivityNotificationsDebugAudit(entries, lastPoll) { if ( !activityNotificationsDebugAuditCardEl || !activityNotificationsDebugAuditEl ) { return; } if (!effectiveDebugEnabled) { activityNotificationsDebugAuditCardEl.classList.add("qortal-hidden"); return; } activityNotificationsDebugAuditCardEl.classList.remove("qortal-hidden"); const list = Array.isArray(entries) ? entries : []; if (!list.length && (!lastPoll || typeof lastPoll !== "object")) { activityNotificationsDebugAuditEl.textContent = "No debug activity yet."; return; } activityNotificationsDebugAuditEl.textContent = JSON.stringify( { lastPoll: lastPoll && typeof lastPoll === "object" ? lastPoll : {}, dismissAudit: list, }, null, 2 ); } function normalizeTrackedNamesFilter(value) { const rawList = Array.isArray(value) ? value : typeof value === "string" ? value.split(/[\n,]+/) : []; const out = []; const seen = new Set(); rawList.forEach(function (item) { const normalized = String(item || "").trim(); if (!normalized) { return; } const key = normalized.toLowerCase(); if (seen.has(key)) { return; } seen.add(key); out.push(normalized.slice(0, 120)); }); return out.slice(0, 50); } function normalizeGroupOption(entry) { if (!entry || typeof entry !== "object") { return null; } const idCandidates = [ entry.groupId, entry.groupID, entry.group, entry.id, entry.groupNumber, ]; let groupId = ""; idCandidates.some(function (candidate) { const value = String(candidate == null ? "" : candidate).trim(); if (!value) { return false; } groupId = value; return true; }); if (!groupId) { return null; } const nameCandidates = [ entry.groupName, entry.name, entry.groupname, entry.group_name, ]; let groupName = ""; nameCandidates.some(function (candidate) { const value = String(candidate == null ? "" : candidate).trim(); if (!value) { return false; } groupName = value; return true; }); const ownerName = String(entry.ownerName || entry.owner || "").trim(); const isAdmin = entry.isAdmin === true || String(entry.isAdmin || "").toLowerCase() === "true"; return { groupId: groupId, groupName: groupName || "Group " + groupId, ownerName: ownerName, isAdmin: Boolean(isAdmin), raw: entry, }; } function renderActivityNotificationsGroupsSelect() { if (!activityNotificationsGroupsSelectEl) { return; } const select = activityNotificationsGroupsSelectEl; select.innerHTML = ""; const placeholder = document.createElement("option"); placeholder.value = ""; if ( !Array.isArray(activityNotificationsGroupsOptions) || activityNotificationsGroupsOptions.length === 0 ) { placeholder.textContent = "No groups loaded"; select.appendChild(placeholder); select.disabled = true; return; } placeholder.textContent = "Select a group…"; select.appendChild(placeholder); select.disabled = false; const adminGroups = activityNotificationsGroupsOptions.filter(function ( group ) { return Boolean(group.isAdmin); }); const memberGroups = activityNotificationsGroupsOptions.filter(function ( group ) { return !group.isAdmin; }); function appendGroupOption(target, group) { const option = document.createElement("option"); option.value = String(group.groupId || ""); const ownerSuffix = group.ownerName ? " • owner: " + group.ownerName : ""; option.textContent = (group.isAdmin ? "★ " : "") + group.groupName + " [" + group.groupId + "]" + ownerSuffix; target.appendChild(option); } if (adminGroups.length) { const adminOptGroup = document.createElement("optgroup"); adminOptGroup.label = "Admin Groups"; adminGroups.forEach(function (group) { appendGroupOption(adminOptGroup, group); }); select.appendChild(adminOptGroup); } if (memberGroups.length) { const memberOptGroup = document.createElement("optgroup"); memberOptGroup.label = "Member Groups"; memberGroups.forEach(function (group) { appendGroupOption(memberOptGroup, group); }); select.appendChild(memberOptGroup); } updateActivityGroupActionButtons(); } async function loadActivityNotificationsGroupsOptions() { if (!activityNotificationsGroupsSelectEl) { return []; } activityNotificationsGroupsSelectEl.disabled = true; activityNotificationsGroupsSelectEl.innerHTML = ''; let groups = []; try { let list = []; let memberLookupFailed = false; let context = null; try { context = await ensureWalletContext(); } catch (error) { const message = String((error && error.message) || "").toLowerCase(); if (message.includes("no linked account found")) { activityNotificationsGroupsOptions = []; renderActivityNotificationsGroupsSelect(); updateActivityGroupActionButtons(); setActivityNotificationsStatus( "Group helper is waiting for your linked account context. It will retry automatically.", false ); if (activityNotificationsGroupsRetryTimer) { window.clearTimeout(activityNotificationsGroupsRetryTimer); } activityNotificationsGroupsRetryTimer = window.setTimeout( function () { activityNotificationsGroupsRetryTimer = null; loadActivityNotificationsGroupsOptions().catch(function () {}); }, 1500 ); return []; } throw error; } const address = String((context && context.address) || "").trim(); if (address) { const memberPath = "groups/member/" + address; const memberUrl = buildGatewayProxyUrl(memberPath); if (memberUrl) { const memberResponse = await fetch(memberUrl, { method: "GET", headers: getCsrfHeaders(), }); let memberPayload; try { memberPayload = await memberResponse.json(); } catch (_error) { memberPayload = null; } if (!memberResponse.ok) { memberLookupFailed = true; } else if (Array.isArray(memberPayload)) { list = memberPayload; } } } else { setActivityNotificationsStatus( "No linked Qortal address found for member group lookup.", true ); } if (memberLookupFailed) { // Fallback only if membership endpoint fails in this environment. const payload = await qortalRequest("LIST_GROUPS", { limit: 0, offset: 0, reverse: false, }); list = Array.isArray(payload) ? payload : payload && Array.isArray(payload.groups) ? payload.groups : []; setActivityNotificationsStatus( "Member groups lookup failed; using LIST_GROUPS fallback.", false ); } const normalized = []; const seen = new Set(); list.forEach(function (entry) { const group = normalizeGroupOption(entry); if (!group) { return; } const key = String(group.groupId).trim(); if (!key || seen.has(key)) { return; } seen.add(key); normalized.push(group); }); normalized.sort(function (a, b) { if (a.isAdmin !== b.isAdmin) { return a.isAdmin ? -1 : 1; } const aName = String(a.groupName || "").toLowerCase(); const bName = String(b.groupName || "").toLowerCase(); if (aName < bName) return -1; if (aName > bName) return 1; return String(a.groupId || "").localeCompare( String(b.groupId || ""), undefined, { numeric: true } ); }); activityNotificationsGroupsOptions = normalized; groups = normalized; renderActivityNotificationsGroupsSelect(); updateActivityGroupActionButtons(); } catch (error) { activityNotificationsGroupsOptions = []; renderActivityNotificationsGroupsSelect(); updateActivityGroupActionButtons(); setActivityNotificationsStatus( "Failed to load group list for helper: " + (error && error.message ? error.message : "Unknown error"), true ); } return groups; } function getSelectedActivityNotificationGroup() { if (!activityNotificationsGroupsSelectEl) { return null; } const selectedId = String( activityNotificationsGroupsSelectEl.value || "" ).trim(); if (!selectedId) { return null; } return ( (activityNotificationsGroupsOptions || []).find(function (group) { return String(group.groupId || "") === selectedId; }) || null ); } function updateActivityGroupActionButtons() { const selected = getSelectedActivityNotificationGroup(); const label = selected && selected.groupName ? String(selected.groupName).trim() : "selected group"; if (activityNotificationsAddGroupThreadsButton) { activityNotificationsAddGroupThreadsButton.textContent = "Add " + label + " Thread notifications"; } if (activityNotificationsAddGroupAnnouncementsButton) { activityNotificationsAddGroupAnnouncementsButton.textContent = "Add " + label + " Announcement notifications"; } } function normalizeTrackedIdentifierEntry(entry) { if (!entry || typeof entry !== "object") { return null; } const identifier = String(entry.identifier || entry.id || "").trim(); if (!identifier) { return null; } const kindRaw = String(entry.kind || "identifier") .trim() .toLowerCase(); const kind = ["identifier", "name", "tag", "channel"].includes(kindRaw) ? kindRaw : "identifier"; const matchMode = "prefix"; const source = String(entry.source || "").trim(); const label = String(entry.label || "").trim(); const names = normalizeTrackedNamesFilter( entry.names || entry.qdnNames || [] ); return { identifier: identifier.slice(0, 160), kind: kind, matchMode: matchMode, source: source.slice(0, 80), label: label.slice(0, 160), names: names, enabled: entry.enabled !== false, isDefault: Boolean(entry.isDefault), createdAt: String(entry.createdAt || new Date().toISOString()), }; } function buildQmailInboxDefaultsForCurrentNames() { const defaults = []; const contextNames = activeWalletContext && Array.isArray(activeWalletContext.names) ? activeWalletContext.names : []; const primaryName = activeWalletContext && activeWalletContext.primaryName ? String(activeWalletContext.primaryName).trim() : ""; const allNames = []; if (primaryName) { allNames.push(primaryName); } contextNames.forEach(function (entry) { if (typeof entry === "string" && entry.trim()) { allNames.push(entry.trim()); return; } if ( entry && typeof entry === "object" && typeof entry.name === "string" && entry.name.trim() ) { allNames.push(entry.name.trim()); } }); Array.from(new Set(allNames)).forEach(function (name) { const safeName = String(name || "").trim(); if (!safeName) { return; } defaults.push({ identifier: "_mail_qortal_qmail_" + safeName, kind: "identifier", matchMode: "prefix", source: "Q-Mail", label: "Q-Mail messages for " + safeName, names: [], enabled: true, isDefault: true, }); }); return defaults; } function normalizeActivityNotificationsConfig(raw) { const config = raw && typeof raw === "object" ? raw : {}; const channels = config.channels && typeof config.channels === "object" ? config.channels : {}; const tracked = Array.isArray(config.tracked) ? config.tracked : []; const dedup = []; const seen = new Set(); tracked.forEach(function (entry) { const normalized = normalizeTrackedIdentifierEntry(entry); if (!normalized) { return; } const key = normalized.kind.toLowerCase() + "|" + normalized.matchMode.toLowerCase() + "|" + normalized.identifier.toLowerCase() + "|" + normalized.source.toLowerCase() + "|" + normalized.names .map(function (name) { return name.toLowerCase(); }) .join(","); if (seen.has(key)) { return; } seen.add(key); dedup.push(normalized); }); return { enabled: config.enabled !== false, autoDefaults: config.autoDefaults !== false, channels: { nextcloud: channels.nextcloud !== false, email: Boolean(channels.email), }, tracked: dedup, }; } function ensureActivityNotificationsState() { if (!activityNotificationsConfig) { activityNotificationsConfig = normalizeActivityNotificationsConfig({}); } return activityNotificationsConfig; } function syncActivityNotificationControlsFromState() { const config = ensureActivityNotificationsState(); if (activityNotificationsEnabledEl) { activityNotificationsEnabledEl.checked = Boolean(config.enabled); } if (activityNotificationsChannelNextcloudEl) { activityNotificationsChannelNextcloudEl.checked = Boolean( config.channels.nextcloud ); } if (activityNotificationsChannelEmailEl) { activityNotificationsChannelEmailEl.checked = Boolean( config.channels.email ); } if (activityNotificationsAutoDefaultsEl) { activityNotificationsAutoDefaultsEl.checked = Boolean( config.autoDefaults ); } } function readActivityNotificationControlsIntoState() { const config = ensureActivityNotificationsState(); config.enabled = activityNotificationsEnabledEl ? activityNotificationsEnabledEl.checked : true; config.channels.nextcloud = activityNotificationsChannelNextcloudEl ? activityNotificationsChannelNextcloudEl.checked : true; config.channels.email = activityNotificationsChannelEmailEl ? activityNotificationsChannelEmailEl.checked : false; config.autoDefaults = activityNotificationsAutoDefaultsEl ? activityNotificationsAutoDefaultsEl.checked : true; return config; } function activityTrackedEntryKey(entry) { if (!entry) { return ""; } return [ String(entry.kind || "identifier").toLowerCase(), "prefix", String(entry.identifier || "") .trim() .toLowerCase(), String(entry.source || "") .trim() .toLowerCase(), normalizeTrackedNamesFilter(entry.names || entry.qdnNames || []) .map(function (name) { return name.toLowerCase(); }) .join(","), ].join("|"); } function renderActivityNotificationsTrackedList() { if ( !activityNotificationsTrackedListEl || !activityNotificationsTrackedEmptyEl ) { return; } const config = ensureActivityNotificationsState(); activityNotificationsTrackedListEl.innerHTML = ""; if (!Array.isArray(config.tracked) || config.tracked.length === 0) { activityNotificationsTrackedEmptyEl.classList.remove("qortal-hidden"); return; } activityNotificationsTrackedEmptyEl.classList.add("qortal-hidden"); config.tracked.forEach(function (entry, index) { const row = document.createElement("div"); row.className = "qortal-card qortal-activity-tracked-item"; const toggle = document.createElement("input"); toggle.type = "checkbox"; toggle.className = "qortal-activity-tracked-toggle"; toggle.checked = entry.enabled !== false; toggle.title = "Enable or disable this tracked identifier"; toggle.addEventListener("change", function () { const state = ensureActivityNotificationsState(); if (!state.tracked[index]) { return; } state.tracked[index].enabled = toggle.checked; saveActivityNotificationsConfig({ silent: true }).catch(function ( error ) { setActivityNotificationsStatus( error.message || "Failed to save notification state", true ); }); }); const summaryWrap = document.createElement("div"); summaryWrap.className = "qortal-activity-tracked-summary"; summaryWrap.appendChild(toggle); const enabledLabel = document.createElement("span"); enabledLabel.className = "qortal-activity-tracked-enabled-label"; enabledLabel.textContent = "Enabled"; summaryWrap.appendChild(enabledLabel); const text = document.createElement("span"); text.className = "qortal-activity-tracked-text"; const label = entry.label ? String(entry.label).trim() : ""; const source = entry.source ? String(entry.source).trim() : ""; const names = normalizeTrackedNamesFilter(entry.names || []); text.textContent = (label ? label + " → " : "") + entry.identifier + (source ? " (" + source + ")" : "") + (names.length ? " • names: " + names.join(", ") : "") + (entry.isDefault ? " • default" : ""); summaryWrap.appendChild(text); row.appendChild(summaryWrap); const actions = document.createElement("div"); actions.className = "qortal-activity-tracked-actions"; const markReadButton = document.createElement("button"); markReadButton.className = "button"; markReadButton.type = "button"; markReadButton.textContent = "Mark Read"; markReadButton.addEventListener("click", async function () { try { setActivityNotificationsStatus( "Marking checkpoint read for tracked prefix…", false ); await markActivityNotificationsReadCheckpoint("selected", [entry]); } catch (error) { setActivityNotificationsStatus( error.message || "Failed to update read checkpoint", true ); } }); actions.appendChild(markReadButton); const removeButton = document.createElement("button"); removeButton.className = "button qortal-activity-remove-button"; removeButton.type = "button"; removeButton.textContent = "×"; removeButton.title = "Remove notification"; removeButton.setAttribute("aria-label", "Remove notification"); removeButton.addEventListener("click", function () { const state = ensureActivityNotificationsState(); state.tracked.splice(index, 1); renderActivityNotificationsTrackedList(); saveActivityNotificationsConfig({ silent: true }).catch(function ( error ) { setActivityNotificationsStatus( error.message || "Failed to save notification changes", true ); }); }); actions.appendChild(removeButton); row.appendChild(actions); const details = document.createElement("details"); details.className = "qortal-activity-tracked-details"; const summary = document.createElement("summary"); summary.textContent = "More details"; details.appendChild(summary); const pre = document.createElement("pre"); pre.className = "qortal-status qortal-compact-status"; pre.textContent = JSON.stringify( { identifier: entry.identifier, source: entry.source || "", label: entry.label || "", kind: entry.kind || "identifier", matchMode: "prefix", names: normalizeTrackedNamesFilter(entry.names || []), enabled: entry.enabled !== false, isDefault: Boolean(entry.isDefault), }, null, 2 ); details.appendChild(pre); row.appendChild(details); activityNotificationsTrackedListEl.appendChild(row); }); } function renderActivityNotificationsAvailableDefaults() { if ( !activityNotificationsDefaultsListEl || !activityNotificationsDefaultsEmptyEl ) { return; } const state = ensureActivityNotificationsState(); activityNotificationsDefaultsListEl.innerHTML = ""; const list = Array.isArray(activityNotificationsAvailableDefaults) ? activityNotificationsAvailableDefaults : []; if (!list.length) { activityNotificationsDefaultsEmptyEl.classList.remove("qortal-hidden"); return; } activityNotificationsDefaultsEmptyEl.classList.add("qortal-hidden"); list.forEach(function (entry) { const normalized = normalizeTrackedIdentifierEntry(entry); if (!normalized) { return; } const row = document.createElement("div"); row.className = "qortal-chip"; row.style.display = "flex"; row.style.gap = "8px"; row.style.alignItems = "center"; row.style.flexWrap = "wrap"; const activeKey = activityTrackedEntryKey(normalized); const activeEntry = (state.tracked || []).find(function (tracked) { return activityTrackedEntryKey(tracked) === activeKey; }) || null; const isActive = Boolean(activeEntry && activeEntry.enabled !== false); const activeToggle = document.createElement("input"); activeToggle.type = "checkbox"; activeToggle.disabled = true; activeToggle.checked = isActive; activeToggle.title = isActive ? "Active default" : "Not active"; row.appendChild(activeToggle); const text = document.createElement("span"); text.textContent = (normalized.label ? normalized.label + " → " : "") + normalized.identifier + (normalized.source ? " (" + normalized.source + ")" : "") + (normalized.names && normalized.names.length ? " • publisher names: " + normalized.names.join(", ") : ""); row.appendChild(text); const addButton = document.createElement("button"); addButton.className = "button"; addButton.type = "button"; addButton.textContent = isActive ? "Active" : "Activate"; addButton.disabled = isActive; addButton.addEventListener("click", async function () { const added = addTrackedActivityIdentifier(normalized, { mergeDefaults: true, }); if (!added) { setActivityNotificationsStatus( "That default is already active.", true ); return; } renderActivityNotificationsAvailableDefaults(); renderActivityNotificationsTrackedList(); await persistActivityNotificationsAfterAdd( "Default notification activated and saved." ); }); row.appendChild(addButton); activityNotificationsDefaultsListEl.appendChild(row); }); } function addTrackedActivityIdentifier(entry, options) { const normalized = normalizeTrackedIdentifierEntry(entry); if (!normalized) { return false; } const state = ensureActivityNotificationsState(); const key = activityTrackedEntryKey(normalized); const existingIndex = state.tracked.findIndex(function (candidate) { return activityTrackedEntryKey(candidate) === key; }); if (existingIndex !== -1) { if (options && options.mergeDefaults) { let changedExisting = false; const wasDefault = Boolean(state.tracked[existingIndex].isDefault); const nextIsDefault = wasDefault || Boolean(normalized.isDefault); state.tracked[existingIndex].isDefault = nextIsDefault; if (wasDefault !== nextIsDefault) { changedExisting = true; } if (!state.tracked[existingIndex].label && normalized.label) { state.tracked[existingIndex].label = normalized.label; changedExisting = true; } if (!state.tracked[existingIndex].source && normalized.source) { state.tracked[existingIndex].source = normalized.source; changedExisting = true; } if ( !Array.isArray(state.tracked[existingIndex].names) || state.tracked[existingIndex].names.length === 0 ) { state.tracked[existingIndex].names = normalized.names; changedExisting = true; } if ( normalized.isDefault && state.tracked[existingIndex].enabled === false ) { state.tracked[existingIndex].enabled = true; changedExisting = true; } return changedExisting; } return false; } state.tracked.push(normalized); return true; } function inferAvailableActivityDefaultsFromDashboard() { const defaults = []; const qappCandidates = []; if (Array.isArray(qappsCards)) { qappsCards.forEach(function (card) { if (card && typeof card === "object" && card.name) { qappCandidates.push(String(card.name)); } if (card && typeof card === "object" && card.address) { const extracted = extractAppNameFromQortalAddress(card.address); if (extracted) { qappCandidates.push(extracted); } } }); } if (Array.isArray(qappsNames)) { qappsNames.forEach(function (name) { if (name) { qappCandidates.push(String(name)); } }); } const seenSources = new Set(); const prefixCatalog = { "q-mail": ["_mail_qortal_qmail_"], qmail: ["_mail_qortal_qmail_"], qmails: ["_mail_qortal_qmail_"], "q-tube": ["qtube_", "qc_v1_qtube_"], qtube: ["qtube_", "qc_v1_qtube_"], quitter: ["MhNiRYdzkaP9dz"], "q-blog": ["q-blog"], qblog: ["q-blog"], "q-share": ["qshare"], qshare: ["qshare"], "q-shop": ["q-store"], qshop: ["q-store"], }; qappCandidates.forEach(function (rawName) { const normalized = String(rawName || "").trim(); if (!normalized) { return; } const lower = normalized.toLowerCase(); let sourceLabel = ""; let prefixes = []; Object.keys(prefixCatalog).some(function (key) { if (lower === key) { prefixes = prefixCatalog[key]; sourceLabel = normalized; return true; } return false; }); if (!prefixes || prefixes.length === 0) { return; } prefixes.forEach(function (prefix) { const dedupeKey = sourceLabel.toLowerCase() + "|" + String(prefix).toLowerCase(); if (seenSources.has(dedupeKey)) { return; } seenSources.add(dedupeKey); defaults.push({ identifier: prefix, kind: "identifier", matchMode: "prefix", source: sourceLabel, label: sourceLabel + " prefix", names: [], enabled: true, isDefault: true, }); }); }); buildQmailInboxDefaultsForCurrentNames().forEach(function (entry) { defaults.push(entry); }); return defaults; } function ensureActivityNotificationDefaults() { const state = ensureActivityNotificationsState(); if (!state.autoDefaults) { return false; } let changed = false; buildQmailInboxDefaultsForCurrentNames().forEach(function (entry) { if (addTrackedActivityIdentifier(entry, { mergeDefaults: true })) { changed = true; } }); if (changed) { renderActivityNotificationsTrackedList(); } return changed; } async function loadActivityNotificationsConfig() { if (!userActivityNotificationsConfigUrl) { setActivityNotificationsStatus( "Activity notifications config endpoint is not configured.", true ); return; } try { const payload = await requestJson(userActivityNotificationsConfigUrl, { method: "GET", headers: getCsrfHeaders(), }); activityNotificationsConfig = normalizeActivityNotificationsConfig( payload.data || {} ); activityNotificationsDebugDismissAudit = payload && payload.debug && Array.isArray(payload.debug.dismissAudit) ? payload.debug.dismissAudit : []; activityNotificationsDebugLastPoll = payload && payload.debug && payload.debug.lastPoll && typeof payload.debug.lastPoll === "object" ? payload.debug.lastPoll : null; const serverDefaults = payload.suggestions && Array.isArray(payload.suggestions.defaults) ? payload.suggestions.defaults : []; activityNotificationsAvailableDefaults = []; const defaultsSeen = new Set(); serverDefaults .concat(inferAvailableActivityDefaultsFromDashboard()) .forEach(function (entry) { const normalized = normalizeTrackedIdentifierEntry(entry); if (!normalized) { return; } const key = activityTrackedEntryKey(normalized); if (defaultsSeen.has(key)) { return; } defaultsSeen.add(key); activityNotificationsAvailableDefaults.push(normalized); }); activityNotificationsLoaded = true; const defaultsChanged = ensureActivityNotificationDefaults(); syncActivityNotificationControlsFromState(); renderActivityNotificationsAvailableDefaults(); renderActivityNotificationsTrackedList(); renderActivityNotificationsDebugAudit( activityNotificationsDebugDismissAudit, activityNotificationsDebugLastPoll ); if (defaultsChanged) { try { await saveActivityNotificationsConfig({ silent: true }); } catch (_error) { // Ignore silent auto-persist errors; explicit save still works. } } if (activityNotificationsGroupsSelectEl) { loadActivityNotificationsGroupsOptions().catch(function () {}); } setActivityNotificationsStatus( { ok: true, loaded: true, trackedCount: activityNotificationsConfig.tracked.length, }, false ); } catch (error) { renderActivityNotificationsDebugAudit([], null); setActivityNotificationsStatus( error.message || "Failed to load activity notification preferences", true ); } } async function saveActivityNotificationsConfig(options) { const saveOptions = options && typeof options === "object" ? options : {}; const silent = Boolean(saveOptions.silent); if (!userActivityNotificationsSaveUrl) { throw new Error("Activity notifications save endpoint is not configured"); } const config = normalizeActivityNotificationsConfig( readActivityNotificationControlsIntoState() ); activityNotificationsConfig = config; ensureActivityNotificationDefaults(); const form = new URLSearchParams(); form.set("config", JSON.stringify(activityNotificationsConfig)); const payload = await requestJson(userActivityNotificationsSaveUrl, { method: "POST", headers: getCsrfHeaders(), body: form, }); activityNotificationsConfig = normalizeActivityNotificationsConfig( payload.data || activityNotificationsConfig ); renderActivityNotificationsAvailableDefaults(); renderActivityNotificationsTrackedList(); if (!silent) { setActivityNotificationsStatus( { ok: true, saved: true, trackedCount: activityNotificationsConfig.tracked.length, }, false ); } return payload; } async function persistActivityNotificationsAfterAdd(successMessage) { try { await saveActivityNotificationsConfig({ silent: true }); setActivityNotificationsStatus( successMessage || "Notification added and saved.", false ); } catch (error) { setActivityNotificationsStatus( (successMessage || "Notification added, but save failed.") + " " + (error.message || ""), true ); } } async function sendActivityNotificationsTest() { if (!userActivityNotificationsTestUrl) { throw new Error("Activity notifications test endpoint is not configured"); } const payload = await requestJson(userActivityNotificationsTestUrl, { method: "POST", headers: getCsrfHeaders(), body: new URLSearchParams(), }); setActivityNotificationsStatus(payload.data || payload, false); return payload; } async function testTrackedActivityNotifications() { if (!userActivityNotificationsTestTrackedUrl) { throw new Error("Tracked notifications test endpoint is not configured"); } const form = new URLSearchParams(); form.set("dispatch", "1"); const payload = await requestJson(userActivityNotificationsTestTrackedUrl, { method: "POST", headers: getCsrfHeaders(), body: form, }); if ( effectiveDebugEnabled && payload && payload.debug && payload.debug.lastPoll ) { activityNotificationsDebugLastPoll = payload.debug.lastPoll; renderActivityNotificationsDebugAudit( activityNotificationsDebugDismissAudit, activityNotificationsDebugLastPoll ); } setActivityNotificationsStatus( effectiveDebugEnabled ? payload : payload.data || payload, false ); return payload; } async function markActivityNotificationsReadCheckpoint( scope, trackedEntries ) { if (!userActivityNotificationsReadCheckpointUrl) { throw new Error( "Activity notifications read checkpoint endpoint is not configured" ); } const form = new URLSearchParams(); form.set("scope", scope === "selected" ? "selected" : "all"); form.set("timestamp", String(Date.now())); if ( scope === "selected" && Array.isArray(trackedEntries) && trackedEntries.length ) { form.set( "tracked", JSON.stringify( trackedEntries .map(function (entry) { const normalized = normalizeTrackedIdentifierEntry(entry); return normalized || {}; }) .filter(function (entry) { return entry && entry.identifier; }) ) ); } const payload = await requestJson( userActivityNotificationsReadCheckpointUrl, { method: "POST", headers: getCsrfHeaders(), body: form, } ); setActivityNotificationsStatus(payload.data || payload, false); return payload; } function highRiskConfirmationMessage(action, details) { const info = details && typeof details === "object" ? details : {}; const lines = [ "NuQloud blocked this request for an extra safety check.", "", "Action: " + String(info.requestType || action || "Qortal 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 qortalRequest(action, payload) { if (!requestUrl) { throw new Error(modeCopy.qappsRequestMissing); } let workingPayload = Object.assign({}, payload || {}); const postOnce = async function () { const response = await fetch(requestUrl, { method: "POST", headers: getCsrfHeaders("application/json"), body: JSON.stringify({ requestType: action, payload: workingPayload }), }); const json = await response.json(); return { response, json }; }; let outcome = await postOnce(); if ( (!outcome.response.ok || outcome.json.ok === false || outcome.json.error) && outcome.json.error === "high_risk_confirmation_required" ) { const details = outcome.json.details || {}; const token = details && typeof details.confirmationToken === "string" ? details.confirmationToken : ""; if (!token || !window.confirm(highRiskConfirmationMessage(action, details))) { const error = new Error("high_risk_confirmation_cancelled"); error.status = outcome.response.status; error.details = details; error.payload = outcome.json; throw error; } workingPayload = Object.assign({}, workingPayload, { highRiskConfirmationToken: token, }); if (payload && typeof payload === "object") { payload.highRiskConfirmationToken = token; } outcome = await postOnce(); } const response = outcome.response; const json = outcome.json; if (!response.ok || json.ok === false || json.error) { const error = new Error(json.error || "qortal_request_failed"); error.status = response.status; error.details = json.details || null; error.payload = json; throw error; } return json.data !== undefined ? json.data : json; } function setNodeStatusCardState(cardEl, state) { if (!cardEl) { return; } cardEl.classList.remove( "qortal-node-status-card--warn", "qortal-node-status-card--error" ); if (state === "warn") { cardEl.classList.add("qortal-node-status-card--warn"); } else if (state === "error") { cardEl.classList.add("qortal-node-status-card--error"); } } function parseNodeStatusNumber(value) { if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string") { const parsed = Number.parseFloat(value.trim()); if (Number.isFinite(parsed)) { return parsed; } } return null; } function parseNodeStatusBoolean(value) { if (typeof value === "boolean") { return value; } if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "true" || normalized === "1" || normalized === "yes") { return true; } if (normalized === "false" || normalized === "0" || normalized === "no") { return false; } } if (typeof value === "number" && Number.isFinite(value)) { return value > 0; } return null; } function findNodeStatusValue(root, candidateKeys, maxDepth) { if (!root || typeof root !== "object") { return undefined; } const queue = [{ value: root, depth: 0 }]; while (queue.length > 0) { const current = queue.shift(); if (!current || !current.value || typeof current.value !== "object") { continue; } for (const key of candidateKeys) { if (Object.prototype.hasOwnProperty.call(current.value, key)) { return current.value[key]; } } if (current.depth >= maxDepth) { continue; } const values = Array.isArray(current.value) ? current.value : Object.values(current.value); values.forEach(function (entry) { if (entry && typeof entry === "object") { queue.push({ value: entry, depth: current.depth + 1 }); } }); } return undefined; } function getNodeStatusSnapshot(payload) { const source = payload && typeof payload === "object" ? payload : {}; const height = parseNodeStatusNumber( findNodeStatusValue( source, ["height", "myHeight", "blockHeight", "chainHeight", "currentHeight"], 4 ) ); let syncPercent = parseNodeStatusNumber( findNodeStatusValue(source, ["syncPercent", "syncPercentage", "sync"], 4) ); if (syncPercent !== null && syncPercent > 0 && syncPercent <= 1) { syncPercent *= 100; } if (syncPercent !== null) { syncPercent = Math.max(0, Math.min(100, syncPercent)); } let synchronizing = parseNodeStatusBoolean( findNodeStatusValue( source, ["synchronizing", "isSynchronizing", "syncing"], 4 ) ); if (synchronizing === null && syncPercent !== null) { synchronizing = syncPercent < 100; } const peers = parseNodeStatusNumber( findNodeStatusValue( source, [ "peers", "peerCount", "numberOfConnections", "connectedPeers", "connections", ], 4 ) ); const dataPeers = parseNodeStatusNumber( findNodeStatusValue( source, [ "numberOfDataConnections", "dataPeers", "dataPeerCount", "numberOfDataPeers", "connectedDataPeers", ], 4 ) ); return { height: height !== null ? Math.max(0, Math.floor(height)) : null, syncPercent, synchronizing, peers: peers !== null ? Math.max(0, Math.floor(peers)) : null, dataPeers: dataPeers !== null ? Math.max(0, Math.floor(dataPeers)) : null, }; } function setNodeStatusValues(snapshot, isUnavailable) { if (nodeStatusHeightEl) { nodeStatusHeightEl.textContent = snapshot.height !== null ? String(snapshot.height) : isUnavailable ? "Unavailable" : "—"; } if (nodeStatusSyncEl) { nodeStatusSyncEl.textContent = snapshot.syncPercent !== null ? (Number.isInteger(snapshot.syncPercent) ? String(snapshot.syncPercent) : snapshot.syncPercent.toFixed(2)) + "%" : isUnavailable ? "Unavailable" : "—"; } if (nodeStatusSynchronizingEl) { nodeStatusSynchronizingEl.textContent = snapshot.synchronizing === null ? isUnavailable ? "Unavailable" : "—" : snapshot.synchronizing ? "Yes" : "No"; } if (nodeStatusPeersEl) { nodeStatusPeersEl.textContent = snapshot.peers !== null ? String(snapshot.peers) : isUnavailable ? "Unavailable" : "—"; } if (nodeStatusDataPeersEl) { nodeStatusDataPeersEl.textContent = snapshot.dataPeers !== null ? String(snapshot.dataPeers) : isUnavailable ? "Unavailable" : "—"; } } function applyNodeStatusStyles(snapshot, isUnavailable) { setNodeStatusCardState(nodeStatusHeightCardEl, null); setNodeStatusCardState(nodeStatusSyncCardEl, null); setNodeStatusCardState(nodeStatusSynchronizingCardEl, null); setNodeStatusCardState(nodeStatusPeersCardEl, null); setNodeStatusCardState(nodeStatusDataPeersCardEl, null); if (isUnavailable) { setNodeStatusCardState(nodeStatusHeightCardEl, "error"); setNodeStatusCardState(nodeStatusSyncCardEl, "error"); setNodeStatusCardState(nodeStatusSynchronizingCardEl, "error"); setNodeStatusCardState(nodeStatusPeersCardEl, "error"); setNodeStatusCardState(nodeStatusDataPeersCardEl, "error"); return; } if (snapshot.syncPercent !== null && snapshot.syncPercent < 100) { setNodeStatusCardState(nodeStatusSyncCardEl, "warn"); } if (snapshot.synchronizing === true) { setNodeStatusCardState(nodeStatusSynchronizingCardEl, "warn"); } if (snapshot.peers !== null && snapshot.peers < 3) { setNodeStatusCardState(nodeStatusPeersCardEl, "error"); } if (snapshot.dataPeers !== null && snapshot.dataPeers < 3) { setNodeStatusCardState(nodeStatusDataPeersCardEl, "error"); } } async function refreshNodeStatus() { const hasNodeStatusUI = nodeStatusHeightEl || nodeStatusSyncEl || nodeStatusSynchronizingEl || nodeStatusPeersEl || nodeStatusDataPeersEl; if (!hasNodeStatusUI) { return; } try { const payload = await qortalRequest("GET_NODE_STATUS", {}); const snapshot = getNodeStatusSnapshot(payload); setNodeStatusValues(snapshot, false); applyNodeStatusStyles(snapshot, false); } catch (_error) { const unavailableSnapshot = { height: null, syncPercent: null, synchronizing: null, peers: null, dataPeers: null, }; setNodeStatusValues(unavailableSnapshot, true); applyNodeStatusStyles(unavailableSnapshot, true); } } function extractMappings(payload) { if (!payload) { return []; } if (Array.isArray(payload.mappings)) { return payload.mappings; } if (payload.data && Array.isArray(payload.data.mappings)) { return payload.data.mappings; } return []; } function pickMapping(mappings) { if (!Array.isArray(mappings)) { return null; } let fallback = null; for (const entry of mappings) { if (!entry || typeof entry !== "object") { continue; } if (entry.walletId && entry.status === "linked") { return entry; } if (!fallback && entry.walletId) { fallback = entry; } if (!fallback && entry.qortalAddress) { fallback = entry; } } return fallback; } function extractBalanceAmount(balance) { const pickAmount = function (value) { if (!value || typeof value !== "object") { return undefined; } return ( value.balance ?? value.available ?? value.confirmed ?? value.amount ?? value.confirmedBalance ?? value.total ?? value.value ); }; if (Array.isArray(balance)) { for (const entry of balance) { const amount = pickAmount(entry); if (amount !== undefined && amount !== null && amount !== "") { return amount; } } return undefined; } if (balance && typeof balance === "object") { return pickAmount(balance); } if (balance !== undefined && balance !== null && balance !== "") { return balance; } return undefined; } function looksLikeQortalAddress(value) { const candidate = (value || "").trim(); return /^Q[1-9A-HJ-NP-Za-km-z]{20,}$/.test(candidate); } function normalizeNameInput(value) { return String(value || "") .trim() .replace(/^@+/, ""); } function normalizeNameForCompare(value) { return normalizeNameInput(value).toLowerCase(); } function hasNameInList(names, desiredName) { const normalizedDesired = normalizeNameForCompare(desiredName); if (!normalizedDesired) { return false; } if (!Array.isArray(names)) { return false; } return names.some(function (entry) { if (typeof entry === "string") { return normalizeNameForCompare(entry) === normalizedDesired; } if ( entry && typeof entry === "object" && typeof entry.name === "string" ) { return normalizeNameForCompare(entry.name) === normalizedDesired; } return false; }); } function validateRegisterName(name) { const trimmed = String(name || "").trim(); if (!trimmed) { return "Name is required"; } if (trimmed.length < 3) { return "Name must be at least 3 characters"; } if (trimmed.length > 40) { return "Name must be 40 characters or fewer"; } if (/[\u0000-\u001f\u007f]/.test(trimmed)) { return "Name contains unsupported control characters"; } return ""; } function mapRegisterNameError(error) { const raw = String((error && error.message) || "").trim(); if (!raw) { return "Name registration failed"; } const lower = raw.toLowerCase(); if ( lower.includes("name is already registered") || lower.includes("already registered") ) { return "This name is already registered. Choose another name."; } if (lower.includes("transaction invalid") && lower.includes("nam")) { return "Name is unavailable or invalid. Choose another name and try again."; } if (lower.includes("invalid name")) { return "Name is invalid. Choose another name and try again."; } if ( lower.includes("not enough balance") || lower.includes("insufficient") ) { return "Not enough " + modeCopy.txUnit + " balance to register a name."; } return raw; } async function isNameAlreadyRegistered(name) { const desired = String(name || "").trim(); if (!desired) { return false; } try { const result = await qortalRequest("GET_NAME_DATA", { name: desired }); if ( result && typeof result === "object" && typeof result.name === "string" && result.name.trim() !== "" ) { return true; } if (typeof result === "string" && result.trim() !== "") { return true; } } catch (error) { const message = String((error && error.message) || "").toLowerCase(); if ( message.includes("unknown name") || message.includes("name unknown") || message.includes("not found") ) { return false; } } const url = buildGatewayProxyUrl("names/" + desired); if (!url) { return false; } try { const response = await fetch(url, { method: "GET", headers: getCsrfHeaders(), }); if (response.status === 404) { return false; } const text = (await response.text()).trim(); if (!response.ok) { const lower = text.toLowerCase(); if ( lower.includes("unknown name") || lower.includes("name unknown") || lower.includes('"error":401') || lower.includes("not found") ) { return false; } return false; } if (text === "") { return false; } return true; } catch (_error) { return false; } } function encodeGatewayPath(path) { return String(path || "") .split("/") .map(function (segment) { return encodeURIComponent(segment); }) .join("/"); } function buildGatewayProxyUrl(path) { if (!gatewayProxyTemplate) { return ""; } if (gatewayProxyTemplate.includes("__PATH__")) { return gatewayProxyTemplate.replace("__PATH__", encodeGatewayPath(path)); } return gatewayProxyTemplate + encodeURIComponent(path); } 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 parsed = new URL(url, window.location.origin); if (!parsed.searchParams.has("theme")) { parsed.searchParams.set("theme", detectTheme()); } const lang = detectLanguage(); if (lang && !parsed.searchParams.has("lang")) { parsed.searchParams.set("lang", lang); } return parsed.toString(); } catch (_error) { return url; } } async function fetchNodeBalance(address) { if (!nodeBalanceUrl || !address) { throw new Error("node_balance_url_missing"); } const response = await requestJson( nodeBalanceUrl + "?address=" + encodeURIComponent(address), { method: "GET", headers: getCsrfHeaders(), } ); if (Object.prototype.hasOwnProperty.call(response, "balance")) { return response.balance; } if (response.ok && response.data !== undefined) { return response.data; } return response; } function updateNames(names, primaryName) { if (!namesEl || !namesEmptyEl) { return; } namesEl.innerHTML = ""; const labels = []; if (primaryName) { labels.push(primaryName); } if (Array.isArray(names)) { names.forEach(function (entry) { if (typeof entry === "string" && entry.trim() !== "") { labels.push(entry.trim()); } else if ( entry && typeof entry === "object" && typeof entry.name === "string" && entry.name.trim() !== "" ) { labels.push(entry.name.trim()); } }); } const unique = Array.from(new Set(labels)); if (unique.length === 0) { namesEmptyEl.classList.remove("qortal-hidden"); return; } namesEmptyEl.classList.add("qortal-hidden"); unique.forEach(function (name) { const chip = document.createElement("span"); chip.className = "qortal-chip"; chip.textContent = name; namesEl.appendChild(chip); }); ensureActivityNotificationDefaults(); } function setTransactionsStatus(message, isError) { if (!transactionsStatusEl) { return; } transactionsStatusEl.textContent = message || ""; transactionsStatusEl.classList.toggle("qortal-error", Boolean(isError)); transactionsStatusEl.classList.toggle("qortal-hidden", !message); } function txMarker(tx) { if (!tx || typeof tx !== "object") { return ""; } const signature = String(tx.signature || "").trim(); if (signature) { return "sig:" + signature; } const reference = String(tx.reference || "").trim(); if (reference) { return "ref:" + reference; } const timestamp = Number(tx.timestamp || 0); const type = normalizeTxType(tx); const creator = String( tx.creator || tx.creatorAddress || tx.sender || "" ).trim(); if (Number.isFinite(timestamp) && timestamp > 0) { return ( "ts:" + String(timestamp) + "|type:" + type + "|creator:" + creator ); } return ""; } function setTransactionsPendingBanner(count) { pendingConfirmedTxCount = Number.isFinite(Number(count)) ? Math.max(0, Math.floor(Number(count))) : 0; if (!transactionsPendingWrapEl || !transactionsPendingNoteEl) { return; } if (pendingConfirmedTxCount <= 0) { transactionsPendingWrapEl.classList.add("qortal-hidden"); transactionsPendingNoteEl.textContent = "New transactions are available."; return; } transactionsPendingWrapEl.classList.remove("qortal-hidden"); transactionsPendingNoteEl.textContent = String(pendingConfirmedTxCount) + " new transaction" + (pendingConfirmedTxCount === 1 ? "" : "s") + " available."; } function clearTransactionsView(statusMessage) { if (transactionsListEl) { transactionsListEl.innerHTML = ""; } if (transactionsEmptyEl) { transactionsEmptyEl.classList.add("qortal-hidden"); } latestRenderedConfirmedTxMarker = ""; confirmedTransactionsOffset = 0; confirmedTransactionsHasMore = false; confirmedTransactionsLoadingMore = false; renderedConfirmedTxMarkers = new Set(); if (transactionsLoadMoreWrapEl) { transactionsLoadMoreWrapEl.classList.add("qortal-hidden"); } if (transactionsLoadMoreButton) { transactionsLoadMoreButton.disabled = false; transactionsLoadMoreButton.textContent = "Load Next 100"; } if (transactionsLoadMoreNoteEl) { transactionsLoadMoreNoteEl.textContent = "Load older transactions."; } setTransactionsPendingBanner(0); setTransactionsStatus(statusMessage || modeCopy.transactionsNoData, false); clearUnconfirmedTransactionsView(modeCopy.unconfirmedTransactionsNone); } function setTransactionsLoadMoreState(hasMore, isLoading) { confirmedTransactionsHasMore = Boolean(hasMore); confirmedTransactionsLoadingMore = Boolean(isLoading); if (transactionsLoadMoreWrapEl) { transactionsLoadMoreWrapEl.classList.toggle( "qortal-hidden", !confirmedTransactionsHasMore && !confirmedTransactionsLoadingMore ); } if (transactionsLoadMoreButton) { transactionsLoadMoreButton.disabled = confirmedTransactionsLoadingMore; transactionsLoadMoreButton.textContent = confirmedTransactionsLoadingMore ? "Loading…" : "Load Next 100"; } if (transactionsLoadMoreNoteEl) { const shownCount = transactionsListEl ? transactionsListEl.children.length : 0; transactionsLoadMoreNoteEl.textContent = confirmedTransactionsHasMore ? "Showing " + String(shownCount) + " transaction(s)." : "Load older transactions."; } } function setUnconfirmedTransactionsStatus(message, isError) { if (!unconfirmedTransactionsStatusEl) { return; } unconfirmedTransactionsStatusEl.textContent = message || ""; unconfirmedTransactionsStatusEl.classList.toggle( "qortal-error", Boolean(isError) ); } function clearUnconfirmedTransactionsView(message) { if (unconfirmedTransactionsListEl) { unconfirmedTransactionsListEl.innerHTML = ""; } setUnconfirmedTransactionsStatus( message || modeCopy.unconfirmedTransactionsNone, false ); showCard(unconfirmedTransactionsCardEl, false); } function showUnconfirmedTransactionsError(message) { if (unconfirmedTransactionsListEl) { unconfirmedTransactionsListEl.innerHTML = ""; } setUnconfirmedTransactionsStatus( message || modeCopy.unconfirmedTransactionsFailed, true ); showCard(unconfirmedTransactionsCardEl, true); } function formatTransactionTimestamp(timestamp) { const numeric = Number(timestamp); if (Number.isFinite(numeric)) { try { return new Date(numeric).toLocaleString(); } catch (_error) { return "Unknown date"; } } const parsed = Date.parse(String(timestamp || "")); if (!Number.isFinite(parsed)) { return "Unknown date"; } try { return new Date(parsed).toLocaleString(); } catch (_error) { return "Unknown date"; } } function extractTxAddressHints(tx) { if (!tx || typeof tx !== "object") { return []; } const raw = [ tx.creator, tx.creatorAddress, tx.address, tx.sender, tx.owner, tx.ownerAddress, tx.account, tx.recipient, tx.recipientAddress, tx.to, tx.toAddress, ]; return raw .map(function (value) { return String(value || "").trim(); }) .filter(function (value) { return value !== ""; }); } function extractTxPublicKeyHints(tx) { if (!tx || typeof tx !== "object") { return []; } const raw = [ tx.creatorPublicKey, tx.publicKey, tx.senderPublicKey, tx.ownerPublicKey, tx.creator, ]; return raw .map(function (value) { return String(value || "").trim(); }) .filter(function (value) { return value !== ""; }); } function txBelongsToContext(tx, context, requireHints) { const contextAddress = String((context && context.address) || "").trim(); const contextPublicKey = String( resolveContextPublicKey(context) || "" ).trim(); const addressHints = extractTxAddressHints(tx); const keyHints = extractTxPublicKeyHints(tx); const hasHints = addressHints.length > 0 || keyHints.length > 0; if (!hasHints) { return !requireHints; } if ( contextAddress && addressHints.some(function (value) { return value === contextAddress; }) ) { return true; } if ( contextPublicKey && keyHints.some(function (value) { return value === contextPublicKey; }) ) { return true; } return false; } function isChatUnconfirmedTx(tx) { const normalizedType = normalizeTxType(tx); if (normalizedType === "CHAT") { return true; } if (tx && typeof tx === "object") { if (tx.chatReference) { return true; } const txType = String(tx.txType || "").toUpperCase(); if (txType === "CHAT") { return true; } } return false; } function renderUnconfirmedItem(tx, kind) { const item = document.createElement("div"); item.className = "qortal-unconfirmed-item"; if (kind === "chat") { item.classList.add("qortal-unconfirmed-item-chat"); } const typeEl = document.createElement("div"); typeEl.className = "qortal-unconfirmed-type"; typeEl.textContent = normalizeTxType(tx); item.appendChild(typeEl); const metaEl = document.createElement("div"); metaEl.className = "qortal-unconfirmed-meta"; const signature = tx && tx.signature ? truncateText(String(tx.signature), 18) : "no signature"; metaEl.textContent = formatTransactionTimestamp(tx && tx.timestamp) + " • " + signature; item.appendChild(metaEl); return item; } function buildUnconfirmedSection(title, transactions, options) { const config = options && typeof options === "object" ? options : {}; const kind = String(config.kind || "default").trim(); const collapsible = Boolean(config.collapsible); const section = document.createElement("section"); section.className = "qortal-unconfirmed-section"; if (kind === "chat") { section.classList.add("qortal-unconfirmed-section-chat"); } const head = document.createElement("div"); head.className = "qortal-unconfirmed-section-head"; const titleEl = document.createElement("h5"); titleEl.className = "qortal-unconfirmed-section-title"; titleEl.textContent = title; head.appendChild(titleEl); const countEl = document.createElement("span"); countEl.className = "qortal-unconfirmed-section-count"; countEl.textContent = String( Array.isArray(transactions) ? transactions.length : 0 ); head.appendChild(countEl); const body = document.createElement("div"); body.className = "qortal-unconfirmed-section-body"; if (Array.isArray(transactions) && transactions.length > 0) { const fragment = document.createDocumentFragment(); transactions.forEach(function (tx) { fragment.appendChild(renderUnconfirmedItem(tx, kind)); }); body.appendChild(fragment); } else { const empty = document.createElement("p"); empty.className = "qortal-note"; empty.textContent = "None"; body.appendChild(empty); } if (collapsible) { const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "button qortal-unconfirmed-section-toggle"; const applyToggleText = function () { toggle.textContent = unconfirmedChatCollapsed ? "Show" : "Hide"; }; const applyCollapsedState = function () { body.classList.toggle("qortal-hidden", unconfirmedChatCollapsed); }; applyToggleText(); applyCollapsedState(); toggle.addEventListener("click", function () { unconfirmedChatCollapsed = !unconfirmedChatCollapsed; applyToggleText(); applyCollapsedState(); }); head.appendChild(toggle); } section.appendChild(head); section.appendChild(body); return section; } function renderUnconfirmedTransactions(transactions, context) { if (!Array.isArray(transactions) || transactions.length === 0) { clearUnconfirmedTransactionsView(modeCopy.unconfirmedTransactionsNone); return; } if (!unconfirmedTransactionsListEl) { return; } const matchedTransactions = transactions.filter(function (tx) { return txBelongsToContext(tx, context, false); }); // The fetch layer already prefers account-scoped unconfirmed endpoints and // falls back to a globally filtered list. Some node responses omit the // address/public-key hints this renderer looks for, so don't hide valid // scoped results just because the second heuristic pass finds no match. const filtered = matchedTransactions.length > 0 ? matchedTransactions : transactions.slice(); if (filtered.length === 0) { clearUnconfirmedTransactionsView( "No active unconfirmed transactions for this linked account." ); return; } const chatTransactions = []; const nonChatTransactions = []; filtered.forEach(function (tx) { if (isChatUnconfirmedTx(tx)) { chatTransactions.push(tx); return; } nonChatTransactions.push(tx); }); if (nonChatTransactions.length === 0) { clearUnconfirmedTransactionsView(modeCopy.unconfirmedTransactionsNone); return; } unconfirmedTransactionsListEl.innerHTML = ""; const fragment = document.createDocumentFragment(); fragment.appendChild( buildUnconfirmedSection("Transactions", nonChatTransactions) ); if (chatTransactions.length > 0) { fragment.appendChild( buildUnconfirmedSection("Chat Messages", chatTransactions, { collapsible: true, kind: "chat", }) ); } unconfirmedTransactionsListEl.appendChild(fragment); let statusText = "Showing " + String(nonChatTransactions.length) + " active unconfirmed transaction(s) for this account."; if (chatTransactions.length > 0) { statusText += " " + String(chatTransactions.length) + " unconfirmed chat message(s) were ignored."; } setUnconfirmedTransactionsStatus(statusText, false); showCard(unconfirmedTransactionsCardEl, true); } function normalizeTxType(tx) { const raw = tx && tx.type !== undefined && tx.type !== null ? String(tx.type).toUpperCase() : "UNKNOWN"; if (raw === "18") { return "CHAT"; } if (raw === "ARBITRARY") { return publishTxTypeLabel; } return raw; } function formatDateYmd(timestamp) { const numeric = Number(timestamp); if (Number.isFinite(numeric)) { try { return new Date(numeric).toISOString().slice(0, 10); } catch (_error) { return "unknown date"; } } const parsed = Date.parse(String(timestamp || "")); if (!Number.isFinite(parsed)) { return "unknown date"; } try { return new Date(parsed).toISOString().slice(0, 10); } catch (_error) { return "unknown date"; } } function truncateText(value, maxLength) { const text = String(value || ""); if (text.length <= maxLength) { return text; } return text.slice(0, Math.max(0, maxLength - 3)) + "..."; } function isPaymentLikeTx(tx) { if (!tx || typeof tx !== "object") { return false; } const type = String(tx.type || "").toUpperCase(); return ( type.includes("PAYMENT") || type === "SEND_COIN" || Object.prototype.hasOwnProperty.call(tx, "recipient") || Object.prototype.hasOwnProperty.call(tx, "amount") ); } function extractTxRecipient(tx) { if (!tx || typeof tx !== "object") { return ""; } const candidates = [ tx.recipient, tx.recipientAddress, tx.to, tx.toAddress, tx.owner, ]; for (const candidate of candidates) { if (typeof candidate === "string" && looksLikeQortalAddress(candidate)) { return candidate; } } return ""; } function extractTxAmount(tx) { if (!tx || typeof tx !== "object") { return ""; } const amount = tx.amount ?? tx.quantity ?? tx.totalAmount ?? tx.total ?? tx.value; if (amount === undefined || amount === null || amount === "") { return ""; } return String(amount); } function extractTxCreatorAddress(tx) { if (!tx || typeof tx !== "object") { return ""; } const candidates = [ tx.creator, tx.creatorAddress, tx.address, tx.sender, tx.owner, tx.ownerAddress, ]; for (const candidate of candidates) { const value = String(candidate || "").trim(); if (looksLikeQortalAddress(value)) { return value; } } return ""; } function extractTxCreatorPublicKey(tx) { if (!tx || typeof tx !== "object") { return ""; } const candidates = [ tx.creatorPublicKey, tx.publicKey, tx.senderPublicKey, tx.ownerPublicKey, tx.creator, ]; for (const candidate of candidates) { const value = String(candidate || "").trim(); if (value !== "") { return value; } } return ""; } function getTxDirection(tx, context) { const contextAddress = String((context && context.address) || "").trim(); const contextPublicKey = String( resolveContextPublicKey(context) || "" ).trim(); const creatorAddress = extractTxCreatorAddress(tx); const creatorPublicKey = extractTxCreatorPublicKey(tx); const recipientAddress = extractTxRecipient(tx); if ( (contextAddress && creatorAddress && contextAddress === creatorAddress) || (contextPublicKey && creatorPublicKey && contextPublicKey === creatorPublicKey) ) { return "outgoing"; } if ( contextAddress && recipientAddress && contextAddress === recipientAddress ) { return "incoming"; } if (creatorAddress && contextAddress && creatorAddress !== contextAddress) { return "incoming"; } return "neutral"; } function extractTransactionsArray(payload) { if (Array.isArray(payload)) { return payload; } if (!payload || typeof payload !== "object") { return []; } if (Array.isArray(payload.transactions)) { return payload.transactions; } if (payload.data && Array.isArray(payload.data.transactions)) { return payload.data.transactions; } if (payload.data && Array.isArray(payload.data)) { return payload.data; } return []; } function resolveTxConfirmationStatus(tx) { if (!tx || typeof tx !== "object") { return "UNKNOWN"; } const explicitValues = [tx.confirmationStatus, tx.txStatus, tx.status]; for (const value of explicitValues) { const normalized = String(value || "") .trim() .toUpperCase(); if (!normalized) { continue; } if (normalized.includes("UNCONFIRMED")) { return "UNCONFIRMED"; } if (normalized.includes("CONFIRMED")) { return "CONFIRMED"; } } const confirmations = Number( tx.confirmations ?? tx.confirmationCount ?? tx.numConfirmations ); if (Number.isFinite(confirmations)) { if (confirmations > 0) { return "CONFIRMED"; } if (confirmations === 0) { return "UNCONFIRMED"; } } const blockHeight = Number( tx.blockHeight ?? tx.height ?? tx.block ?? tx.blockIndex ); if (Number.isFinite(blockHeight)) { if (blockHeight > 0) { return "CONFIRMED"; } if (blockHeight === 0) { return "UNCONFIRMED"; } } if (isChatUnconfirmedTx(tx)) { return "UNCONFIRMED"; } return "UNKNOWN"; } function filterTransactionsByConfirmationStatus( transactions, requestedStatus ) { if (!Array.isArray(transactions) || transactions.length === 0) { return []; } const expected = String(requestedStatus || "") .trim() .toUpperCase(); if (expected !== "CONFIRMED" && expected !== "UNCONFIRMED") { return transactions; } const resolved = transactions.map(function (tx) { return { tx: tx, status: resolveTxConfirmationStatus(tx), }; }); const hasKnownStatuses = resolved.some(function (entry) { return entry.status !== "UNKNOWN"; }); if (!hasKnownStatuses) { return transactions; } if (expected === "UNCONFIRMED") { return resolved .filter(function (entry) { return entry.status === "UNCONFIRMED"; }) .map(function (entry) { return entry.tx; }); } return resolved .filter(function (entry) { return entry.status !== "UNCONFIRMED"; }) .map(function (entry) { return entry.tx; }); } async function resolveAddressLabel(address) { const normalized = String(address || "").trim(); if (!looksLikeQortalAddress(normalized)) { return ""; } if (txRecipientLabelCache.has(normalized)) { return txRecipientLabelCache.get(normalized) || ""; } let label = ""; try { const primary = await qortalRequest("GET_PRIMARY_NAME", { address: normalized, }); if (typeof primary === "string" && primary.trim() !== "") { label = primary.trim(); } } catch (_error) { // fallback below } if (!label) { try { const url = buildGatewayProxyUrl("names/primary/" + normalized); if (url) { const response = await fetch(url, { method: "GET", headers: getCsrfHeaders(), }); if (response.ok) { const text = (await response.text()).trim(); if (text.startsWith("{") || text.startsWith("[")) { const parsed = JSON.parse(text || "{}"); if (typeof parsed === "string") { label = parsed.trim(); } else if ( parsed && typeof parsed === "object" && typeof parsed.name === "string" ) { label = parsed.name.trim(); } } else if (text !== "") { label = text; } } } } catch (_error) { // ignore } } txRecipientLabelCache.set(normalized, label || ""); return label || ""; } async function fetchTransactionsByPath(path, confirmationStatus, options) { const normalizedPath = String(path || "").trim(); if (!normalizedPath) { return []; } const opts = options && typeof options === "object" ? options : {}; const limit = Number.isFinite(Number(opts.limit)) && Number(opts.limit) > 0 ? Math.max(1, Math.floor(Number(opts.limit))) : transactionPageSize; const offset = Number.isFinite(Number(opts.offset)) && Number(opts.offset) > 0 ? Math.max(0, Math.floor(Number(opts.offset))) : 0; const baseUrl = buildGatewayProxyUrl(normalizedPath); if (!baseUrl) { throw new Error("Gateway proxy URL is not configured for transactions"); } const params = new URLSearchParams({ confirmationStatus: String(confirmationStatus || "CONFIRMED") .trim() .toUpperCase() || "CONFIRMED", limit: String(limit), offset: String(offset), reverse: "true", }); const separator = baseUrl.includes("?") ? "&" : "?"; const response = await fetch(baseUrl + separator + params.toString(), { method: "GET", headers: getCsrfHeaders(), }); const text = await response.text(); if (!response.ok) { throw new Error( "Failed to load transactions (HTTP " + response.status + ")" ); } if (!text.trim()) { return []; } let parsed; try { parsed = JSON.parse(text); } catch (_error) { throw new Error("Invalid transactions response"); } const transactions = extractTransactionsArray(parsed); return filterTransactionsByConfirmationStatus( transactions, confirmationStatus ); } async function fetchAddressTransactions( address, confirmationStatus, options ) { const normalized = String(address || "").trim(); if (!normalized) { return []; } return fetchTransactionsByPath( "transactions/address/" + normalized, confirmationStatus, options ); } async function fetchCreatorTransactions( publicKey, confirmationStatus, options ) { const normalized = String(publicKey || "").trim(); if (!normalized) { return []; } return fetchTransactionsByPath( "transactions/creator/" + normalized, confirmationStatus, options ); } async function fetchAccountTransactions( context, confirmationStatus, options ) { const address = String((context && context.address) || "").trim(); const publicKey = String(resolveContextPublicKey(context) || "").trim(); if (!address && !publicKey) { return []; } if (address) { try { return await fetchAddressTransactions( address, confirmationStatus, options ); } catch (_addressError) { // Fall back to creator endpoint for nodes/environments without address endpoint support. } } if (publicKey) { return fetchCreatorTransactions(publicKey, confirmationStatus, options); } return []; } async function fetchUnconfirmedTransactionsForContext(context) { try { const scoped = await fetchAccountTransactions(context, "UNCONFIRMED"); if (Array.isArray(scoped) && scoped.length > 0) { return scoped; } } catch (_error) { // Fall through to global unconfirmed fallback. } try { const globalUnconfirmed = await fetchTransactionsByPath( "transactions/unconfirmed", "UNCONFIRMED" ); if (!Array.isArray(globalUnconfirmed) || globalUnconfirmed.length === 0) { return []; } return globalUnconfirmed.filter(function (tx) { return txBelongsToContext(tx, context, false); }); } catch (_error) { return []; } } function resolveContextPublicKey(context) { return ( (context && context.accountDetails && (context.accountDetails.publicKey || context.accountDetails.publicKey58 || context.accountDetails.ownerPublicKey)) || (context && context.wallet && (context.wallet.publicKey || context.wallet.publicKey58)) || "" ); } async function refreshUnconfirmedTransactions(context) { if (!context || !context.hasWallet) { clearUnconfirmedTransactionsView(modeCopy.unconfirmedTransactionsNone); return; } const hasAddressOrKey = Boolean( String((context && context.address) || "").trim() || String(resolveContextPublicKey(context) || "").trim() ); if (!hasAddressOrKey) { clearUnconfirmedTransactionsView(modeCopy.unconfirmedTransactionsNone); return; } setUnconfirmedTransactionsStatus( modeCopy.unconfirmedTransactionsLoading, false ); try { const unconfirmedTransactions = await fetchUnconfirmedTransactionsForContext(context); renderUnconfirmedTransactions(unconfirmedTransactions, context); } catch (error) { showUnconfirmedTransactionsError( error && error.message ? error.message : modeCopy.unconfirmedTransactionsFailed ); } } function buildTxSummary(tx, labelsByAddress, context) { const txType = normalizeTxType(tx); const timestampText = formatTransactionTimestamp(tx && tx.timestamp); const direction = getTxDirection(tx, context); const directionLabel = direction === "incoming" ? "Incoming" : direction === "outgoing" ? "Outgoing" : "Activity"; if (txType === publishTxTypeLabel) { const identifier = tx && tx.identifier ? truncateText(tx.identifier, 54) : "no identifier"; const nameLabel = tx && tx.name ? " by " + truncateText(tx.name, 28) : ""; return { title: txType + ": " + identifier, meta: directionLabel + nameLabel, timestampText: timestampText, direction: direction, }; } if (isPaymentLikeTx(tx)) { const amount = extractTxAmount(tx); const recipientAddress = extractTxRecipient(tx); const recipientName = recipientAddress ? labelsByAddress.get(recipientAddress) || "" : ""; const recipientLabel = recipientName || (recipientAddress ? truncateText(recipientAddress, 22) : "unknown recipient"); const amountLabel = amount !== "" ? formatTransactionAmount(amount) + " " + modeCopy.txUnit : modeCopy.txUnknownAmount; return { title: txType + ": " + amountLabel + " to " + recipientLabel, meta: directionLabel, timestampText: timestampText, direction: direction, }; } const signature = tx && tx.signature ? "sig " + truncateText(tx.signature, 16) : ""; return { title: txType, meta: signature ? directionLabel + " • " + signature : directionLabel, timestampText: timestampText, direction: direction, }; } function isNoTransactionsYetError(error) { const message = String( error && error.message ? error.message : error || "" ).toLowerCase(); if (!message) { return false; } return ( message.includes("http 404") || message.includes("transactions not found") || message.includes("transaction not found") || message.includes("no transactions") ); } function countTransactionsBeforeMarker(transactions, marker) { if (!Array.isArray(transactions) || !marker) { return 0; } let count = 0; for (let index = 0; index < transactions.length; index += 1) { const current = txMarker(transactions[index]); if (current && current === marker) { return count; } count += 1; } return count; } async function refreshTransactions(context, options) { const opts = options && typeof options === "object" ? options : {}; const autoCheckOnly = Boolean(opts.autoCheckOnly); const append = Boolean(opts.append); if (!transactionsListEl || !transactionsStatusEl || !transactionsEmptyEl) { return; } if (!context || !context.hasWallet) { clearTransactionsView(modeCopy.transactionsLinkWallet); return; } const hasAddressOrKey = Boolean( String((context && context.address) || "").trim() || String(resolveContextPublicKey(context) || "").trim() ); if (!hasAddressOrKey) { clearTransactionsView( "Linked account identity unavailable for transaction history." ); return; } if (!autoCheckOnly && !append) { setTransactionsStatus(modeCopy.transactionsLoading, false); setUnconfirmedTransactionsStatus( modeCopy.unconfirmedTransactionsLoading, false ); } if (!autoCheckOnly && !append) { transactionsEmptyEl.classList.add("qortal-hidden"); transactionsListEl.innerHTML = ""; confirmedTransactionsOffset = 0; renderedConfirmedTxMarkers = new Set(); setTransactionsPendingBanner(0); setTransactionsLoadMoreState(false, false); } if (append) { setTransactionsLoadMoreState(true, true); } try { const transactionsPromise = fetchAccountTransactions( context, "CONFIRMED", { limit: transactionPageSize, offset: autoCheckOnly ? 0 : append ? confirmedTransactionsOffset : 0, } ); const unconfirmedPromise = autoCheckOnly || append ? Promise.resolve([]) : fetchUnconfirmedTransactionsForContext(context); const settledResults = await Promise.allSettled([ transactionsPromise, unconfirmedPromise, ]); const transactionsResult = settledResults[0]; const unconfirmedResult = settledResults[1]; if (!autoCheckOnly) { if (unconfirmedResult && unconfirmedResult.status === "fulfilled") { renderUnconfirmedTransactions(unconfirmedResult.value, context); } else if ( unconfirmedResult && unconfirmedResult.status === "rejected" ) { showUnconfirmedTransactionsError( unconfirmedResult.reason && unconfirmedResult.reason.message ? unconfirmedResult.reason.message : modeCopy.unconfirmedTransactionsFailed ); } else { clearUnconfirmedTransactionsView( modeCopy.unconfirmedTransactionsNone ); } } if (!transactionsResult || transactionsResult.status !== "fulfilled") { throw ( (transactionsResult && transactionsResult.reason) || new Error(modeCopy.transactionsFailed) ); } const transactions = transactionsResult.value; if (!Array.isArray(transactions) || transactions.length === 0) { if (append) { setTransactionsLoadMoreState(false, false); setTransactionsStatus( modeCopy.transactionsShowing.replace( "{count}", String(transactionsListEl.children.length) ), false ); return; } if (!autoCheckOnly) { transactionsEmptyEl.classList.remove("qortal-hidden"); setTransactionsStatus(modeCopy.transactionsNone, false); latestRenderedConfirmedTxMarker = ""; } return; } const latestMarker = txMarker(transactions[0]); if (autoCheckOnly) { if (!latestRenderedConfirmedTxMarker) { latestRenderedConfirmedTxMarker = latestMarker; return; } if (latestMarker && latestMarker !== latestRenderedConfirmedTxMarker) { const pendingCount = Math.max( 1, countTransactionsBeforeMarker( transactions, latestRenderedConfirmedTxMarker ) ); setTransactionsPendingBanner(pendingCount); setTransactionsStatus( 'New transactions available. Click "Show New Transactions".', false ); } return; } const recipientAddresses = Array.from( new Set( transactions .filter(function (tx) { return isPaymentLikeTx(tx); }) .map(function (tx) { return extractTxRecipient(tx); }) .filter(function (address) { return looksLikeQortalAddress(address); }) ) ); const labelsByAddress = new Map(); if (recipientAddresses.length > 0) { const labelEntries = await Promise.all( recipientAddresses.map(async function (address) { const label = await resolveAddressLabel(address); return [address, label]; }) ); labelEntries.forEach(function (entry) { labelsByAddress.set(entry[0], entry[1]); }); } const fragment = document.createDocumentFragment(); const compactMode = isPoweredMode && !effectiveDebugEnabled; const renderedBefore = transactionsListEl.children.length; transactions.forEach(function (tx) { const marker = txMarker(tx); if (marker && renderedConfirmedTxMarkers.has(marker)) { return; } if (marker) { renderedConfirmedTxMarkers.add(marker); } const summary = buildTxSummary(tx, labelsByAddress, context); const item = document.createElement(compactMode ? "div" : "details"); item.className = "qortal-tx-item"; if (summary.direction === "incoming") { item.classList.add("qortal-tx-item--incoming"); } else if (summary.direction === "outgoing") { item.classList.add("qortal-tx-item--outgoing"); } else { item.classList.add("qortal-tx-item--neutral"); } const summaryEl = document.createElement( compactMode ? "div" : "summary" ); summaryEl.className = "qortal-tx-summary"; const mainEl = document.createElement("span"); mainEl.className = "qortal-tx-main"; const titleEl = document.createElement("span"); titleEl.className = "qortal-tx-title"; titleEl.textContent = summary.title; titleEl.title = summary.title; const metaEl = document.createElement("span"); metaEl.className = "qortal-tx-meta"; metaEl.textContent = summary.meta; metaEl.title = summary.meta; const timeEl = document.createElement("span"); timeEl.className = "qortal-tx-time"; timeEl.textContent = summary.timestampText || "Unknown date"; timeEl.title = summary.timestampText || "Unknown date"; mainEl.appendChild(titleEl); mainEl.appendChild(metaEl); summaryEl.appendChild(mainEl); summaryEl.appendChild(timeEl); item.appendChild(summaryEl); if (!compactMode) { const detailEl = document.createElement("pre"); detailEl.className = "qortal-tx-detail"; detailEl.textContent = JSON.stringify(tx, null, 2); item.appendChild(detailEl); } fragment.appendChild(item); }); transactionsListEl.appendChild(fragment); confirmedTransactionsOffset += transactions.length; setTransactionsLoadMoreState( transactions.length >= transactionPageSize, false ); latestRenderedConfirmedTxMarker = latestMarker; setTransactionsPendingBanner(0); const renderedAfter = transactionsListEl.children.length; if (append && renderedAfter === renderedBefore) { setTransactionsLoadMoreState(false, false); } setTransactionsStatus( modeCopy.transactionsShowing.replace("{count}", String(renderedAfter)), false ); } catch (error) { if (isNoTransactionsYetError(error)) { if (!autoCheckOnly) { transactionsEmptyEl.textContent = modeCopy.transactionsNewAccount; transactionsEmptyEl.classList.remove("qortal-hidden"); setTransactionsStatus(modeCopy.transactionsNewAccount, false); } return; } if (!autoCheckOnly) { if (append) { setTransactionsLoadMoreState(confirmedTransactionsHasMore, false); } setTransactionsStatus( error.message || modeCopy.transactionsFailed, true ); transactionsEmptyEl.classList.add("qortal-hidden"); } } } function requestStatusClass(status) { const normalized = String(status || "").toLowerCase(); if (normalized === "sent") { return "status-sent"; } if (normalized === "denied") { return "status-denied"; } return "status-pending"; } function requestStatusLabel(status) { const normalized = String(status || "").toLowerCase(); if (normalized === "sent") { return "Sent"; } if (normalized === "denied") { return "Denied"; } return "Pending"; } function normalizeRequestType(value) { const normalized = String(value || "") .trim() .toLowerCase(); if (normalized === "install_credit_grant") { return "install_credit_grant"; } return "internal_credit_request"; } function requestTypeLabel(value) { const normalized = normalizeRequestType(value); if (normalized === "install_credit_grant") { return isPoweredMode ? "Install Grant" : "Initial Grant"; } return "Internal Request"; } function formatRequestAmount(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) { return isPoweredMode ? "0" : "0.00000000"; } if (isPoweredMode) { return formatCreditsAmount(qortToCredits(numeric)); } return numeric.toFixed(8); } function formatTransactionAmount(value) { const raw = String(value ?? "").trim(); if (raw === "" || !Number.isFinite(Number(raw))) { return ""; } if (!isPoweredMode) { return raw; } return formatCreditsAmount(qortToCredits(Number(raw))); } function defaultInitialGrantDisplayAmount() { return isPoweredMode ? formatCreditsAmount(qortToCredits(INITIAL_GRANT_QORT)) : String(INITIAL_GRANT_QORT); } function parseInitialGrantAmountInput(inputEl) { const raw = inputEl ? String(inputEl.value || "").trim() : ""; const value = parsePositiveNumber(raw, isPoweredMode ? "Credits" : "QORT"); if (isPoweredMode) { if (!Number.isInteger(value)) { throw new Error(modeCopy.wholeCreditOnlyError); } return creditsToQort(value); } return value; } function clearRequestLists() { if (userRequestsListEl) { userRequestsListEl.innerHTML = ""; } if (adminRequestsListEl) { adminRequestsListEl.innerHTML = ""; } } function buildRequestCard(request, userTotals, adminControls) { const item = document.createElement("div"); const status = String(request.status || "pending").toLowerCase(); const requestType = normalizeRequestType( request.requestType || "internal_credit_request" ); item.className = "qortal-request-item " + requestStatusClass(status); const head = document.createElement("div"); head.className = "qortal-request-head"; const title = document.createElement("p"); title.className = "qortal-request-title"; const userLabel = request.userDisplayName || request.userId || "User"; title.textContent = adminControls ? userLabel + " • " + requestTypeLabel(requestType) + " • " + requestStatusLabel(status) : requestTypeLabel(requestType) + " • " + requestStatusLabel(status); const date = document.createElement("span"); date.className = "qortal-request-date"; date.textContent = formatDateYmd( request.createdAt || request.updatedAt || "" ); head.appendChild(title); head.appendChild(date); item.appendChild(head); const meta = document.createElement("div"); meta.className = "qortal-request-meta"; const primaryName = request.primaryName ? String(request.primaryName) : "none"; const address = request.address ? truncateText(String(request.address), 32) : "unknown"; const totalSent = formatRequestAmount(request.totalSent || 0); const userTotal = adminControls ? formatRequestAmount(userTotals[String(request.userId || "")] || 0) : totalSent; const metaParts = [ "Type: " + requestTypeLabel(requestType), "Address: " + address, "Primary name: " + primaryName, "Request total sent: " + totalSent + " " + modeCopy.txUnit, "User total sent: " + userTotal + " " + modeCopy.txUnit, ]; if (requestType === "install_credit_grant") { const relayStatus = String(request.relayStatus || "").trim(); const externalState = String(request.externalState || "").trim(); const paymenterCustomerId = String( request.paymenterCustomerId || "" ).trim(); if (relayStatus) { metaParts.push("CHD relay: " + relayStatus); } if (externalState) { metaParts.push("Connector state: " + externalState); } if (paymenterCustomerId) { metaParts.push("Billing customer: #" + paymenterCustomerId); } } meta.textContent = metaParts.join(" • "); item.appendChild(meta); if (requestType === "install_credit_grant" && request.relayError) { const relayError = document.createElement("div"); relayError.className = "qortal-request-message"; relayError.textContent = "CHD relay error: " + String(request.relayError); item.appendChild(relayError); } const messages = Array.isArray(request.messages) ? request.messages : []; if (messages.length > 0) { const list = document.createElement("ul"); list.className = "qortal-request-message-list"; messages.slice(-6).forEach(function (entry) { const li = document.createElement("li"); li.className = "qortal-request-message"; const from = entry && entry.fromDisplayName ? String(entry.fromDisplayName) : "Admin"; const message = entry && entry.message ? String(entry.message) : ""; const at = entry && entry.at ? formatDateYmd(String(entry.at)) : ""; li.textContent = from + (at ? " (" + at + ")" : "") + ": " + message; list.appendChild(li); }); item.appendChild(list); } if (adminControls) { const actions = document.createElement("div"); actions.className = "qortal-request-actions"; const amountControl = document.createElement("div"); amountControl.className = "qortal-request-send-control"; const amountLabel = document.createElement("label"); amountLabel.className = "qortal-label"; const amountInputId = "qortal-initial-grant-amount-" + String(request.id); amountLabel.setAttribute("for", amountInputId); amountLabel.textContent = isPoweredMode ? "Credits" : "QORT"; amountControl.appendChild(amountLabel); const amountInput = document.createElement("input"); amountInput.id = amountInputId; amountInput.type = "number"; amountInput.min = isPoweredMode ? "1" : "0.00000001"; amountInput.step = isPoweredMode ? "1" : "0.00000001"; amountInput.value = defaultInitialGrantDisplayAmount(); amountInput.disabled = status !== "pending"; amountControl.appendChild(amountInput); actions.appendChild(amountControl); const sendButton = document.createElement("button"); sendButton.className = "button button-primary"; sendButton.textContent = "Send"; sendButton.disabled = status !== "pending"; sendButton.addEventListener("click", function () { sendInitialQortForRequest(request, amountInput); }); actions.appendChild(sendButton); const messageButton = document.createElement("button"); messageButton.className = "button"; messageButton.textContent = "Message"; messageButton.addEventListener("click", function () { sendAdminMessageForRequest(request); }); actions.appendChild(messageButton); const denyButton = document.createElement("button"); denyButton.className = "button button-error"; denyButton.textContent = "Deny"; denyButton.disabled = status !== "pending"; denyButton.addEventListener("click", function () { denyInitialQortRequest(request); }); actions.appendChild(denyButton); item.appendChild(actions); } return item; } function extractRequestListPayload(payload) { if (!payload || typeof payload !== "object") { return { requests: [], ownRequests: [], userTotals: {}, summary: null, pagination: null, isAdmin: false, }; } const data = payload.data && typeof payload.data === "object" ? payload.data : payload; const requests = Array.isArray(data.requests) ? data.requests : []; const ownRequests = Array.isArray(data.ownRequests) ? data.ownRequests : []; const userTotals = data.userTotals && typeof data.userTotals === "object" ? data.userTotals : {}; return { requests: requests, ownRequests: ownRequests, userTotals: userTotals, summary: data.summary && typeof data.summary === "object" ? data.summary : null, pagination: data.pagination && typeof data.pagination === "object" ? data.pagination : null, isAdmin: data.isAdmin === true, }; } function updateAdminOpsSummary(requests, userTotals, summary) { if (!adminOpsCardEl || !shouldShowAdminQueueSection(activeWalletContext)) { return; } if (summary && typeof summary === "object") { if (adminOpsPendingEl) { adminOpsPendingEl.textContent = String(summary.pendingCount || 0); } if (adminOpsSentEl) { adminOpsSentEl.textContent = String(summary.sentCount || 0); } if (adminOpsDeniedEl) { adminOpsDeniedEl.textContent = String(summary.deniedCount || 0); } if (adminOpsUsersEl) { adminOpsUsersEl.textContent = String(summary.usersCount || 0); } if (adminOpsTotalSentEl) { adminOpsTotalSentEl.textContent = formatRequestAmount(summary.totalSent || 0) + " " + modeCopy.txUnit; } return; } const list = Array.isArray(requests) ? requests : []; let pendingCount = 0; let sentCount = 0; let deniedCount = 0; let totalSentQort = 0; const queueUsers = new Set(); list.forEach(function (request) { const status = String( request && request.status ? request.status : "pending" ).toLowerCase(); if (status === "sent") { sentCount += 1; } else if (status === "denied") { deniedCount += 1; } else { pendingCount += 1; } const amount = Number( request && request.totalSent ? request.totalSent : 0 ); if (Number.isFinite(amount) && amount > 0) { totalSentQort += amount; } const userId = request && request.userId ? String(request.userId).trim() : ""; if (userId !== "") { queueUsers.add(userId); } }); const totals = userTotals && typeof userTotals === "object" ? userTotals : {}; const usersWithTotals = Object.keys(totals).filter(function (userId) { return String(userId || "").trim() !== ""; }).length; const usersCount = Math.max(queueUsers.size, usersWithTotals); if (adminOpsPendingEl) { adminOpsPendingEl.textContent = String(pendingCount); } if (adminOpsSentEl) { adminOpsSentEl.textContent = String(sentCount); } if (adminOpsDeniedEl) { adminOpsDeniedEl.textContent = String(deniedCount); } if (adminOpsUsersEl) { adminOpsUsersEl.textContent = String(usersCount); } if (adminOpsTotalSentEl) { adminOpsTotalSentEl.textContent = formatRequestAmount(totalSentQort) + " " + modeCopy.txUnit; } } function updateAdminRequestsPagination(pagination) { if ( !adminRequestsPaginationEl || !adminRequestsPrevButton || !adminRequestsNextButton || !adminRequestsPageEl ) { return; } const data = pagination && typeof pagination === "object" ? pagination : {}; const total = Number(data.total || 0); const limit = Math.max(1, Number(data.limit || ADMIN_REQUESTS_PAGE_SIZE)); const offset = Math.max(0, Number(data.offset || 0)); const hasMore = data.hasMore === true; if (!Number.isFinite(total) || total <= limit) { adminRequestsPaginationEl.classList.add("qortal-hidden"); return; } const start = Math.min(offset + 1, total); const end = Math.min(offset + limit, total); adminRequestsPageEl.textContent = "Showing " + start + "-" + end + " of " + total; adminRequestsPrevButton.disabled = offset <= 0; adminRequestsNextButton.disabled = !hasMore; adminRequestsPaginationEl.classList.remove("qortal-hidden"); } function renderInitialQortRequests( requests, userTotals, ownRequestList, pagination, summary ) { const ownRequests = Array.isArray(ownRequestList) ? ownRequestList : Array.isArray(requests) ? requests.filter(function (request) { return String(request.userId || "") === String(currentUserId || ""); }) : []; const userScoped = isAdmin ? ownRequests : Array.isArray(requests) ? requests : []; hasUserRequestHistory = userScoped.length > 0; hasPendingInitialGrantRequest = ownRequests.some(function (request) { return ( String( request && request.status ? request.status : "pending" ).toLowerCase() === "pending" ); }); applyRequestCopy(activeWalletContext); const showUserRequests = shouldShowUserRequestsSection(activeWalletContext); const showAdminQueue = shouldShowAdminQueueSection(activeWalletContext); if (!showUserRequests && !showAdminQueue) { clearRequestLists(); showCard(userRequestsCardEl, false); showCard(adminRequestsCardEl, false); showCard(adminOpsCardEl, false); if (adminRequestsPaginationEl) { adminRequestsPaginationEl.classList.add("qortal-hidden"); } return; } clearRequestLists(); showCard(userRequestsCardEl, showUserRequests); showCard(adminRequestsCardEl, showAdminQueue); showCard(adminOpsCardEl, showAdminQueue); if (showUserRequests && !hasPendingInitialGrantRequest) { setSectionCollapsed( userRequestsToggleButton, userRequestsBodyEl, "user_requests" ); } if (requestQortButton) { requestQortButton.disabled = hasPendingInitialGrantRequest || !activeWalletContext.hasWallet; requestQortButton.title = hasPendingInitialGrantRequest ? "A request is already pending. Check your Requests section below." : ""; } if (showUserRequests && userRequestsListEl && userRequestsEmptyEl) { if (userScoped.length === 0) { userRequestsEmptyEl.classList.remove("qortal-hidden"); } else { userRequestsEmptyEl.classList.add("qortal-hidden"); const userFragment = document.createDocumentFragment(); userScoped.forEach(function (request) { userFragment.appendChild( buildRequestCard(request, userTotals, false) ); }); userRequestsListEl.appendChild(userFragment); } } if ( !showAdminQueue || !adminRequestsCardEl || !adminRequestsListEl || !adminRequestsEmptyEl ) { updateAdminOpsSummary(requests, userTotals, summary); if (adminRequestsPaginationEl) { adminRequestsPaginationEl.classList.add("qortal-hidden"); } return; } adminRequestsCardEl.classList.remove("qortal-hidden"); if (!Array.isArray(requests) || requests.length === 0) { adminRequestsEmptyEl.classList.remove("qortal-hidden"); if (adminRequestsPaginationEl) { adminRequestsPaginationEl.classList.add("qortal-hidden"); } updateAdminOpsSummary(requests, userTotals, summary); return; } adminRequestsEmptyEl.classList.add("qortal-hidden"); const adminFragment = document.createDocumentFragment(); requests.forEach(function (request) { adminFragment.appendChild(buildRequestCard(request, userTotals, true)); }); adminRequestsListEl.appendChild(adminFragment); updateAdminRequestsPagination(pagination); updateAdminOpsSummary(requests, userTotals, summary); } async function refreshInitialQortRequests() { if (!initialQortRequestsUrl) { return; } try { const requestListUrl = new URL( initialQortRequestsUrl, window.location.origin ); if (isAdmin) { requestListUrl.searchParams.set( "limit", String(ADMIN_REQUESTS_PAGE_SIZE) ); requestListUrl.searchParams.set("offset", String(adminRequestsOffset)); } const payload = await requestJson(requestListUrl.toString(), { method: "GET", headers: getCsrfHeaders(), }); const parsed = extractRequestListPayload(payload); if (parsed.pagination && Number.isFinite(Number(parsed.pagination.offset))) { adminRequestsOffset = Math.max(0, Number(parsed.pagination.offset)); } renderInitialQortRequests( parsed.requests, parsed.userTotals, parsed.ownRequests, parsed.pagination, parsed.summary ); } catch (_error) { // Keep UI functional if request list endpoint is unavailable. } } async function runInitialQortAdminAction(requestId, action, extra) { if (!initialQortActionUrl) { throw new Error( isPoweredMode ? "Initial credits admin action endpoint is not configured" : "Initial QORT admin action endpoint is not configured" ); } const body = new URLSearchParams(); body.set("requestId", String(requestId || "")); body.set("action", String(action || "")); const details = extra && typeof extra === "object" ? extra : {}; if (details.message) { body.set("message", String(details.message)); } if (details.amount !== undefined && details.amount !== null) { body.set("amount", String(details.amount)); } if (details.txSignature) { body.set("txSignature", String(details.txSignature)); } return requestJson(initialQortActionUrl, { method: "POST", headers: getCsrfHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), body: body.toString(), }); } async function fetchPrimaryName(address) { if (!address) { return ""; } try { const primary = await qortalRequest("GET_PRIMARY_NAME", { address: address, }); if (typeof primary === "string" && primary.trim() !== "") { return primary.trim(); } } catch (_error) { // continue to fallback } try { const url = buildGatewayProxyUrl("names/primary/" + address); if (!url) { return ""; } const response = await fetch(url, { method: "GET", headers: getCsrfHeaders(), }); if (!response.ok) { return ""; } const text = (await response.text()).trim(); if (!text) { return ""; } if (text.startsWith("{") || text.startsWith("[")) { const parsed = JSON.parse(text); if (typeof parsed === "string") { return parsed.trim(); } if ( parsed && typeof parsed === "object" && typeof parsed.name === "string" ) { return parsed.name.trim(); } return ""; } return text; } catch (_error) { return ""; } } function updateWalletFields(mapping, wallet, balancePayload) { if (walletIdEl) { walletIdEl.textContent = formatValue( (mapping && mapping.walletId) || (wallet && wallet.walletId) ); } if (publicKeyEl) { publicKeyEl.textContent = formatValue( wallet && (wallet.publicKey || wallet.publicKey58) ); } if (balanceRawEl) { balanceRawEl.textContent = formatValue(balancePayload); } } function updateDashboardState(context) { const hasWallet = Boolean(context && context.hasWallet); applyUserPublishingVisibility(context); showCard(onboardingCard, !hasWallet); showCard(walletPane, hasWallet || isAdmin); if (!hasWallet) { if (addressEl) { addressEl.textContent = "—"; } if (balanceEl) { balanceEl.textContent = "—"; } if (primaryNameEl) { primaryNameEl.textContent = "—"; } updateNames([], ""); if (registerNameButton) { registerNameButton.disabled = true; } if (sendButton) { sendButton.disabled = true; } if (quickSendButton) { quickSendButton.disabled = true; } if (sendFeeEditButton) { sendFeeEditButton.disabled = true; } updateBalanceVisualState(null); if (requestQortWrapEl) { requestQortWrapEl.classList.add("qortal-hidden"); } clearTransactionsView(modeCopy.transactionsLinkWallet); return; } if (addressEl) { addressEl.textContent = formatValue(context.address); } if (balanceEl) { balanceEl.textContent = formatBalance(context.balance); } if (primaryNameEl) { primaryNameEl.textContent = context.primaryName ? context.primaryName : "—"; } updateNames(context.names, context.primaryName || ""); if (sendButton) { sendButton.disabled = false; } if (quickSendButton) { quickSendButton.disabled = false; } if (sendFeeEditButton) { sendFeeEditButton.disabled = false; } updateBalanceVisualState(context.balance); if (!canUserPublish && !isAdmin) { if (requestQortWrapEl) { requestQortWrapEl.classList.add("qortal-hidden"); } if (registerNameButton) { registerNameButton.disabled = true; registerNameButton.title = "User publishing is disabled by admin"; } if (sendButton) { sendButton.disabled = true; } if (quickSendButton) { quickSendButton.disabled = true; } if (sendFeeEditButton) { sendFeeEditButton.disabled = true; } return; } const numericBalance = parseBalanceNumber(context.balance); const adminQueueEligible = isAdminQueueEligible(context); const showInitialQortButton = adminQueueEligible ? false : isAdmin ? true : shouldShowInitialQortRequest(context.balance); if (requestQortWrapEl) { requestQortWrapEl.classList.toggle( "qortal-hidden", !showInitialQortButton ); } const canRegisterName = Number.isFinite(numericBalance) && numericBalance >= 1.25; if (registerNameButton) { const canUseRegister = canRegisterName && !pendingNameRegistration; registerNameButton.disabled = !canUseRegister; if (pendingNameRegistration) { registerNameButton.title = modeCopy.registerTitlePending; } else { registerNameButton.title = canRegisterName ? modeCopy.registerTitleReady : modeCopy.registerTitleNeedsBalance; } } } function normalizeApprovalScope(scope, requestType) { const normalizedScope = String(scope || "").trim(); if (normalizedScope) { return normalizedScope; } return String(requestType || "") .trim() .toUpperCase(); } function isApprovalRequiredError(error) { if (!error) { return false; } const message = error.message || ""; return typeof message === "string" && message.includes("approval_required"); } function isWalletLockedError(error) { if (!error) { return false; } const message = error.message || ""; if (typeof message === "string" && message.includes("wallet_locked")) { return true; } const details = error.details; if (details && typeof details === "object") { if (details.error === "wallet_locked") { return true; } const nestedDetail = details.data && details.data.detail ? details.data.detail : null; if ( nestedDetail && typeof nestedDetail === "object" && nestedDetail.error === "wallet_locked" ) { return true; } } return false; } function extractApprovalContext(error, requestType) { const context = { scope: "", walletId: "", }; function captureFromValue(value, depth) { if (depth > 3 || value === null || value === undefined) { return; } if (typeof value === "string") { if (!context.scope) { const scopeMatch = value.match(/"scope"\s*:\s*"([^"]+)"/); if (scopeMatch && scopeMatch[1]) { context.scope = scopeMatch[1]; } } if (!context.walletId) { const walletMatch = value.match(/"walletId"\s*:\s*"([^"]+)"/); if (walletMatch && walletMatch[1]) { context.walletId = walletMatch[1]; } } const jsonStart = value.indexOf("{"); const jsonEnd = value.lastIndexOf("}"); if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) { try { const parsed = JSON.parse(value.slice(jsonStart, jsonEnd + 1)); captureFromValue(parsed, depth + 1); } catch (_err) { // ignore parse errors } } return; } if (typeof value !== "object") { return; } if (!context.scope && typeof value.scope === "string") { context.scope = value.scope; } if (!context.walletId && typeof value.walletId === "string") { context.walletId = value.walletId; } if (value.detail !== undefined) { captureFromValue(value.detail, depth + 1); } if (value.data !== undefined) { captureFromValue(value.data, depth + 1); } if (value.error !== undefined) { captureFromValue(value.error, depth + 1); } } captureFromValue(error && error.details ? error.details : null, 0); captureFromValue(error && error.message ? error.message : "", 0); context.scope = normalizeApprovalScope(context.scope, requestType); if (!context.walletId) { context.walletId = (activeWalletContext && activeWalletContext.mapping && activeWalletContext.mapping.walletId) || (activeWalletContext && activeWalletContext.wallet && activeWalletContext.wallet.walletId) || ""; } return context; } function parseUnlockStatus(value) { let candidate = value; for (let i = 0; i < 4; i += 1) { if ( !candidate || typeof candidate !== "object" || candidate.data === undefined || candidate.data === null ) { break; } if (typeof candidate.data !== "object") { break; } candidate = candidate.data; } if ( !candidate || typeof candidate !== "object" || typeof candidate.unlocked !== "boolean" ) { return null; } const unlocked = candidate.unlocked; const rawExpires = candidate.expiresAt !== undefined ? candidate.expiresAt : candidate.expires_at; let expiresAtMs = null; if ( typeof rawExpires === "number" && Number.isFinite(rawExpires) && rawExpires > 0 ) { expiresAtMs = rawExpires < 10_000_000_000 ? rawExpires * 1000 : rawExpires; } else if ( typeof rawExpires === "string" && /^\d+$/.test(rawExpires.trim()) ) { const parsed = Number(rawExpires.trim()); if (Number.isFinite(parsed) && parsed > 0) { expiresAtMs = parsed < 10_000_000_000 ? parsed * 1000 : parsed; } } return { unlocked, expiresAtMs }; } function markWalletUnknown() { walletLockState = { known: false, unlocked: null, expiresAtMs: null }; } function markWalletLocked() { walletLockState = { known: true, unlocked: false, expiresAtMs: null }; } function markWalletUnlocked(expiresAtMs) { walletLockState = { known: true, unlocked: true, expiresAtMs: typeof expiresAtMs === "number" ? expiresAtMs : null, }; } function walletStatusPresentation() { if (!walletLockState.known) { return { text: "Account lock: unknown (password may be required)", className: "qortal-wallet-status-unknown", showUnlockFields: true, }; } if (!walletLockState.unlocked) { return { text: "Account lock: LOCKED (password required)", className: "qortal-wallet-status-locked", showUnlockFields: true, }; } if ( walletLockState.expiresAtMs && walletLockState.expiresAtMs > Date.now() ) { const minutes = Math.max( 1, Math.ceil((walletLockState.expiresAtMs - Date.now()) / 60000) ); return { text: "Account lock: UNLOCKED for ~" + minutes + " minute(s)", className: "qortal-wallet-status-ok", showUnlockFields: false, }; } return { text: "Account lock: UNLOCKED", className: "qortal-wallet-status-ok", showUnlockFields: false, }; } function updateApprovalWalletStatus() { const status = walletStatusPresentation(); if (approvalWalletStatusEl) { approvalWalletStatusEl.textContent = status.text; approvalWalletStatusEl.classList.remove( "qortal-wallet-status-ok", "qortal-wallet-status-locked", "qortal-wallet-status-unknown" ); approvalWalletStatusEl.classList.add(status.className); } if (approvalUnlockFieldsEl) { approvalUnlockFieldsEl.classList.toggle( "qortal-hidden", !status.showUnlockFields ); } if (!status.showUnlockFields && approvalPasswordEl) { approvalPasswordEl.value = ""; } return status; } function showApprovalError(message) { if (!approvalErrorEl) { return; } approvalErrorEl.textContent = message || ""; approvalErrorEl.classList.toggle("qortal-hidden", !message); } async function refreshWalletLockState(walletId) { const id = String(walletId || "").trim(); if (!id || !qappsUnlockStatusUrl) { markWalletUnknown(); return false; } try { const joiner = qappsUnlockStatusUrl.indexOf("?") === -1 ? "?" : "&"; const response = await fetch( qappsUnlockStatusUrl + joiner + "walletId=" + encodeURIComponent(id), { method: "GET", headers: getCsrfHeaders(), } ); const payload = await response.json(); if (!response.ok || (payload && payload.error)) { markWalletUnknown(); return false; } const parsed = parseUnlockStatus(payload); if (!parsed) { markWalletUnknown(); return false; } if (!parsed.unlocked) { markWalletLocked(); return true; } markWalletUnlocked(parsed.expiresAtMs); return true; } catch (_error) { markWalletUnknown(); return false; } } function closeApprovalModal() { if (!approvalModal) { return; } approvalModal.classList.add("qortal-hidden"); showApprovalError(""); if (approvalPasswordEl) { approvalPasswordEl.value = ""; approvalPasswordEl.onkeydown = null; } if (approvalConfirmButton) { approvalConfirmButton.disabled = false; approvalConfirmButton.textContent = "Unlock and Send"; approvalConfirmButton.onclick = null; } if (approvalCancelButton) { approvalCancelButton.disabled = false; approvalCancelButton.onclick = null; } } async function showApprovalModal(context) { if (!approvalModal || !approvalConfirmButton || !approvalCancelButton) { return { action: "cancel" }; } if (approvalRequestEl) { approvalRequestEl.textContent = String(context.requestType || "ACTION"); } showApprovalError(""); updateApprovalWalletStatus(); approvalModal.classList.remove("qortal-hidden"); if ( approvalUnlockFieldsEl && !approvalUnlockFieldsEl.classList.contains("qortal-hidden") && approvalPasswordEl ) { approvalPasswordEl.focus(); approvalPasswordEl.onkeydown = function (event) { if (event.key === "Enter" && !approvalConfirmButton.disabled) { event.preventDefault(); approvalConfirmButton.click(); } }; } else { approvalConfirmButton.focus(); } return new Promise(function (resolve) { approvalConfirmButton.onclick = function () { const status = walletStatusPresentation(); const password = approvalPasswordEl ? approvalPasswordEl.value.trim() : ""; if (status.showUnlockFields && !password) { showApprovalError( "Account password is required because account appears locked." ); return; } approvalConfirmButton.disabled = true; approvalConfirmButton.textContent = "Unlocking..."; approvalCancelButton.disabled = true; resolve({ action: "approve", password: password, ttlSeconds: defaultApprovalTempMinutes * 60, }); }; approvalCancelButton.onclick = function () { closeApprovalModal(); resolve({ action: "cancel" }); }; }); } async function unlockWallet(walletId, password, ttlSeconds) { if (!qappsUnlockUrl) { throw new Error("Unlock endpoint is not configured"); } const response = await fetch(qappsUnlockUrl, { method: "POST", headers: getCsrfHeaders("application/json"), body: JSON.stringify({ walletId, password, ...(ttlSeconds ? { ttlSeconds } : {}), }), }); const payload = await response.json(); if (!response.ok || payload.ok === false || payload.error) { markWalletLocked(); throw new Error(payload.error || "account_unlock_failed"); } const expiresAtMs = ttlSeconds ? Date.now() + Number(ttlSeconds) * 1000 : null; markWalletUnlocked(expiresAtMs); return payload; } async function approveOnce(walletId, scope, requestType) { if (!qappsApproveUrl) { throw new Error("Approval endpoint is not configured"); } const response = await fetch(qappsApproveUrl, { method: "POST", headers: getCsrfHeaders("application/json"), body: JSON.stringify({ walletId, scope, app: "qortal://NC/ACCOUNT", requestType, }), }); const payload = await response.json(); if (!response.ok || payload.ok === false || payload.error) { throw new Error(payload.error || "approval_failed"); } const token = payload.data && payload.data.approvalToken ? payload.data.approvalToken : payload.approvalToken; if (!token) { throw new Error("approval_token_missing"); } return token; } async function sendWithApproval(payload, requestType) { const action = String(requestType || "") .trim() .toUpperCase(); if (!action) { throw new Error("requestType is required"); } let workingPayload = Object.assign({}, payload || {}); let attempt = 0; while (attempt < 4) { try { return await qortalRequest(action, workingPayload); } catch (error) { const approvalRequired = isApprovalRequiredError(error); const walletLocked = isWalletLockedError(error); if (!approvalRequired && !walletLocked) { throw error; } const approvalContext = extractApprovalContext(error, action); const walletId = approvalContext.walletId || workingPayload.walletId || (activeWalletContext && activeWalletContext.mapping && activeWalletContext.mapping.walletId) || ""; if (!walletId) { throw error; } await refreshWalletLockState(walletId); updateApprovalWalletStatus(); if (walletStatusPresentation().showUnlockFields) { const decision = await showApprovalModal({ requestType: action, scope: approvalContext.scope || action, walletId: walletId, }); if (!decision || decision.action === "cancel") { throw new Error("approval_cancelled"); } if (decision.password) { try { await unlockWallet( walletId, decision.password, decision.ttlSeconds ); } catch (unlockError) { showApprovalError(unlockError.message || "account_unlock_failed"); throw unlockError; } } } const scope = normalizeApprovalScope(approvalContext.scope, action); const token = await approveOnce(walletId, scope, action); workingPayload = Object.assign({}, payload || {}, { approvalToken: token, }); closeApprovalModal(); attempt += 1; } } throw new Error("approval_failed"); } function parsePositiveNumber(raw, fieldName) { const value = Number(raw); if (!Number.isFinite(value) || value <= 0) { throw new Error(fieldName + " must be a positive number"); } return value; } function extractAddressFromNameInfo(input) { if (typeof input === "string") { return looksLikeQortalAddress(input) ? input : ""; } if (Array.isArray(input)) { for (const entry of input) { const nested = extractAddressFromNameInfo(entry); if (nested) { return nested; } } return ""; } if (!input || typeof input !== "object") { return ""; } const directCandidates = [ input.owner, input.address, input.ownerAddress, input.owner_address, input.qortalAddress, input.qortal_address, input.registrant, input.creatorAddress, ]; for (const candidate of directCandidates) { if (typeof candidate === "string" && looksLikeQortalAddress(candidate)) { return candidate; } } const nestedCandidates = [ input.nameData, input.name_info, input.nameInfo, input.result, input.data, ]; for (const nested of nestedCandidates) { const candidate = extractAddressFromNameInfo(nested); if (candidate) { return candidate; } } return ""; } async function fetchNameDataViaGateway(name) { const normalizedName = normalizeNameInput(name); if (!normalizedName) { return null; } const url = buildGatewayProxyUrl("names/" + normalizedName); if (!url) { return null; } const response = await fetch(url, { method: "GET", headers: getCsrfHeaders(), }); if (!response.ok) { return null; } const text = await response.text(); if (!text) { return null; } try { return JSON.parse(text); } catch (_error) { return null; } } async function resolveRecipientAddress(input) { const raw = (input || "").trim(); if (!raw) { throw new Error("Recipient is required"); } if (looksLikeQortalAddress(raw)) { return raw; } const normalizedName = normalizeNameInput(raw); if (!normalizedName) { throw new Error("Recipient is required"); } try { const info = await qortalRequest("GET_NAME_DATA", { name: normalizedName, }); const address = extractAddressFromNameInfo(info); if (address) { return address; } } catch (_error) { // continue fallback } try { const gatewayInfo = await fetchNameDataViaGateway(normalizedName); const address = extractAddressFromNameInfo(gatewayInfo); if (address) { return address; } } catch (_error) { // final error below } throw new Error('Name "' + raw + '" does not resolve to a Qortal address'); } function extractSenderFields(wallet, accountDetails) { const account = accountDetails && typeof accountDetails === "object" ? accountDetails : {}; const senderPublicKey = account.publicKey || account.publicKey58 || account.ownerPublicKey || (wallet && (wallet.publicKey || wallet.publicKey58)) || ""; const reference = account.lastReference || account.reference || account.lastreference || account.last_reference || ""; return { senderPublicKey: typeof senderPublicKey === "string" ? senderPublicKey : "", reference: typeof reference === "string" ? reference : "", }; } async function tryHydrateSenderFields(context, senderFields) { if (senderFields.reference && senderFields.senderPublicKey) { return senderFields; } const address = (context && (context.address || (context.mapping && context.mapping.qortalAddress))) || ""; if (!address) { return senderFields; } try { const accountData = await qortalRequest("GET_ACCOUNT_DATA", { address }); const refreshed = extractSenderFields( context ? context.wallet : null, accountData ); return { senderPublicKey: refreshed.senderPublicKey || senderFields.senderPublicKey || "", reference: refreshed.reference || senderFields.reference || "", }; } catch (_error) { return senderFields; } } async function ensureWalletContext() { if ( activeWalletContext.hasWallet && activeWalletContext.mapping && activeWalletContext.wallet ) { return activeWalletContext; } await refreshAll("context"); if (!activeWalletContext.hasWallet) { throw new Error("No linked account found"); } return activeWalletContext; } async function sendCoinTransfer(recipientInput, amount, fee) { const recipientAddress = await resolveRecipientAddress(recipientInput); const context = await ensureWalletContext(); let accountDetails = context.accountDetails; if (!accountDetails) { try { accountDetails = await qortalRequest("GET_USER_ACCOUNT", {}); } catch (_error) { accountDetails = null; } } let senderFields = extractSenderFields(context.wallet, accountDetails); senderFields = await tryHydrateSenderFields(context, senderFields); const payload = { coin: "QORT", walletId: (context.mapping && context.mapping.walletId) || (context.wallet && context.wallet.walletId) || undefined, recipient: recipientAddress, amount: amount, fee: fee, }; if (senderFields.reference && senderFields.senderPublicKey) { payload.tx = { timestamp: Date.now(), reference: senderFields.reference, fee: fee, senderPublicKey: senderFields.senderPublicKey, recipient: recipientAddress, amount: amount, }; } const result = await sendWithApproval(payload, "SEND_COIN"); return { result: result, recipientAddress: recipientAddress, }; } function extractSignatureFromResult(payload) { if (!payload || typeof payload !== "object") { return ""; } if ( typeof payload.signature === "string" && payload.signature.trim() !== "" ) { return payload.signature.trim(); } const nestedCandidates = [ payload.result, payload.data, payload.tx, payload.transaction, ]; for (const candidate of nestedCandidates) { const signature = extractSignatureFromResult(candidate); if (signature !== "") { return signature; } } if (Array.isArray(payload.results)) { for (const candidate of payload.results) { const signature = extractSignatureFromResult(candidate); if (signature !== "") { return signature; } } } return ""; } function setQuickSendError(message) { if (!quickSendErrorEl) { return; } const text = String(message || "").trim(); quickSendErrorEl.textContent = text; quickSendErrorEl.classList.toggle("qortal-hidden", text === ""); } function openQuickSendModal() { if (!quickSendModal) { return; } setQuickSendError(""); if (quickSendRecipientEl && sendRecipientEl) { quickSendRecipientEl.value = String(sendRecipientEl.value || "").trim(); } if (quickSendAmountEl && sendAmountEl) { quickSendAmountEl.value = String(sendAmountEl.value || "").trim(); } quickSendModal.classList.remove("qortal-hidden"); if (quickSendRecipientEl) { quickSendRecipientEl.focus(); } } function closeQuickSendModal() { if (!quickSendModal) { return; } quickSendModal.classList.add("qortal-hidden"); setQuickSendError(""); } async function executeSendTransfer( recipientInput, amountInputRaw, feeRaw, triggerButton ) { if (!sendResultEl) { return; } const amountInput = parsePositiveNumber( String(amountInputRaw || "").trim(), "Amount" ); const feeValue = String(feeRaw || "").trim(); let amountQort = amountInput; let feeQort = feeValue === "" ? 0.01 : parsePositiveNumber(feeValue, "Fee"); let displayAmount = amountInput; let displayFee = feeQort; if (isPoweredMode) { if (!Number.isInteger(amountInput)) { throw new Error(modeCopy.wholeCreditOnlyError); } displayFee = 1; amountQort = creditsToQort(amountInput); feeQort = QORT_PER_CREDIT; } if (triggerButton) { triggerButton.disabled = true; } setStatus(modeCopy.sendPendingLabel, false); const transfer = await sendCoinTransfer( String(recipientInput || "").trim(), amountQort, feeQort ); sendResultEl.textContent = JSON.stringify( { ok: true, recipientInput: String(recipientInput || "").trim(), recipientAddress: transfer.recipientAddress, amount: displayAmount, fee: displayFee, unit: modeCopy.txUnit, result: transfer.result, }, null, 2 ); setStatus(modeCopy.sendSuccessLabel, false); notifyDashboard( buildSendSuccessNotice( String(recipientInput || "").trim() || transfer.recipientAddress, displayAmount, modeCopy.txUnit ), false ); await refreshAll("manual"); } async function sendQort() { if ( !sendRecipientEl || !sendAmountEl || !sendFeeEl || !sendResultEl || !sendButton ) { return; } if (!canUserPublish && !isAdmin) { setStatus("User publishing is disabled by admin", true); return; } try { await executeSendTransfer( sendRecipientEl.value.trim(), sendAmountEl.value.trim(), sendFeeEl.value.trim(), sendButton ); } catch (error) { closeApprovalModal(); sendResultEl.textContent = JSON.stringify( { ok: false, error: error.message || "send_failed", }, null, 2 ); setStatus(error.message || "send failed", true); } finally { if (sendButton) { sendButton.disabled = !activeWalletContext.hasWallet; } } } async function submitQuickSend() { if ( !quickSendRecipientEl || !quickSendAmountEl || !quickSendConfirmButton ) { return; } try { setQuickSendError(""); await executeSendTransfer( quickSendRecipientEl.value.trim(), quickSendAmountEl.value.trim(), "0.01", quickSendConfirmButton ); if (sendRecipientEl) { sendRecipientEl.value = quickSendRecipientEl.value.trim(); } if (sendAmountEl) { sendAmountEl.value = quickSendAmountEl.value.trim(); } if (sendFeeEl) { sendFeeEl.value = "0.01"; } closeQuickSendModal(); } catch (error) { closeApprovalModal(); setQuickSendError(error.message || "send_failed"); } finally { if (quickSendConfirmButton) { quickSendConfirmButton.disabled = false; } } } async function sendInitialQortForRequest(request, amountInput) { if (!request || !request.id || !request.address) { setStatus("Invalid request payload", true); return; } try { const grantAmountQort = parseInitialGrantAmountInput(amountInput); setStatus(modeCopy.initialGrantSending, false); const transfer = await sendCoinTransfer( String(request.address), grantAmountQort, 0.01 ); const txSignature = extractSignatureFromResult(transfer.result); await runInitialQortAdminAction(String(request.id), "send", { amount: grantAmountQort, txSignature: txSignature, }); setStatus(modeCopy.initialGrantSent, false); await refreshInitialQortRequests(); await refreshAll("manual"); } catch (error) { closeApprovalModal(); setStatus(error.message || modeCopy.initialGrantFailed, true); } } async function sendAdminMessageForRequest(request) { if (!request || !request.id) { return; } const message = window.prompt("Send a message to this user:"); if (!message || !message.trim()) { return; } try { await runInitialQortAdminAction(String(request.id), "message", { message: message.trim(), }); setStatus("Message sent to user", false); await refreshInitialQortRequests(); } catch (error) { setStatus(error.message || "Failed to send message", true); } } async function denyInitialQortRequest(request) { if (!request || !request.id) { return; } const message = window.prompt("Optional deny reason (recommended):", ""); if (!window.confirm(modeCopy.initialGrantDenyConfirm)) { return; } try { await runInitialQortAdminAction(String(request.id), "deny", { message: message ? message.trim() : "", }); setStatus("Request denied", false); await refreshInitialQortRequests(); } catch (error) { setStatus(error.message || "Failed to deny request", true); } } function openRegisterModal() { if (!registerModal) { return; } resetRegisterModalState(); registerModal.classList.remove("qortal-hidden"); if (registerNameInputEl) { registerNameInputEl.disabled = false; registerNameInputEl.focus(); } } function closeRegisterModal() { if (!registerModal) { return; } registerModal.classList.add("qortal-hidden"); resetRegisterModalState(); } async function registerName() { try { const context = await ensureWalletContext(); const numericBalance = Number(context.balance); if (!Number.isFinite(numericBalance) || numericBalance < 1.25) { throw new Error( "At least " + modeCopy.registerMinimumAmount + " " + modeCopy.txUnit + " is required to register a name" ); } const desiredName = normalizeNameInput( registerNameInputEl ? registerNameInputEl.value : "" ); const validationMessage = validateRegisterName(desiredName); if (validationMessage) { throw new Error(validationMessage); } setStatus("checking name availability…", false); const alreadyRegistered = await isNameAlreadyRegistered(desiredName); if (alreadyRegistered) { throw new Error( "This name is already registered. Choose another name." ); } if (registerConfirmButton) { registerConfirmButton.disabled = true; } setStatus("registering name…", false); const payload = { walletId: (context.mapping && context.mapping.walletId) || (context.wallet && context.wallet.walletId) || undefined, name: desiredName, }; const result = await sendWithApproval(payload, "REGISTER_NAME"); pendingNameRegistration = true; pendingRegisteredName = normalizeNameForCompare(desiredName); showRegisterSuccess(desiredName, result); setStatus("name registration successful", false); await refreshAll("manual"); } catch (error) { const friendlyError = mapRegisterNameError(error); setRegisterError(friendlyError); if (registerSubmitActionsEl) { registerSubmitActionsEl.classList.remove("qortal-hidden"); } if (registerSuccessActionsEl) { registerSuccessActionsEl.classList.add("qortal-hidden"); } if (registerSuccessEl) { registerSuccessEl.classList.add("qortal-hidden"); } if (registerSuccessNoteEl) { registerSuccessNoteEl.classList.add("qortal-hidden"); } setRegisterResult({ ok: false, error: friendlyError }, true); if (registerResultEl) { registerResultEl.classList.remove("qortal-hidden"); } setStatus(friendlyError, true); } finally { if (registerConfirmButton) { registerConfirmButton.disabled = false; } } } async function createWallet() { if (!userCreateWalletUrl || !createPasswordEl || !createButton) { return; } const password = createPasswordEl.value.trim(); const kdfThreads = createKdfEl ? createKdfEl.value.trim() : ""; if (!password) { setCreateResult("Password is required.", true); return; } createButton.disabled = true; setCreateResult("Creating account...", false); setStatus("creating account…", false); try { const body = new URLSearchParams(); body.set("password", password); if (kdfThreads) { body.set("kdfThreads", kdfThreads); } const payload = await requestJson(userCreateWalletUrl, { method: "POST", headers: getCsrfHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), body: body.toString(), }); setCreateResult(JSON.stringify(payload, null, 2), false); createPasswordEl.value = ""; if (createKdfEl) { createKdfEl.value = ""; } await refreshAll("manual"); setStatus("account created", false); } catch (error) { setCreateResult(error.message || "account_creation_failed", true); setStatus(error.message || "account creation failed", true); } finally { createButton.disabled = false; } } async function requestInitialQort() { if (!canUserPublish) { setRequestFeedback("User publishing is disabled by admin.", true); return; } if (!userRequestInitialQortUrl) { setRequestQortResult( { ok: false, error: "Request endpoint is not configured" }, true ); setRequestFeedback("Request endpoint is not configured.", true); return; } const context = activeWalletContext; if (!context || !context.hasWallet || !context.address) { setRequestQortResult( { ok: false, error: "A linked account is required first" }, true ); setRequestFeedback("A linked account is required first.", true); return; } if (hasPendingInitialGrantRequest) { const pendingMessage = "A request is already pending. Check your Requests section for updates."; setRequestFeedback(pendingMessage, false); setStatus("request already pending", false); window.alert(pendingMessage); return; } if (requestQortButton) { requestQortButton.disabled = true; } if (requestQortHelp) { requestQortHelp.classList.remove("qortal-hidden"); } const installGrantMode = isInstallGrantRequestMode(context); setStatus( installGrantMode ? "sending new install credit grant request…" : isPoweredMode ? "sending initial credits request…" : "sending initial QORT request…", false ); try { const body = new URLSearchParams(); body.set("address", context.address); body.set( "balance", context.balance !== undefined && context.balance !== null ? String(context.balance) : "" ); body.set("primaryName", context.primaryName || ""); body.set( "names", Array.isArray(context.names) ? JSON.stringify(context.names) : "[]" ); body.set( "requestType", installGrantMode ? "install_credit_grant" : "internal_credit_request" ); const payload = await requestJson(userRequestInitialQortUrl, { method: "POST", headers: getCsrfHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), body: body.toString(), }); if (effectiveDebugEnabled) { setRequestQortResult(payload, false); } const duplicateRequest = Boolean( payload && payload.data && (payload.data.isDuplicate || (payload.data.request && payload.data.request.isDuplicate)) ); const requestDate = payload && payload.data && payload.data.request && payload.data.request.createdAt ? formatDateYmd(payload.data.request.createdAt) : formatDateYmd(Date.now()); let successMessage = ""; if (installGrantMode) { successMessage = duplicateRequest ? "Install credit grant request already pending (requested on " + requestDate + "). Check your Requests section for updates." : "Install credit grant request sent on " + requestDate + ". Check your Requests section for updates."; setRequestFeedback(successMessage, false); setStatus( duplicateRequest ? "install grant request already pending" : "install grant request sent", false ); } else { successMessage = duplicateRequest ? "Initial request already pending (requested on " + requestDate + "). Check your Requests section for updates." : "Request sent on " + requestDate + ". Check your Requests section for updates."; setRequestFeedback(successMessage, false); setStatus( duplicateRequest ? "request already pending" : "request sent to admins", false ); } hasUserRequestHistory = true; window.alert(successMessage); await refreshInitialQortRequests(); } catch (error) { if (effectiveDebugEnabled) { setRequestQortResult( { ok: false, error: error.message || "request_failed" }, true ); } setRequestFeedback(error.message || "request_failed", true); setStatus(error.message || "request failed", true); } finally { if (requestQortButton) { requestQortButton.disabled = false; } } } async function copyAddress() { const address = activeWalletContext && activeWalletContext.address ? activeWalletContext.address : ""; if (!address) { setStatus("No address available to copy", true); return; } try { await navigator.clipboard.writeText(address); setStatus(modeCopy.copyAddressStatus, false); } catch (_error) { const input = document.createElement("input"); input.value = address; document.body.appendChild(input); input.select(); document.execCommand("copy"); input.remove(); setStatus(modeCopy.copyAddressStatus, false); } } function sanitizeFilenameToken(value, fallback) { const raw = String( value === undefined || value === null ? "" : value ).trim(); let normalized = raw; if (typeof normalized.normalize === "function") { normalized = normalized.normalize("NFKD").replace(/[\u0300-\u036f]/g, ""); } const cleaned = normalized .replace(/[^A-Za-z0-9._-]+/g, "_") .replace(/^_+|_+$/g, ""); return cleaned || fallback; } function resolveBackupUserToken() { const primaryName = activeWalletContext && activeWalletContext.primaryName ? String(activeWalletContext.primaryName).trim() : ""; if (primaryName !== "") { return sanitizeFilenameToken(primaryName, "user"); } const firstKnownName = activeWalletContext && Array.isArray(activeWalletContext.names) && activeWalletContext.names.length > 0 ? String(activeWalletContext.names[0] || "").trim() : ""; if (firstKnownName !== "") { return sanitizeFilenameToken(firstKnownName, "user"); } return sanitizeFilenameToken(currentUserId || "user", "user"); } function resolveBackupCloudDomainToken() { let host = ""; if (nextcloudPublicUrl) { try { host = new URL(nextcloudPublicUrl).hostname || ""; } catch (_error) { host = ""; } } if (!host && window.location && window.location.hostname) { host = window.location.hostname; } return sanitizeFilenameToken(host, "cloud"); } function buildBackupFilename(address) { const userToken = resolveBackupUserToken(); const cloudDomainToken = resolveBackupCloudDomainToken(); const addressToken = sanitizeFilenameToken(address, "account"); return ( "nuqloud-backup-" + userToken + "-" + cloudDomainToken + "-" + addressToken + ".json" ); } function resolveBackupMappingDisplayName(mapping) { if (!mapping || typeof mapping !== "object") { return ""; } const mappingName = String( mapping.primaryName || mapping.registeredName || mapping.name || mapping.qortalName || "" ).trim(); if (mappingName) { return mappingName; } const mappingAddress = String(mapping.qortalAddress || "").trim(); if ( mappingAddress && activeWalletContext && String(activeWalletContext.address || "").trim() === mappingAddress ) { const activePrimary = String( activeWalletContext.primaryName || "" ).trim(); if (activePrimary) { return activePrimary; } if ( Array.isArray(activeWalletContext.names) && activeWalletContext.names.length > 0 ) { for (const entry of activeWalletContext.names) { if (typeof entry === "string" && entry.trim() !== "") { return entry.trim(); } if ( entry && typeof entry === "object" && typeof entry.name === "string" && entry.name.trim() !== "" ) { return entry.name.trim(); } } } } return mappingAddress; } function toggleAdvanced() { if (!advancedBody || !advancedToggleButton) { return; } const willShow = advancedBody.classList.contains("qortal-hidden"); advancedBody.classList.toggle("qortal-hidden", !willShow); advancedToggleButton.textContent = willShow ? "Hide" : "Show"; } function normalizeBackupObject(value) { if (!value || typeof value !== "object") { return null; } if (value.backup && typeof value.backup === "object") { return value.backup; } if (value.data && typeof value.data === "object") { return normalizeBackupObject(value.data); } return value; } function downloadJsonFile(filename, payload) { const serialized = JSON.stringify(payload, null, 2); const blob = new Blob([serialized], { type: "application/json" }); const blobUrl = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = blobUrl; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(blobUrl); } function setBackupModalError(message) { if (!backupModalErrorEl) { return; } const text = String(message || "").trim(); backupModalErrorEl.textContent = text; backupModalErrorEl.classList.toggle("qortal-hidden", text === ""); } function setBackupModalStatus(message, isError) { if (!backupModalStatusEl) { return; } const text = String(message || "").trim(); backupModalStatusEl.textContent = text; backupModalStatusEl.classList.toggle("qortal-hidden", text === ""); backupModalStatusEl.classList.toggle("qortal-error", Boolean(isError)); } function getBackupPasswordDraft() { const currentPassword = backupPasswordEl ? String(backupPasswordEl.value || "").trim() : ""; const reEncryptEnabled = Boolean( backupReencryptToggleEl && backupReencryptToggleEl.checked ); if (reEncryptEnabled) { return backupNewPasswordEl ? String(backupNewPasswordEl.value || "").trim() : ""; } return currentPassword; } function buildDavUrl(pathSegments) { const segments = Array.isArray(pathSegments) ? pathSegments : []; const base = getNextcloudBaseUrl(); const userId = encodeURIComponent(String(currentUserId || "").trim()); const encodedPath = segments .map(function (segment) { return encodeURIComponent( String(segment || "").replace(/^\/+|\/+$/g, "") ); }) .filter(function (segment) { return segment !== ""; }) .join("/"); return base + "/remote.php/dav/files/" + userId + "/" + encodedPath; } async function ensureDavFolder(pathSegments) { if (!currentUserId) { throw new Error("Current user is not available for file save."); } const normalized = Array.isArray(pathSegments) ? pathSegments : []; for (let i = 0; i < normalized.length; i += 1) { const partialPath = normalized.slice(0, i + 1); const response = await fetch(buildDavUrl(partialPath), { method: "MKCOL", headers: getCsrfHeaders(), }); if (response.ok || response.status === 405 || response.status === 301) { continue; } throw new Error("Unable to create Files folder: " + response.status); } } async function saveBackupToNextcloudFiles(filename, backup) { const folderName = isPoweredMode ? "NuQloud Auth Files" : "Qortal Backups"; await ensureDavFolder([folderName]); const response = await fetch(buildDavUrl([folderName, filename]), { method: "PUT", headers: getCsrfHeaders("application/json"), body: JSON.stringify(backup, null, 2), }); if (!response.ok) { throw new Error("Files upload failed with status " + response.status); } return folderName; } function getPasswordsAppUrl() { return getNextcloudBaseUrl() + "/apps/passwords/"; } async function handoffPasswordToPasswordsApp(passwordValue, label) { const password = String(passwordValue || "").trim(); if (!password) { throw new Error("Enter the backup password first."); } const draft = { label: label, username: activeWalletContext && activeWalletContext.primaryName ? String(activeWalletContext.primaryName) : String(currentUserId || "NuQloud User"), url: getNextcloudBaseUrl(), password: password, notes: "Saved from the Qortal Integration dashboard backup modal.", savedAt: new Date().toISOString(), }; try { if (window.sessionStorage) { window.sessionStorage.setItem( passwordsDraftStorageKey, JSON.stringify(draft) ); } } catch (_error) { // ignore sessionStorage failures } let copied = false; try { await navigator.clipboard.writeText(password); copied = true; } catch (_error) { copied = false; } const openedWindow = window.open( getPasswordsAppUrl(), "_blank", "noopener" ); if (!openedWindow) { throw new Error("Unable to open the Passwords app in a new tab."); } return copied; } function getBackupWalletOptions() { const options = []; const seenWalletIds = new Set(); if (Array.isArray(availableWalletMappings)) { availableWalletMappings.forEach(function (entry) { if (!entry || typeof entry !== "object") { return; } const walletId = entry.walletId ? String(entry.walletId).trim() : ""; if (!walletId || seenWalletIds.has(walletId)) { return; } seenWalletIds.add(walletId); const address = entry.qortalAddress ? String(entry.qortalAddress).trim() : ""; const status = entry.status ? String(entry.status).trim() : ""; const displayName = resolveBackupMappingDisplayName(entry); const displayAddress = address ? truncateText(address, 18) : ""; const labelBase = displayName || displayAddress || "linked account"; const labelWithAddress = displayName && displayAddress && displayName !== address ? labelBase + " • " + displayAddress : labelBase; options.push({ walletId: walletId, address: address, label: labelWithAddress + (status ? " • " + status : ""), }); }); } const activeWalletId = (activeWalletContext && activeWalletContext.mapping && activeWalletContext.mapping.walletId) || (activeWalletContext && activeWalletContext.wallet && activeWalletContext.wallet.walletId) || ""; if (activeWalletId && !seenWalletIds.has(activeWalletId)) { const activeName = String( activeWalletContext && activeWalletContext.primaryName ? activeWalletContext.primaryName : "" ).trim(); const activeAddress = activeWalletContext && activeWalletContext.address ? String(activeWalletContext.address).trim() : ""; const fallbackLabel = activeName || activeAddress || "linked account"; const fallbackWithAddress = activeName && activeAddress ? fallbackLabel + " • " + truncateText(activeAddress, 18) : fallbackLabel; options.push({ walletId: String(activeWalletId), address: activeAddress, label: fallbackWithAddress, }); } return options; } function refreshBackupWalletSelect() { if (!backupWalletSelectEl) { return []; } const options = getBackupWalletOptions(); backupWalletSelectEl.innerHTML = ""; if (options.length === 0) { const emptyOption = document.createElement("option"); emptyOption.value = ""; emptyOption.textContent = "No linked accounts available"; backupWalletSelectEl.appendChild(emptyOption); return []; } options.forEach(function (entry) { const optionEl = document.createElement("option"); optionEl.value = entry.walletId; optionEl.textContent = entry.label; if ( activeWalletContext && activeWalletContext.mapping && activeWalletContext.mapping.walletId === entry.walletId ) { optionEl.selected = true; } backupWalletSelectEl.appendChild(optionEl); }); return options; } function syncBackupReencryptFields() { if (!backupReencryptFieldsEl || !backupReencryptToggleEl) { return; } const showFields = backupReencryptToggleEl.checked; backupReencryptFieldsEl.classList.toggle("qortal-hidden", !showFields); if (!showFields && backupNewPasswordEl) { backupNewPasswordEl.value = ""; } if (!showFields && backupNewPasswordConfirmEl) { backupNewPasswordConfirmEl.value = ""; } } function openBackupModal() { if (!backupModal) { return; } const options = refreshBackupWalletSelect(); if (options.length === 0) { window.alert("No linked account found. Create or link an account first."); return; } setBackupModalError(""); setBackupModalStatus("", false); if (backupPasswordEl) { backupPasswordEl.value = ""; } if (backupReencryptToggleEl) { backupReencryptToggleEl.checked = false; } syncBackupReencryptFields(); backupModal.classList.remove("qortal-hidden"); if (backupPasswordEl) { backupPasswordEl.focus(); } } function closeBackupModal() { if (!backupModal) { return; } backupModal.classList.add("qortal-hidden"); setBackupModalError(""); setBackupModalStatus("", false); } async function createBackupFile(action) { if (!userBackupWalletUrl) { window.alert("Backup endpoint is not configured."); return; } const walletId = backupWalletSelectEl ? String(backupWalletSelectEl.value || "").trim() : ""; if (!walletId) { setBackupModalError("Select a linked account first."); return; } const password = backupPasswordEl ? String(backupPasswordEl.value || "").trim() : ""; if (!password) { setBackupModalError("Current backup password is required."); throw new Error("Current backup password is required."); } const reEncryptEnabled = Boolean( backupReencryptToggleEl && backupReencryptToggleEl.checked ); const reEncryptPassword = reEncryptEnabled && backupNewPasswordEl ? String(backupNewPasswordEl.value || "").trim() : ""; const newPassword = reEncryptEnabled && backupNewPasswordConfirmEl ? String(backupNewPasswordConfirmEl.value || "").trim() : ""; if (reEncryptEnabled && !reEncryptPassword) { setBackupModalError("Enter a new backup password or disable re-encrypt."); throw new Error("Enter a new backup password or disable re-encrypt."); } if (reEncryptEnabled && !newPassword) { setBackupModalError("Confirm the new backup password."); throw new Error("Confirm the new backup password."); } if (reEncryptEnabled && reEncryptPassword !== newPassword) { setBackupModalError("New backup password fields must match."); throw new Error("New backup password fields must match."); } setBackupModalError(""); setBackupModalStatus("", false); if (backupModalConfirmButton) { backupModalConfirmButton.disabled = true; } if (backupSaveFileButton) { backupSaveFileButton.disabled = true; } if (backupSavePasswordButton) { backupSavePasswordButton.disabled = true; } try { const body = new URLSearchParams(); body.set("walletId", walletId); body.set("password", password); if (reEncryptPassword) { body.set("reEncryptPassword", reEncryptPassword); body.set("newPassword", newPassword); } const payload = await requestJson(userBackupWalletUrl, { method: "POST", headers: getCsrfHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), body: body.toString(), }); const backup = normalizeBackupObject(payload); if (!backup || typeof backup !== "object") { throw new Error("Backup payload was invalid"); } const selectedEntry = getBackupWalletOptions().find(function (entry) { return entry.walletId === walletId; }); const address = selectedEntry && selectedEntry.address ? selectedEntry.address : activeWalletContext && activeWalletContext.address ? activeWalletContext.address : walletId; const filename = buildBackupFilename(address); const effectivePassword = reEncryptPassword || password; if (action === "save") { const folderName = await saveBackupToNextcloudFiles(filename, backup); const successMessage = modeCopy.saveBackupToFilesSuccess + " Folder: " + folderName + "."; setBackupModalStatus(successMessage, false); setStatus(successMessage, false); return { filename: filename, backup: backup, password: effectivePassword, }; } downloadJsonFile(filename, backup); const successMessage = modeCopy.downloadBackupButton + " downloaded: " + filename + ". Store it safely."; setStatus(successMessage, false); closeBackupModal(); window.alert(successMessage); return { filename: filename, backup: backup, password: effectivePassword, }; } catch (error) { setBackupModalError(error.message || "Failed to create backup"); throw error; } finally { if (backupModalConfirmButton) { backupModalConfirmButton.disabled = false; } if (backupSaveFileButton) { backupSaveFileButton.disabled = false; } if (backupSavePasswordButton) { backupSavePasswordButton.disabled = false; } } } async function downloadBackupFile() { try { await createBackupFile("download"); } catch (_error) { // error already surfaced in modal } } async function saveBackupFileToInstance() { try { await createBackupFile("save"); } catch (_error) { // error already surfaced in modal } } async function saveBackupPasswordToPasswordsApp() { try { setBackupModalError(""); setBackupModalStatus("", false); const passwordValue = getBackupPasswordDraft(); const label = isPoweredMode ? "NuQloud Authentication File" : "Qortal Backup Password"; const copied = await handoffPasswordToPasswordsApp(passwordValue, label); setBackupModalStatus( copied ? modeCopy.passwordsAppOpened : "Passwords app opened in a new tab. Use the password from this dialog to create a new entry.", false ); } catch (error) { setBackupModalError(error.message || modeCopy.passwordsAppUnavailable); } } async function refreshAll(context) { if (inFlight) { return; } inFlight = true; setStatus("loading…", false); const nodeStatusPromise = refreshNodeStatus(); const billingSyncPromise = refreshBillingConnectorState(context); try { const mappingsPayload = await requestJson(mappingsUrl, { method: "GET", headers: getCsrfHeaders(), }); const mappings = extractMappings(mappingsPayload); availableWalletMappings = Array.isArray(mappings) ? mappings.filter(function (entry) { return entry && typeof entry === "object"; }) : []; refreshBackupWalletSelect(); const mapping = pickMapping(mappings); const hasWallet = Boolean(mapping && mapping.walletId); if (!hasWallet) { activeWalletContext = { hasWallet: false, mapping: mapping || null, wallet: null, address: "", balance: null, primaryName: "", names: [], accountDetails: null, }; updateDashboardState(activeWalletContext); if (detailsEl) { detailsEl.textContent = "Link an account to enable account details."; } await refreshInitialQortRequests(); applyPendingHighlights(); setStatus("account not linked", true); return; } const wallet = await qortalRequest("GET_USER_WALLET", { coin: "QORT" }); let address = wallet && (wallet.address || wallet.address0 || (wallet.addresses && wallet.addresses[0])); if (!address && mapping && mapping.qortalAddress) { address = mapping.qortalAddress; } let balancePayload = null; let balanceAmount = undefined; try { balancePayload = await qortalRequest( "GET_WALLET_BALANCE", address ? { coin: "QORT", address: address } : { coin: "QORT" } ); balanceAmount = extractBalanceAmount(balancePayload); } catch (error) { balancePayload = { error: error.message || "balance_fetch_failed" }; } if ( (balanceAmount === undefined || balanceAmount === null || balanceAmount === "") && address ) { try { const directBalance = await qortalRequest("GET_BALANCE", { address: address, }); balanceAmount = extractBalanceAmount(directBalance); if ( balanceAmount !== undefined && balanceAmount !== null && balanceAmount !== "" ) { balancePayload = directBalance; } } catch (_error) { // continue fallback } } if ( (balanceAmount === undefined || balanceAmount === null || balanceAmount === "") && address ) { try { const nodeBalance = await fetchNodeBalance(address); balanceAmount = extractBalanceAmount(nodeBalance); if ( balanceAmount !== undefined && balanceAmount !== null && balanceAmount !== "" ) { balancePayload = nodeBalance; } } catch (_error) { // ignore } } let names = []; if (address) { try { names = await qortalRequest("GET_ACCOUNT_NAMES", { address: address, }); } catch (_error) { names = []; } } if ( !Array.isArray(names) && names && typeof names === "object" && Array.isArray(names.names) ) { names = names.names; } if (!Array.isArray(names)) { names = []; } let primaryName = await fetchPrimaryName(address || ""); if (!primaryName && Array.isArray(names) && names.length > 0) { const first = names[0]; if (typeof first === "string" && first.trim() !== "") { primaryName = first.trim(); } else if ( first && typeof first === "object" && typeof first.name === "string" && first.name.trim() !== "" ) { primaryName = first.name.trim(); } } if (pendingNameRegistration && pendingRegisteredName !== "") { const normalizedPrimary = normalizeNameForCompare(primaryName || ""); const confirmed = normalizedPrimary === pendingRegisteredName || hasNameInList(names, pendingRegisteredName); if (confirmed) { pendingNameRegistration = false; pendingRegisteredName = ""; } } let accountDetails = null; try { accountDetails = await qortalRequest("GET_USER_ACCOUNT", {}); } catch (error) { if (address) { try { accountDetails = await qortalRequest("GET_ACCOUNT_DATA", { address: address, }); } catch (err) { accountDetails = { error: err.message || "account_fetch_failed" }; } } else { accountDetails = { error: error.message || "account_fetch_failed" }; } } activeWalletContext = { hasWallet: true, mapping: mapping, wallet: wallet, address: address || "", balance: balanceAmount, primaryName: primaryName, names: names, accountDetails: accountDetails, }; updateWalletFields(mapping, wallet, balancePayload); updateDashboardState(activeWalletContext); if (activityNotificationsLoaded) { const defaultsChanged = ensureActivityNotificationDefaults(); renderActivityNotificationsAvailableDefaults(); if (defaultsChanged) { saveActivityNotificationsConfig({ silent: true }).catch(function () { // Ignore silent auto-persist errors; explicit save remains available. }); } } applyPendingHighlights(); if (detailsEl) { detailsEl.textContent = JSON.stringify(accountDetails, null, 2); } await refreshTransactions(activeWalletContext, { autoCheckOnly: context === "auto", }); await refreshWalletLockState( mapping.walletId || (wallet && wallet.walletId) || "" ); await refreshInitialQortRequests(); setStatus(context === "test" ? "test ok" : "ok", false); } catch (error) { if (detailsEl) { detailsEl.textContent = error.message || "Failed to refresh"; } setStatus(error.message || "error", true); } finally { await billingSyncPromise; await nodeStatusPromise; updateLastRefresh(); inFlight = false; } } function startLive() { if (intervalId) { clearInterval(intervalId); } if (unconfirmedIntervalId) { clearInterval(unconfirmedIntervalId); } intervalId = window.setInterval(function () { if (document.hidden) { return; } refreshAll("auto"); }, refreshIntervalMs); unconfirmedIntervalId = window.setInterval(function () { if (document.hidden) { return; } refreshUnconfirmedTransactions(activeWalletContext); }, unconfirmedRefreshIntervalMs); liveEnabled = true; if (liveToggleButton) { liveToggleButton.textContent = "Pause Live Refresh"; } } function stopLive() { if (intervalId) { clearInterval(intervalId); intervalId = null; } if (unconfirmedIntervalId) { clearInterval(unconfirmedIntervalId); unconfirmedIntervalId = null; } liveEnabled = false; if (liveToggleButton) { liveToggleButton.textContent = "Resume Live Refresh"; } } cloudAppInstalled = detectInstalledCloudApp(); if (window.matchMedia) { const displayModeQuery = window.matchMedia("(display-mode: standalone)"); if (typeof displayModeQuery.addEventListener === "function") { displayModeQuery.addEventListener("change", function () { cloudAppInstalled = detectInstalledCloudApp(); setInstallPwaLink(); }); } } window.addEventListener("appinstalled", function () { cloudAppInstalled = true; setInstallPwaLink(); }); setSettingsLink(); setInstallPwaLink(); setAdminSettingsLink(); setChdAdminLink(); applyModeCopy(); applyPurchasesCopy(); updatePurchasesSupportUi(); applyUserPublishingVisibility(activeWalletContext); initializeSectionToggle( appGalleryToggleButton, appGalleryBodyEl, "app_gallery", true ); initializeSectionToggle( userRequestsToggleButton, userRequestsBodyEl, "user_requests", false ); initializeSectionToggle( adminRequestsToggleButton, adminRequestsBodyEl, "admin_requests", false ); initializeSectionToggle( adminOpsToggleButton, adminOpsBodyEl, "admin_ops", true ); initializeSectionToggle( inviteManagementToggleButton, inviteManagementBodyEl, "invite_management", true ); initializeSectionToggle( purchasesToggleButton, purchasesBodyEl, "purchases", true ); initializeSectionToggle(namesToggleButton, namesBodyEl, "names", false); initializeSectionToggle( activityNotificationsToggleButton, activityNotificationsBodyEl, "activity_notifications", false ); updatePurchasesLoadUi(); renderPurchases(); refreshBillingCatalog("init"); renderQappsGallery(); syncActivityNotificationControlsFromState(); renderActivityNotificationsTrackedList(); loadActivityNotificationsConfig(); applyRequestCopy(activeWalletContext); resetRegisterModalState(); setFeeEditable(false); refreshAll("init"); if (isAdmin) { refreshInvites({ showLoading: true }); } startLive(); if (purchasesSupportSendButton) { purchasesSupportSendButton.addEventListener("click", function () { sendPurchasesSupportRequest().catch(function (error) { setPurchasesSupportStatus( error && error.message ? error.message : modeCopy.purchasesSupportFailed, true ); }); }); } if (refreshButton) { refreshButton.addEventListener("click", function () { refreshAll("manual"); if (activityNotificationsLoaded) { ensureActivityNotificationDefaults(); } }); } if (inviteCreateButtonEl) { inviteCreateButtonEl.addEventListener("click", function () { createInvite().catch(function (error) { const message = error && error.message ? error.message : "Failed to create invite"; setInviteCreateResult(message, true); setStatus(message, true); notifyDashboard(message, true); }); }); } if (inviteRefreshButtonEl) { inviteRefreshButtonEl.addEventListener("click", function () { refreshInvites({ showLoading: true }); }); } if (adminRequestsPrevButton) { adminRequestsPrevButton.addEventListener("click", function () { adminRequestsOffset = Math.max( 0, adminRequestsOffset - ADMIN_REQUESTS_PAGE_SIZE ); refreshInitialQortRequests(); }); } if (adminRequestsNextButton) { adminRequestsNextButton.addEventListener("click", function () { adminRequestsOffset += ADMIN_REQUESTS_PAGE_SIZE; refreshInitialQortRequests(); }); } if (activityNotificationsAddButton) { activityNotificationsAddButton.addEventListener("click", async function () { const identifier = activityNotificationsNewIdentifierEl ? activityNotificationsNewIdentifierEl.value.trim() : ""; if (!identifier) { setActivityNotificationsStatus("Identifier is required.", true); return; } const added = addTrackedActivityIdentifier({ identifier: identifier, label: activityNotificationsNewLabelEl ? activityNotificationsNewLabelEl.value.trim() : "", kind: activityNotificationsNewKindEl ? activityNotificationsNewKindEl.value : "identifier", matchMode: "prefix", source: activityNotificationsNewSourceEl ? activityNotificationsNewSourceEl.value.trim() : "", names: activityNotificationsNewNamesEl ? activityNotificationsNewNamesEl.value : "", enabled: true, isDefault: false, }); if (!added) { setActivityNotificationsStatus( "That identifier is already tracked.", true ); return; } if (activityNotificationsNewIdentifierEl) { activityNotificationsNewIdentifierEl.value = ""; } if (activityNotificationsNewLabelEl) { activityNotificationsNewLabelEl.value = ""; } if (activityNotificationsNewSourceEl) { activityNotificationsNewSourceEl.value = ""; } if (activityNotificationsNewNamesEl) { activityNotificationsNewNamesEl.value = ""; } renderActivityNotificationsTrackedList(); await persistActivityNotificationsAfterAdd( "Notification added and saved." ); }); } if (activityNotificationsAddGroupThreadsButton) { activityNotificationsAddGroupThreadsButton.addEventListener( "click", async function () { const group = getSelectedActivityNotificationGroup(); if (!group || !group.groupId) { setActivityNotificationsStatus( "Select a group to add a threads notification.", true ); return; } const groupId = String(group.groupId); const groupLabel = String(group.groupName || "Group " + groupId); const added = addTrackedActivityIdentifier({ identifier: "thmsg-grp-" + groupId + "-thread", label: "Threads notification - " + groupLabel, kind: "identifier", matchMode: "prefix", source: "Groups", names: [], enabled: true, isDefault: false, }); if (!added) { setActivityNotificationsStatus( "That group threads notification is already active.", true ); return; } renderActivityNotificationsTrackedList(); await persistActivityNotificationsAfterAdd( "Group threads notification added and saved." ); } ); } if (activityNotificationsAddGroupAnnouncementsButton) { activityNotificationsAddGroupAnnouncementsButton.addEventListener( "click", async function () { const group = getSelectedActivityNotificationGroup(); if (!group || !group.groupId) { setActivityNotificationsStatus( "Select a group to add an announcements notification.", true ); return; } const groupId = String(group.groupId); const groupLabel = String(group.groupName || "Group " + groupId); const added = addTrackedActivityIdentifier({ identifier: "grp-" + groupId + "-anc", label: "Announcements notification - " + groupLabel, kind: "identifier", matchMode: "prefix", source: "Groups", names: [], enabled: true, isDefault: false, }); if (!added) { setActivityNotificationsStatus( "That group announcements notification is already active.", true ); return; } renderActivityNotificationsTrackedList(); await persistActivityNotificationsAfterAdd( "Group announcements notification added and saved." ); } ); } if (activityNotificationsRefreshGroupsButton) { activityNotificationsRefreshGroupsButton.addEventListener( "click", async function () { try { setActivityNotificationsStatus("Loading groups for helper…", false); const groups = await loadActivityNotificationsGroupsOptions(); setActivityNotificationsStatus( { ok: true, helper: "groups", loadedGroups: Array.isArray(groups) ? groups.length : 0, }, false ); } catch (error) { setActivityNotificationsStatus( error.message || "Failed to refresh groups", true ); } } ); } if (activityNotificationsGroupsSelectEl) { activityNotificationsGroupsSelectEl.addEventListener("change", function () { updateActivityGroupActionButtons(); }); } if (activityNotificationsSaveButton) { activityNotificationsSaveButton.addEventListener( "click", async function () { try { setActivityNotificationsStatus( "Saving activity notification preferences…", false ); await saveActivityNotificationsConfig(); } catch (error) { setActivityNotificationsStatus( error.message || "Failed to save activity notification preferences", true ); } } ); } if (activityNotificationsTestButton) { activityNotificationsTestButton.addEventListener( "click", async function () { try { setActivityNotificationsStatus("Sending test notification…", false); await sendActivityNotificationsTest(); } catch (error) { setActivityNotificationsStatus( error.message || "Failed to send test notification", true ); } } ); } if (activityNotificationsTestTrackedButton) { activityNotificationsTestTrackedButton.addEventListener( "click", async function () { try { setActivityNotificationsStatus( "Testing tracked notifications (searching QDN + sending matches)…", false ); await testTrackedActivityNotifications(); } catch (error) { setActivityNotificationsStatus( error.message || "Failed to test tracked notifications", true ); } } ); } if (activityNotificationsMarkReadAllButton) { activityNotificationsMarkReadAllButton.addEventListener( "click", async function () { try { setActivityNotificationsStatus( "Marking read checkpoint for all tracked prefixes…", false ); await markActivityNotificationsReadCheckpoint("all"); } catch (error) { setActivityNotificationsStatus( error.message || "Failed to update read checkpoints", true ); } } ); } initializeSectionToggle( activityNotificationsDefaultsToggleButton, activityNotificationsDefaultsBodyEl, "activity_notification_defaults", false ); if (testButton) { testButton.addEventListener("click", function () { refreshAll("test"); }); } if (liveToggleButton) { liveToggleButton.addEventListener("click", function () { if (liveEnabled) { stopLive(); } else { startLive(); } }); } if (createButton) { createButton.addEventListener("click", function () { createWallet(); }); } if (copyAddressButton) { copyAddressButton.addEventListener("click", function () { copyAddress(); }); } if (downloadBackupButton) { downloadBackupButton.addEventListener("click", function () { openBackupModal(); }); } if (backupSaveFileButton) { backupSaveFileButton.addEventListener("click", function () { saveBackupFileToInstance(); }); } if (backupSavePasswordButton) { backupSavePasswordButton.addEventListener("click", function () { saveBackupPasswordToPasswordsApp(); }); } if (backupReencryptToggleEl) { backupReencryptToggleEl.addEventListener("change", function () { syncBackupReencryptFields(); }); } if (backupModalCancelButton) { backupModalCancelButton.addEventListener("click", function () { closeBackupModal(); }); } if (backupModalConfirmButton) { backupModalConfirmButton.addEventListener("click", function () { downloadBackupFile(); }); } if (backupPasswordEl) { backupPasswordEl.addEventListener("keydown", function (event) { if (event.key === "Enter") { event.preventDefault(); downloadBackupFile(); } }); } if (backupNewPasswordEl) { backupNewPasswordEl.addEventListener("keydown", function (event) { if (event.key === "Enter") { event.preventDefault(); downloadBackupFile(); } }); } if (backupModal) { backupModal.addEventListener("click", function (event) { if (event.target === backupModal) { closeBackupModal(); } }); } if (chdAnnouncementsOpenLink) { chdAnnouncementsOpenLink.href = "/apps/qortal_integration/qapps?qapp=" + encodeURIComponent(normalizeBrowserAddress("qortal://CHDC")); } if (requestQortButton) { requestQortButton.addEventListener("click", function () { requestInitialQort(); }); } if (advancedToggleButton) { advancedToggleButton.addEventListener("click", function () { toggleAdvanced(); }); } if (purchaseCreditsNowButton) { purchaseCreditsNowButton.addEventListener("click", function () { openPurchasesWithHighlight(true); }); } if (sendButton) { sendButton.addEventListener("click", function () { sendQort(); }); } if (quickSendButton) { quickSendButton.addEventListener("click", function () { openQuickSendModal(); }); } if (quickSendCancelButton) { quickSendCancelButton.addEventListener("click", function () { closeQuickSendModal(); }); } if (quickSendConfirmButton) { quickSendConfirmButton.addEventListener("click", function () { submitQuickSend(); }); } if (quickSendRecipientEl) { quickSendRecipientEl.addEventListener("keydown", function (event) { if (event.key === "Enter") { event.preventDefault(); submitQuickSend(); } }); } if (quickSendAmountEl) { quickSendAmountEl.addEventListener("keydown", function (event) { if (event.key === "Enter") { event.preventDefault(); submitQuickSend(); } }); } if (quickSendModal) { quickSendModal.addEventListener("click", function (event) { if (event.target === quickSendModal) { closeQuickSendModal(); } }); } if (sendFeeEditButton) { sendFeeEditButton.addEventListener("click", function () { if (!sendFeeEl) { return; } setFeeEditable(sendFeeEl.readOnly); }); } if (transactionsRefreshButton) { transactionsRefreshButton.addEventListener("click", function () { refreshTransactions(activeWalletContext); }); } if (transactionsShowNewButton) { transactionsShowNewButton.addEventListener("click", function () { refreshTransactions(activeWalletContext); }); } if (transactionsLoadMoreButton) { transactionsLoadMoreButton.addEventListener("click", function () { if (confirmedTransactionsLoadingMore) { return; } refreshTransactions(activeWalletContext, { append: true }); }); } if (unconfirmedTransactionsRefreshButton) { unconfirmedTransactionsRefreshButton.addEventListener("click", function () { refreshTransactions(activeWalletContext); }); } if (registerNameButton) { registerNameButton.addEventListener("click", function () { openRegisterModal(); }); } if (registerCancelButton) { registerCancelButton.addEventListener("click", function () { closeRegisterModal(); }); } if (registerConfirmButton) { registerConfirmButton.addEventListener("click", function () { registerName(); }); } if (registerOutputToggleButton) { registerOutputToggleButton.addEventListener("click", function () { toggleRegisterAdvancedOutput(); }); } if (registerOkButton) { registerOkButton.addEventListener("click", function () { closeRegisterModal(); }); } if (registerNameInputEl) { registerNameInputEl.addEventListener("keydown", function (event) { if ( event.key === "Enter" && registerConfirmButton && !registerConfirmButton.disabled ) { event.preventDefault(); registerName(); } }); } })();