(function () { const root = document.getElementById("chd-admin-root"); if (!root) { return; } const overviewUrl = root.dataset.overviewUrl || ""; const connectorRoutingConfigUrl = root.dataset.connectorRoutingConfigUrl || ""; const saveConnectorRoutingConfigUrl = root.dataset.saveConnectorRoutingConfigUrl || ""; const saveConnectorAdminTokenUrl = root.dataset.saveConnectorAdminTokenUrl || ""; const saveBillingAutoFulfillmentUrl = root.dataset.saveBillingAutoFulfillmentUrl || ""; const runBillingAutoFulfillmentUrl = root.dataset.runBillingAutoFulfillmentUrl || ""; const backfillBillingAutoFulfillmentUrl = root.dataset.backfillBillingAutoFulfillmentUrl || ""; const connectorSummaryUrl = root.dataset.connectorSummaryUrl || ""; const connectorInstancesUrl = root.dataset.connectorInstancesUrl || ""; const connectorInstanceDetailUrl = root.dataset.connectorInstanceDetailUrl || ""; const paymenterClientsUrl = root.dataset.paymenterClientsUrl || ""; const paymenterOrdersUrl = root.dataset.paymenterOrdersUrl || ""; const paymenterSnapshotUrl = root.dataset.paymenterSnapshotUrl || ""; const paymenterSourceSyncUrl = root.dataset.paymenterSourceSyncUrl || ""; const paymenterBackupsUrl = root.dataset.paymenterBackupsUrl || ""; const paymenterInvoicesUrl = root.dataset.paymenterInvoicesUrl || ""; const paymenterInvoiceItemsUrl = root.dataset.paymenterInvoiceItemsUrl || ""; const saveConnectorInstanceTargetUrl = root.dataset.saveConnectorInstanceTargetUrl || ""; const saveConnectorInstanceMetadataUrl = root.dataset.saveConnectorInstanceMetadataUrl || ""; const saveConnectorInstanceStateUrl = root.dataset.saveConnectorInstanceStateUrl || ""; const instanceOperationsUrl = root.dataset.instanceOperationsUrl || ""; const saveInstanceOperationUrl = root.dataset.saveInstanceOperationUrl || ""; const updateInstanceOperationStatusUrl = root.dataset.updateInstanceOperationStatusUrl || ""; const introGrantTaskActionUrl = root.dataset.introGrantTaskActionUrl || ""; const connectorCatalogConfigUrl = root.dataset.connectorCatalogConfigUrl || ""; const saveConnectorCatalogConfigUrl = root.dataset.saveConnectorCatalogConfigUrl || ""; const qappsUrl = root.dataset.qappsUrl || "/apps/qortal_integration/qapps"; const qappsRequestUrl = root.dataset.qappsRequestUrl || ""; const qappsApproveUrl = root.dataset.qappsApproveUrl || ""; const qappsUnlockUrl = root.dataset.qappsUnlockUrl || ""; const qappsUnlockStatusUrl = root.dataset.qappsUnlockStatusUrl || ""; const gatewayProxyTemplate = root.dataset.gatewayProxyTemplate || "/apps/qortal_integration/gateway/__PATH__"; const chdcQappAddress = root.dataset.chdcQappAddress || "qortal://CHDC"; const refreshButton = document.getElementById("chd-admin-refresh"); const connectorRefreshButton = document.getElementById( "chd-admin-connector-refresh" ); const instancesRefreshButton = document.getElementById( "chd-admin-instances-refresh" ); const instanceLoadButton = document.getElementById("chd-admin-instance-load"); const instanceSelectEl = document.getElementById("chd-admin-instance-select"); const overviewEl = document.getElementById("chd-admin-overview"); const overviewSummaryEl = document.getElementById( "chd-admin-overview-summary" ); const overviewStatusEl = document.getElementById("chd-admin-overview-status"); const overviewToggleButton = document.getElementById( "chd-admin-overview-toggle" ); const overviewAllButton = document.getElementById("chd-admin-overview-all"); const connectorRoutingProfileSelectEl = document.getElementById( "chd-connector-routing-profile-select" ); const connectorRoutingTestUrlEl = document.getElementById( "chd-connector-routing-test-url" ); const connectorRoutingProductionUrlEl = document.getElementById( "chd-connector-routing-production-url" ); const connectorRoutingSaveButton = document.getElementById( "chd-connector-routing-save" ); const connectorRoutingStatusEl = document.getElementById( "chd-connector-routing-status" ); const connectorSummaryEl = document.getElementById( "chd-admin-connector-summary" ); const connectorSummaryViewEl = document.getElementById( "chd-admin-connector-summary-view" ); const connectorStatusEl = document.getElementById( "chd-admin-connector-status" ); const connectorToggleButton = document.getElementById( "chd-admin-connector-toggle" ); const connectorAllButton = document.getElementById("chd-admin-connector-all"); const instancesListEl = document.getElementById("chd-admin-instances-list"); const instancesSummaryEl = document.getElementById( "chd-admin-instances-summary" ); const instancesCompactEl = document.getElementById( "chd-admin-instances-compact" ); const instancesToggleButton = document.getElementById( "chd-admin-instances-toggle" ); const instancesAllButton = document.getElementById("chd-admin-instances-all"); const instanceDetailEl = document.getElementById("chd-admin-instance-detail"); const instanceDetailSummaryEl = document.getElementById( "chd-admin-instance-detail-summary" ); const instanceDetailStatusEl = document.getElementById( "chd-admin-instance-detail-status" ); const instanceDetailToggleButton = document.getElementById( "chd-admin-instance-detail-toggle" ); const instanceDetailAllButton = document.getElementById( "chd-admin-instance-detail-all" ); const instanceConnectorProfileEl = document.getElementById( "chd-admin-instance-connector-profile" ); const instanceConnectorUrlEl = document.getElementById( "chd-admin-instance-connector-url" ); const instanceConnectorSaveButton = document.getElementById( "chd-admin-instance-connector-save" ); const instanceRemoveButton = document.getElementById( "chd-admin-instance-remove" ); const instanceConnectorStatusEl = document.getElementById( "chd-admin-instance-connector-status" ); const instanceMspContactNameEl = document.getElementById( "chd-admin-instance-msp-contact-name" ); const instanceMspContactEmailEl = document.getElementById( "chd-admin-instance-msp-contact-email" ); const instancePluginAdminEmailEl = document.getElementById( "chd-admin-instance-plugin-admin-email" ); const instanceAdminQortalAddressEl = document.getElementById( "chd-admin-instance-admin-qortal-address" ); const instanceMetadataSaveButton = document.getElementById( "chd-admin-instance-metadata-save" ); const instanceMetadataStatusEl = document.getElementById( "chd-admin-instance-metadata-status" ); const removedInstancesSummaryEl = document.getElementById( "chd-admin-removed-instances-summary" ); const removedInstancesListEl = document.getElementById( "chd-admin-removed-instances-list" ); const userOrdersSummaryEl = document.getElementById( "chd-admin-user-orders-summary" ); const userOrdersListEl = document.getElementById( "chd-admin-user-orders-list" ); const identityIndexSummaryEl = document.getElementById( "chd-admin-identity-summary" ); const identityIndexListEl = document.getElementById( "chd-admin-identity-list" ); const paymenterProfileViewEl = document.getElementById( "chd-admin-paymenter-profile" ); const paymenterStatusEl = document.getElementById( "chd-admin-paymenter-status" ); const paymenterClientsRefreshButton = document.getElementById( "chd-admin-paymenter-clients-refresh" ); const paymenterClientsSummaryEl = document.getElementById( "chd-admin-paymenter-clients-summary" ); const paymenterClientsListEl = document.getElementById( "chd-admin-paymenter-clients-list" ); const paymenterOrdersRefreshButton = document.getElementById( "chd-admin-paymenter-orders-refresh" ); const paymenterOrdersSummaryEl = document.getElementById( "chd-admin-paymenter-orders-summary" ); const paymenterOrdersListEl = document.getElementById( "chd-admin-paymenter-orders-list" ); const paymenterSnapshotRefreshButton = document.getElementById( "chd-admin-paymenter-snapshot-refresh" ); const paymenterSnapshotSummaryEl = document.getElementById( "chd-admin-paymenter-snapshot-summary" ); const paymenterSnapshotStatusEl = document.getElementById( "chd-admin-paymenter-snapshot-status" ); const paymenterSnapshotEl = document.getElementById( "chd-admin-paymenter-snapshot" ); const paymenterSnapshotToggleButton = document.getElementById( "chd-admin-paymenter-snapshot-toggle" ); const paymenterSnapshotAllButton = document.getElementById( "chd-admin-paymenter-snapshot-all" ); const paymenterSyncDryRunButton = document.getElementById( "chd-admin-paymenter-sync-dry-run" ); const paymenterSyncRunButton = document.getElementById( "chd-admin-paymenter-sync-run" ); const paymenterBackupsRefreshButton = document.getElementById( "chd-admin-paymenter-backups-refresh" ); const paymenterBackupsSummaryEl = document.getElementById( "chd-admin-paymenter-backups-summary" ); const paymenterBackupsListEl = document.getElementById( "chd-admin-paymenter-backups-list" ); const paymenterBackupsEl = document.getElementById( "chd-admin-paymenter-backups" ); const paymenterBackupsToggleButton = document.getElementById( "chd-admin-paymenter-backups-toggle" ); const paymenterBackupsAllButton = document.getElementById( "chd-admin-paymenter-backups-all" ); const paymenterInvoicesRefreshButton = document.getElementById( "chd-admin-paymenter-invoices-refresh" ); const paymenterInvoicesSummaryEl = document.getElementById( "chd-admin-paymenter-invoices-summary" ); const paymenterInvoicesListEl = document.getElementById( "chd-admin-paymenter-invoices-list" ); const paymenterInvoiceItemsRefreshButton = document.getElementById( "chd-admin-paymenter-invoice-items-refresh" ); const paymenterInvoiceItemsSummaryEl = document.getElementById( "chd-admin-paymenter-invoice-items-summary" ); const paymenterInvoiceItemsListEl = document.getElementById( "chd-admin-paymenter-invoice-items-list" ); const billingAutoFulfillmentEnabledEl = document.getElementById( "chd-admin-billing-auto-fulfillment-enabled" ); const billingAutoFulfillmentSaveButton = document.getElementById( "chd-admin-billing-auto-fulfillment-save" ); const billingAutoFulfillmentRunButton = document.getElementById( "chd-admin-billing-auto-fulfillment-run" ); const billingAutoFulfillmentBackfillSinceEl = document.getElementById( "chd-admin-billing-auto-fulfillment-backfill-since" ); const billingAutoFulfillmentBackfillInvoiceEl = document.getElementById( "chd-admin-billing-auto-fulfillment-backfill-invoice" ); const billingAutoFulfillmentBackfillButton = document.getElementById( "chd-admin-billing-auto-fulfillment-backfill" ); const billingAutoFulfillmentStatusEl = document.getElementById( "chd-admin-billing-auto-fulfillment-status" ); const billingAutoFulfillmentSummaryEl = document.getElementById( "chd-admin-billing-auto-fulfillment-summary" ); const connectorAdminTokenCardEl = document.getElementById( "chd-admin-connector-admin-token-card" ); const connectorAdminTokenInputEl = document.getElementById( "chd-admin-connector-admin-token" ); const connectorAdminTokenSaveButton = document.getElementById( "chd-admin-connector-admin-token-save" ); const connectorAdminTokenStatusEl = document.getElementById( "chd-admin-connector-admin-token-status" ); const operationsRefreshButton = document.getElementById( "chd-admin-operations-refresh" ); const operationsSummaryEl = document.getElementById( "chd-admin-operations-summary" ); const operationsListEl = document.getElementById("chd-admin-operations-list"); const operationsFormEl = document.getElementById("chd-admin-operations-form"); const operationsUserSelectEl = document.getElementById("chd-admin-ops-user"); const operationsTypeSelectEl = document.getElementById("chd-admin-ops-type"); const operationsTitleInputEl = document.getElementById("chd-admin-ops-title"); const operationsCodeInputEl = document.getElementById("chd-admin-ops-code"); const operationsNotesInputEl = document.getElementById("chd-admin-ops-notes"); const catalogConfigStatusEl = document.getElementById( "chd-admin-catalog-config-status" ); const catalogConfigRefreshButton = document.getElementById( "chd-admin-catalog-config-refresh" ); const catalogConfigSaveButton = document.getElementById( "chd-admin-catalog-config-save" ); const paymenterProfileSelectEl = document.getElementById( "chd-paymenter-profile-select" ); const catalogScopeProfileSelectEl = document.getElementById( "chd-catalog-scope-profile-select" ); const paymenterProfileStatusEl = document.getElementById( "chd-paymenter-profile-status" ); const connectorRuntimeDiagnosticsEl = document.getElementById( "chd-connector-runtime-diagnostics" ); const packageListEl = document.getElementById("chd-package-list"); const packageAddButton = document.getElementById("chd-package-add-row"); const addonListEl = document.getElementById("chd-addon-list"); const addonAddButton = document.getElementById("chd-addon-add-row"); const oneTimeListEl = document.getElementById("chd-onetime-list"); const oneTimeAddButton = document.getElementById("chd-onetime-add-row"); const audienceSelectEl = document.getElementById("chd-audience-select"); const audienceCreateNameEl = document.getElementById( "chd-audience-create-name" ); const audienceListEl = document.getElementById("chd-audience-list"); const audienceAddButton = document.getElementById("chd-audience-add-row"); const audienceRemoveCurrentButton = document.getElementById( "chd-audience-remove-current" ); const audienceManageStatusEl = document.getElementById( "chd-audience-manage-status" ); const audienceEditorEl = document.getElementById("chd-audience-editor"); const extraOffersListEl = document.getElementById("chd-extra-offers-list"); const extraOffersAddButton = document.getElementById( "chd-extra-offers-add-row" ); const previewAudienceEl = document.getElementById("chd-preview-audience"); const previewRefreshButton = document.getElementById("chd-preview-refresh"); const catalogPreviewEl = document.getElementById("chd-admin-catalog-preview"); const catalogPreviewRenderEl = document.getElementById( "chd-admin-catalog-preview-render" ); const chdcOpenLink = document.getElementById("chd-admin-chdc-open"); const approvalModal = document.getElementById("chd-admin-qortal-approval"); const approvalRequestEl = document.getElementById( "chd-admin-qortal-approval-request" ); const approvalScopeEl = document.getElementById( "chd-admin-qortal-approval-scope" ); const approvalWalletStatusEl = document.getElementById( "chd-admin-qortal-approval-wallet-status" ); const approvalUnlockFieldsEl = document.getElementById( "chd-admin-qortal-approval-unlock-fields" ); const approvalPasswordEl = document.getElementById( "chd-admin-qortal-approval-password" ); const approvalTtlEl = document.getElementById( "chd-admin-qortal-approval-ttl" ); const approvalErrorEl = document.getElementById( "chd-admin-qortal-approval-error" ); const approvalConfirmButton = document.getElementById( "chd-admin-qortal-approval-confirm" ); const approvalCancelButton = document.getElementById( "chd-admin-qortal-approval-cancel" ); const infoPanels = { overview: createInfoPanel( overviewEl, overviewToggleButton, overviewAllButton ), connectorSummary: createInfoPanel( connectorSummaryEl, connectorToggleButton, connectorAllButton ), instances: createInfoPanel( instancesListEl, instancesToggleButton, instancesAllButton ), instanceDetail: createInfoPanel( instanceDetailEl, instanceDetailToggleButton, instanceDetailAllButton ), paymenterSnapshot: createInfoPanel( paymenterSnapshotEl, paymenterSnapshotToggleButton, paymenterSnapshotAllButton ), paymenterBackups: createInfoPanel( paymenterBackupsEl, paymenterBackupsToggleButton, paymenterBackupsAllButton ), }; const OPERATION_STATUS_ORDER = { pending: 0, in_progress: 1, blocked: 2, completed: 3, }; let connectorInstancesCache = []; let activeInstanceId = ""; let activeInstanceKnownUsers = []; let activeInstanceDetailData = null; let activeInstanceOperations = []; let availableConnectorProfiles = []; let paymenterClientsCache = []; let connectorIdentityIndexCache = []; let connectorRoutingState = { activeProfile: "", profiles: { test: { baseUrl: "" }, production: { baseUrl: "" }, }, effectiveBaseUrl: "", fallbackBaseUrl: "", }; let catalogConfigState = { selectedCatalogScopeProfile: "", profileConfigs: {}, packageRows: [], addonRows: [], oneTimeRows: [], audienceRows: [], extraOffers: [], selectedAudienceKey: "", runtime: { activePaymenterProfile: "", availablePaymenterProfiles: [], availableCatalogConfigProfiles: [], availableConnectorProfiles: [], currentPaymenterBaseUrl: "", diagnostics: {}, }, }; let connectorCatalogPreview = { subscriptions: [], addons: [] }; const DEFAULT_AUDIENCES = [ { key: "NuQloud-individual", description: "Users on existing NuQloud instances who are not on shared team billing.", }, { key: "NuQloud-team", description: "Existing NuQloud teams with more than one paid member under the same monthly package.", }, { key: "NuQloud-Branded", description: "Organizations or communities already running fully branded hosted NuQloud instances.", }, ]; let defaultPackageRows = [ { code: "nuqloud_starter", label: "Starter" }, { code: "nuqloud_advanced", label: "Advanced" }, { code: "nuqloud_pro", label: "Pro" }, { code: "nuqloud_team_starter", label: "Team Starter" }, { code: "nuqloud_team_advanced", label: "Team Advanced" }, { code: "nuqloud_team_pro", label: "Team Pro" }, { code: "nuqloud_branded", label: "NuQloud Branded" }, { code: "nuqloud_branded_pro", label: "Branded Pro" }, { code: "nuqloud_branded_enterprise", label: "Branded Enterprise" }, ]; let walletLockState = { known: false, unlocked: null, expiresAtMs: null, }; const qdnLoadingMarkers = [ "loading from qdn", "fetching content from qdn", "qdn is still building", ]; const qdnStatusTitles = { NOT_STARTED: "Request queued", BLOCKED: "Blocked", BUILDING: "Building", BUILD_FAILED: "Build failed", DOWNLOADING: "Downloading", MISSING_DATA: "Waiting for chunk data", DOWNLOADED: "Downloaded", PUBLISHED: "Published", READY: "Ready", UNSUPPORTED: "Unsupported", NOT_PUBLISHED: "Not published", }; const qdnStatusNumericMap = { 0: "NOT_STARTED", 1: "BLOCKED", 2: "BUILDING", 3: "BUILD_FAILED", 4: "DOWNLOADING", 5: "MISSING_DATA", 6: "DOWNLOADED", 7: "PUBLISHED", 8: "READY", 9: "UNSUPPORTED", 10: "NOT_PUBLISHED", }; const chdcQdnRetryDelayMs = 4000; const chdcQdnReloadFallbackDelayMs = 7000; const chdcQdnRetryMaxAttempts = 240; let chdcQdnRetryTimer = null; let chdcQdnRetryAttempts = 0; let chdcQdnResourceTarget = null; let chdcQdnStatusFailures = 0; let chdcQdnLastSnapshot = ""; function getRequestHeaders(contentType) { const headers = {}; if (contentType) { headers["Content-Type"] = contentType; } if (typeof OC !== "undefined" && OC.requestToken) { headers.requesttoken = OC.requestToken; } return headers; } function buildLegacyPayloadBody(payload) { const body = new URLSearchParams(); body.set("payload", JSON.stringify(payload || {})); return body.toString(); } async function requestJson(url, options) { const response = await fetch(url, options); const responseText = await response.text(); let payload = null; if (responseText) { try { payload = JSON.parse(responseText); } catch (_error) { payload = null; } } if (!response.ok || !payload || payload.ok === false) { const errorMessage = payload && payload.error ? payload.error : "HTTP " + String(response.status); const error = new Error(errorMessage); error.status = response.status; error.details = payload && payload.details ? payload.details : null; error.payload = payload; throw error; } return payload; } async function qortalRequest(action, payload) { if (!qappsRequestUrl) { throw new Error("Qortal request endpoint is not configured."); } const response = await fetch(qappsRequestUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify({ requestType: String(action || "") .trim() .toUpperCase(), payload: payload || {}, }), }); const json = await response.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 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 { captureFromValue( JSON.parse(value.slice(jsonStart, jsonEnd + 1)), depth + 1 ); } catch (_error) { // Ignore parse failures from mixed broker strings. } } 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); return context; } function parseUnlockStatus(value) { let candidate = value; for (let index = 0; index < 4; index += 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 rawExpires = candidate.expiresAt !== undefined ? candidate.expiresAt : candidate.expires_at; let expiresAtMs = null; if ( typeof rawExpires === "number" && Number.isFinite(rawExpires) && rawExpires > 0 ) { expiresAtMs = rawExpires < 10000000000 ? 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 < 10000000000 ? parsed * 1000 : parsed; } } return { unlocked: candidate.unlocked, expiresAtMs: 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: "Wallet lock: unknown (password may be required)", className: "chd-wallet-status-unknown", showUnlockFields: true, }; } if (!walletLockState.unlocked) { return { text: "Wallet lock: LOCKED (password required)", className: "chd-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: "Wallet lock: UNLOCKED for ~" + minutes + " minute(s)", className: "chd-wallet-status-ok", showUnlockFields: false, }; } return { text: "Wallet lock: UNLOCKED", className: "chd-wallet-status-ok", showUnlockFields: false, }; } function updateApprovalWalletStatus() { const status = walletStatusPresentation(); if (approvalWalletStatusEl) { approvalWalletStatusEl.textContent = status.text; approvalWalletStatusEl.classList.remove( "chd-wallet-status-ok", "chd-wallet-status-locked", "chd-wallet-status-unknown" ); approvalWalletStatusEl.classList.add(status.className); } if (approvalUnlockFieldsEl) { approvalUnlockFieldsEl.classList.toggle( "chd-hidden", !status.showUnlockFields ); } if (!status.showUnlockFields && approvalPasswordEl) { approvalPasswordEl.value = ""; } return status; } function showApprovalError(message) { if (!approvalErrorEl) { return; } approvalErrorEl.textContent = message || ""; approvalErrorEl.classList.toggle("chd-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: getRequestHeaders(), credentials: "same-origin", } ); 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("chd-hidden"); showApprovalError(""); if (approvalPasswordEl) { approvalPasswordEl.value = ""; approvalPasswordEl.onkeydown = null; } if (approvalConfirmButton) { approvalConfirmButton.disabled = false; approvalConfirmButton.textContent = "Approve 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"); } if (approvalScopeEl) { approvalScopeEl.textContent = String(context.scope || "ACTION"); } showApprovalError(""); updateApprovalWalletStatus(); approvalModal.classList.remove("chd-hidden"); if ( approvalUnlockFieldsEl && !approvalUnlockFieldsEl.classList.contains("chd-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( "Wallet password is required because the wallet appears locked." ); return; } approvalConfirmButton.disabled = true; approvalConfirmButton.textContent = "Approving..."; approvalCancelButton.disabled = true; resolve({ action: "approve", password: password, ttlSeconds: approvalTtlEl && approvalTtlEl.checked ? 600 : undefined, }); }; 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: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify({ walletId: walletId, password: password, ...(ttlSeconds ? { ttlSeconds: ttlSeconds } : {}), }), }); const payload = await response.json(); if (!response.ok || payload.ok === false || payload.error) { markWalletLocked(); throw new Error(payload.error || "wallet_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: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify({ walletId: walletId, scope: scope, app: "qortal://NC/CHD_ADMIN", requestType: 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 sendCoinWithApproval(recipientAddress, amountQort, feeQort) { const basePayload = { coin: "QORT", recipient: String(recipientAddress || "").trim(), amount: amountQort, fee: feeQort, }; let workingPayload = Object.assign({}, basePayload); let attempt = 0; while (attempt < 4) { try { const result = await qortalRequest("SEND_COIN", workingPayload); const txSignature = extractSignatureFromResult(result); if (!txSignature) { throw new Error("SEND_COIN did not return a transaction signature."); } closeApprovalModal(); return { result: result, txSignature: txSignature, }; } catch (error) { const approvalRequired = isApprovalRequiredError(error); const walletLocked = isWalletLockedError(error); if (!approvalRequired && !walletLocked) { throw error; } const approvalContext = extractApprovalContext(error, "SEND_COIN"); const walletId = approvalContext.walletId; if (!walletId) { throw error; } await refreshWalletLockState(walletId); updateApprovalWalletStatus(); const decision = await showApprovalModal({ requestType: "SEND_COIN", scope: approvalContext.scope || "SEND_COIN", }); 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 || "wallet_unlock_failed"); throw unlockError; } } const approvalToken = await approveOnce( walletId, normalizeApprovalScope(approvalContext.scope, "SEND_COIN"), "SEND_COIN" ); workingPayload = Object.assign({}, basePayload, { walletId: walletId, approvalToken: approvalToken, }); closeApprovalModal(); attempt += 1; } } throw new Error("approval_failed"); } 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 (let index = 0; index < nestedCandidates.length; index += 1) { const signature = extractSignatureFromResult(nestedCandidates[index]); if (signature !== "") { return signature; } } if (Array.isArray(payload.results)) { for (let index = 0; index < payload.results.length; index += 1) { const signature = extractSignatureFromResult(payload.results[index]); if (signature !== "") { return signature; } } } return ""; } function encodeGatewayPath(path) { return String(path || "") .split("/") .map(function (segment) { return encodeURIComponent(segment); }) .join("/"); } function buildGatewayProxyUrl(path) { const cleanedPath = String(path || "") .trim() .replace(/^\/+/, ""); if (!cleanedPath) { return ""; } if (gatewayProxyTemplate.indexOf("__PATH__") !== -1) { return gatewayProxyTemplate.replace( "__PATH__", encodeGatewayPath(cleanedPath) ); } const separator = gatewayProxyTemplate.endsWith("/") ? "" : "/"; return gatewayProxyTemplate + separator + encodeGatewayPath(cleanedPath); } function cloneJsonValue(value, fallback) { try { return JSON.parse(JSON.stringify(value)); } catch (_error) { return fallback; } } 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; } } function splitPathAndSuffix(value) { const raw = String(value || "").trim(); if (!raw) { return { path: "", suffix: "" }; } const queryIndex = raw.indexOf("?"); const hashIndex = raw.indexOf("#"); let splitIndex = -1; if (queryIndex >= 0 && hashIndex >= 0) { splitIndex = Math.min(queryIndex, hashIndex); } else if (queryIndex >= 0) { splitIndex = queryIndex; } else if (hashIndex >= 0) { splitIndex = hashIndex; } if (splitIndex < 0) { return { path: raw, suffix: "" }; } return { path: raw.slice(0, splitIndex), suffix: raw.slice(splitIndex), }; } function normalizeBrowserAddress(address) { const raw = String(address || "").trim(); if (!raw) { return ""; } if (!raw.toLowerCase().startsWith("qortal://")) { return raw; } const split = splitPathAndSuffix(raw); const afterScheme = split.path .slice("qortal://".length) .replace(/^\/+/, ""); if (!afterScheme) { return raw; } if (!afterScheme.includes("/")) { return "qortal://WEBSITE/" + afterScheme + split.suffix; } const segments = afterScheme.split("/").filter(function (segment) { return Boolean(segment); }); if (segments.length < 2) { return raw; } return "qortal://" + segments.join("/") + split.suffix; } function qortalAddressToRenderPath(address) { const normalized = normalizeBrowserAddress(address); if (!normalized || !normalized.toLowerCase().startsWith("qortal://")) { return ""; } const split = splitPathAndSuffix(normalized); const rawPath = split.path.slice("qortal://".length).replace(/^\/+/, ""); const segments = rawPath.split("/").filter(function (segment) { return Boolean(segment); }); if (segments.length < 2) { return ""; } return "render/" + segments.join("/") + split.suffix; } function buildChdcEmbedUrl() { if (!qappsUrl) { return ""; } const normalizedAddress = normalizeBrowserAddress(chdcQappAddress); 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"); return appendThemeParams(parsed.toString()); } catch (_error) { return ""; } } function updateChdcOpenUrl() { if (!chdcOpenLink) { return; } const qappParam = encodeURIComponent( normalizeBrowserAddress(chdcQappAddress) || chdcQappAddress ); const separator = qappsUrl.indexOf("?") === -1 ? "?" : "&"; chdcOpenLink.href = qappsUrl + separator + "qapp=" + qappParam; } 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 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: percentLoaded, localChunkCount: localChunkCount, totalChunkCount: 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, credentials: "same-origin", }); 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: json, }; } finally { if (timeoutId) { window.clearTimeout(timeoutId); } } } function parseChdcQdnResourceTarget(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 buildChdcQdnStatusUrls(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 probeChdcQdnResourceStatus(target) { const urls = buildChdcQdnStatusUrls(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 status endpoint } } return { ok: false }; } function clearChdcQdnRetry(reason) { if (chdcQdnRetryTimer) { window.clearTimeout(chdcQdnRetryTimer); chdcQdnRetryTimer = null; } chdcQdnStatusFailures = 0; chdcQdnLastSnapshot = ""; } function scheduleChdcQdnRetry(delayOverrideMs) { if (chdcQdnRetryTimer) { return; } if (chdcQdnRetryAttempts >= chdcQdnRetryMaxAttempts) { return; } const nextDelay = typeof delayOverrideMs === "number" && Number.isFinite(delayOverrideMs) ? Math.max(1000, Math.floor(delayOverrideMs)) : chdcQdnRetryDelayMs; chdcQdnRetryTimer = window.setTimeout(function () { chdcQdnRetryTimer = null; runChdcQdnRetryTick().catch(function () { scheduleChdcQdnRetry(chdcQdnReloadFallbackDelayMs); }); }, nextDelay); } async function runChdcQdnRetryTick() { if (chdcQdnRetryAttempts >= chdcQdnRetryMaxAttempts) { return; } chdcQdnRetryAttempts += 1; let shouldReload = true; let delayMs = chdcQdnRetryDelayMs; if (chdcQdnResourceTarget) { const statusResult = await probeChdcQdnResourceStatus( chdcQdnResourceTarget ); if (statusResult.ok) { chdcQdnStatusFailures = 0; const statusSnapshot = statusResult.status + ":" + (statusResult.percentLoaded !== null ? statusResult.percentLoaded : "") + ":" + (statusResult.localChunkCount !== null ? statusResult.localChunkCount : ""); if (statusSnapshot !== chdcQdnLastSnapshot) { chdcQdnLastSnapshot = statusSnapshot; } if ( statusResult.status === "READY" || isTerminalQdnStatus(statusResult.status) ) { clearChdcQdnRetry("content_loaded"); chdcQdnRetryAttempts = 0; return; } shouldReload = false; } else { chdcQdnStatusFailures += 1; delayMs = chdcQdnReloadFallbackDelayMs; if (chdcQdnStatusFailures <= 3) { shouldReload = false; } } } if (shouldReload) { clearChdcQdnRetry("retry_no_embed"); return; } scheduleChdcQdnRetry(delayMs); } function cleanString(value) { return String(value || "").trim(); } function isRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function coerceBoolean(value) { if (typeof value === "boolean") { return value; } if (typeof value === "number") { return value !== 0; } const normalized = cleanString(value).toLowerCase(); if (!normalized) { return false; } return ( normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "set" || normalized === "configured" || normalized === "enabled" || normalized === "ok" ); } function firstNonEmptyString(candidates, fallback) { const list = Array.isArray(candidates) ? candidates : []; for (let i = 0; i < list.length; i += 1) { const candidate = cleanString(list[i]); if (candidate !== "") { return candidate; } } return fallback || ""; } function firstArray(candidates) { const list = Array.isArray(candidates) ? candidates : []; for (let i = 0; i < list.length; i += 1) { if (Array.isArray(list[i])) { return list[i]; } } return []; } function firstObject(candidates) { const list = Array.isArray(candidates) ? candidates : []; for (let i = 0; i < list.length; i += 1) { if (isRecord(list[i])) { return list[i]; } } return {}; } function normalizeStatusLevel(level) { const normalized = cleanString(level).toLowerCase(); if ( normalized === "issues" || normalized === "partial" || normalized === "good" ) { return normalized; } return "partial"; } function combineStatusLevel(current, next) { const rank = { good: 0, partial: 1, issues: 2, }; const normalizedCurrent = normalizeStatusLevel(current); const normalizedNext = normalizeStatusLevel(next); return rank[normalizedNext] > rank[normalizedCurrent] ? normalizedNext : normalizedCurrent; } function levelTitle(level) { const normalized = normalizeStatusLevel(level); if (normalized === "good") { return "GOOD"; } if (normalized === "issues") { return "ISSUES"; } return "PARTIALLY GOOD"; } function evaluateSyncStatus(value) { const statusText = cleanString(value); if (!statusText) { return { level: "partial", label: "Unknown" }; } const normalized = statusText.toLowerCase(); if ( normalized.indexOf("success") !== -1 || normalized.indexOf("ok") !== -1 || normalized.indexOf("ready") !== -1 || normalized.indexOf("healthy") !== -1 ) { return { level: "good", label: statusText }; } if ( normalized.indexOf("pending") !== -1 || normalized.indexOf("warn") !== -1 || normalized.indexOf("partial") !== -1 ) { return { level: "partial", label: statusText }; } return { level: "issues", label: statusText }; } function evaluateDomainStatus(value) { const stateText = cleanString(value); if (!stateText) { return { level: "partial", label: "Unknown" }; } const normalized = stateText.toLowerCase(); if ( normalized.indexOf("verified") !== -1 || normalized.indexOf("active") !== -1 || normalized.indexOf("valid") !== -1 || normalized === "ok" ) { return { level: "good", label: stateText }; } if ( normalized.indexOf("pending") !== -1 || normalized.indexOf("queued") !== -1 || normalized.indexOf("review") !== -1 || normalized.indexOf("requested") !== -1 ) { return { level: "partial", label: stateText }; } return { level: "issues", label: stateText }; } function normalizeOperationStatus(status) { const normalized = cleanString(status).toLowerCase(); if ( normalized === "complete" || normalized === "done" || normalized === "fulfilled" ) { return "completed"; } if (normalized === "progress" || normalized === "started") { return "in_progress"; } if (normalized === "error" || normalized === "failed") { return "blocked"; } if ( normalized === "completed" || normalized === "in_progress" || normalized === "blocked" ) { return normalized; } return "pending"; } function operationStatusLabel(status) { const normalized = normalizeOperationStatus(status); if (normalized === "completed") { return "Completed"; } if (normalized === "in_progress") { return "In Progress"; } if (normalized === "blocked") { return "Blocked"; } return "Pending"; } function operationStatusLevel(status) { const normalized = normalizeOperationStatus(status); if (normalized === "completed") { return "good"; } if (normalized === "blocked") { return "issues"; } return "partial"; } function operationTaskSortComparator(a, b) { const priorityA = operationActionPriority(a); const priorityB = operationActionPriority(b); if (priorityA !== priorityB) { return priorityA - priorityB; } const statusA = normalizeOperationStatus(a && a.status ? a.status : ""); const statusB = normalizeOperationStatus(b && b.status ? b.status : ""); const statusRankA = Object.prototype.hasOwnProperty.call( OPERATION_STATUS_ORDER, statusA ) ? OPERATION_STATUS_ORDER[statusA] : 0; const statusRankB = Object.prototype.hasOwnProperty.call( OPERATION_STATUS_ORDER, statusB ) ? OPERATION_STATUS_ORDER[statusB] : 0; if (statusRankA !== statusRankB) { return statusRankA - statusRankB; } const sourceA = cleanString(a && a.source ? a.source : ""); const sourceB = cleanString(b && b.source ? b.source : ""); if (sourceA !== sourceB) { if (sourceA === "detected") { return -1; } if (sourceB === "detected") { return 1; } } const updatedAtA = cleanString(a && a.updatedAt ? a.updatedAt : ""); const updatedAtB = cleanString(b && b.updatedAt ? b.updatedAt : ""); if (updatedAtA !== updatedAtB) { return updatedAtA > updatedAtB ? -1 : 1; } const titleA = cleanString(a && a.title ? a.title : ""); const titleB = cleanString(b && b.title ? b.title : ""); return titleA.localeCompare(titleB); } function operationRequiresAction(task) { const status = normalizeOperationStatus( task && task.status ? task.status : "" ); if (status === "completed") { return false; } const taskType = cleanString(task && task.taskType ? task.taskType : ""); if (taskType === "intro_grant") { return true; } return status === "pending" || status === "blocked"; } function operationActionPriority(task) { const status = normalizeOperationStatus( task && task.status ? task.status : "" ); const taskType = cleanString(task && task.taskType ? task.taskType : ""); if (taskType === "intro_grant" && status !== "completed") { return 0; } if (status === "pending") { return 1; } if (status === "blocked") { return 2; } if (status === "in_progress") { return 3; } return 4; } function operationActionLabel(task) { const status = normalizeOperationStatus( task && task.status ? task.status : "" ); const taskType = cleanString(task && task.taskType ? task.taskType : ""); if (taskType === "intro_grant" && status !== "completed") { return "Grant Review"; } if (status === "blocked") { return "Needs Review"; } return "Needs Action"; } function buildOperationTaskPayload(task) { const record = isRecord(task) ? task : {}; return { id: cleanString(record.id), source: cleanString(record.source) || "manual", detectionKey: cleanString(record.detectionKey), taskType: cleanString(record.taskType) || "manual", title: cleanString(record.title), status: normalizeOperationStatus(record.status), nextcloudUserId: cleanString(record.nextcloudUserId), paymenterCustomerId: cleanString(record.paymenterCustomerId), userLabel: cleanString(record.userLabel), orderRef: cleanString(record.orderRef), productCode: cleanString(record.productCode), quantity: cleanString(record.quantity), notes: cleanString(record.notes), createdAt: cleanString(record.createdAt), updatedAt: cleanString(record.updatedAt), completedAt: cleanString(record.completedAt), meta: cloneJsonValue(isRecord(record.meta) ? record.meta : {}, {}), }; } function summarizeTaskCounts(tasks) { const list = Array.isArray(tasks) ? tasks : []; let actionRequiredCount = 0; let grantRequestCount = 0; list.forEach(function (task) { if (!isRecord(task)) { return; } if (operationRequiresAction(task)) { actionRequiredCount += 1; } if ( cleanString(task.taskType) === "intro_grant" && normalizeOperationStatus(task.status) !== "completed" ) { grantRequestCount += 1; } }); return { taskCount: list.length, actionRequiredCount: actionRequiredCount, grantRequestCount: grantRequestCount, }; } function normalizeKnownUserEntry(entry) { const record = isRecord(entry) ? entry : {}; return { nextcloudUserId: firstNonEmptyString([ record.nextcloudUserId, record.userId, record.uid, ]), displayName: firstNonEmptyString([record.displayName, record.name]), email: firstNonEmptyString([record.email]), qortalAddress: firstNonEmptyString([ record.qortalAddress, record.address, ]), paymenterCustomerId: firstNonEmptyString([ record.paymenterCustomerId, record.customerId, ]), raw: record, }; } function collectKnownUsers(detailData) { const data = isRecord(detailData) ? detailData : {}; const summary = firstObject([data.summary]); const users = firstArray([ summary.knownUsers, data.knownUsers, data.users, summary.users, ]); return users.map(normalizeKnownUserEntry).filter(function (entry) { return ( entry.nextcloudUserId || entry.displayName || entry.paymenterCustomerId ); }); } function normalizeCommerceItem(entry) { const record = isRecord(entry) ? entry : {}; const statusText = firstNonEmptyString( [record.status, record.state, record.orderStatus], "" ); return { orderRef: firstNonEmptyString( [record.orderId, record.id, record.reference, record.externalId], "" ), productCode: firstNonEmptyString( [ record.code, record.productCode, record.planCode, record.serviceCode, record.itemCode, ], "" ), status: inferTaskStatusFromText(statusText), statusLabel: statusText, createdAt: firstNonEmptyString([record.createdAt], ""), updatedAt: firstNonEmptyString([record.updatedAt], ""), completedAt: firstNonEmptyString( [record.completedAt, record.fulfilledAt], "" ), nextcloudUserId: firstNonEmptyString( [record.nextcloudUserId, record.userId, record.uid], "" ), paymenterCustomerId: firstNonEmptyString( [record.paymenterCustomerId, record.customerId], "" ), label: firstNonEmptyString([record.title, record.name, record.label], ""), }; } function collectUserCommerceRows(detailData, knownUsers) { const data = isRecord(detailData) ? detailData : {}; const summary = firstObject([data.summary]); const users = Array.isArray(knownUsers) ? knownUsers : []; const userRowsByKey = {}; const userRowsByCustomerId = {}; const userRowsByNextcloudUserId = {}; const ensureUserRow = function (user) { const entry = isRecord(user) ? user : {}; const nextcloudUserId = firstNonEmptyString([entry.nextcloudUserId], ""); const customerId = firstNonEmptyString([entry.paymenterCustomerId], ""); const displayName = firstNonEmptyString( [entry.displayName, nextcloudUserId], "Unknown User" ); const key = nextcloudUserId || "customer:" + customerId || "user:" + String(Object.keys(userRowsByKey).length + 1); if (!Object.prototype.hasOwnProperty.call(userRowsByKey, key)) { userRowsByKey[key] = { nextcloudUserId: nextcloudUserId, displayName: displayName, email: firstNonEmptyString([entry.email], ""), qortalAddress: firstNonEmptyString([entry.qortalAddress], ""), paymenterCustomerId: customerId, items: [], itemsByKey: {}, }; } const row = userRowsByKey[key]; if (customerId) { userRowsByCustomerId[customerId] = row; } if (nextcloudUserId) { userRowsByNextcloudUserId[nextcloudUserId] = row; } return row; }; users.forEach(function (user) { ensureUserRow(user); }); const appendItem = function (itemRaw, fallbackUser) { const item = normalizeCommerceItem(itemRaw); if (!item.orderRef && !item.productCode && !item.label) { return; } let row = null; if ( item.paymenterCustomerId && Object.prototype.hasOwnProperty.call( userRowsByCustomerId, item.paymenterCustomerId ) ) { row = userRowsByCustomerId[item.paymenterCustomerId]; } if ( !row && item.nextcloudUserId && Object.prototype.hasOwnProperty.call( userRowsByNextcloudUserId, item.nextcloudUserId ) ) { row = userRowsByNextcloudUserId[item.nextcloudUserId]; } if (!row && fallbackUser) { row = ensureUserRow(fallbackUser); } if (!row) { row = ensureUserRow({ nextcloudUserId: item.nextcloudUserId, displayName: item.nextcloudUserId || (item.paymenterCustomerId ? "Customer #" + item.paymenterCustomerId : "Unassigned User"), paymenterCustomerId: item.paymenterCustomerId, }); } const itemKey = [ cleanString(item.orderRef), cleanString(item.productCode), cleanString(item.label), cleanString(item.createdAt), cleanString(item.updatedAt), ].join("|"); if ( !itemKey || Object.prototype.hasOwnProperty.call(row.itemsByKey, itemKey) ) { return; } row.itemsByKey[itemKey] = true; row.items.push(item); }; const flattenedArrays = function (sources) { const out = []; (Array.isArray(sources) ? sources : []).forEach(function (value) { if (!Array.isArray(value)) { return; } value.forEach(function (entry) { out.push(entry); }); }); return out; }; flattenedArrays([ summary.orders, summary.purchases, summary.services, summary.subscriptions, data.orders, data.purchases, data.services, data.subscriptions, ]).forEach(function (entry) { appendItem(entry, null); }); users.forEach(function (user) { const userRaw = isRecord(user.raw) ? user.raw : {}; flattenedArrays([ userRaw.orders, userRaw.purchases, userRaw.services, userRaw.subscriptions, ]).forEach(function (entry) { appendItem(entry, user); }); }); const rows = Object.keys(userRowsByKey).map(function (key) { const row = userRowsByKey[key]; const items = Array.isArray(row.items) ? row.items.slice() : []; items.sort(function (a, b) { const updatedA = cleanString(a.updatedAt) || cleanString(a.createdAt); const updatedB = cleanString(b.updatedAt) || cleanString(b.createdAt); if (updatedA !== updatedB) { return updatedA > updatedB ? -1 : 1; } const codeA = cleanString(a.productCode) || cleanString(a.label); const codeB = cleanString(b.productCode) || cleanString(b.label); return codeA.localeCompare(codeB); }); let pendingCount = 0; let inProgressCount = 0; let blockedCount = 0; let completedCount = 0; const uniqueCodes = []; const codeSet = {}; items.forEach(function (item) { const status = normalizeOperationStatus(item.status); if (status === "completed") { completedCount += 1; } else if (status === "in_progress") { inProgressCount += 1; } else if (status === "blocked") { blockedCount += 1; } else { pendingCount += 1; } const code = cleanString(item.productCode) || cleanString(item.label); if (!code || Object.prototype.hasOwnProperty.call(codeSet, code)) { return; } codeSet[code] = true; uniqueCodes.push(code); }); return { nextcloudUserId: cleanString(row.nextcloudUserId), displayName: cleanString(row.displayName), email: cleanString(row.email), qortalAddress: cleanString(row.qortalAddress), paymenterCustomerId: cleanString(row.paymenterCustomerId), totalItems: items.length, pendingCount: pendingCount, inProgressCount: inProgressCount, blockedCount: blockedCount, completedCount: completedCount, recentCodes: uniqueCodes.slice(0, 4), items: items.slice(0, 12), }; }); rows.sort(function (a, b) { const nameA = cleanString(a.displayName) || cleanString(a.nextcloudUserId); const nameB = cleanString(b.displayName) || cleanString(b.nextcloudUserId); return nameA.localeCompare(nameB); }); return rows; } function buildOperationDetectionKey(instanceId, fields) { const parts = [ cleanString(instanceId).toLowerCase(), cleanString(fields.source || "detected").toLowerCase(), cleanString(fields.taskType || "").toLowerCase(), cleanString(fields.nextcloudUserId || "").toLowerCase(), cleanString(fields.paymenterCustomerId || "").toLowerCase(), cleanString(fields.orderRef || "").toLowerCase(), cleanString(fields.productCode || "").toLowerCase(), cleanString(fields.title || "").toLowerCase(), ]; return parts.join("|").replace(/\s+/g, " ").trim(); } function inferTaskStatusFromText(value) { const normalized = cleanString(value).toLowerCase(); if (!normalized) { return "pending"; } if ( normalized.indexOf("complete") !== -1 || normalized.indexOf("fulfilled") !== -1 || normalized.indexOf("paid") !== -1 || normalized.indexOf("active") !== -1 || normalized.indexOf("provisioned") !== -1 ) { return "completed"; } if ( normalized.indexOf("progress") !== -1 || normalized.indexOf("processing") !== -1 ) { return "in_progress"; } if ( normalized.indexOf("block") !== -1 || normalized.indexOf("fail") !== -1 || normalized.indexOf("error") !== -1 ) { return "blocked"; } return "pending"; } function normalizeGrantRequestRecord(entry) { const record = isRecord(entry) ? entry : {}; const requestId = firstNonEmptyString( [record.requestId, record.id, record.ref, record.externalRequestId], "" ); const statusText = firstNonEmptyString([record.status, record.state], ""); const subject = firstObject([record.billingSubject, record.requester]); return { requestId: requestId, status: inferTaskStatusFromText(statusText), statusLabel: statusText, nextcloudUserId: firstNonEmptyString( [record.nextcloudUserId, record.userId, subject.nextcloudUserId], "" ), displayName: firstNonEmptyString( [ record.displayName, record.userLabel, subject.displayName, subject.name, ], "" ), paymenterCustomerId: firstNonEmptyString( [record.paymenterCustomerId, record.customerId], "" ), qortalAddress: firstNonEmptyString( [ record.qortalAddress, record.address, subject.qortalAddress, subject.address, ], "" ), amount: firstNonEmptyString( [record.amount, record.grantAmountQort, record.grantRequiredAmount], "" ), message: firstNonEmptyString([record.message, record.notes], ""), createdAt: firstNonEmptyString( [record.requestedAt, record.createdAt], "" ), updatedAt: firstNonEmptyString( [record.updatedAt, record.approvedAt, record.rejectedAt], "" ), completedAt: firstNonEmptyString( [record.completedAt, record.approvedAt, record.rejectedAt], "" ), raw: record, }; } function collectIntroGrantRecords(detailData) { const data = isRecord(detailData) ? detailData : {}; const summary = firstObject([data.summary]); const details = firstObject([data.details]); const grants = firstObject([data.grants, summary.grants, details.grants]); const rawCandidates = [ firstArray([ data.introGrants, data.grantRequests, data.introGrantRequests, ]), firstArray([ summary.introGrants, summary.grantRequests, summary.introGrantRequests, ]), firstArray([ details.introGrants, details.grantRequests, details.introGrantRequests, ]), firstArray([grants.intro, grants.requests, grants.items]), ]; const records = []; rawCandidates.forEach(function (candidate) { (Array.isArray(candidate) ? candidate : []).forEach(function (entry) { const normalized = normalizeGrantRequestRecord(entry); if ( !normalized.requestId && !normalized.nextcloudUserId && !normalized.qortalAddress ) { return; } records.push(normalized); }); }); const deduped = {}; records.forEach(function (entry) { const key = cleanString(entry.requestId) || [ cleanString(entry.nextcloudUserId), cleanString(entry.qortalAddress), cleanString(entry.createdAt), ].join("|"); if (!key) { return; } deduped[key] = entry; }); return Object.keys(deduped).map(function (key) { return deduped[key]; }); } function detectOperationsFromInstanceDetail(instanceId, detailData) { const data = isRecord(detailData) ? detailData : {}; const summary = firstObject([data.summary]); const summaryInstance = firstObject([summary.instance, data.instance]); const knownUsers = collectKnownUsers(data); const userByCustomerId = {}; knownUsers.forEach(function (entry) { const customerId = cleanString(entry.paymenterCustomerId); if (customerId) { userByCustomerId[customerId] = entry; } }); const detected = []; const addDetected = function (candidate) { const title = cleanString(candidate.title); if (!title) { return; } const normalizedTask = { id: cleanString(candidate.id), source: "detected", detectionKey: buildOperationDetectionKey(instanceId, candidate), taskType: cleanString(candidate.taskType) || "manual", title: title, status: normalizeOperationStatus(candidate.status || "pending"), nextcloudUserId: cleanString(candidate.nextcloudUserId), paymenterCustomerId: cleanString(candidate.paymenterCustomerId), userLabel: cleanString(candidate.userLabel), orderRef: cleanString(candidate.orderRef), productCode: cleanString(candidate.productCode), quantity: cleanString(candidate.quantity), notes: cleanString(candidate.notes), createdAt: cleanString(candidate.createdAt), updatedAt: cleanString(candidate.updatedAt), completedAt: cleanString(candidate.completedAt), meta: isRecord(candidate.meta) ? candidate.meta : {}, }; if (!normalizedTask.detectionKey) { return; } detected.push(normalizedTask); }; const checkoutType = cleanString( summaryInstance.lastCheckoutType || data.lastCheckoutType ).toLowerCase(); const checkoutCode = firstNonEmptyString([ summaryInstance.lastCheckoutOneTimeCode, summaryInstance.lastCheckoutPackageTier, data.lastCheckoutOneTimeCode, data.lastCheckoutPackageTier, ]); const checkoutAt = firstNonEmptyString([ summaryInstance.lastCheckoutAt, data.lastCheckoutAt, ]); if (checkoutType === "one_time" && checkoutCode) { addDetected({ taskType: checkoutCode.indexOf("credits") !== -1 ? "publish_credits" : "manual", title: "Fulfill one-time order: " + checkoutCode, productCode: checkoutCode, orderRef: checkoutCode, status: "pending", createdAt: checkoutAt, updatedAt: checkoutAt, }); } if (checkoutType === "subscription" && checkoutCode) { addDetected({ taskType: checkoutCode.indexOf("replication") !== -1 ? "replication_setup" : "storage_upgrade", title: "Provision subscription tier: " + checkoutCode, productCode: checkoutCode, orderRef: checkoutCode, status: "pending", createdAt: checkoutAt, updatedAt: checkoutAt, }); } const collectOrderArrays = function () { return firstArray([ summary.orders, summary.purchases, summary.services, data.orders, data.purchases, data.services, ]); }; collectOrderArrays().forEach(function (entry) { if (!isRecord(entry)) { return; } const customerId = firstNonEmptyString([ entry.paymenterCustomerId, entry.customerId, ]); const user = customerId && userByCustomerId[customerId] ? userByCustomerId[customerId] : null; const code = firstNonEmptyString([ entry.code, entry.productCode, entry.planCode, entry.serviceCode, entry.itemCode, ]); const title = firstNonEmptyString([ entry.title, entry.name, entry.label, code ? "Fulfill order item: " + code : "", ]); addDetected({ taskType: code.indexOf("credits") !== -1 ? "publish_credits" : code.indexOf("replication") !== -1 ? "replication_setup" : "manual", title: title, nextcloudUserId: firstNonEmptyString([ entry.nextcloudUserId, user ? user.nextcloudUserId : "", ]), paymenterCustomerId: customerId, userLabel: firstNonEmptyString([ entry.userLabel, user ? user.displayName || user.nextcloudUserId : "", ]), orderRef: firstNonEmptyString([ entry.orderId, entry.id, entry.reference, entry.externalId, ]), productCode: code, quantity: firstNonEmptyString([entry.quantity, entry.qty]), status: inferTaskStatusFromText( firstNonEmptyString([entry.status, entry.state, entry.orderStatus]) ), createdAt: firstNonEmptyString([entry.createdAt]), updatedAt: firstNonEmptyString([entry.updatedAt]), completedAt: firstNonEmptyString([ entry.completedAt, entry.fulfilledAt, ]), }); }); knownUsers.forEach(function (user) { const userOrders = firstArray([ user.raw.orders, user.raw.purchases, user.raw.services, user.raw.subscriptions, ]); userOrders.forEach(function (entry) { if (!isRecord(entry)) { return; } const code = firstNonEmptyString([ entry.code, entry.productCode, entry.planCode, entry.serviceCode, ]); const title = firstNonEmptyString([ entry.title, entry.name, entry.label, code ? "Fulfill user order: " + code : "", ]); addDetected({ taskType: code.indexOf("credits") !== -1 ? "publish_credits" : code.indexOf("replication") !== -1 ? "replication_setup" : "manual", title: title, nextcloudUserId: user.nextcloudUserId, paymenterCustomerId: user.paymenterCustomerId, userLabel: user.displayName || user.nextcloudUserId, orderRef: firstNonEmptyString([ entry.orderId, entry.id, entry.reference, ]), productCode: code, quantity: firstNonEmptyString([entry.quantity, entry.qty]), status: inferTaskStatusFromText( firstNonEmptyString([entry.status, entry.state]) ), createdAt: firstNonEmptyString([entry.createdAt]), updatedAt: firstNonEmptyString([entry.updatedAt]), completedAt: firstNonEmptyString([ entry.completedAt, entry.fulfilledAt, ]), }); }); }); collectIntroGrantRecords(detailData).forEach(function (grant) { const titleBits = ["Intro grant request"]; if (cleanString(grant.displayName)) { titleBits.push(cleanString(grant.displayName)); } else if (cleanString(grant.nextcloudUserId)) { titleBits.push(cleanString(grant.nextcloudUserId)); } addDetected({ taskType: "intro_grant", title: titleBits.join(": "), nextcloudUserId: cleanString(grant.nextcloudUserId), paymenterCustomerId: cleanString(grant.paymenterCustomerId), userLabel: cleanString(grant.displayName), orderRef: cleanString(grant.requestId), productCode: "intro_grant", quantity: cleanString(grant.amount), status: normalizeOperationStatus(grant.status || "pending"), notes: cleanString(grant.message), createdAt: cleanString(grant.createdAt), updatedAt: cleanString(grant.updatedAt || grant.createdAt), completedAt: cleanString(grant.completedAt), meta: { grantRequest: grant.raw, }, }); }); const dedupedByKey = {}; detected.forEach(function (entry) { const key = cleanString(entry.detectionKey); if (!key) { return; } if (!Object.prototype.hasOwnProperty.call(dedupedByKey, key)) { dedupedByKey[key] = entry; return; } const existing = dedupedByKey[key]; const existingStatus = normalizeOperationStatus(existing.status); const incomingStatus = normalizeOperationStatus(entry.status); if (existingStatus === "completed" && incomingStatus !== "completed") { return; } dedupedByKey[key] = entry; }); return Object.keys(dedupedByKey).map(function (key) { return dedupedByKey[key]; }); } function formatDateTime(value) { const raw = cleanString(value); if (!raw) { return "Not set"; } const parsed = new Date(raw); if (Number.isNaN(parsed.getTime())) { return raw; } return parsed.toLocaleString(); } function createInfoPanel(rawEl, toggleButton, allButton) { return { rawEl: rawEl || null, toggleButton: toggleButton || null, allButton: allButton || null, rawVisible: false, showAll: false, loading: false, message: "", errorMessage: "", fullData: null, focusedData: null, }; } function infoPanelHasData(panel) { return panel && panel.fullData !== null && panel.fullData !== undefined; } function setInfoPanelLoading(panel, message) { if (!panel) { return; } panel.loading = true; panel.errorMessage = ""; panel.message = cleanString(message) || "Loading..."; panel.fullData = null; panel.focusedData = null; renderInfoPanel(panel); } function setInfoPanelEmpty(panel, message) { if (!panel) { return; } panel.loading = false; panel.errorMessage = ""; panel.message = cleanString(message) || "No data available."; panel.fullData = null; panel.focusedData = null; panel.showAll = false; renderInfoPanel(panel); } function setInfoPanelError(panel, message) { if (!panel) { return; } panel.loading = false; panel.errorMessage = cleanString(message) || "Unknown error."; panel.message = ""; panel.fullData = null; panel.focusedData = null; panel.showAll = false; panel.rawVisible = true; renderInfoPanel(panel); } function setInfoPanelData(panel, fullData, focusedData) { if (!panel) { return; } panel.loading = false; panel.errorMessage = ""; panel.message = ""; panel.fullData = fullData; panel.focusedData = focusedData; if (!infoPanelHasData(panel)) { panel.showAll = false; } renderInfoPanel(panel); } function setInfoPanelRawVisible(panel, rawVisible) { if (!panel) { return; } panel.rawVisible = Boolean(rawVisible); renderInfoPanel(panel); } function setInfoPanelShowAll(panel, showAll) { if (!panel) { return; } if (!infoPanelHasData(panel)) { panel.showAll = false; renderInfoPanel(panel); return; } panel.showAll = Boolean(showAll); if (panel.showAll) { panel.rawVisible = true; } renderInfoPanel(panel); } function stringifyPanelData(value) { if (typeof value === "string") { return value; } try { return JSON.stringify(value || {}, null, 2); } catch (_error) { return String(value); } } function renderInfoPanel(panel) { if (!panel || !panel.rawEl) { return; } let content = ""; if (panel.errorMessage) { content = panel.errorMessage; } else if (panel.loading) { content = panel.message || "Loading..."; } else if (infoPanelHasData(panel)) { const payload = panel.showAll ? panel.fullData : panel.focusedData !== null && panel.focusedData !== undefined ? panel.focusedData : panel.fullData; content = stringifyPanelData(payload); } else { content = panel.message || "No data available."; } panel.rawEl.textContent = content; panel.rawEl.classList.toggle("chd-hidden", !panel.rawVisible); if (panel.toggleButton) { panel.toggleButton.textContent = panel.rawVisible ? "Hide Raw" : "Show Raw"; } if (panel.allButton) { panel.allButton.disabled = !infoPanelHasData(panel); panel.allButton.textContent = panel.showAll ? "Show Focused Info" : "Show All Info"; } } function bindInfoPanelControls(panel) { if (!panel) { return; } if (panel.toggleButton) { panel.toggleButton.addEventListener("click", function () { panel.rawVisible = !panel.rawVisible; renderInfoPanel(panel); }); } if (panel.allButton) { panel.allButton.addEventListener("click", function () { if (!infoPanelHasData(panel)) { return; } panel.showAll = !panel.showAll; if (panel.showAll) { panel.rawVisible = true; } renderInfoPanel(panel); }); } renderInfoPanel(panel); } function renderSummaryItems(container, items, emptyMessage) { if (!container) { return; } container.innerHTML = ""; const list = Array.isArray(items) ? items : []; if (!list.length) { container.classList.add("chd-summary-empty"); container.textContent = emptyMessage || "No summary data."; return; } container.classList.remove("chd-summary-empty"); list.forEach(function (entry) { if (!entry || typeof entry !== "object") { return; } const item = document.createElement("div"); item.className = "chd-summary-item"; if (entry.wide) { item.classList.add("chd-summary-item--wide"); } const label = document.createElement("span"); label.className = "chd-summary-label"; label.textContent = cleanString(entry.label) || "Info"; const value = document.createElement("span"); value.className = "chd-summary-value"; value.textContent = cleanString(entry.value) || "Not set"; item.appendChild(label); item.appendChild(value); container.appendChild(item); }); } function buildStatusPill(label, value, level) { const pill = document.createElement("span"); pill.className = "chd-status-pill"; pill.dataset.level = normalizeStatusLevel(level); pill.textContent = cleanString(label).toUpperCase() + ": " + (cleanString(value) || "Unknown"); return pill; } function renderStatusPills(container, statuses) { if (!container) { return; } container.innerHTML = ""; const list = Array.isArray(statuses) ? statuses : []; if (!list.length) { container.classList.add("chd-hidden"); return; } container.classList.remove("chd-hidden"); list.forEach(function (entry) { if (!entry || typeof entry !== "object") { return; } container.appendChild( buildStatusPill(entry.label, entry.value, entry.level) ); }); } function summarizeOverviewData(payloadData) { const data = isRecord(payloadData) ? payloadData : {}; const chd = firstObject([data.chd]); const billingAutomation = firstObject([data.billingAutomation]); const entitlements = firstObject([data.entitlements]); const designStatus = firstObject([data.designStatus]); const mode = firstNonEmptyString( [data.mode, data.sovereignMode, data.scMode], "powered" ); const baseUrl = firstNonEmptyString([chd.baseUrl]); const instanceId = firstNonEmptyString([chd.instanceId]); const connectorAdminTokenSet = coerceBoolean(chd.connectorAdminTokenSet); const packageTier = firstNonEmptyString( [entitlements.packageTier], "Not set" ); const billingProvider = firstNonEmptyString( [chd.billingProvider], "Not set" ); const billingSyncMode = firstNonEmptyString( [chd.billingSyncMode], "Not set" ); const lastSyncAtRaw = firstNonEmptyString([chd.lastSyncAt]); const automationEnabled = coerceBoolean(billingAutomation.enabled); const automationLastRunAt = firstNonEmptyString([ billingAutomation.lastRunCompletedAt, billingAutomation.lastRunStartedAt, ]); const automationLastRunStatus = firstNonEmptyString( [billingAutomation.lastRunStatus], "idle" ); const automationLevel = (() => { const normalized = automationLastRunStatus.toLowerCase(); if ( normalized.indexOf("success") !== -1 || normalized.indexOf("ok") !== -1 || normalized.indexOf("complete") !== -1 || normalized.indexOf("fulfilled") !== -1 ) { return "good"; } if ( normalized.indexOf("skip") !== -1 || normalized.indexOf("idle") !== -1 || normalized.indexOf("pending") !== -1 ) { return "partial"; } return automationEnabled ? "issues" : "partial"; })(); const syncStatus = evaluateSyncStatus(chd.lastSyncStatus); const connectorLevel = baseUrl && instanceId ? "good" : "issues"; const tokenLevel = coerceBoolean(chd.instanceTokenSet) ? "good" : "issues"; const registrationLevel = coerceBoolean(chd.registrationTokenSet) ? "good" : "partial"; const summaryItems = [ { label: "Mode", value: mode }, { label: "Instance ID", value: instanceId || "Missing" }, { label: "Connector URL", value: baseUrl || "Missing", wide: true }, { label: "Package Tier", value: packageTier }, { label: "Billing Provider", value: billingProvider }, { label: "Sync Mode", value: billingSyncMode }, { label: "Invite Automation", value: automationEnabled ? "Enabled" : "Disabled", }, { label: "Automation Last Run", value: automationLastRunAt ? formatDateTime(automationLastRunAt) : "Never", wide: true, }, { label: "Last Sync", value: lastSyncAtRaw ? formatDateTime(lastSyncAtRaw) : "Never", }, ]; const statusItems = [ { label: "Connector", value: connectorLevel === "good" ? "Configured" : "Config Missing", level: connectorLevel, }, { label: "Instance Token", value: coerceBoolean(chd.instanceTokenSet) ? "Present" : "Missing", level: tokenLevel, }, { label: "Registration Token", value: coerceBoolean(chd.registrationTokenSet) ? "Present" : "Missing", level: registrationLevel, }, { label: "Connector Admin", value: connectorAdminTokenSet ? "Configured" : "Missing", level: connectorAdminTokenSet ? "good" : "issues", }, { label: "Invite Fulfillment", value: automationEnabled ? "Enabled" : "Disabled", level: automationEnabled ? "good" : "partial", }, { label: "Auto Run", value: automationLastRunStatus, level: automationLevel, }, { label: "Sync", value: syncStatus.label, level: syncStatus.level }, ]; const focusedData = { mode: mode, connection: { baseUrl: baseUrl, instanceId: instanceId, instanceTokenSet: coerceBoolean(chd.instanceTokenSet), registrationTokenSet: coerceBoolean(chd.registrationTokenSet), connectorAdminTokenSet: connectorAdminTokenSet, }, sync: { status: firstNonEmptyString([chd.lastSyncStatus]), at: lastSyncAtRaw, message: firstNonEmptyString([chd.lastSyncMessage]), }, billingAutomation: billingAutomation, entitlements: entitlements, designStatus: designStatus, }; return { summaryItems: summaryItems, statusItems: statusItems, focusedData: focusedData, }; } function summarizeBillingAutoFulfillmentData(payloadData) { const data = isRecord(payloadData) ? payloadData : {}; const enabled = coerceBoolean(data.enabled); const lastRunStatus = firstNonEmptyString([data.lastRunStatus], "idle"); const lastRunStartedAt = firstNonEmptyString([data.lastRunStartedAt]); const lastRunCompletedAt = firstNonEmptyString([data.lastRunCompletedAt]); const lastSuccessfulRunCompletedAt = firstNonEmptyString([ data.lastSuccessfulRunCompletedAt, ]); const lastRunSince = firstNonEmptyString([data.lastRunSince]); const lastRunMessage = firstNonEmptyString([data.lastRunMessage]); const runCount = coerceFiniteNumber(data.lastRunCount) || 0; const fulfilledCount = coerceFiniteNumber(data.lastRunFulfilledCount) || 0; const skippedCount = coerceFiniteNumber(data.lastRunSkippedCount) || 0; const errorCount = coerceFiniteNumber(data.lastRunErrorCount) || 0; const processedCount = coerceFiniteNumber(data.processedCount) || 0; const fulfilledStoredCount = coerceFiniteNumber(data.fulfilledCount) || 0; const pendingStoredCount = coerceFiniteNumber(data.pendingCount) || 0; const storedErrorCount = coerceFiniteNumber(data.errorCount) || 0; const lastRunLevel = (() => { const normalized = lastRunStatus.toLowerCase(); if ( normalized.indexOf("success") !== -1 || normalized.indexOf("ok") !== -1 || normalized.indexOf("complete") !== -1 || normalized.indexOf("fulfilled") !== -1 ) { return "good"; } if ( normalized.indexOf("skip") !== -1 || normalized.indexOf("idle") !== -1 || normalized.indexOf("pending") !== -1 ) { return "partial"; } return "issues"; })(); return { summaryItems: [ { label: "Automation", value: enabled ? "Enabled" : "Disabled", }, { label: "Last Run", value: lastRunCompletedAt ? formatDateTime(lastRunCompletedAt) : lastRunStartedAt ? formatDateTime(lastRunStartedAt) : "Never", wide: true, }, { label: "Last Successful Scan", value: lastSuccessfulRunCompletedAt ? formatDateTime(lastSuccessfulRunCompletedAt) : "Never", wide: true, }, { label: "Since", value: lastRunSince ? formatDateTime(lastRunSince) : "Initial lookback", }, { label: "Run Status", value: lastRunStatus }, { label: "Run Message", value: lastRunMessage || "No run recorded yet.", wide: true }, { label: "Processed", value: String(runCount) }, { label: "Fulfilled", value: String(fulfilledCount) }, { label: "Skipped", value: String(skippedCount) }, { label: "Errors", value: String(errorCount) }, { label: "Stored Orders", value: String(processedCount) }, { label: "Stored Fulfilled", value: String(fulfilledStoredCount) }, { label: "Stored Pending", value: String(pendingStoredCount) }, { label: "Stored Errors", value: String(storedErrorCount) }, ], statusItems: [ { label: "Invite Fulfillment", value: enabled ? "Enabled" : "Disabled", level: enabled ? "good" : "partial", }, { label: "Last Run", value: lastRunStatus, level: lastRunLevel, }, ], focusedData: { enabled: enabled, lastRunStartedAt: lastRunStartedAt, lastRunCompletedAt: lastRunCompletedAt, lastSuccessfulRunCompletedAt: lastSuccessfulRunCompletedAt, lastRunSince: lastRunSince, lastRunStatus: lastRunStatus, lastRunMessage: lastRunMessage, lastRunCount: runCount, lastRunFulfilledCount: fulfilledCount, lastRunSkippedCount: skippedCount, lastRunErrorCount: errorCount, processedCount: processedCount, fulfilledCount: fulfilledStoredCount, pendingCount: pendingStoredCount, errorCount: storedErrorCount, }, }; } function splitAddonsByType(addons) { const list = Array.isArray(addons) ? addons : []; let monthlyCount = 0; let oneTimeCount = 0; list.forEach(function (entry) { if (!entry || typeof entry !== "object") { return; } if (isMonthlyAddonPreviewEntry(entry)) { monthlyCount += 1; return; } const purchaseType = cleanString(entry.purchaseType).toLowerCase(); const purchaseInterval = cleanString( entry.purchaseInterval ).toLowerCase(); if (purchaseType === "one_time" || purchaseInterval === "one_time") { oneTimeCount += 1; return; } if (!purchaseType && !purchaseInterval) { oneTimeCount += 1; } }); return { monthlyCount: monthlyCount, oneTimeCount: oneTimeCount, }; } function isMonthlyAddonPreviewEntry(entry) { const purchaseType = trimString(entry && entry.purchaseType).toLowerCase(); const purchaseInterval = trimString( entry && entry.purchaseInterval ).toLowerCase(); if (purchaseType === "monthly_addon" || purchaseInterval === "monthly") { return true; } const code = trimString( entry && (entry.code || entry.id || entry.productId) ).toLowerCase(); if (!code) { return false; } return ( code.startsWith("capacity_") || code.startsWith("capacity-") || code.includes("capacity") || code.includes("storage") || code.includes("on-server capaticy") || code.includes("_monthly") || code.includes("-monthly") ); } function summarizeConnectorData(payloadData) { const data = isRecord(payloadData) ? payloadData : {}; const source = firstObject([data.source]); const details = firstObject([data.details]); const verification = firstObject([data.verification]); const entitlements = firstObject([data.entitlements]); const catalog = firstObject([data.catalog, details.catalog]); const packages = firstArray([ catalog.subscriptions, catalog.packages, data.subscriptions, data.packages, ]); const addons = firstArray([catalog.addons, data.addons]); const extraOffers = firstArray([catalog.extraOffers, data.extraOffers]); const oneTimeProducts = firstArray([catalog.oneTime, data.oneTime]); const split = splitAddonsByType(addons); const packageCount = packages.length; const monthlyAddonCount = split.monthlyCount; const oneTimeCount = split.oneTimeCount || oneTimeProducts.length; const extraOffersCount = extraOffers.length; const instanceId = firstNonEmptyString([ source.instanceId, data.instanceId, details.instanceId, ]); const nextcloudPublicUrl = firstNonEmptyString([ source.nextcloudPublicUrl, data.nextcloudPublicUrl, details.nextcloudPublicUrl, ]); const packageTier = firstNonEmptyString( [entitlements.packageTier, details.packageTier, data.packageTier], "Not set" ); const customerId = firstNonEmptyString([ verification.paymenterCustomerId, details.paymenterCustomerId, data.paymenterCustomerId, ]); const domainStatus = evaluateDomainStatus( firstNonEmptyString([ verification.domainVerificationState, details.domainVerificationState, data.domainVerificationState, ]) ); const syncStatus = evaluateSyncStatus( firstNonEmptyString([ details.lastSyncStatus, source.lastSyncStatus, data.lastSyncStatus, ]) ); const lastSyncAtRaw = firstNonEmptyString([ details.lastSyncAt, source.lastSyncAt, data.lastSyncAt, data.fetchedAt, ]); const summaryItems = [ { label: "Instance ID", value: instanceId || "Missing" }, { label: "Nextcloud URL", value: nextcloudPublicUrl || "Missing", wide: true, }, { label: "Package Tier", value: packageTier }, { label: "Paymenter Customer", value: customerId || "Not linked" }, { label: "Packages", value: String(packageCount) }, { label: "Monthly Add-ons", value: String(monthlyAddonCount) }, { label: "One-time Products", value: String(oneTimeCount) }, { label: "Extra Offers", value: String(extraOffersCount) }, { label: "Last Sync", value: lastSyncAtRaw ? formatDateTime(lastSyncAtRaw) : "Not set", }, ]; const connectorLevel = instanceId && nextcloudPublicUrl ? "good" : "issues"; const customerLevel = customerId ? "good" : "partial"; const statusItems = [ { label: "Connector", value: connectorLevel === "good" ? "Configured" : "Config Missing", level: connectorLevel, }, { label: "Domain", value: domainStatus.label, level: domainStatus.level }, { label: "Sync", value: syncStatus.label, level: syncStatus.level }, { label: "Billing Link", value: customerId ? "Present" : "Missing", level: customerLevel, }, ]; const focusedData = { source: { instanceId: instanceId, nextcloudPublicUrl: nextcloudPublicUrl, }, verification: { paymenterCustomerId: customerId, domainVerificationState: domainStatus.label, }, entitlements: { packageTier: packageTier, creditsBalance: firstNonEmptyString([entitlements.creditsBalance]), monthlyCredits: firstNonEmptyString([entitlements.monthlyCredits]), }, catalogCounts: { packages: packageCount, monthlyAddons: monthlyAddonCount, oneTimeProducts: oneTimeCount, extraOffers: extraOffersCount, }, lastSync: { status: syncStatus.label, at: lastSyncAtRaw, }, }; return { summaryItems: summaryItems, statusItems: statusItems, focusedData: focusedData, }; } function summarizeInstanceHealth(instance) { const entry = isRecord(instance) ? instance : {}; const instanceId = firstNonEmptyString([entry.instanceId]); const nextcloudPublicUrl = firstNonEmptyString([entry.nextcloudPublicUrl]); const paymenterCustomerId = firstNonEmptyString([ entry.paymenterCustomerId, ]); const paymenterName = firstNonEmptyString([ entry.paymenterDisplayName, [ trimString(entry.paymenterFirstName), trimString(entry.paymenterLastName), ] .filter(Boolean) .join(" "), ]); const legacyAdminEmail = firstNonEmptyString([entry.adminEmail]); const mspContactName = firstNonEmptyString([ entry.mspContactName, paymenterName, ]); const mspContactEmail = firstNonEmptyString([ entry.mspContactEmail, legacyAdminEmail, ]); const pluginAdminEmail = firstNonEmptyString([entry.pluginAdminEmail]); const paymenterEmail = firstNonEmptyString([ entry.paymenterEmail, entry.lastPaymenterResolvedEmail, ]); const adminQortalAddress = firstNonEmptyString([entry.adminQortalAddress]); const domainStatus = evaluateDomainStatus( firstNonEmptyString([entry.domainVerificationState]) ); const syncStatus = evaluateSyncStatus( firstNonEmptyString([entry.lastSyncStatus]) ); let level = "good"; if (!instanceId) { level = combineStatusLevel(level, "issues"); } if (!nextcloudPublicUrl) { level = combineStatusLevel(level, "partial"); } if (domainStatus.level === "issues") { level = combineStatusLevel(level, "issues"); } return { level: level, instanceId: instanceId, nextcloudPublicUrl: nextcloudPublicUrl, paymenterCustomerId: paymenterCustomerId, mspContactName: mspContactName, mspContactEmail: mspContactEmail, legacyAdminEmail: legacyAdminEmail, pluginAdminEmail: pluginAdminEmail, paymenterEmail: paymenterEmail, adminQortalAddress: adminQortalAddress, domainStatus: domainStatus, syncStatus: syncStatus, lastSyncAt: firstNonEmptyString([entry.lastSyncAt]), knownUsersCount: coerceFiniteNumber(entry.knownUsersCount), lastCheckoutType: firstNonEmptyString([entry.lastCheckoutType]), }; } function buildInstanceFocusedData(entry) { const health = summarizeInstanceHealth(entry); return { instanceId: health.instanceId, nextcloudPublicUrl: health.nextcloudPublicUrl, paymenterCustomerId: health.paymenterCustomerId, mspContactName: health.mspContactName, mspContactEmail: health.mspContactEmail, legacyAdminEmail: health.legacyAdminEmail, pluginAdminEmail: health.pluginAdminEmail, paymenterEmail: health.paymenterEmail, adminQortalAddress: health.adminQortalAddress, domainVerificationState: health.domainStatus.label, lastSyncStatus: health.syncStatus.label, lastSyncAt: health.lastSyncAt, knownUsersCount: health.knownUsersCount, lastCheckoutType: health.lastCheckoutType, billingSummary: firstObject([entry.lastPaymenterSyncSummary]), overallStatus: levelTitle(health.level), }; } function renderInstanceCompactList(instances) { if (!instancesCompactEl) { return; } instancesCompactEl.innerHTML = ""; const list = (Array.isArray(instances) ? instances.slice() : []).sort( function (a, b) { const actionA = coerceFiniteNumber(a && a.actionRequiredCount) || 0; const actionB = coerceFiniteNumber(b && b.actionRequiredCount) || 0; if (actionA !== actionB) { return actionB - actionA; } const grantsA = coerceFiniteNumber(a && a.grantRequestCount) || 0; const grantsB = coerceFiniteNumber(b && b.grantRequestCount) || 0; if (grantsA !== grantsB) { return grantsB - grantsA; } const titleA = firstNonEmptyString( [a && a.instanceId, a && a.nextcloudPublicUrl], "" ); const titleB = firstNonEmptyString( [b && b.instanceId, b && b.nextcloudPublicUrl], "" ); return titleA.localeCompare(titleB); } ); if (!list.length) { instancesCompactEl.textContent = "No connector instances returned."; return; } list.forEach(function (entry) { const health = summarizeInstanceHealth(entry); const row = document.createElement("article"); row.className = "chd-instance-card"; const main = document.createElement("div"); main.className = "chd-instance-main"; const title = document.createElement("div"); title.className = "chd-instance-title"; title.textContent = health.instanceId || "Unknown instance"; main.appendChild(title); const chips = document.createElement("div"); chips.className = "chd-chip-row"; const chipData = [ { label: "URL", value: health.nextcloudPublicUrl || "Missing" }, { label: "Paymenter", value: health.paymenterCustomerId || "Missing" }, { label: "Users", value: health.knownUsersCount === null ? "Unknown" : String(health.knownUsersCount), }, { label: "Domain", value: health.domainStatus.label }, { label: "Sync", value: health.syncStatus.label }, ]; const actionRequiredCount = coerceFiniteNumber(entry.actionRequiredCount) || 0; const grantRequestCount = coerceFiniteNumber(entry.grantRequestCount) || 0; if (actionRequiredCount > 0) { chipData.unshift({ label: "Needs Action", value: String(actionRequiredCount), }); } if (grantRequestCount > 0) { chipData.unshift({ label: "Grant Requests", value: String(grantRequestCount), }); } chipData.forEach(function (chipEntry) { const chip = document.createElement("span"); chip.className = "chd-chip"; chip.textContent = chipEntry.label + ": " + chipEntry.value; chips.appendChild(chip); }); main.appendChild(chips); row.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-instance-actions"; if (actionRequiredCount > 0) { actions.appendChild( buildStatusPill( "Action", "Open Details To Act", grantRequestCount > 0 ? "issues" : "partial" ) ); } actions.appendChild( buildStatusPill( "Overall Status", levelTitle(health.level), health.level ) ); const allInfoButton = document.createElement("button"); allInfoButton.type = "button"; allInfoButton.className = "button"; allInfoButton.textContent = actionRequiredCount > 0 ? "Action Required" : "All Info"; allInfoButton.classList.toggle( "chd-button--action-required", actionRequiredCount > 0 ); allInfoButton.disabled = !health.instanceId; allInfoButton.addEventListener("click", function () { if (!health.instanceId) { return; } if (instanceSelectEl) { instanceSelectEl.value = health.instanceId; } void loadConnectorInstanceDetail(health.instanceId, { showRaw: true, showAll: true, }); }); actions.appendChild(allInfoButton); row.appendChild(actions); instancesCompactEl.appendChild(row); }); } function renderRemovedInstanceList(instances) { renderSummaryItems( removedInstancesSummaryEl, Array.isArray(instances) && instances.length ? [{ label: "Removed Instances", value: String(instances.length) }] : [], "No removed instances." ); if (!removedInstancesListEl) { return; } removedInstancesListEl.innerHTML = ""; const list = Array.isArray(instances) ? instances.slice() : []; list.sort(function (a, b) { const leftTs = Date.parse(trimString(a && a.removedAt)) || 0; const rightTs = Date.parse(trimString(b && b.removedAt)) || 0; return rightTs - leftTs; }); if (!list.length) { removedInstancesListEl.textContent = "No removed instances."; return; } list.forEach(function (entry) { const row = document.createElement("article"); row.className = "chd-instance-card chd-instance-card--removed"; const main = document.createElement("div"); main.className = "chd-instance-main"; const title = document.createElement("div"); title.className = "chd-instance-title"; title.textContent = trimString(entry && entry.instanceId) || "Unknown instance"; main.appendChild(title); const chips = document.createElement("div"); chips.className = "chd-chip-row"; [ { label: "URL", value: trimString(entry && entry.nextcloudPublicUrl) || "Missing", }, { label: "Removed", value: trimString(entry && entry.removedAt) ? formatDateTime(entry.removedAt) : "Unknown", }, { label: "By", value: trimString(entry && entry.removedBy) || "Not recorded", }, { label: "Reason", value: trimString(entry && entry.removalReason) || "Not recorded", }, ].forEach(function (chipEntry) { const chip = document.createElement("span"); chip.className = "chd-chip"; chip.textContent = chipEntry.label + ": " + chipEntry.value; chips.appendChild(chip); }); main.appendChild(chips); row.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-instance-actions"; const restoreButton = document.createElement("button"); restoreButton.type = "button"; restoreButton.className = "button"; restoreButton.textContent = "Restore Instance"; restoreButton.disabled = trimString(entry && entry.instanceId) === ""; restoreButton.addEventListener("click", function () { const instanceId = trimString(entry && entry.instanceId); if (!instanceId) { return; } if ( !window.confirm( "Restore " + instanceId + " to the active connector configuration?" ) ) { return; } void saveConnectorInstanceState(instanceId, "restore").catch(function ( error ) { if (instanceConnectorStatusEl) { instanceConnectorStatusEl.textContent = "Failed to update instance state: " + (error && error.message ? error.message : String(error)); } }); }); actions.appendChild(restoreButton); row.appendChild(actions); removedInstancesListEl.appendChild(row); }); } function summarizeInstancesData(payloadData) { const data = isRecord(payloadData) ? payloadData : {}; const paging = firstObject([data.paging]); const instances = Array.isArray(data.instances) ? data.instances : []; const statusCounts = { good: 0, partial: 0, issues: 0, }; instances.forEach(function (entry) { const level = summarizeInstanceHealth(entry).level; statusCounts[level] += 1; }); const actionableInstances = instances.filter(function (entry) { return (coerceFiniteNumber(entry && entry.actionRequiredCount) || 0) > 0; }).length; const totalActionRequired = instances.reduce(function (sum, entry) { return ( sum + (coerceFiniteNumber(entry && entry.actionRequiredCount) || 0) ); }, 0); const totalGrantRequests = instances.reduce(function (sum, entry) { return sum + (coerceFiniteNumber(entry && entry.grantRequestCount) || 0); }, 0); const summaryItems = [ { label: "Instances Loaded", value: String(instances.length) }, { label: "Instances With Action", value: String(actionableInstances) }, { label: "Pending Actions", value: String(totalActionRequired) }, { label: "Grant Requests", value: String(totalGrantRequests) }, { label: "Good", value: String(statusCounts.good) }, { label: "Partially Good", value: String(statusCounts.partial) }, { label: "Issues", value: String(statusCounts.issues) }, ]; if (coerceFiniteNumber(paging.total) !== null) { summaryItems.push({ label: "Total (Connector)", value: String(coerceFiniteNumber(paging.total)), }); } const focusedData = { counts: statusCounts, instances: instances.map(buildInstanceFocusedData), paging: paging, }; return { instances: instances, summaryItems: summaryItems, focusedData: focusedData, }; } function resolveDetailInstance(detailData) { const data = isRecord(detailData) ? detailData : {}; const topLevelInstance = firstObject([data.instance]); const summaryInstance = firstObject([ data.summary && data.summary.instance ? data.summary.instance : null, ]); const nestedInstance = Object.assign({}, topLevelInstance, summaryInstance); if (Object.keys(nestedInstance).length > 0) { return nestedInstance; } if (cleanString(data.instanceId) !== "") { return data; } const nestedData = firstObject([data.data]); if (cleanString(nestedData.instanceId) !== "") { return nestedData; } const deepInstance = firstObject([nestedData.instance]); return deepInstance; } function summarizeInstanceDetail(detailData, fallbackInstanceId) { const instance = resolveDetailInstance(detailData); const health = summarizeInstanceHealth(instance); const instanceId = health.instanceId || cleanString(fallbackInstanceId); const knownUsers = collectKnownUsers(detailData); const userCommerceRows = collectUserCommerceRows(detailData, knownUsers); const linkedUserCount = userCommerceRows.filter(function (row) { return cleanString(row && row.paymenterCustomerId); }).length; const unlinkedUserCount = Math.max(0, userCommerceRows.length - linkedUserCount); const operationsCandidates = detectOperationsFromInstanceDetail( instanceId, detailData ); const billing = summarizeBillingHealth(instance.lastPaymenterSyncSummary, { services: coerceFiniteNumber(instance.lastPaymenterSyncSummary && instance.lastPaymenterSyncSummary.services), invoices: coerceFiniteNumber(instance.lastPaymenterSyncSummary && instance.lastPaymenterSyncSummary.invoices), }); const summaryItems = [ { label: "Instance ID", value: instanceId || "Missing" }, { label: "Nextcloud URL", value: health.nextcloudPublicUrl || "Missing", wide: true, }, { label: "Paymenter Customer", value: health.paymenterCustomerId || "Missing", }, { label: "NuQloud MSP Contact Name", value: health.mspContactName || "Missing", }, { label: "NuQloud MSP Contact Email", value: health.mspContactEmail || "Missing", }, { label: "Paymenter Email", value: health.paymenterEmail || "Missing" }, { label: "Active Package", value: firstNonEmptyString( [billing.activePackageTier, billing.activeServiceLabel], "Not set" ), }, { label: "Active Services", value: String(billing.activeServices), }, { label: "Suspended Services", value: String(billing.suspendedServices), }, { label: "Cancelled Services", value: String(billing.cancelledServices), }, { label: "Outstanding Invoices", value: String(billing.outstandingInvoices), }, { label: "Overdue Invoices", value: String(billing.overdueInvoices), }, { label: "Billing Health", value: billing.billingHealthLabel, }, { label: "Attention", value: billing.attentionText || "None", wide: true, }, { label: "Future Plugin Admin Email", value: health.pluginAdminEmail || "Not set", }, { label: "Future Plugin Admin Qortal Address", value: health.adminQortalAddress || "Not set", wide: true, }, { label: "Connector Target Profile", value: cleanString(instance.desiredConnectorProfile) || "Not set", }, { label: "Connector Target URL", value: cleanString(instance.desiredConnectorBaseUrl) || "Not set", wide: true, }, { label: "Current Connector URL", value: cleanString(instance.currentConnectorBaseUrl) || "Not set", wide: true, }, { label: "Domain Verification", value: health.domainStatus.label }, { label: "Last Sync", value: health.lastSyncAt ? formatDateTime(health.lastSyncAt) : "Not set", }, { label: "Known Users", value: health.knownUsersCount === null ? "Unknown" : String(health.knownUsersCount), }, { label: "Paymenter Linked Users", value: String(linkedUserCount), }, { label: "Paymenter Unlinked Users", value: String(unlinkedUserCount), }, { label: "Last Checkout Type", value: health.lastCheckoutType || "Not set", }, ]; if ( health.legacyAdminEmail && health.legacyAdminEmail !== health.mspContactEmail ) { summaryItems.splice(5, 0, { label: "Legacy Compatibility Email", value: health.legacyAdminEmail, }); } const statusItems = [ { label: "Overall Status", value: levelTitle(health.level), level: health.level, }, { label: "Domain", value: health.domainStatus.label, level: health.domainStatus.level, }, { label: "Sync", value: health.syncStatus.label, level: health.syncStatus.level, }, { label: "Billing", value: billing.billingHealthLabel, level: billing.billingHealthLevel, }, { label: "Services", value: billing.serviceHealthLabel, level: billing.serviceHealthLevel, }, { label: "Invoices", value: billing.invoiceHealthLabel, level: billing.invoiceHealthLevel, }, { label: "Billing Link", value: health.paymenterCustomerId ? "Present" : "Missing", level: health.paymenterCustomerId ? "good" : "partial", }, ]; const focusedData = { instance: buildInstanceFocusedData(instance), verification: firstObject([detailData && detailData.verification]), summary: firstObject([detailData && detailData.summary]), billing: billing, knownUsers: knownUsers.map(function (entry) { return { nextcloudUserId: entry.nextcloudUserId, displayName: entry.displayName, email: entry.email, qortalAddress: entry.qortalAddress, paymenterCustomerId: entry.paymenterCustomerId, }; }), userCommerce: userCommerceRows, operationsCandidates: operationsCandidates, }; return { summaryItems: summaryItems, statusItems: statusItems, focusedData: focusedData, knownUsers: knownUsers, userCommerceRows: userCommerceRows, operationsCandidates: operationsCandidates, }; } function populateInstanceConnectorControls(instance) { const source = isRecord(instance) ? instance : {}; const desiredProfile = trimString(source.desiredConnectorProfile); const desiredBaseUrl = trimString(source.desiredConnectorBaseUrl); const fallbackProfile = findSelectableConnectorProfile(desiredProfile); const fallbackBaseUrl = trimString( fallbackProfile && fallbackProfile.publicBaseUrl ); const currentBaseUrl = trimString(source.currentConnectorBaseUrl); if (instanceConnectorProfileEl) { const desiredExists = Array.from(instanceConnectorProfileEl.options).some( function (option) { return trimString(option.value) === desiredProfile; } ); instanceConnectorProfileEl.value = desiredExists ? desiredProfile : ""; } if (instanceConnectorUrlEl) { instanceConnectorUrlEl.value = desiredBaseUrl || fallbackBaseUrl; } if (instanceRemoveButton) { instanceRemoveButton.disabled = !activeInstanceId; } if (instanceConnectorStatusEl) { if (!activeInstanceId) { instanceConnectorStatusEl.textContent = "Load an instance to manage connector target."; } else if (desiredProfile || desiredBaseUrl) { let text = "Pending target for next sync/register"; if (desiredProfile) { text += " | Profile: " + desiredProfile; } if (desiredBaseUrl) { text += " | URL: " + desiredBaseUrl; } if (currentBaseUrl) { text += " | Current: " + currentBaseUrl; } instanceConnectorStatusEl.textContent = text; } else { instanceConnectorStatusEl.textContent = currentBaseUrl ? "No connector target override set. Current connector: " + currentBaseUrl : "No connector target override set."; } } } function populateInstanceMetadataControls(instance) { const source = isRecord(instance) ? instance : {}; const paymenterDerivedName = firstNonEmptyString([ source.paymenterDisplayName, [ trimString(source.paymenterFirstName), trimString(source.paymenterLastName), ] .filter(Boolean) .join(" "), ]); if (instanceMspContactNameEl) { instanceMspContactNameEl.value = trimString( source.mspContactName || paymenterDerivedName ); } if (instanceMspContactEmailEl) { instanceMspContactEmailEl.value = trimString( source.mspContactEmail || source.adminEmail ); } if (instancePluginAdminEmailEl) { instancePluginAdminEmailEl.value = trimString(source.pluginAdminEmail); } if (instanceAdminQortalAddressEl) { instanceAdminQortalAddressEl.value = trimString( source.adminQortalAddress ); } if (instanceMetadataSaveButton) { instanceMetadataSaveButton.disabled = !activeInstanceId; } if (instanceMetadataStatusEl) { if (!activeInstanceId) { instanceMetadataStatusEl.textContent = "Load an instance to manage NuQloud MSP contact fields."; return; } const contactEmail = trimString( source.mspContactEmail || source.adminEmail ); const pluginEmail = trimString(source.pluginAdminEmail); const paymenterEmail = trimString( source.paymenterEmail || source.lastPaymenterResolvedEmail ); let text = "NuQloud MSP fields loaded"; if (contactEmail) { text += " | MSP email: " + contactEmail; } if (paymenterEmail) { text += " | Paymenter email: " + paymenterEmail; } if (pluginEmail) { text += " | Plugin admin email: " + pluginEmail; } instanceMetadataStatusEl.textContent = text; } } function populateOperationsUserSelect(knownUsers) { if (!operationsUserSelectEl) { return; } const users = Array.isArray(knownUsers) ? knownUsers : []; operationsUserSelectEl.innerHTML = ""; const instanceOption = document.createElement("option"); instanceOption.value = ""; instanceOption.textContent = "Instance-level task"; operationsUserSelectEl.appendChild(instanceOption); users.forEach(function (entry, index) { const option = document.createElement("option"); option.value = "user:" + String(index); const name = firstNonEmptyString( [entry.displayName, entry.nextcloudUserId], "User" ); const customerId = firstNonEmptyString([entry.paymenterCustomerId], ""); option.textContent = customerId ? name + " (Paymenter #" + customerId + ")" : name; option.dataset.nextcloudUserId = firstNonEmptyString( [entry.nextcloudUserId], "" ); option.dataset.paymenterCustomerId = firstNonEmptyString( [entry.paymenterCustomerId], "" ); option.dataset.userLabel = firstNonEmptyString( [entry.displayName, entry.nextcloudUserId], "" ); operationsUserSelectEl.appendChild(option); }); } function mergeInstanceOperations(persistedTasks, detectedTasks) { const persisted = Array.isArray(persistedTasks) ? persistedTasks : []; const detected = Array.isArray(detectedTasks) ? detectedTasks : []; const merged = []; const persistedByDetectionKey = {}; persisted.forEach(function (entry) { if (!isRecord(entry)) { return; } const task = Object.assign({}, entry); task.status = normalizeOperationStatus(task.status); if (cleanString(task.detectionKey)) { persistedByDetectionKey[cleanString(task.detectionKey)] = task; } merged.push(task); }); detected.forEach(function (entry) { if (!isRecord(entry)) { return; } const detectionKey = cleanString(entry.detectionKey); if (!detectionKey) { return; } if ( Object.prototype.hasOwnProperty.call( persistedByDetectionKey, detectionKey ) ) { const persistedTask = persistedByDetectionKey[detectionKey]; persistedTask.title = cleanString(persistedTask.title) || cleanString(entry.title); persistedTask.taskType = cleanString(persistedTask.taskType) || cleanString(entry.taskType); persistedTask.nextcloudUserId = cleanString(persistedTask.nextcloudUserId) || cleanString(entry.nextcloudUserId); persistedTask.paymenterCustomerId = cleanString(persistedTask.paymenterCustomerId) || cleanString(entry.paymenterCustomerId); persistedTask.userLabel = cleanString(persistedTask.userLabel) || cleanString(entry.userLabel); persistedTask.orderRef = cleanString(persistedTask.orderRef) || cleanString(entry.orderRef); persistedTask.productCode = cleanString(persistedTask.productCode) || cleanString(entry.productCode); persistedTask.quantity = cleanString(persistedTask.quantity) || cleanString(entry.quantity); if ( !cleanString(persistedTask.createdAt) && cleanString(entry.createdAt) ) { persistedTask.createdAt = cleanString(entry.createdAt); } if ( !cleanString(persistedTask.updatedAt) && cleanString(entry.updatedAt) ) { persistedTask.updatedAt = cleanString(entry.updatedAt); } if ( (!persistedTask.meta || !Object.keys(persistedTask.meta).length) && isRecord(entry.meta) ) { persistedTask.meta = entry.meta; } return; } const detectedTask = Object.assign({}, entry); detectedTask.id = ""; detectedTask.status = normalizeOperationStatus(detectedTask.status); merged.push(detectedTask); }); merged.sort(operationTaskSortComparator); return merged; } function renderUserCommerceSummary(rows) { const list = Array.isArray(rows) ? rows : []; const usersWithItems = list.filter(function (row) { return coerceFiniteNumber(row.totalItems) > 0; }).length; const linkedUsers = list.filter(function (row) { return cleanString(row && row.paymenterCustomerId); }).length; let pending = 0; let inProgress = 0; let blocked = 0; let completed = 0; list.forEach(function (row) { pending += coerceFiniteNumber(row.pendingCount) || 0; inProgress += coerceFiniteNumber(row.inProgressCount) || 0; blocked += coerceFiniteNumber(row.blockedCount) || 0; completed += coerceFiniteNumber(row.completedCount) || 0; }); renderSummaryItems( userOrdersSummaryEl, [ { label: "Known Users", value: String(list.length) }, { label: "Users With Order Data", value: String(usersWithItems) }, { label: "Paymenter Linked Users", value: String(linkedUsers) }, { label: "Paymenter Unlinked Users", value: String(Math.max(0, list.length - linkedUsers)), }, { label: "Pending Items", value: String(pending) }, { label: "In Progress Items", value: String(inProgress) }, { label: "Blocked Items", value: String(blocked) }, { label: "Completed Items", value: String(completed) }, ], "No reconciliation data available for this instance." ); } function renderUserCommerceList(rows) { if (!userOrdersListEl) { return; } userOrdersListEl.innerHTML = ""; const list = Array.isArray(rows) ? rows : []; if (!list.length) { userOrdersListEl.textContent = "No instance users returned."; return; } list.forEach(function (row) { if (!isRecord(row)) { return; } const card = document.createElement("article"); card.className = "chd-ops-item"; const main = document.createElement("div"); main.className = "chd-ops-item-main"; const title = document.createElement("div"); title.className = "chd-ops-title"; title.textContent = firstNonEmptyString( [row.displayName, row.nextcloudUserId], "Unknown user" ); main.appendChild(title); const identityParts = []; if (cleanString(row.nextcloudUserId)) { identityParts.push("Nextcloud: " + cleanString(row.nextcloudUserId)); } if (cleanString(row.paymenterCustomerId)) { identityParts.push( "Paymenter: #" + cleanString(row.paymenterCustomerId) ); } if (cleanString(row.email)) { identityParts.push("Email: " + cleanString(row.email)); } if (cleanString(row.qortalAddress)) { identityParts.push("Qortal: " + cleanString(row.qortalAddress)); } if (identityParts.length > 0) { const identity = document.createElement("div"); identity.className = "chd-ops-meta"; identity.textContent = identityParts.join(" | "); main.appendChild(identity); } const totalItems = coerceFiniteNumber(row.totalItems) || 0; const countsMeta = document.createElement("div"); countsMeta.className = "chd-ops-meta"; countsMeta.textContent = "Items: " + String(totalItems) + " | Pending: " + String(coerceFiniteNumber(row.pendingCount) || 0) + " | In Progress: " + String(coerceFiniteNumber(row.inProgressCount) || 0) + " | Blocked: " + String(coerceFiniteNumber(row.blockedCount) || 0) + " | Completed: " + String(coerceFiniteNumber(row.completedCount) || 0); main.appendChild(countsMeta); const recentCodes = Array.isArray(row.recentCodes) ? row.recentCodes.filter(Boolean) : []; if (recentCodes.length > 0) { const recent = document.createElement("div"); recent.className = "chd-ops-meta"; recent.textContent = "Recent Codes: " + recentCodes.join(", "); main.appendChild(recent); } card.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-ops-actions"; let level = "partial"; let label = "No order data"; if (totalItems > 0) { label = "Order data present"; level = "good"; } if ((coerceFiniteNumber(row.blockedCount) || 0) > 0) { label = "Blocked items present"; level = "issues"; } actions.appendChild(buildStatusPill("Data", label, level)); actions.appendChild( buildStatusPill( "Paymenter", cleanString(row.paymenterCustomerId) ? "Linked" : "Unlinked", cleanString(row.paymenterCustomerId) ? "good" : "partial" ) ); card.appendChild(actions); userOrdersListEl.appendChild(card); }); } function renderUserCommerceRows(rows) { renderUserCommerceSummary(rows); renderUserCommerceList(rows); } function normalizeIdentityIndexKey(entry) { const record = isRecord(entry) ? entry : {}; const qortalAddress = cleanString(record.qortalAddress).toLowerCase(); if (qortalAddress) { return "qortal:" + qortalAddress; } const paymenterCustomerId = cleanString( record.paymenterCustomerId ).toLowerCase(); if (paymenterCustomerId) { return "paymenter:" + paymenterCustomerId; } const email = cleanString(record.email).toLowerCase(); if (email) { return "email:" + email; } const nextcloudUserId = cleanString(record.nextcloudUserId).toLowerCase(); if (nextcloudUserId) { return "nextcloud:" + nextcloudUserId; } const displayName = cleanString(record.displayName).toLowerCase(); if (displayName) { return "display:" + displayName; } return ""; } function identityIndexMatchesPaymenterClient(row, client) { const rowPaymenterCustomerId = cleanString(row && row.paymenterCustomerId).toLowerCase(); const clientPaymenterCustomerId = cleanString( (client && client.paymenterCustomerId) || (client && client.customerId) || (client && client.id) ).toLowerCase(); if ( rowPaymenterCustomerId && clientPaymenterCustomerId && rowPaymenterCustomerId === clientPaymenterCustomerId ) { return true; } const rowEmail = cleanString(row && row.email).toLowerCase(); const clientEmail = cleanString(client && client.email).toLowerCase(); if (rowEmail && clientEmail && rowEmail === clientEmail) { return true; } const rowNextcloudUserId = cleanString(row && row.nextcloudUserId).toLowerCase(); const clientUsername = cleanString(client && client.username).toLowerCase(); if (rowNextcloudUserId && clientUsername && rowNextcloudUserId === clientUsername) { return true; } return false; } function upsertIdentityIndexEntry(rowsByKey, entry, instanceId) { if (!isRecord(rowsByKey)) { return null; } const record = isRecord(entry) ? entry : {}; const identityKey = normalizeIdentityIndexKey(record); if (!identityKey) { return null; } const instanceLabel = cleanString(instanceId); let row = Object.prototype.hasOwnProperty.call(rowsByKey, identityKey) ? rowsByKey[identityKey] : null; const displayName = firstNonEmptyString( [ record.displayName, record.nextcloudUserId, record.email, record.qortalAddress, record.paymenterCustomerId, ], "Unknown identity" ); if (!row) { row = { identityKey: identityKey, displayName: displayName, nextcloudUserId: cleanString(record.nextcloudUserId), email: cleanString(record.email), qortalAddress: cleanString(record.qortalAddress), paymenterCustomerId: cleanString(record.paymenterCustomerId), instanceLabels: [], instanceLabelsByKey: {}, instanceCount: 0, }; rowsByKey[identityKey] = row; } else { row.displayName = firstNonEmptyString( [row.displayName, displayName], displayName ); row.nextcloudUserId = firstNonEmptyString( [row.nextcloudUserId, record.nextcloudUserId], "" ); row.email = firstNonEmptyString([row.email, record.email], ""); row.qortalAddress = firstNonEmptyString( [row.qortalAddress, record.qortalAddress], "" ); row.paymenterCustomerId = firstNonEmptyString( [row.paymenterCustomerId, record.paymenterCustomerId], "" ); } if (instanceLabel && !row.instanceLabelsByKey[instanceLabel]) { row.instanceLabelsByKey[instanceLabel] = true; row.instanceLabels.push(instanceLabel); } row.instanceCount = row.instanceLabels.length; return row; } function renderIdentityIndex(rows) { const list = Array.isArray(rows) ? rows : []; const paymenterClients = Array.isArray(paymenterClientsCache) ? paymenterClientsCache : []; const linkedIdentityCount = list.filter(function (row) { return paymenterClients.some(function (client) { return identityIndexMatchesPaymenterClient(row, client); }); }).length; const multiInstanceCount = list.filter(function (row) { return coerceFiniteNumber(row && row.instanceCount) > 1; }).length; const singleInstanceCount = list.filter(function (row) { return coerceFiniteNumber(row && row.instanceCount) === 1; }).length; const matchedPaymenterCount = paymenterClients.filter(function (client) { return list.some(function (row) { return identityIndexMatchesPaymenterClient(row, client); }); }).length; const unmatchedPaymenterCount = Math.max( 0, paymenterClients.length - matchedPaymenterCount ); renderSummaryItems( identityIndexSummaryEl, [ { label: "Identities", value: String(list.length) }, { label: "Single Instance", value: String(singleInstanceCount) }, { label: "Multi Instance", value: String(multiInstanceCount) }, { label: "Linked Identities", value: String(linkedIdentityCount) }, { label: "Paymenter Customers", value: String(paymenterClients.length), }, { label: "Paymenter Matched", value: String(matchedPaymenterCount), }, { label: "Paymenter Unmatched", value: String(unmatchedPaymenterCount), }, ], "Refresh instances to build the identity index." ); if (!identityIndexListEl) { return; } identityIndexListEl.innerHTML = ""; if (!list.length) { identityIndexListEl.textContent = "Refresh instances to build the identity index."; return; } list.forEach(function (row) { if (!isRecord(row)) { return; } const entry = row; const card = document.createElement("article"); card.className = "chd-ops-item"; if (coerceFiniteNumber(entry.instanceCount) > 1) { card.classList.add("chd-ops-item--attention"); } const main = document.createElement("div"); main.className = "chd-ops-item-main"; const title = document.createElement("div"); title.className = "chd-ops-title"; title.textContent = firstNonEmptyString( [ entry.displayName, entry.nextcloudUserId, entry.email, entry.qortalAddress, ], "Unknown identity" ); main.appendChild(title); const metaParts = []; if (cleanString(entry.nextcloudUserId)) { metaParts.push("Nextcloud: " + cleanString(entry.nextcloudUserId)); } if (cleanString(entry.email)) { metaParts.push("Email: " + cleanString(entry.email)); } if (cleanString(entry.qortalAddress)) { metaParts.push("Qortal: " + cleanString(entry.qortalAddress)); } if (cleanString(entry.paymenterCustomerId)) { metaParts.push( "Paymenter: #" + cleanString(entry.paymenterCustomerId) ); } const instanceLabels = Array.isArray(entry.instanceLabels) ? entry.instanceLabels.filter(Boolean) : []; if (instanceLabels.length) { metaParts.push("Instances: " + instanceLabels.join(", ")); } if (metaParts.length) { const meta = document.createElement("div"); meta.className = "chd-ops-meta"; meta.textContent = metaParts.join(" | "); main.appendChild(meta); } card.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-ops-actions"; actions.appendChild( buildStatusPill( "Scope", coerceFiniteNumber(entry.instanceCount) > 1 ? "Multiple Instances" : "Single Instance", coerceFiniteNumber(entry.instanceCount) > 1 ? "partial" : "good" ) ); actions.appendChild( buildStatusPill( "Paymenter", paymenterClients.some(function (client) { return identityIndexMatchesPaymenterClient(entry, client); }) ? "Linked" : "Unlinked", paymenterClients.some(function (client) { return identityIndexMatchesPaymenterClient(entry, client); }) ? "good" : "partial" ) ); card.appendChild(actions); identityIndexListEl.appendChild(card); }); } function setPaymenterStatus(message) { if (paymenterStatusEl) { paymenterStatusEl.textContent = cleanString(message) || "Ready."; } } function renderPaymenterClients(data) { const source = isRecord(data) ? data : {}; const users = Array.isArray(source.users) ? source.users : []; paymenterClientsCache = users.slice(); const paging = firstObject([source.paging]); const runtime = firstObject([source.runtime]); const matchedCount = users.filter(function (entry) { return cleanString(entry && entry.matchedInstanceId); }).length; renderSummaryItems( paymenterClientsSummaryEl, [ { label: "Customers Loaded", value: String(users.length) }, { label: "Matched To Instances", value: String(matchedCount) }, { label: "Effective Backend", value: cleanString(runtime.effectivePaymenterProfile) || "connector default", }, { label: "Base URL", value: cleanString(runtime.effectivePaymenterBaseUrl) || "Not set", wide: true, }, { label: "Total Available", value: String(coerceFiniteNumber(paging.total) || users.length), }, ], "No Paymenter customer data loaded." ); if (!paymenterClientsListEl) { renderIdentityIndex(connectorIdentityIndexCache); return; } paymenterClientsListEl.innerHTML = ""; if (!users.length) { paymenterClientsListEl.textContent = "No Paymenter customers returned."; renderIdentityIndex(connectorIdentityIndexCache); return; } users.forEach(function (entry) { if (!isRecord(entry)) { return; } const row = document.createElement("article"); row.className = "chd-ops-item"; const main = document.createElement("div"); main.className = "chd-ops-item-main"; const title = document.createElement("div"); title.className = "chd-ops-title"; title.textContent = firstNonEmptyString( [entry.displayName, entry.email, entry.username], "Paymenter customer" ); main.appendChild(title); const meta = document.createElement("div"); meta.className = "chd-ops-meta"; meta.textContent = [ cleanString(entry.paymenterCustomerId) ? "Paymenter #" + cleanString(entry.paymenterCustomerId) : "", cleanString(entry.email) ? "Email: " + cleanString(entry.email) : "", cleanString(entry.username) ? "Username: " + cleanString(entry.username) : "", cleanString(entry.customerExternalId) ? "External ID: " + cleanString(entry.customerExternalId) : "", ] .filter(Boolean) .join(" | "); if (meta.textContent) { main.appendChild(meta); } const matched = document.createElement("div"); matched.className = "chd-ops-meta"; matched.textContent = cleanString(entry.matchedInstanceId) ? "Matched instance: " + cleanString(entry.matchedInstanceId) + (cleanString(entry.matchedBy) ? " via " + cleanString(entry.matchedBy) : "") : "No connector instance match detected."; main.appendChild(matched); row.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-ops-actions"; actions.appendChild( buildStatusPill( "Match", cleanString(entry.matchedInstanceId) ? "Connected" : "Unmatched", cleanString(entry.matchedInstanceId) ? "good" : "partial" ) ); row.appendChild(actions); paymenterClientsListEl.appendChild(row); }); renderIdentityIndex(connectorIdentityIndexCache); } function summarizeBillingHealth(summarySource, fallbackCounts) { const summary = firstObject([summarySource]); const fallback = isRecord(fallbackCounts) ? fallbackCounts : {}; const totalServices = coerceFiniteNumber( summary.totalServices ?? summary.services ?? fallback.services ); const activeServices = coerceFiniteNumber(summary.activeServices) || 0; const inactiveServices = coerceFiniteNumber( summary.inactiveServices ?? Math.max(0, (totalServices || 0) - activeServices) ) || 0; const suspendedServices = coerceFiniteNumber(summary.suspendedServices) || 0; const cancelledServices = coerceFiniteNumber(summary.cancelledServices) || 0; const pastDueServices = coerceFiniteNumber(summary.pastDueServices) || 0; const pendingServices = coerceFiniteNumber(summary.pendingServices) || 0; const unknownServices = coerceFiniteNumber(summary.unknownServices) || 0; const totalInvoices = coerceFiniteNumber( summary.totalInvoices ?? summary.invoices ?? fallback.invoices ); const paidInvoices = coerceFiniteNumber(summary.paidInvoices) || 0; const outstandingInvoices = coerceFiniteNumber(summary.outstandingInvoices) || 0; const overdueInvoices = coerceFiniteNumber(summary.overdueInvoices) || 0; const partialInvoices = coerceFiniteNumber(summary.partialInvoices) || 0; const activePackageTiers = firstArray([summary.activePackageTiers]) .map(function (entry) { return cleanString(entry); }) .filter(Boolean); const activeServiceLabels = firstArray([summary.activeServiceLabels]) .map(function (entry) { return cleanString(entry); }) .filter(Boolean); const activePackageTier = firstNonEmptyString([ summary.activePackageTier, activePackageTiers[0], ]); const activeServiceLabel = firstNonEmptyString([ summary.activeServiceLabel, activeServiceLabels[0], ]); const serviceHealthLevel = (() => { if (totalServices && !activeServices) { return "issues"; } if (suspendedServices > 0 || cancelledServices > 0 || pastDueServices > 0) { return "issues"; } if (pendingServices > 0 || unknownServices > 0) { return activeServices > 0 ? "partial" : "issues"; } if (totalServices === null || totalServices === undefined) { return activeServices > 0 ? "good" : "partial"; } return activeServices > 0 ? "good" : "partial"; })(); const invoiceHealthLevel = (() => { if (overdueInvoices > 0) { return "issues"; } if (outstandingInvoices > 0 || partialInvoices > 0) { return "partial"; } return totalInvoices > 0 ? "good" : "partial"; })(); const billingHealthLevel = summary.level ? normalizeStatusLevel(summary.level) : totalServices === 0 && totalInvoices === 0 ? "partial" : combineStatusLevel(serviceHealthLevel, invoiceHealthLevel); const billingHealthLabel = firstNonEmptyString([ summary.label, levelTitle(billingHealthLevel), ]); const serviceHealthLabel = (() => { if (serviceHealthLevel === "good") { return "Healthy"; } if (serviceHealthLevel === "issues") { return activeServices > 0 ? "Attention needed" : "No active service"; } return "Needs review"; })(); const invoiceHealthLabel = (() => { if (invoiceHealthLevel === "good") { return "Current"; } if (invoiceHealthLevel === "issues") { return "Overdue"; } return "Outstanding"; })(); const alertCount = coerceFiniteNumber( summary.billingAlertCount ?? summary.alertCount ?? (Array.isArray(summary.alerts) ? summary.alerts.length : 0) ) || 0; const attentionParts = []; if (activeServiceLabel) { attentionParts.push("Active: " + activeServiceLabel); } else if (activePackageTier) { attentionParts.push("Package: " + activePackageTier); } if (suspendedServices > 0) { attentionParts.push("Suspended: " + String(suspendedServices)); } if (cancelledServices > 0) { attentionParts.push("Cancelled: " + String(cancelledServices)); } if (overdueInvoices > 0) { attentionParts.push("Overdue invoices: " + String(overdueInvoices)); } else if (outstandingInvoices > 0) { attentionParts.push("Outstanding invoices: " + String(outstandingInvoices)); } return { totalServices: totalServices || 0, activeServices: activeServices, inactiveServices: inactiveServices, suspendedServices: suspendedServices, cancelledServices: cancelledServices, pastDueServices: pastDueServices, pendingServices: pendingServices, unknownServices: unknownServices, totalInvoices: totalInvoices || 0, paidInvoices: paidInvoices, outstandingInvoices: outstandingInvoices, overdueInvoices: overdueInvoices, partialInvoices: partialInvoices, activePackageTier: activePackageTier, activePackageTiers: activePackageTiers, activeServiceLabel: activeServiceLabel, activeServiceLabels: activeServiceLabels, serviceHealthLevel: serviceHealthLevel, invoiceHealthLevel: invoiceHealthLevel, billingHealthLevel: billingHealthLevel, billingHealthLabel: billingHealthLabel, serviceHealthLabel: serviceHealthLabel, invoiceHealthLabel: invoiceHealthLabel, alertCount: alertCount, attentionText: attentionParts.join(" | "), }; } function countSnapshotCollection(value) { if (Array.isArray(value)) { return value.length; } const source = isRecord(value) ? value : {}; const candidates = [ source.data, source.items, source.records, source.entries, ]; for (let index = 0; index < candidates.length; index += 1) { if (Array.isArray(candidates[index])) { return candidates[index].length; } } if (isRecord(source.data)) { return 1; } if (isRecord(source)) { return Object.keys(source).length > 0 ? 1 : 0; } return 0; } function summarizePaymenterSnapshotPayload(data) { const source = isRecord(data) ? data : {}; const runtime = firstObject([source.runtime]); const request = firstObject([source.request]); const snapshot = firstObject([source.snapshot]); const snapshotData = firstObject([snapshot.data]); const user = firstObject([snapshotData.user]); const billing = summarizeBillingHealth(snapshotData.billingSummary, { services: countSnapshotCollection(snapshotData.services), invoices: countSnapshotCollection(snapshotData.invoices), }); const summaryItems = [ { label: "Requested Instance", value: cleanString(request.instanceId) || cleanString(activeInstanceId) || "Not set", }, { label: "Requested Email", value: cleanString(request.requestedEmail) || "Not set", }, { label: "Resolved Customer ID", value: firstNonEmptyString( [user.id, user.customer_id, user.user_id], "Not resolved" ), }, { label: "Paymenter Email", value: firstNonEmptyString([user.email], "Not set"), }, { label: "Display Name", value: firstNonEmptyString( [user.name, user.full_name, user.company_name], "Not set" ), }, { label: "Active Package", value: firstNonEmptyString( [billing.activePackageTier, billing.activeServiceLabel], "Not set" ), }, { label: "Active Services", value: String(billing.activeServices), }, { label: "Suspended Services", value: String(billing.suspendedServices), }, { label: "Cancelled Services", value: String(billing.cancelledServices), }, { label: "Outstanding Invoices", value: String(billing.outstandingInvoices), }, { label: "Overdue Invoices", value: String(billing.overdueInvoices), }, { label: "Billing Health", value: billing.billingHealthLabel, }, { label: "Attention", value: billing.attentionText || "None", wide: true, }, { label: "Services", value: String(countSnapshotCollection(snapshotData.services)), }, { label: "Orders", value: String(countSnapshotCollection(snapshotData.orders)), }, { label: "Invoices", value: String(countSnapshotCollection(snapshotData.invoices)), }, { label: "Invoice Items", value: String(countSnapshotCollection(snapshotData.invoiceItems)), }, { label: "Credits", value: String(countSnapshotCollection(snapshotData.credits)), }, { label: "Properties", value: String(countSnapshotCollection(snapshotData.properties)), }, { label: "Effective Backend", value: cleanString(runtime.effectivePaymenterProfile) || "connector default", }, { label: "Base URL", value: cleanString(runtime.effectivePaymenterBaseUrl) || "Not set", wide: true, }, ]; const warnings = Array.isArray(snapshot.warnings) ? snapshot.warnings : []; const statusItems = [ { label: "Snapshot", value: cleanString(user.id || user.customer_id || user.user_id) ? "Resolved" : "No customer resolved", level: cleanString(user.id || user.customer_id || user.user_id) ? "good" : "partial", }, { label: "Billing Health", value: billing.billingHealthLabel, level: billing.billingHealthLevel, }, { label: "Service Health", value: billing.serviceHealthLabel, level: billing.serviceHealthLevel, }, { label: "Invoice Health", value: billing.invoiceHealthLabel, level: billing.invoiceHealthLevel, }, { label: "Warnings", value: warnings.length ? String(warnings.length) : "None", level: warnings.length ? "partial" : "good", }, ]; return { summaryItems: summaryItems, statusItems: statusItems, focusedData: { request: request, runtime: runtime, user: user, billing: billing, counts: { services: countSnapshotCollection(snapshotData.services), orders: countSnapshotCollection(snapshotData.orders), invoices: countSnapshotCollection(snapshotData.invoices), invoiceItems: countSnapshotCollection(snapshotData.invoiceItems), credits: countSnapshotCollection(snapshotData.credits), properties: countSnapshotCollection(snapshotData.properties), }, warnings: warnings, }, }; } function renderPaymenterBackups(data) { const source = isRecord(data) ? data : {}; const backups = Array.isArray(source.backups) ? source.backups : []; const paging = firstObject([source.paging]); renderSummaryItems( paymenterBackupsSummaryEl, [ { label: "Backups Loaded", value: String(backups.length) }, { label: "Total Available", value: String(coerceFiniteNumber(paging.total) || backups.length), }, { label: "Selected Instance Scope", value: cleanString(activeInstanceId) || "All instances", }, ], "No connector backup history available." ); if (!paymenterBackupsListEl) { return; } paymenterBackupsListEl.innerHTML = ""; if (!backups.length) { paymenterBackupsListEl.textContent = "No connector backup history returned."; return; } backups.forEach(function (entry) { if (!isRecord(entry)) { return; } const row = document.createElement("article"); row.className = "chd-ops-item"; const main = document.createElement("div"); main.className = "chd-ops-item-main"; const title = document.createElement("div"); title.className = "chd-ops-title"; title.textContent = firstNonEmptyString( [entry.instanceId, entry.filename], "Backup" ); main.appendChild(title); const meta = document.createElement("div"); meta.className = "chd-ops-meta"; meta.textContent = [ cleanString(entry.action) ? "Action: " + cleanString(entry.action) : "", cleanString(entry.performedAt) ? "At: " + formatDateTime(entry.performedAt) : "", cleanString(entry.effectivePaymenterProfile) ? "Backend: " + cleanString(entry.effectivePaymenterProfile) : "", cleanString(entry.matchSource) ? "Match: " + cleanString(entry.matchSource) : "", ] .filter(Boolean) .join(" | "); if (meta.textContent) { main.appendChild(meta); } const notices = Array.isArray(entry.notices) ? entry.notices.filter(Boolean) : []; if (notices.length) { const note = document.createElement("div"); note.className = "chd-ops-meta"; note.textContent = notices.join(" | "); main.appendChild(note); } row.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-ops-actions"; actions.appendChild( buildStatusPill( "Backup", cleanString(entry.action) || "Snapshot", "good" ) ); row.appendChild(actions); paymenterBackupsListEl.appendChild(row); }); } function renderPaymenterOrders(data) { const source = isRecord(data) ? data : {}; const orders = Array.isArray(source.orders) ? source.orders : []; const paging = firstObject([source.paging]); const filters = firstObject([source.filters]); const runtime = firstObject([source.runtime]); renderSummaryItems( paymenterOrdersSummaryEl, [ { label: "Orders Loaded", value: String(orders.length) }, { label: "Total Available", value: String(coerceFiniteNumber(paging.total) || orders.length), }, { label: "Selected Scope", value: cleanString(filters.customerId) || cleanString(filters.instanceId) || cleanString(activeInstanceId) || "All instances", }, { label: "Effective Backend", value: cleanString(runtime.effectivePaymenterProfile) || "connector default", }, ], "No Paymenter orders returned." ); if (!paymenterOrdersListEl) { return; } paymenterOrdersListEl.innerHTML = ""; if (!orders.length) { paymenterOrdersListEl.textContent = "No Paymenter orders returned."; return; } orders.forEach(function (entry) { if (!isRecord(entry)) { return; } const row = document.createElement("article"); row.className = "chd-ops-item"; const main = document.createElement("div"); main.className = "chd-ops-item-main"; const title = document.createElement("div"); title.className = "chd-ops-title"; title.textContent = firstNonEmptyString( [ entry.orderId ? "Order #" + cleanString(entry.orderId) : "", entry.customerName, entry.customerEmail, ], "Paymenter order" ); main.appendChild(title); const meta = document.createElement("div"); meta.className = "chd-ops-meta"; meta.textContent = [ cleanString(entry.status) ? "Status: " + cleanString(entry.status) : "", cleanString(entry.paymentStatus) ? "Payment: " + cleanString(entry.paymentStatus) : "", cleanString(entry.total) ? "Total: " + cleanString(entry.total) + (cleanString(entry.currency) ? " " + cleanString(entry.currency) : "") : "", cleanString(entry.createdAt) ? "Created: " + formatDateTime(entry.createdAt) : "", cleanString(entry.customerEmail) ? "Email: " + cleanString(entry.customerEmail) : "", cleanString(entry.paymenterCustomerId) ? "Paymenter #" + cleanString(entry.paymenterCustomerId) : "", ] .filter(Boolean) .join(" | "); if (meta.textContent) { main.appendChild(meta); } const relation = document.createElement("div"); relation.className = "chd-ops-meta"; relation.textContent = [ cleanString(entry.customerName) ? "Customer: " + cleanString(entry.customerName) : "", cleanString(entry.instanceId) ? "Instance: " + cleanString(entry.instanceId) : "", ] .filter(Boolean) .join(" | "); if (relation.textContent) { main.appendChild(relation); } row.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-ops-actions"; const statusValue = cleanString(entry.status) || "Order"; const normalizedStatus = statusValue.toLowerCase(); const hasWord = function (token) { return new RegExp("(^|[^a-z])" + token + "([^a-z]|$)").test( normalizedStatus ); }; let statusLevel = "partial"; if ( normalizedStatus.includes("cancel") || normalizedStatus.includes("suspend") || normalizedStatus.includes("past due") || normalizedStatus.includes("past_due") || normalizedStatus.includes("overdue") || normalizedStatus.includes("failed") ) { statusLevel = "issues"; } else if ( (normalizedStatus.includes("paid") && !normalizedStatus.includes("unpaid") && !normalizedStatus.includes("partial")) || (normalizedStatus.includes("complete") && !normalizedStatus.includes("incomplete")) || hasWord("active") ) { statusLevel = "good"; } actions.appendChild(buildStatusPill("Order", statusValue, statusLevel)); row.appendChild(actions); paymenterOrdersListEl.appendChild(row); }); } function renderPaymenterInvoices(data) { const source = isRecord(data) ? data : {}; const invoices = Array.isArray(source.invoices) ? source.invoices : []; const paging = firstObject([source.paging]); const filters = firstObject([source.filters]); const runtime = firstObject([source.runtime]); renderSummaryItems( paymenterInvoicesSummaryEl, [ { label: "Invoices Loaded", value: String(invoices.length) }, { label: "Total Available", value: String(coerceFiniteNumber(paging.total) || invoices.length), }, { label: "Selected Scope", value: cleanString(filters.customerId) || cleanString(filters.instanceId) || cleanString(activeInstanceId) || "All instances", }, { label: "Effective Backend", value: cleanString(runtime.effectivePaymenterProfile) || "connector default", }, ], "No Paymenter invoices returned." ); if (!paymenterInvoicesListEl) { return; } paymenterInvoicesListEl.innerHTML = ""; if (!invoices.length) { paymenterInvoicesListEl.textContent = "No Paymenter invoices returned."; return; } invoices.forEach(function (entry) { if (!isRecord(entry)) { return; } const row = document.createElement("article"); row.className = "chd-ops-item"; const main = document.createElement("div"); main.className = "chd-ops-item-main"; const title = document.createElement("div"); title.className = "chd-ops-title"; title.textContent = firstNonEmptyString( [ entry.invoiceId ? "Invoice #" + cleanString(entry.invoiceId) : "", entry.customerName, entry.customerEmail, ], "Paymenter invoice" ); main.appendChild(title); const meta = document.createElement("div"); meta.className = "chd-ops-meta"; meta.textContent = [ cleanString(entry.status) ? "Status: " + cleanString(entry.status) : "", cleanString(entry.dueAt) ? "Due: " + formatDateTime(entry.dueAt) : "", cleanString(entry.createdAt) ? "Created: " + formatDateTime(entry.createdAt) : "", cleanString(entry.currency) ? "Currency: " + cleanString(entry.currency) : "", cleanString(entry.paymenterCustomerId) ? "Paymenter #" + cleanString(entry.paymenterCustomerId) : "", cleanString(entry.itemCount) ? "Items: " + cleanString(entry.itemCount) : "", ] .filter(Boolean) .join(" | "); if (meta.textContent) { main.appendChild(meta); } const relation = document.createElement("div"); relation.className = "chd-ops-meta"; relation.textContent = [ cleanString(entry.customerName) ? "Customer: " + cleanString(entry.customerName) : "", cleanString(entry.customerEmail) ? "Email: " + cleanString(entry.customerEmail) : "", cleanString(entry.instanceId) ? "Instance: " + cleanString(entry.instanceId) : "", ] .filter(Boolean) .join(" | "); if (relation.textContent) { main.appendChild(relation); } row.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-ops-actions"; const statusValue = cleanString(entry.status) || "Invoice"; const normalizedStatus = statusValue.toLowerCase(); let statusLevel = "partial"; if ( normalizedStatus.includes("paid") && !normalizedStatus.includes("unpaid") && !normalizedStatus.includes("partial") && !normalizedStatus.includes("past due") && !normalizedStatus.includes("past_due") ) { statusLevel = "good"; } else if ( normalizedStatus.includes("cancel") || normalizedStatus.includes("void") || normalizedStatus.includes("past due") || normalizedStatus.includes("past_due") || normalizedStatus.includes("overdue") || normalizedStatus.includes("unpaid") || normalizedStatus.includes("failed") ) { statusLevel = "issues"; } actions.appendChild(buildStatusPill("Invoice", statusValue, statusLevel)); row.appendChild(actions); paymenterInvoicesListEl.appendChild(row); }); } function renderPaymenterInvoiceItems(data) { const source = isRecord(data) ? data : {}; const invoiceItems = Array.isArray(source.invoiceItems) ? source.invoiceItems : []; const paging = firstObject([source.paging]); const filters = firstObject([source.filters]); const runtime = firstObject([source.runtime]); renderSummaryItems( paymenterInvoiceItemsSummaryEl, [ { label: "Items Loaded", value: String(invoiceItems.length) }, { label: "Total Available", value: String( coerceFiniteNumber(paging.total) || invoiceItems.length ), }, { label: "Selected Scope", value: cleanString(filters.referenceType) || cleanString(filters.referenceId) || cleanString(activeInstanceId) || "All invoice items", }, { label: "Effective Backend", value: cleanString(runtime.effectivePaymenterProfile) || "connector default", }, ], "No Paymenter invoice items returned." ); if (!paymenterInvoiceItemsListEl) { return; } paymenterInvoiceItemsListEl.innerHTML = ""; if (!invoiceItems.length) { paymenterInvoiceItemsListEl.textContent = "No Paymenter invoice items returned."; return; } invoiceItems.forEach(function (entry) { if (!isRecord(entry)) { return; } const row = document.createElement("article"); row.className = "chd-ops-item"; const main = document.createElement("div"); main.className = "chd-ops-item-main"; const title = document.createElement("div"); title.className = "chd-ops-title"; title.textContent = firstNonEmptyString( [ entry.description, entry.invoiceItemId ? "Item #" + cleanString(entry.invoiceItemId) : "", entry.referenceType, ], "Invoice item" ); main.appendChild(title); const meta = document.createElement("div"); meta.className = "chd-ops-meta"; meta.textContent = [ cleanString(entry.quantity) ? "Qty: " + cleanString(entry.quantity) : "", cleanString(entry.price) ? "Price: " + cleanString(entry.price) : "", cleanString(entry.referenceType) ? "Reference: " + cleanString(entry.referenceType) + (cleanString(entry.referenceId) ? " #" + cleanString(entry.referenceId) : "") : "", cleanString(entry.createdAt) ? "Created: " + formatDateTime(entry.createdAt) : "", ] .filter(Boolean) .join(" | "); if (meta.textContent) { main.appendChild(meta); } row.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-ops-actions"; actions.appendChild( buildStatusPill( "Item", cleanString(entry.referenceType) || "Line item", "good" ) ); row.appendChild(actions); paymenterInvoiceItemsListEl.appendChild(row); }); } async function loadPaymenterClients() { if (!paymenterClientsSummaryEl || !paymenterClientsUrl) { return; } setPaymenterStatus("Loading Paymenter customers..."); renderSummaryItems( paymenterClientsSummaryEl, [], "Loading Paymenter customers..." ); if (paymenterClientsListEl) { paymenterClientsListEl.textContent = "Loading Paymenter customers..."; } const url = new URL(paymenterClientsUrl, window.location.origin); const profile = getSelectedPaymenterViewProfile(); if (profile) { url.searchParams.set("paymenterProfile", profile); } url.searchParams.set("limit", "100"); try { const payload = await requestJson(url.toString(), { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); renderPaymenterClients( payload && payload.data && typeof payload.data === "object" ? payload.data : {} ); setPaymenterStatus("Paymenter customers refreshed."); } catch (error) { const errorMessage = "Failed to load Paymenter customers: " + (error && error.message ? error.message : String(error)); renderSummaryItems(paymenterClientsSummaryEl, [], errorMessage); if (paymenterClientsListEl) { paymenterClientsListEl.textContent = errorMessage; } setPaymenterStatus(errorMessage); } } async function loadPaymenterOrders() { if (!paymenterOrdersSummaryEl || !paymenterOrdersUrl) { return; } setPaymenterStatus("Loading Paymenter orders..."); renderSummaryItems( paymenterOrdersSummaryEl, [], "Loading Paymenter orders..." ); if (paymenterOrdersListEl) { paymenterOrdersListEl.textContent = "Loading Paymenter orders..."; } const url = new URL(paymenterOrdersUrl, window.location.origin); const detailInstance = resolveDetailInstance( activeInstanceDetailData || {} ); const profile = getSelectedPaymenterViewProfile(); if (trimString(activeInstanceId)) { url.searchParams.set("instanceId", trimString(activeInstanceId)); } if (trimString(detailInstance && detailInstance.paymenterCustomerId)) { url.searchParams.set( "customerId", trimString(detailInstance.paymenterCustomerId) ); } if (profile) { url.searchParams.set("paymenterProfile", profile); } url.searchParams.set("limit", "100"); try { const payload = await requestJson(url.toString(), { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); renderPaymenterOrders( payload && payload.data && typeof payload.data === "object" ? payload.data : {} ); setPaymenterStatus("Paymenter orders refreshed."); } catch (error) { const rawErrorMessage = error && error.message ? error.message : String(error); const unavailableMessage = "Paymenter orders are unavailable from this backend. Invoices and service records are the better source of truth for ongoing billing."; const errorMessage = rawErrorMessage.indexOf("handleAdminPaymenterOrdersRequest is not defined") !== -1 || rawErrorMessage.indexOf("500 Internal Server Error") !== -1 ? unavailableMessage : "Failed to load Paymenter orders: " + rawErrorMessage; renderSummaryItems(paymenterOrdersSummaryEl, [], errorMessage); if (paymenterOrdersListEl) { paymenterOrdersListEl.textContent = errorMessage; } setPaymenterStatus(errorMessage); } } async function loadPaymenterSnapshotForActiveInstance() { const panel = infoPanels.paymenterSnapshot; if (!panel) { return; } if (!paymenterSnapshotUrl) { renderSummaryItems( paymenterSnapshotSummaryEl, [], "Paymenter snapshot endpoint is not configured." ); renderStatusPills(paymenterSnapshotStatusEl, []); setInfoPanelError( panel, "Paymenter snapshot endpoint is not configured." ); return; } const instanceId = trimString(activeInstanceId); if (!instanceId) { renderSummaryItems( paymenterSnapshotSummaryEl, [], "Select an instance to load Paymenter snapshot data." ); renderStatusPills(paymenterSnapshotStatusEl, []); setInfoPanelEmpty( panel, "Select an instance to load Paymenter snapshot data." ); return; } const detailInstance = resolveDetailInstance( activeInstanceDetailData || {} ); const url = new URL(paymenterSnapshotUrl, window.location.origin); url.searchParams.set("instanceId", instanceId); const customerId = trimString( detailInstance && detailInstance.paymenterCustomerId ); const requestedEmail = trimString( detailInstance && (detailInstance.mspContactEmail || detailInstance.adminEmail) ); const profile = getSelectedPaymenterViewProfile(); if (customerId) { url.searchParams.set("customerId", customerId); } if (requestedEmail) { url.searchParams.set("requestedEmail", requestedEmail); } if (profile) { url.searchParams.set("paymenterProfile", profile); } setInfoPanelLoading(panel, "Loading Paymenter snapshot..."); renderSummaryItems( paymenterSnapshotSummaryEl, [], "Loading Paymenter snapshot..." ); renderStatusPills(paymenterSnapshotStatusEl, []); try { const payload = await requestJson(url.toString(), { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; const presentation = summarizePaymenterSnapshotPayload(data); renderSummaryItems( paymenterSnapshotSummaryEl, presentation.summaryItems, "No Paymenter snapshot returned." ); renderStatusPills(paymenterSnapshotStatusEl, presentation.statusItems); setInfoPanelData(panel, data, presentation.focusedData); } catch (error) { const errorMessage = "Failed to load Paymenter snapshot: " + (error && error.message ? error.message : String(error)); renderSummaryItems(paymenterSnapshotSummaryEl, [], errorMessage); renderStatusPills(paymenterSnapshotStatusEl, []); setInfoPanelError(panel, errorMessage); } } async function loadPaymenterBackups() { const panel = infoPanels.paymenterBackups; if (!panel) { return; } if (!paymenterBackupsUrl) { renderSummaryItems( paymenterBackupsSummaryEl, [], "Paymenter backups endpoint is not configured." ); setInfoPanelError(panel, "Paymenter backups endpoint is not configured."); return; } const url = new URL(paymenterBackupsUrl, window.location.origin); if (trimString(activeInstanceId)) { url.searchParams.set("instanceId", trimString(activeInstanceId)); } url.searchParams.set("limit", "20"); setInfoPanelLoading(panel, "Loading connector backup history..."); renderSummaryItems( paymenterBackupsSummaryEl, [], "Loading connector backup history..." ); if (paymenterBackupsListEl) { paymenterBackupsListEl.textContent = "Loading connector backup history..."; } try { const payload = await requestJson(url.toString(), { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; renderPaymenterBackups(data); setInfoPanelData(panel, data, { backups: Array.isArray(data.backups) ? data.backups : [], paging: data.paging && typeof data.paging === "object" ? data.paging : {}, }); } catch (error) { const errorMessage = "Failed to load connector backup history: " + (error && error.message ? error.message : String(error)); renderSummaryItems(paymenterBackupsSummaryEl, [], errorMessage); if (paymenterBackupsListEl) { paymenterBackupsListEl.textContent = errorMessage; } setInfoPanelError(panel, errorMessage); } } async function loadPaymenterInvoices() { if (!paymenterInvoicesSummaryEl || !paymenterInvoicesUrl) { return; } setPaymenterStatus("Loading Paymenter invoices..."); renderSummaryItems( paymenterInvoicesSummaryEl, [], "Loading Paymenter invoices..." ); if (paymenterInvoicesListEl) { paymenterInvoicesListEl.textContent = "Loading Paymenter invoices..."; } const url = new URL(paymenterInvoicesUrl, window.location.origin); const detailInstance = resolveDetailInstance( activeInstanceDetailData || {} ); const profile = getSelectedPaymenterViewProfile(); if (trimString(activeInstanceId)) { url.searchParams.set("instanceId", trimString(activeInstanceId)); } if (trimString(detailInstance && detailInstance.paymenterCustomerId)) { url.searchParams.set( "customerId", trimString(detailInstance.paymenterCustomerId) ); } if (profile) { url.searchParams.set("paymenterProfile", profile); } url.searchParams.set("limit", "100"); try { const payload = await requestJson(url.toString(), { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); renderPaymenterInvoices( payload && payload.data && typeof payload.data === "object" ? payload.data : {} ); setPaymenterStatus("Paymenter invoices refreshed."); } catch (error) { const errorMessage = "Failed to load Paymenter invoices: " + (error && error.message ? error.message : String(error)); renderSummaryItems(paymenterInvoicesSummaryEl, [], errorMessage); if (paymenterInvoicesListEl) { paymenterInvoicesListEl.textContent = errorMessage; } setPaymenterStatus(errorMessage); } } async function loadPaymenterInvoiceItems() { if (!paymenterInvoiceItemsSummaryEl || !paymenterInvoiceItemsUrl) { return; } setPaymenterStatus("Loading Paymenter invoice items..."); renderSummaryItems( paymenterInvoiceItemsSummaryEl, [], "Loading Paymenter invoice items..." ); if (paymenterInvoiceItemsListEl) { paymenterInvoiceItemsListEl.textContent = "Loading Paymenter invoice items..."; } const url = new URL(paymenterInvoiceItemsUrl, window.location.origin); const profile = getSelectedPaymenterViewProfile(); if (profile) { url.searchParams.set("paymenterProfile", profile); } url.searchParams.set("limit", "100"); try { const payload = await requestJson(url.toString(), { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); renderPaymenterInvoiceItems( payload && payload.data && typeof payload.data === "object" ? payload.data : {} ); setPaymenterStatus("Paymenter invoice items refreshed."); } catch (error) { const errorMessage = "Failed to load Paymenter invoice items: " + (error && error.message ? error.message : String(error)); renderSummaryItems(paymenterInvoiceItemsSummaryEl, [], errorMessage); if (paymenterInvoiceItemsListEl) { paymenterInvoiceItemsListEl.textContent = errorMessage; } setPaymenterStatus(errorMessage); } } async function runPaymenterSourceSync(dryRun) { if (!paymenterSourceSyncUrl) { throw new Error("Paymenter source sync endpoint is not configured."); } const instanceId = trimString(activeInstanceId); const profile = getSelectedPaymenterViewProfile(); const scopeLabel = instanceId ? "instance " + instanceId : "all active connector instances"; setPaymenterStatus( (dryRun ? "Dry-running" : "Running") + " source-of-truth sync for " + scopeLabel + "..." ); const payload = {}; if (instanceId) { payload.instanceId = instanceId; } if (profile) { payload.paymenterProfile = profile; } if (dryRun) { payload.dryRun = true; } const response = await requestJson(paymenterSourceSyncUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify(payload), }); const data = response && response.data && typeof response.data === "object" ? response.data : {}; const changed = coerceFiniteNumber(data.changed) || 0; const failed = coerceFiniteNumber(data.failed) || 0; const processed = coerceFiniteNumber(data.processed) || 0; const summaryMessage = (dryRun ? "Dry run complete" : "Source-of-truth sync complete") + " | Processed: " + processed + " | Changed: " + changed + " | Failed: " + failed; setPaymenterStatus(summaryMessage); await loadConnectorInstances(); if (instanceId) { if (instanceSelectEl) { instanceSelectEl.value = instanceId; } await loadConnectorInstanceDetail(instanceId); } await loadPaymenterClients(); await loadPaymenterBackups(); return data; } function renderOperationsSummary(tasks) { if (!operationsSummaryEl) { return; } const list = Array.isArray(tasks) ? tasks : []; const counts = { pending: 0, in_progress: 0, blocked: 0, completed: 0, }; list.forEach(function (entry) { const status = normalizeOperationStatus( entry && entry.status ? entry.status : "" ); if (Object.prototype.hasOwnProperty.call(counts, status)) { counts[status] += 1; } }); const actionableCount = list.filter(operationRequiresAction).length; const introGrantCount = list.filter(function (entry) { return ( cleanString(entry && entry.taskType ? entry.taskType : "") === "intro_grant" && normalizeOperationStatus(entry && entry.status ? entry.status : "") !== "completed" ); }).length; const fulfillmentCount = list.filter(function (entry) { return ( cleanString(entry && entry.taskType ? entry.taskType : "") !== "intro_grant" && normalizeOperationStatus(entry && entry.status ? entry.status : "") === "pending" ); }).length; const items = [ { label: "Requires Action", value: String(actionableCount) }, { label: "Grant Requests", value: String(introGrantCount) }, { label: "Pending Fulfillment", value: String(fulfillmentCount) }, { label: "Pending", value: String(counts.pending) }, { label: "Blocked", value: String(counts.blocked) }, { label: "In Progress", value: String(counts.in_progress) }, { label: "Completed", value: String(counts.completed) }, { label: "Tasks", value: String(list.length) }, ]; renderSummaryItems(operationsSummaryEl, items, "No operation tasks."); } async function saveInstanceOperationTask(instanceId, task) { if (!saveInstanceOperationUrl) { throw new Error("Instance operation save endpoint is not configured."); } const payloadBody = { instanceId: String(instanceId || ""), task: buildOperationTaskPayload(task), }; const payload = await requestJson(saveInstanceOperationUrl, { method: "POST", headers: getRequestHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), credentials: "same-origin", body: buildLegacyPayloadBody(payloadBody), }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; return isRecord(data.task) ? data.task : null; } async function setInstanceOperationTaskStatus(instanceId, taskId, status) { if (!updateInstanceOperationStatusUrl) { throw new Error("Instance operation status endpoint is not configured."); } const payloadBody = { instanceId: String(instanceId || ""), taskId: String(taskId || ""), status: normalizeOperationStatus(status), }; const payload = await requestJson(updateInstanceOperationStatusUrl, { method: "POST", headers: getRequestHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), credentials: "same-origin", body: buildLegacyPayloadBody(payloadBody), }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; return isRecord(data.task) ? data.task : null; } async function runIntroGrantTaskAction(instanceId, task, action, options) { if (!introGrantTaskActionUrl) { throw new Error("Intro grant action endpoint is not configured."); } const extra = isRecord(options) ? options : {}; const payloadBody = { instanceId: String(instanceId || ""), task: buildOperationTaskPayload(task), action: String(action || ""), message: cleanString(extra.message), amount: extra.amount, sendFunds: extra.sendFunds ? 1 : 0, transfer: isRecord(extra.transfer) ? cloneJsonValue(extra.transfer, {}) : undefined, }; const payload = await requestJson(introGrantTaskActionUrl, { method: "POST", headers: getRequestHeaders( "application/x-www-form-urlencoded; charset=UTF-8" ), credentials: "same-origin", body: buildLegacyPayloadBody(payloadBody), }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; return { task: isRecord(data.task) ? data.task : null, connector: isRecord(data.connector) ? data.connector : {}, transfer: isRecord(data.transfer) ? data.transfer : {}, }; } async function ensurePersistedTask(instanceId, task) { if (!isRecord(task)) { throw new Error("Invalid operation task payload."); } const existingId = cleanString(task.id); if (existingId) { return task; } const persistedTask = await saveInstanceOperationTask(instanceId, task); if (!isRecord(persistedTask) || !cleanString(persistedTask.id)) { throw new Error("Failed to persist operation task."); } return persistedTask; } function renderOperationsList(tasks) { if (!operationsListEl) { return; } operationsListEl.innerHTML = ""; const list = Array.isArray(tasks) ? tasks : []; if (!list.length) { operationsListEl.textContent = "No operation tasks yet."; return; } list.forEach(function (task) { if (!isRecord(task)) { return; } const normalizedStatus = normalizeOperationStatus(task.status); const actionRequired = operationRequiresAction(task); const row = document.createElement("article"); row.className = "chd-ops-item"; if (actionRequired) { row.classList.add("chd-ops-item--attention"); } if (cleanString(task.taskType) === "intro_grant") { row.classList.add("chd-ops-item--grant"); } const main = document.createElement("div"); main.className = "chd-ops-item-main"; const title = document.createElement("div"); title.className = "chd-ops-title"; title.textContent = cleanString(task.title) || "Untitled operation"; main.appendChild(title); const metaParts = []; const source = cleanString(task.source) || "manual"; metaParts.push("Source: " + source); if (cleanString(task.taskType)) { metaParts.push("Type: " + cleanString(task.taskType)); } if (cleanString(task.userLabel)) { metaParts.push("User: " + cleanString(task.userLabel)); } else if (cleanString(task.nextcloudUserId)) { metaParts.push("User: " + cleanString(task.nextcloudUserId)); } if (cleanString(task.paymenterCustomerId)) { metaParts.push("Paymenter: #" + cleanString(task.paymenterCustomerId)); } if (cleanString(task.orderRef)) { metaParts.push("Order: " + cleanString(task.orderRef)); } if (cleanString(task.productCode)) { metaParts.push("Code: " + cleanString(task.productCode)); } const meta = document.createElement("div"); meta.className = "chd-ops-meta"; meta.textContent = metaParts.join(" | "); main.appendChild(meta); if (cleanString(task.notes)) { const notes = document.createElement("div"); notes.className = "chd-ops-meta"; notes.textContent = "Notes: " + cleanString(task.notes); main.appendChild(notes); } const grantMeta = isRecord(task.meta) && isRecord(task.meta.grantRequest) ? task.meta.grantRequest : null; if (grantMeta) { const grantParts = []; const qortalAddress = firstNonEmptyString( [grantMeta.qortalAddress, grantMeta.address], "" ); const amount = firstNonEmptyString( [ grantMeta.amount, grantMeta.grantAmountQort, grantMeta.grantRequiredAmount, ], "" ); if (qortalAddress) { grantParts.push("Qortal: " + qortalAddress); } if (amount) { grantParts.push("Requested: " + amount + " QORT"); } if (grantParts.length > 0) { const grantDetails = document.createElement("div"); grantDetails.className = "chd-ops-meta"; grantDetails.textContent = grantParts.join(" | "); main.appendChild(grantDetails); } } const updatedMeta = document.createElement("div"); updatedMeta.className = "chd-ops-meta"; updatedMeta.textContent = "Updated: " + formatDateTime( cleanString(task.updatedAt) || cleanString(task.createdAt) ); main.appendChild(updatedMeta); row.appendChild(main); const actions = document.createElement("div"); actions.className = "chd-ops-actions"; if (actionRequired) { actions.appendChild( buildStatusPill( "Action", operationActionLabel(task), normalizedStatus === "blocked" ? "issues" : "partial" ) ); } actions.appendChild( buildStatusPill( "Status", operationStatusLabel(normalizedStatus), operationStatusLevel(normalizedStatus) ) ); const applyStatusChange = async function (nextStatus) { if (!activeInstanceId) { return; } try { const persistedTask = await ensurePersistedTask( activeInstanceId, task ); await setInstanceOperationTaskStatus( activeInstanceId, persistedTask.id, nextStatus ); await loadInstanceOperations( activeInstanceId, activeInstanceDetailData ); } catch (error) { window.alert( "Failed to update task status: " + (error && error.message ? error.message : String(error)) ); } }; if ( normalizedStatus === "pending" && cleanString(task.taskType) !== "intro_grant" ) { const startButton = document.createElement("button"); startButton.type = "button"; startButton.className = "button"; startButton.textContent = "Start"; startButton.addEventListener("click", function () { void applyStatusChange("in_progress"); }); actions.appendChild(startButton); } if ( cleanString(task.taskType) === "intro_grant" && normalizedStatus !== "completed" ) { const defaultGrantAmount = firstNonEmptyString( [ grantMeta && (grantMeta.amount || grantMeta.grantAmountQort || grantMeta.grantRequiredAmount), task.quantity, ], "2" ); const amountControl = document.createElement("label"); amountControl.className = "chd-ops-amount-control"; amountControl.textContent = "Grant QORT"; const amountInput = document.createElement("input"); amountInput.type = "number"; amountInput.min = "0.00000001"; amountInput.step = "0.00000001"; amountInput.value = defaultGrantAmount; amountInput.setAttribute("aria-label", "Grant amount in QORT"); amountControl.appendChild(amountInput); actions.appendChild(amountControl); const approveButton = document.createElement("button"); approveButton.type = "button"; approveButton.className = "button button-primary"; approveButton.textContent = "Approve + Send QORT"; approveButton.addEventListener("click", function () { const amountInputValue = cleanString(amountInput.value); const messageInput = window.prompt( "Optional approval message", cleanString(task.notes) ); void (async function () { const persistedTask = await ensurePersistedTask( activeInstanceId, task ); const qortalAddress = firstNonEmptyString( [ grantMeta && (grantMeta.qortalAddress || grantMeta.address), persistedTask.meta && persistedTask.meta.grantRequest && (persistedTask.meta.grantRequest.qortalAddress || persistedTask.meta.grantRequest.address), ], "" ); if (!qortalAddress) { throw new Error( "Grant request is missing the recipient Qortal address." ); } const amountQort = Number(amountInputValue); if (!Number.isFinite(amountQort) || amountQort <= 0) { throw new Error("Grant amount must be greater than zero."); } const transferResult = await sendCoinWithApproval( qortalAddress, amountQort, 0.01 ); await runIntroGrantTaskAction( activeInstanceId, persistedTask, "approve", { amount: amountInputValue, message: cleanString(messageInput), sendFunds: false, transfer: { recipient: qortalAddress, amountQort: amountQort, txSignature: transferResult.txSignature, result: transferResult.result, }, } ); await loadInstanceOperations( activeInstanceId, activeInstanceDetailData ); })().catch(function (error) { closeApprovalModal(); window.alert( "Failed to approve intro grant: " + (error && error.message ? error.message : String(error)) ); }); }); actions.appendChild(approveButton); const rejectButton = document.createElement("button"); rejectButton.type = "button"; rejectButton.className = "button button-error"; rejectButton.textContent = "Reject"; rejectButton.addEventListener("click", function () { const messageInput = window.prompt( "Optional rejection message", cleanString(task.notes) ); void (async function () { const persistedTask = await ensurePersistedTask( activeInstanceId, task ); await runIntroGrantTaskAction( activeInstanceId, persistedTask, "reject", { message: cleanString(messageInput), } ); await loadInstanceOperations( activeInstanceId, activeInstanceDetailData ); })().catch(function (error) { window.alert( "Failed to reject intro grant: " + (error && error.message ? error.message : String(error)) ); }); }); actions.appendChild(rejectButton); } if ( cleanString(task.taskType) !== "intro_grant" && normalizedStatus !== "completed" ) { const completeButton = document.createElement("button"); completeButton.type = "button"; completeButton.className = "button button-primary"; completeButton.textContent = "Mark Complete"; completeButton.addEventListener("click", function () { void applyStatusChange("completed"); }); actions.appendChild(completeButton); } else if (cleanString(task.taskType) !== "intro_grant") { const reopenButton = document.createElement("button"); reopenButton.type = "button"; reopenButton.className = "button"; reopenButton.textContent = "Reopen"; reopenButton.addEventListener("click", function () { void applyStatusChange("pending"); }); actions.appendChild(reopenButton); } row.appendChild(actions); operationsListEl.appendChild(row); }); } async function fetchPersistedInstanceOperations(instanceId) { const normalizedInstanceId = cleanString(instanceId); if (!normalizedInstanceId || !instanceOperationsUrl) { return []; } const url = new URL(instanceOperationsUrl, window.location.origin); url.searchParams.set("instanceId", normalizedInstanceId); const payload = await requestJson(url.toString(), { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; return firstArray([data.tasks]); } async function fetchConnectorInstanceDetailData(instanceId) { const normalizedInstanceId = cleanString(instanceId); if (!normalizedInstanceId || !connectorInstanceDetailUrl) { return {}; } const url = new URL(connectorInstanceDetailUrl, window.location.origin); url.searchParams.set("instanceId", normalizedInstanceId); const payload = await requestJson(url.toString(), { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); return payload && payload.data && typeof payload.data === "object" ? payload.data : {}; } async function scanInstancesForPendingTasks(instances) { const list = Array.isArray(instances) ? instances : []; if (!list.length || !connectorInstanceDetailUrl) { connectorIdentityIndexCache = []; renderIdentityIndex(connectorIdentityIndexCache); return; } const identityRowsByKey = {}; for (let i = 0; i < list.length; i += 1) { const entry = isRecord(list[i]) ? list[i] : null; const instanceId = cleanString( entry && entry.instanceId ? entry.instanceId : "" ); if (!instanceId) { continue; } try { const detailData = await fetchConnectorInstanceDetailData(instanceId); const detectedTasks = detectOperationsFromInstanceDetail( instanceId, detailData ); const persistedTasks = await fetchPersistedInstanceOperations( instanceId ); const mergedTasks = mergeInstanceOperations( persistedTasks, detectedTasks ); const counts = summarizeTaskCounts(mergedTasks); const knownUsers = collectKnownUsers(detailData); knownUsers.forEach(function (user) { upsertIdentityIndexEntry(identityRowsByKey, user, instanceId); }); entry.actionRequiredCount = counts.actionRequiredCount; entry.grantRequestCount = counts.grantRequestCount; entry.operationTaskCount = counts.taskCount; } catch (_error) { entry.actionRequiredCount = coerceFiniteNumber(entry.actionRequiredCount) || 0; entry.grantRequestCount = coerceFiniteNumber(entry.grantRequestCount) || 0; entry.operationTaskCount = coerceFiniteNumber(entry.operationTaskCount) || 0; } const incremental = summarizeInstancesData({ instances: connectorInstancesCache, }); renderSummaryItems( instancesSummaryEl, incremental.summaryItems, "No instances returned." ); renderInstanceCompactList(connectorInstancesCache); } connectorIdentityIndexCache = Object.keys(identityRowsByKey) .map(function (key) { return identityRowsByKey[key]; }) .sort(function (a, b) { const instanceCountA = coerceFiniteNumber(a && a.instanceCount) || 0; const instanceCountB = coerceFiniteNumber(b && b.instanceCount) || 0; if (instanceCountA !== instanceCountB) { return instanceCountB - instanceCountA; } const nameA = firstNonEmptyString( [ a && a.displayName, a && a.nextcloudUserId, a && a.email, a && a.qortalAddress, ], "" ).toLowerCase(); const nameB = firstNonEmptyString( [ b && b.displayName, b && b.nextcloudUserId, b && b.email, b && b.qortalAddress, ], "" ).toLowerCase(); return nameA.localeCompare(nameB); }); renderIdentityIndex(connectorIdentityIndexCache); const refreshed = summarizeInstancesData({ instances: connectorInstancesCache, }); renderSummaryItems( instancesSummaryEl, refreshed.summaryItems, "No instances returned." ); renderInstanceCompactList(connectorInstancesCache); } async function loadInstanceOperations(instanceId, detailData) { const normalizedInstanceId = cleanString(instanceId); activeInstanceId = normalizedInstanceId; activeInstanceDetailData = isRecord(detailData) ? detailData : activeInstanceDetailData; if (!normalizedInstanceId) { renderSummaryItems( operationsSummaryEl, [], "Select an instance to load operations." ); if (operationsListEl) { operationsListEl.textContent = "Select an instance to load operations."; } activeInstanceOperations = []; return; } if (operationsListEl) { operationsListEl.textContent = "Loading operations..."; } renderSummaryItems(operationsSummaryEl, [], "Loading operations..."); const detectedTasks = detectOperationsFromInstanceDetail( normalizedInstanceId, activeInstanceDetailData ); try { const persistedTasks = await fetchPersistedInstanceOperations( normalizedInstanceId ); const merged = mergeInstanceOperations(persistedTasks, detectedTasks); activeInstanceOperations = merged; renderOperationsSummary(merged); renderOperationsList(merged); } catch (error) { const fallback = mergeInstanceOperations([], detectedTasks); activeInstanceOperations = fallback; renderOperationsSummary(fallback); renderOperationsList(fallback); if (operationsSummaryEl) { operationsSummaryEl.classList.add("chd-summary-empty"); operationsSummaryEl.textContent = "Stored operations could not be loaded; showing detected tasks only."; } } } function updateInstanceSelect(instances) { if (!instanceSelectEl) { return; } const currentSelected = String(instanceSelectEl.value || "").trim(); instanceSelectEl.innerHTML = ""; const list = Array.isArray(instances) ? instances : []; list.forEach(function (entry) { if (!entry || typeof entry !== "object") { return; } const instanceId = String(entry.instanceId || "").trim(); if (!instanceId) { return; } const labelParts = [instanceId]; const url = String(entry.nextcloudPublicUrl || "").trim(); if (url) { labelParts.push(url); } const option = document.createElement("option"); option.value = instanceId; option.textContent = labelParts.join(" — "); if (currentSelected && currentSelected === instanceId) { option.selected = true; } instanceSelectEl.appendChild(option); }); if (!instanceSelectEl.value && instanceSelectEl.options.length > 0) { instanceSelectEl.selectedIndex = 0; } } async function loadOverview() { const panel = infoPanels.overview; if (!panel) { return; } if (!overviewUrl) { renderSummaryItems( overviewSummaryEl, [], "Overview endpoint is not configured." ); renderStatusPills(overviewStatusEl, []); setInfoPanelError(panel, "Overview endpoint is not configured."); renderConnectorAdminTokenControls({ connectorAdminTokenSet: true }); renderBillingAutoFulfillmentControls({}); return; } setInfoPanelLoading(panel, "Loading overview..."); renderSummaryItems(overviewSummaryEl, [], "Loading overview..."); renderStatusPills(overviewStatusEl, []); try { const payload = await requestJson(overviewUrl, { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; const presentation = summarizeOverviewData(data); renderSummaryItems( overviewSummaryEl, presentation.summaryItems, "No overview data returned." ); renderStatusPills(overviewStatusEl, presentation.statusItems); setInfoPanelData(panel, data, presentation.focusedData); renderConnectorAdminTokenControls( data && isRecord(data.chd) ? data.chd : {} ); renderBillingAutoFulfillmentControls( data && isRecord(data.billingAutomation) ? data.billingAutomation : {} ); } catch (error) { const errorMessage = "Failed to load overview: " + (error && error.message ? error.message : String(error)); renderSummaryItems(overviewSummaryEl, [], errorMessage); renderStatusPills(overviewStatusEl, []); setInfoPanelError(panel, errorMessage); renderConnectorAdminTokenControls({ connectorAdminTokenSet: true }); renderBillingAutoFulfillmentControls({}); } } function renderBillingAutoFulfillmentControls(payloadData) { const data = isRecord(payloadData) ? payloadData : {}; const presentation = summarizeBillingAutoFulfillmentData(data); if (billingAutoFulfillmentEnabledEl) { billingAutoFulfillmentEnabledEl.checked = coerceBoolean(data.enabled); billingAutoFulfillmentEnabledEl.disabled = !saveBillingAutoFulfillmentUrl; } if (billingAutoFulfillmentSaveButton) { billingAutoFulfillmentSaveButton.disabled = !saveBillingAutoFulfillmentUrl; } if (billingAutoFulfillmentRunButton) { billingAutoFulfillmentRunButton.disabled = !runBillingAutoFulfillmentUrl; } if (billingAutoFulfillmentBackfillButton) { billingAutoFulfillmentBackfillButton.disabled = !backfillBillingAutoFulfillmentUrl; } renderSummaryItems( billingAutoFulfillmentSummaryEl, presentation.summaryItems, "No automation data available." ); renderStatusPills( billingAutoFulfillmentStatusEl, presentation.statusItems ); } function renderConnectorAdminTokenControls(payloadData) { const data = isRecord(payloadData) ? payloadData : {}; const tokenConfigured = coerceBoolean(data.connectorAdminTokenSet); if (!connectorAdminTokenCardEl) { return; } if (tokenConfigured) { connectorAdminTokenCardEl.classList.add("chd-hidden"); if (connectorAdminTokenInputEl) { connectorAdminTokenInputEl.value = ""; } if (connectorAdminTokenStatusEl) { connectorAdminTokenStatusEl.textContent = "Connector admin token is configured."; } return; } connectorAdminTokenCardEl.classList.remove("chd-hidden"); if (connectorAdminTokenInputEl) { connectorAdminTokenInputEl.disabled = !saveConnectorAdminTokenUrl; } if (connectorAdminTokenSaveButton) { connectorAdminTokenSaveButton.disabled = !saveConnectorAdminTokenUrl; } if (connectorAdminTokenStatusEl) { connectorAdminTokenStatusEl.textContent = saveConnectorAdminTokenUrl ? "Enter the shared connector admin token to enable connector admin access." : "Connector admin token save endpoint is not configured."; } } async function loadConnectorSummary() { const panel = infoPanels.connectorSummary; if (!panel) { return; } if (!connectorSummaryUrl) { renderSummaryItems( connectorSummaryViewEl, [], "Connector summary endpoint is not configured." ); renderStatusPills(connectorStatusEl, []); setInfoPanelError(panel, "Connector summary endpoint is not configured."); return; } setInfoPanelLoading(panel, "Loading connector snapshot..."); renderSummaryItems( connectorSummaryViewEl, [], "Loading connector snapshot..." ); renderStatusPills(connectorStatusEl, []); try { const payload = await requestJson(connectorSummaryUrl, { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; const presentation = summarizeConnectorData(data); renderSummaryItems( connectorSummaryViewEl, presentation.summaryItems, "No connector snapshot data returned." ); renderStatusPills(connectorStatusEl, presentation.statusItems); setInfoPanelData(panel, data, presentation.focusedData); } catch (error) { const errorMessage = "Failed to load connector summary: " + (error && error.message ? error.message : String(error)); renderSummaryItems(connectorSummaryViewEl, [], errorMessage); renderStatusPills(connectorStatusEl, []); setInfoPanelError(panel, errorMessage); } } function setInstanceDetailIdle(message) { renderSummaryItems( instanceDetailSummaryEl, [], message || "Select an instance to view details." ); renderStatusPills(instanceDetailStatusEl, []); setInfoPanelEmpty( infoPanels.instanceDetail, message || "Select an instance to view details." ); activeInstanceId = ""; activeInstanceKnownUsers = []; activeInstanceDetailData = null; activeInstanceOperations = []; populateOperationsUserSelect([]); renderSummaryItems( operationsSummaryEl, [], "Select an instance to load operations." ); if (operationsListEl) { operationsListEl.textContent = "Select an instance to load operations."; } renderSummaryItems( userOrdersSummaryEl, [], "Select an instance to load reconciliation data." ); if (userOrdersListEl) { userOrdersListEl.textContent = "Select an instance to load reconciliation data."; } renderSummaryItems( paymenterSnapshotSummaryEl, [], "Select an instance to load Paymenter snapshot data." ); renderStatusPills(paymenterSnapshotStatusEl, []); setInfoPanelEmpty( infoPanels.paymenterSnapshot, "Select an instance to load Paymenter snapshot data." ); if (instanceConnectorProfileEl) { instanceConnectorProfileEl.value = ""; } if (instanceConnectorUrlEl) { instanceConnectorUrlEl.value = ""; } if (instanceConnectorStatusEl) { instanceConnectorStatusEl.textContent = "Load an instance to manage connector target."; } if (instanceMspContactNameEl) { instanceMspContactNameEl.value = ""; } if (instanceMspContactEmailEl) { instanceMspContactEmailEl.value = ""; } if (instancePluginAdminEmailEl) { instancePluginAdminEmailEl.value = ""; } if (instanceAdminQortalAddressEl) { instanceAdminQortalAddressEl.value = ""; } if (instanceMetadataSaveButton) { instanceMetadataSaveButton.disabled = true; } if (instanceMetadataStatusEl) { instanceMetadataStatusEl.textContent = "Load an instance to manage NuQloud MSP contact fields."; } if (instanceRemoveButton) { instanceRemoveButton.disabled = true; } void loadPaymenterBackups(); } async function loadConnectorRoutingConfig() { if (!connectorRoutingStatusEl) { return; } if (!connectorRoutingConfigUrl) { connectorRoutingStatusEl.textContent = "Connector routing endpoint is not configured."; return; } connectorRoutingStatusEl.textContent = "Loading connector routing..."; try { const payload = await requestJson(connectorRoutingConfigUrl, { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); applyConnectorRoutingMeta( payload && payload.data && typeof payload.data === "object" ? payload.data : {} ); renderConnectorRoutingControls(); } catch (error) { connectorRoutingStatusEl.textContent = "Failed to load connector routing: " + (error && error.message ? error.message : String(error)); } } async function saveConnectorRoutingConfig() { if (!connectorRoutingStatusEl) { return; } if (!saveConnectorRoutingConfigUrl) { connectorRoutingStatusEl.textContent = "Connector routing save endpoint is not configured."; return; } const payload = { config: { activeProfile: connectorRoutingProfileSelectEl ? trimString(connectorRoutingProfileSelectEl.value) : "", profiles: { test: { baseUrl: connectorRoutingTestUrlEl ? trimString(connectorRoutingTestUrlEl.value) : "", }, production: { baseUrl: connectorRoutingProductionUrlEl ? trimString(connectorRoutingProductionUrlEl.value) : "", }, }, }, }; connectorRoutingStatusEl.textContent = "Saving connector routing..."; try { const response = await requestJson(saveConnectorRoutingConfigUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify(payload), }); applyConnectorRoutingMeta( response && response.data && typeof response.data === "object" ? response.data : {} ); renderConnectorRoutingControls(); void loadOverview(); void loadConnectorSummary(); void loadConnectorInstances().then(function () { if (activeInstanceId) { return loadConnectorInstanceDetail(activeInstanceId); } return Promise.resolve(); }); void loadConnectorCatalogConfig(); } catch (error) { connectorRoutingStatusEl.textContent = "Failed to save connector routing: " + (error && error.message ? error.message : String(error)); } } async function saveBillingAutoFulfillment() { if (!billingAutoFulfillmentStatusEl) { return; } if (!saveBillingAutoFulfillmentUrl) { billingAutoFulfillmentStatusEl.textContent = "Automatic invite fulfillment save endpoint is not configured."; return; } const enabled = billingAutoFulfillmentEnabledEl ? Boolean(billingAutoFulfillmentEnabledEl.checked) : false; if (billingAutoFulfillmentSaveButton) { billingAutoFulfillmentSaveButton.disabled = true; } billingAutoFulfillmentStatusEl.textContent = "Saving automatic invite fulfillment settings..."; try { const response = await requestJson(saveBillingAutoFulfillmentUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify({ enabled: enabled, }), }); const data = response && response.data && typeof response.data === "object" ? response.data : {}; renderBillingAutoFulfillmentControls(data); void loadOverview(); } catch (error) { billingAutoFulfillmentStatusEl.textContent = "Failed to save automatic invite fulfillment: " + (error && error.message ? error.message : String(error)); } finally { if (billingAutoFulfillmentSaveButton) { billingAutoFulfillmentSaveButton.disabled = !saveBillingAutoFulfillmentUrl; } } } async function saveConnectorAdminToken() { if (!connectorAdminTokenStatusEl) { return; } if (!saveConnectorAdminTokenUrl) { connectorAdminTokenStatusEl.textContent = "Connector admin token save endpoint is not configured."; return; } const token = connectorAdminTokenInputEl ? trimString(connectorAdminTokenInputEl.value) : ""; if (!token) { connectorAdminTokenStatusEl.textContent = "Enter the connector admin token before saving."; return; } if (connectorAdminTokenSaveButton) { connectorAdminTokenSaveButton.disabled = true; } if (connectorAdminTokenInputEl) { connectorAdminTokenInputEl.disabled = true; } connectorAdminTokenStatusEl.textContent = "Saving connector admin token..."; try { const response = await requestJson(saveConnectorAdminTokenUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify({ connectorAdminToken: token, }), }); const data = response && response.data && typeof response.data === "object" ? response.data : {}; if (connectorAdminTokenInputEl) { connectorAdminTokenInputEl.value = ""; } renderConnectorAdminTokenControls(data); connectorAdminTokenStatusEl.textContent = "Connector admin token saved."; void loadOverview(); } catch (error) { if (error && error.status === 409) { if (connectorAdminTokenInputEl) { connectorAdminTokenInputEl.value = ""; } renderConnectorAdminTokenControls({ connectorAdminTokenSet: true }); connectorAdminTokenStatusEl.textContent = "Connector admin token is already configured. Refreshing overview..."; void loadOverview(); } else { connectorAdminTokenStatusEl.textContent = "Failed to save connector admin token: " + (error && error.message ? error.message : String(error)); } } finally { if (connectorAdminTokenSaveButton) { connectorAdminTokenSaveButton.disabled = !saveConnectorAdminTokenUrl || Boolean( connectorAdminTokenCardEl && connectorAdminTokenCardEl.classList.contains("chd-hidden") ); } if (connectorAdminTokenInputEl) { connectorAdminTokenInputEl.disabled = !saveConnectorAdminTokenUrl || Boolean( connectorAdminTokenCardEl && connectorAdminTokenCardEl.classList.contains("chd-hidden") ); } } } async function runBillingAutoFulfillment() { if (!billingAutoFulfillmentStatusEl) { return; } if (!runBillingAutoFulfillmentUrl) { billingAutoFulfillmentStatusEl.textContent = "Manual invite fulfillment endpoint is not configured."; return; } if ( typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm( "Run invite fulfillment now? This will check paid orders and send invite emails." ) ) { return; } if (billingAutoFulfillmentRunButton) { billingAutoFulfillmentRunButton.disabled = true; } billingAutoFulfillmentStatusEl.textContent = "Running invite fulfillment now..."; try { const response = await requestJson(runBillingAutoFulfillmentUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify({ force: true, }), }); const data = response && response.data && typeof response.data === "object" ? response.data : {}; const message = firstNonEmptyString([data.message], "Manual run complete."); billingAutoFulfillmentStatusEl.textContent = message; if (data.overview && isRecord(data.overview)) { renderBillingAutoFulfillmentControls(data.overview); } void loadOverview(); } catch (error) { billingAutoFulfillmentStatusEl.textContent = "Failed to run invite fulfillment: " + (error && error.message ? error.message : String(error)); } finally { if (billingAutoFulfillmentRunButton) { billingAutoFulfillmentRunButton.disabled = !runBillingAutoFulfillmentUrl; } } } function normalizeBackfillSince(value) { const raw = trimString(value); if (!raw) { return ""; } const parsed = new Date(raw); if (Number.isNaN(parsed.getTime())) { return ""; } return parsed.toISOString(); } async function backfillBillingAutoFulfillment() { if (!billingAutoFulfillmentStatusEl) { return; } if (!backfillBillingAutoFulfillmentUrl) { billingAutoFulfillmentStatusEl.textContent = "Backfill invite fulfillment endpoint is not configured."; return; } const since = normalizeBackfillSince( billingAutoFulfillmentBackfillSinceEl ? billingAutoFulfillmentBackfillSinceEl.value : "" ); const invoiceId = billingAutoFulfillmentBackfillInvoiceEl ? trimString(billingAutoFulfillmentBackfillInvoiceEl.value) : ""; if (!since && !invoiceId) { billingAutoFulfillmentStatusEl.textContent = "Choose a backfill date or enter an invoice ID."; return; } if ( typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm( "Run invite backfill now? Monthly services require a current paid invoice, and stale invoices will be skipped." ) ) { return; } if (billingAutoFulfillmentBackfillButton) { billingAutoFulfillmentBackfillButton.disabled = true; } billingAutoFulfillmentStatusEl.textContent = "Running invite fulfillment backfill..."; try { const response = await requestJson(backfillBillingAutoFulfillmentUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify({ since: since, invoiceId: invoiceId, }), }); const data = response && response.data && typeof response.data === "object" ? response.data : {}; billingAutoFulfillmentStatusEl.textContent = firstNonEmptyString( [data.message], "Backfill complete." ); if (data.overview && isRecord(data.overview)) { renderBillingAutoFulfillmentControls(data.overview); } void loadOverview(); } catch (error) { billingAutoFulfillmentStatusEl.textContent = "Failed to run invite backfill: " + (error && error.message ? error.message : String(error)); } finally { if (billingAutoFulfillmentBackfillButton) { billingAutoFulfillmentBackfillButton.disabled = !backfillBillingAutoFulfillmentUrl; } } } async function loadConnectorInstances() { const panel = infoPanels.instances; if (!panel) { return; } if (!connectorInstancesUrl) { renderSummaryItems( instancesSummaryEl, [], "Connector instances endpoint is not configured." ); if (instancesCompactEl) { instancesCompactEl.textContent = "Connector instances endpoint is not configured."; } setInfoPanelError( panel, "Connector instances endpoint is not configured." ); setInstanceDetailIdle("Select an instance to view details."); return; } setInfoPanelLoading(panel, "Loading instances..."); renderSummaryItems(instancesSummaryEl, [], "Loading instances..."); if (instancesCompactEl) { instancesCompactEl.textContent = "Loading instances..."; } try { const payload = await requestJson(connectorInstancesUrl, { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; const presentation = summarizeInstancesData(data); const instances = presentation.instances; connectorInstancesCache = instances; updateInstanceSelect(instances); renderSummaryItems( instancesSummaryEl, presentation.summaryItems, "No instances returned." ); renderInstanceCompactList(instances); setInfoPanelData(panel, data, presentation.focusedData); await loadRemovedConnectorInstances(); void scanInstancesForPendingTasks(instances); if (!instances.length) { setInstanceDetailIdle("No instance details available yet."); } } catch (error) { const errorMessage = "Failed to load connector instances: " + (error && error.message ? error.message : String(error)); renderSummaryItems(instancesSummaryEl, [], errorMessage); if (instancesCompactEl) { instancesCompactEl.textContent = errorMessage; } setInfoPanelError(panel, errorMessage); renderRemovedInstanceList([]); setInstanceDetailIdle("Select an instance to view details."); } } async function loadRemovedConnectorInstances() { if (!removedInstancesListEl && !removedInstancesSummaryEl) { return; } if (!connectorInstancesUrl) { renderRemovedInstanceList([]); return; } const url = new URL(connectorInstancesUrl, window.location.origin); url.searchParams.set("removed", "1"); try { const payload = await requestJson(url.toString(), { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); const data = payload && payload.data && typeof payload.data === "object" ? payload.data : {}; renderRemovedInstanceList( Array.isArray(data.instances) ? data.instances : [] ); } catch (_error) { renderRemovedInstanceList([]); } } async function loadConnectorInstanceDetail(instanceId, options) { const panel = infoPanels.instanceDetail; if (!panel) { return; } const normalizedInstanceId = String(instanceId || "").trim(); if (!normalizedInstanceId) { setInstanceDetailIdle("Select an instance to view details."); return; } if (!connectorInstanceDetailUrl) { renderSummaryItems( instanceDetailSummaryEl, [], "Connector instance detail endpoint is not configured." ); renderStatusPills(instanceDetailStatusEl, []); setInfoPanelError( panel, "Connector instance detail endpoint is not configured." ); return; } setInfoPanelLoading( panel, "Loading details for " + normalizedInstanceId + "..." ); renderSummaryItems( instanceDetailSummaryEl, [], "Loading details for " + normalizedInstanceId + "..." ); renderStatusPills(instanceDetailStatusEl, []); try { const data = await fetchConnectorInstanceDetailData(normalizedInstanceId); const presentation = summarizeInstanceDetail(data, normalizedInstanceId); activeInstanceKnownUsers = Array.isArray(presentation.knownUsers) ? presentation.knownUsers : []; populateOperationsUserSelect(activeInstanceKnownUsers); activeInstanceDetailData = data; activeInstanceId = normalizedInstanceId; populateInstanceConnectorControls(resolveDetailInstance(data)); populateInstanceMetadataControls(resolveDetailInstance(data)); renderSummaryItems( instanceDetailSummaryEl, presentation.summaryItems, "No detail data returned." ); renderStatusPills(instanceDetailStatusEl, presentation.statusItems); renderUserCommerceRows(presentation.userCommerceRows); setInfoPanelData(panel, data, presentation.focusedData); const detailOptions = options && typeof options === "object" ? options : {}; if (Object.prototype.hasOwnProperty.call(detailOptions, "showAll")) { setInfoPanelShowAll(panel, Boolean(detailOptions.showAll)); } if (Object.prototype.hasOwnProperty.call(detailOptions, "showRaw")) { setInfoPanelRawVisible(panel, Boolean(detailOptions.showRaw)); } await loadPaymenterSnapshotForActiveInstance(); await loadPaymenterBackups(); await loadInstanceOperations(normalizedInstanceId, data); } catch (error) { const errorMessage = "Failed to load instance detail: " + (error && error.message ? error.message : String(error)); renderSummaryItems(instanceDetailSummaryEl, [], errorMessage); renderStatusPills(instanceDetailStatusEl, []); setInfoPanelError(panel, errorMessage); renderSummaryItems( userOrdersSummaryEl, [], "Failed to load reconciliation data because instance detail failed." ); if (userOrdersListEl) { userOrdersListEl.textContent = "Failed to load reconciliation data."; } renderSummaryItems( paymenterSnapshotSummaryEl, [], "Failed to load Paymenter snapshot because instance detail failed." ); renderStatusPills(paymenterSnapshotStatusEl, []); setInfoPanelError( infoPanels.paymenterSnapshot, "Failed to load Paymenter snapshot because instance detail failed." ); renderSummaryItems( operationsSummaryEl, [], "Failed to load operations because instance detail failed." ); if (operationsListEl) { operationsListEl.textContent = "Failed to load operations."; } } } async function saveInstanceConnectorTarget() { if (!instanceConnectorStatusEl) { return; } if (!saveConnectorInstanceTargetUrl) { instanceConnectorStatusEl.textContent = "Connector target save endpoint is not configured."; return; } const instanceId = trimString(activeInstanceId); if (!instanceId) { instanceConnectorStatusEl.textContent = "Load an instance before applying a connector target."; return; } const selectedProfile = instanceConnectorProfileEl ? trimString(instanceConnectorProfileEl.value) : ""; let resolvedBaseUrl = instanceConnectorUrlEl ? trimString(instanceConnectorUrlEl.value) : ""; if (!resolvedBaseUrl && selectedProfile) { const profileEntry = findSelectableConnectorProfile(selectedProfile); resolvedBaseUrl = trimString(profileEntry && profileEntry.publicBaseUrl); if (instanceConnectorUrlEl && resolvedBaseUrl) { instanceConnectorUrlEl.value = resolvedBaseUrl; } } const payload = { instanceId: instanceId, connectorProfile: selectedProfile, connectorBaseUrl: resolvedBaseUrl, }; instanceConnectorStatusEl.textContent = "Saving connector target..."; try { const response = await requestJson(saveConnectorInstanceTargetUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify(payload), }); const target = response && response.data && response.data.connectorTarget && typeof response.data.connectorTarget === "object" ? response.data.connectorTarget : {}; instanceConnectorStatusEl.textContent = "Connector target saved. The instance will pick it up on the next registration or billing sync."; if (instanceConnectorProfileEl) { instanceConnectorProfileEl.value = trimString(target.connectorProfile); } if (instanceConnectorUrlEl) { instanceConnectorUrlEl.value = trimString(target.connectorBaseUrl); } await loadConnectorInstanceDetail(instanceId); } catch (error) { instanceConnectorStatusEl.textContent = "Failed to save connector target: " + (error && error.message ? error.message : String(error)); } } async function saveInstanceMetadata() { if (!instanceMetadataStatusEl) { return; } if (!saveConnectorInstanceMetadataUrl) { instanceMetadataStatusEl.textContent = "Instance metadata save endpoint is not configured."; return; } const instanceId = trimString(activeInstanceId); if (!instanceId) { instanceMetadataStatusEl.textContent = "Load an instance before saving NuQloud MSP contact fields."; return; } const payload = { instanceId: instanceId, mspContactName: instanceMspContactNameEl ? trimString(instanceMspContactNameEl.value) : "", mspContactEmail: instanceMspContactEmailEl ? trimString(instanceMspContactEmailEl.value) : "", pluginAdminEmail: instancePluginAdminEmailEl ? trimString(instancePluginAdminEmailEl.value) : "", adminQortalAddress: instanceAdminQortalAddressEl ? trimString(instanceAdminQortalAddressEl.value) : "", }; instanceMetadataStatusEl.textContent = "Saving NuQloud MSP contact fields..."; try { const response = await requestJson(saveConnectorInstanceMetadataUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify(payload), }); const metadata = response && response.data && response.data.metadata && typeof response.data.metadata === "object" ? response.data.metadata : {}; if (instanceMspContactNameEl) { instanceMspContactNameEl.value = trimString(metadata.mspContactName); } if (instanceMspContactEmailEl) { instanceMspContactEmailEl.value = trimString(metadata.mspContactEmail); } if (instancePluginAdminEmailEl) { instancePluginAdminEmailEl.value = trimString( metadata.pluginAdminEmail ); } if (instanceAdminQortalAddressEl) { instanceAdminQortalAddressEl.value = trimString( metadata.adminQortalAddress ); } const mirrored = Boolean( response && response.data && response.data.compatibility && response.data.compatibility.mirroredLegacyAdminEmail ); instanceMetadataStatusEl.textContent = mirrored ? "NuQloud MSP contact fields saved. Legacy connector email mirror updated for compatibility." : "NuQloud MSP contact fields saved."; await loadConnectorInstanceDetail(instanceId); } catch (error) { instanceMetadataStatusEl.textContent = "Failed to save NuQloud MSP contact fields: " + (error && error.message ? error.message : String(error)); } } async function saveConnectorInstanceState(instanceId, action) { if (!saveConnectorInstanceStateUrl) { throw new Error("Connector instance state endpoint is not configured."); } const normalizedInstanceId = trimString(instanceId); const normalizedAction = trimString(action).toLowerCase(); if (!normalizedInstanceId) { throw new Error("instanceId is required"); } if (normalizedAction !== "remove" && normalizedAction !== "restore") { throw new Error("Unsupported instance action"); } const response = await requestJson(saveConnectorInstanceStateUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify({ instanceId: normalizedInstanceId, action: normalizedAction, }), }); const backupPath = trimString( response && response.data && response.data.backupPath ); const statusMessage = normalizedAction === "remove" ? "Instance removed from the active configuration." : "Instance restored to the active configuration."; if (instanceConnectorStatusEl) { instanceConnectorStatusEl.textContent = backupPath ? statusMessage + " Backup: " + backupPath : statusMessage; } if ( normalizedAction === "remove" && trimString(activeInstanceId) === normalizedInstanceId ) { setInstanceDetailIdle("Instance removed from active configuration."); } await loadConnectorInstances(); if (normalizedAction === "restore") { if (instanceSelectEl) { instanceSelectEl.value = normalizedInstanceId; } await loadConnectorInstanceDetail(normalizedInstanceId); } } function normalizeCatalogConfigPayload(payload) { if (!payload || typeof payload !== "object") { return {}; } return payload.config && typeof payload.config === "object" ? payload.config : payload; } function buildCatalogProfileStateFromConfig(configValue) { const source = configValue && typeof configValue === "object" ? configValue : {}; const packageSummaryRows = packageSummaryMapToRows( source.packageSummaryMap || {} ); const nextAudienceRows = ensureDefaultAudienceRows( audienceMapToRows(source.audienceCatalogs || {}) ); return { packageRows: ensureDefaultPackageRows( packageMapToRows( source.packageProductMap || {}, source.packagePriceMap || {} ), packageSummaryRows ), addonRows: mapToRows( source.addonProductMap || {}, source.addonLabelMap || {}, source.addonSummaryMap || {}, source.addonPriceMap || {} ), oneTimeRows: mapToRows( source.oneTimeProductMap || {}, source.oneTimeLabelMap || {}, source.oneTimeSummaryMap || {}, source.oneTimePriceMap || {} ), audienceRows: nextAudienceRows, extraOffers: extraOffersToRows(source.extraOffers || []), selectedAudienceKey: trimString( nextAudienceRows[0] && nextAudienceRows[0].key ), }; } function buildProfileConfigsFromConfigValue(configValue) { const source = configValue && typeof configValue === "object" ? configValue : {}; const result = {}; const profileConfigs = source.profileConfigs && typeof source.profileConfigs === "object" ? source.profileConfigs : {}; Object.keys(profileConfigs).forEach(function (profileId) { const normalized = trimString(profileId); if (!normalized) { return; } result[normalized] = buildCatalogProfileStateFromConfig( profileConfigs[profileId] ); }); if (!Object.keys(result).length) { const activeProfile = trimString( source.runtime && source.runtime.activePaymenterProfile ); if (activeProfile) { result[activeProfile] = buildCatalogProfileStateFromConfig(source); } } return result; } function normalizePackageMetaRows(rows) { if (!Array.isArray(rows)) { return []; } return rows .map(function (entry) { if (!entry || typeof entry !== "object") { return null; } const code = trimString(entry.code); if (!code) { return null; } return { code: code, label: trimString(entry.label) || code, summary: trimString(entry.summary), seedInAdmin: entry.seedInAdmin !== false, }; }) .filter(Boolean); } function applyConnectorPackageMeta(payload) { if (!payload || typeof payload !== "object") { return; } const seedRows = normalizePackageMetaRows(payload.packageSeeds); if (seedRows.length) { defaultPackageRows = seedRows; return; } const metaRows = normalizePackageMetaRows(payload.packageMeta).filter( function (entry) { return entry.seedInAdmin !== false; } ); if (metaRows.length) { defaultPackageRows = metaRows; } } function applyConnectorRuntimeMeta(payload) { const runtime = payload && payload.runtime && typeof payload.runtime === "object" ? payload.runtime : {}; catalogConfigState.runtime = { activePaymenterProfile: trimString(runtime.activePaymenterProfile), availablePaymenterProfiles: Array.isArray( runtime.availablePaymenterProfiles ) ? runtime.availablePaymenterProfiles : [], availableCatalogConfigProfiles: Array.isArray( runtime.availableCatalogConfigProfiles ) ? runtime.availableCatalogConfigProfiles : [], availableConnectorProfiles: Array.isArray( runtime.availableConnectorProfiles ) ? runtime.availableConnectorProfiles : [], currentPaymenterBaseUrl: trimString(runtime.currentPaymenterBaseUrl), diagnostics: runtime.diagnostics && typeof runtime.diagnostics === "object" ? runtime.diagnostics : {}, }; availableConnectorProfiles = Array.isArray( runtime.availableConnectorProfiles ) ? runtime.availableConnectorProfiles : []; } function createEmptyCatalogProfileState() { return { packageRows: [], addonRows: [], oneTimeRows: [], audienceRows: [], extraOffers: [], selectedAudienceKey: "", }; } function cloneCatalogProfileState(source) { const safe = source && typeof source === "object" ? source : createEmptyCatalogProfileState(); return { packageRows: Array.isArray(safe.packageRows) ? safe.packageRows.map(function (row) { return { ...row, price: trimString(row && row.price) }; }) : [], addonRows: Array.isArray(safe.addonRows) ? safe.addonRows.map(function (row) { return { ...row, price: trimString(row && row.price) }; }) : [], oneTimeRows: Array.isArray(safe.oneTimeRows) ? safe.oneTimeRows.map(function (row) { return { ...row, price: trimString(row && row.price) }; }) : [], audienceRows: Array.isArray(safe.audienceRows) ? safe.audienceRows.map(function (row) { return { key: trimString(row && row.key), packages: Array.isArray(row && row.packages) ? row.packages.slice() : [], addons: Array.isArray(row && row.addons) ? row.addons.slice() : [], oneTime: Array.isArray(row && row.oneTime) ? row.oneTime.slice() : [], extraOfferCodes: Array.isArray(row && row.extraOfferCodes) ? row.extraOfferCodes.slice() : [], }; }) : [], extraOffers: Array.isArray(safe.extraOffers) ? safe.extraOffers.map(function (row) { return { ...row }; }) : [], selectedAudienceKey: trimString(safe.selectedAudienceKey), }; } function getAvailableCatalogScopeProfiles() { const runtimeProfiles = Array.isArray( catalogConfigState.runtime.availableCatalogConfigProfiles ) ? catalogConfigState.runtime.availableCatalogConfigProfiles : []; const ids = runtimeProfiles .map(function (profile) { return trimString(profile && profile.id); }) .filter(Boolean); Object.keys(catalogConfigState.profileConfigs || {}).forEach(function ( key ) { const normalized = trimString(key); if (normalized && ids.indexOf(normalized) === -1) { ids.push(normalized); } }); return ids; } function persistCurrentCatalogScopeState() { const scopeId = trimString(catalogConfigState.selectedCatalogScopeProfile); if (!scopeId) { return; } catalogConfigState.profileConfigs[scopeId] = cloneCatalogProfileState({ packageRows: catalogConfigState.packageRows, addonRows: catalogConfigState.addonRows, oneTimeRows: catalogConfigState.oneTimeRows, audienceRows: catalogConfigState.audienceRows, extraOffers: catalogConfigState.extraOffers, selectedAudienceKey: catalogConfigState.selectedAudienceKey, }); } function applyCatalogScopeState(scopeId) { const normalized = trimString(scopeId); const stored = normalized && catalogConfigState.profileConfigs[normalized] ? catalogConfigState.profileConfigs[normalized] : createEmptyCatalogProfileState(); const next = cloneCatalogProfileState(stored); catalogConfigState.selectedCatalogScopeProfile = normalized; catalogConfigState.packageRows = ensureDefaultPackageRows(next.packageRows); catalogConfigState.addonRows = next.addonRows; catalogConfigState.oneTimeRows = next.oneTimeRows; catalogConfigState.audienceRows = ensureDefaultAudienceRows( next.audienceRows ); catalogConfigState.extraOffers = next.extraOffers; catalogConfigState.selectedAudienceKey = trimString(next.selectedAudienceKey) || trimString( catalogConfigState.audienceRows[0] && catalogConfigState.audienceRows[0].key ); } function applyConnectorRoutingMeta(payload) { const config = payload && payload.config && typeof payload.config === "object" ? payload.config : {}; const profiles = config && config.profiles && typeof config.profiles === "object" ? config.profiles : {}; const runtime = payload && payload.runtime && typeof payload.runtime === "object" ? payload.runtime : {}; connectorRoutingState = { activeProfile: trimString(config.activeProfile), profiles: { test: { baseUrl: trimString(profiles.test && profiles.test.baseUrl), }, production: { baseUrl: trimString( profiles.production && profiles.production.baseUrl ), }, }, effectiveBaseUrl: trimString(runtime.effectiveBaseUrl), fallbackBaseUrl: trimString(runtime.fallbackBaseUrl), }; } function renderConnectorRoutingControls() { if (connectorRoutingProfileSelectEl) { connectorRoutingProfileSelectEl.value = trimString( connectorRoutingState.activeProfile ); } if (connectorRoutingTestUrlEl) { connectorRoutingTestUrlEl.value = trimString( connectorRoutingState.profiles.test && connectorRoutingState.profiles.test.baseUrl ); } if (connectorRoutingProductionUrlEl) { connectorRoutingProductionUrlEl.value = trimString( connectorRoutingState.profiles.production && connectorRoutingState.profiles.production.baseUrl ); } if (connectorRoutingStatusEl) { const active = trimString(connectorRoutingState.activeProfile) || "default"; const effective = trimString(connectorRoutingState.effectiveBaseUrl); const fallback = trimString(connectorRoutingState.fallbackBaseUrl); let text = "Active connector mode: " + active; if (effective) { text += " | Effective URL: " + effective; } if (fallback && fallback !== effective) { text += " | Fallback URL: " + fallback; } connectorRoutingStatusEl.textContent = text; } } function renderRuntimeDiagnostics() { if (!connectorRuntimeDiagnosticsEl) { return; } const diagnostics = catalogConfigState.runtime && catalogConfigState.runtime.diagnostics && typeof catalogConfigState.runtime.diagnostics === "object" ? catalogConfigState.runtime.diagnostics : {}; const items = [ { label: "Effective Paymenter Profile", value: trimString(diagnostics.effectivePaymenterProfile) || "Not set", }, { label: "Default Paymenter Profile", value: trimString(diagnostics.defaultPaymenterProfile) || "Not set", }, { label: "Admin Active Profile", value: trimString(diagnostics.adminActivePaymenterProfile) || "Not set", }, { label: "Env Active Profile", value: trimString(diagnostics.envActivePaymenterProfile) || "Not set", }, { label: "Effective Paymenter Base URL", value: trimString(diagnostics.effectivePaymenterBaseUrl) || "Not set", wide: true, }, { label: "Catalog Category", value: trimString(diagnostics.effectiveCatalogCategoryId) || "Not set", }, { label: "Checkout Link Endpoint", value: trimString(diagnostics.effectiveCheckoutLinkEndpoint) || "Not set", wide: true, }, { label: "Customer External Field", value: trimString(diagnostics.effectiveCustomerExternalField) || "Not set", }, { label: "Qortal Address Upsert", value: trimString(diagnostics.effectiveQortalAddressUpsertEndpoint) || "Not set", wide: true, }, { label: "Webhook Token", value: diagnostics.effectiveWebhookTokenConfigured ? "Configured" : "Not configured", }, { label: "SSO Bridge", value: diagnostics.effectiveSsoBridgeEnabled ? "Enabled" : "Disabled", }, { label: "Paymenter SSO URL", value: trimString(diagnostics.effectivePaymenterSsoUrl) || "Not set", wide: true, }, { label: "SSO Verify Path", value: trimString(diagnostics.effectiveSsoVerifyPath) || "Not set", wide: true, }, { label: "Scoped Package Count", value: String(diagnostics.scopedPackageCount ?? 0), }, { label: "Scoped Add-on Count", value: String(diagnostics.scopedAddonCount ?? 0), }, { label: "Scoped One-Time Count", value: String(diagnostics.scopedOneTimeCount ?? 0), }, { label: "Scoped Audience Count", value: String(diagnostics.scopedAudienceCount ?? 0), }, { label: "Scoped Extra Offer Count", value: String(diagnostics.scopedExtraOfferCount ?? 0), }, { label: "Profile Scopes Present", value: Array.isArray(diagnostics.availableProfileScopes) && diagnostics.availableProfileScopes.length ? diagnostics.availableProfileScopes.join(", ") : "None", wide: true, }, { label: "SSO Redirect Allowlist", value: Array.isArray(diagnostics.effectiveSsoRedirectAllowlist) && diagnostics.effectiveSsoRedirectAllowlist.length ? diagnostics.effectiveSsoRedirectAllowlist.join(", ") : "None", wide: true, }, ]; renderSummaryItems( connectorRuntimeDiagnosticsEl, items, "Refresh config to load diagnostics." ); } function getSelectablePaymenterProfiles() { const seen = {}; const options = []; (catalogConfigState.runtime.availablePaymenterProfiles || []).forEach( function (profile) { const id = trimString(profile && profile.id); if (!id || Object.prototype.hasOwnProperty.call(seen, id)) { return; } seen[id] = true; options.push({ id: id, baseUrl: trimString(profile && profile.baseUrl), }); } ); const active = trimString( catalogConfigState.runtime.activePaymenterProfile ); if (active && !Object.prototype.hasOwnProperty.call(seen, active)) { options.push({ id: active, baseUrl: trimString(catalogConfigState.runtime.currentPaymenterBaseUrl), }); } return options; } function getSelectedPaymenterViewProfile() { const chosen = trimString( paymenterProfileViewEl && paymenterProfileViewEl.value ); if (chosen) { return chosen; } return trimString(catalogConfigState.runtime.activePaymenterProfile); } function getSelectableConnectorProfiles() { const merged = {}; availableConnectorProfiles.forEach(function (profile) { const id = trimString(profile && profile.id); if (!id) { return; } merged[id] = { id: id, publicBaseUrl: trimString(profile && profile.publicBaseUrl), }; }); const routingProfiles = connectorRoutingState && connectorRoutingState.profiles && typeof connectorRoutingState.profiles === "object" ? connectorRoutingState.profiles : {}; Object.keys(routingProfiles).forEach(function (profileId) { const id = trimString(profileId); if (!id) { return; } const existing = merged[id] && typeof merged[id] === "object" ? merged[id] : { id: id, publicBaseUrl: "" }; const routingBaseUrl = trimString( routingProfiles[profileId] && routingProfiles[profileId].baseUrl ); merged[id] = { id: id, publicBaseUrl: routingBaseUrl || trimString(existing.publicBaseUrl), }; }); return Object.keys(merged) .sort(function (left, right) { return left.localeCompare(right, undefined, { sensitivity: "base" }); }) .map(function (id) { return merged[id]; }); } function findSelectableConnectorProfile(profileId) { const desiredId = trimString(profileId); if (!desiredId) { return null; } return ( getSelectableConnectorProfiles().find(function (entry) { return trimString(entry && entry.id) === desiredId; }) || null ); } function renderRuntimeControls() { renderConnectorRoutingControls(); if (paymenterProfileSelectEl) { paymenterProfileSelectEl.innerHTML = ""; const defaultOption = document.createElement("option"); defaultOption.value = ""; defaultOption.textContent = "Use connector default"; paymenterProfileSelectEl.appendChild(defaultOption); (catalogConfigState.runtime.availablePaymenterProfiles || []).forEach( function (profile) { const id = trimString(profile && profile.id); if (!id) { return; } const option = document.createElement("option"); option.value = id; option.textContent = id + (trimString(profile.baseUrl) ? " (" + trimString(profile.baseUrl) + ")" : ""); paymenterProfileSelectEl.appendChild(option); } ); paymenterProfileSelectEl.value = trimString( catalogConfigState.runtime.activePaymenterProfile ); } if (catalogScopeProfileSelectEl) { const current = trimString(catalogConfigState.selectedCatalogScopeProfile) || trimString(catalogConfigState.runtime.activePaymenterProfile); catalogScopeProfileSelectEl.innerHTML = ""; getAvailableCatalogScopeProfiles().forEach(function (profileId) { const option = document.createElement("option"); option.value = profileId; option.textContent = profileId; catalogScopeProfileSelectEl.appendChild(option); }); if (!catalogScopeProfileSelectEl.options.length) { const fallbackOption = document.createElement("option"); fallbackOption.value = ""; fallbackOption.textContent = "No profile scopes available"; catalogScopeProfileSelectEl.appendChild(fallbackOption); } catalogScopeProfileSelectEl.value = Array.from( catalogScopeProfileSelectEl.options ).some(function (opt) { return trimString(opt.value) === current; }) ? current : trimString( catalogScopeProfileSelectEl.options[0] && catalogScopeProfileSelectEl.options[0].value ); catalogConfigState.selectedCatalogScopeProfile = trimString( catalogScopeProfileSelectEl.value ); } if (paymenterProfileStatusEl) { const active = trimString(catalogConfigState.runtime.activePaymenterProfile) || "connector default"; const baseUrl = trimString( catalogConfigState.runtime.currentPaymenterBaseUrl ); const scope = trimString(catalogConfigState.selectedCatalogScopeProfile) || active; paymenterProfileStatusEl.textContent = "Active backend: " + active + (baseUrl ? " | Base URL: " + baseUrl : "") + " | Editing catalog scope: " + scope; } if (paymenterProfileViewEl) { const current = trimString(paymenterProfileViewEl.value) || trimString(catalogConfigState.runtime.activePaymenterProfile); paymenterProfileViewEl.innerHTML = ""; const defaultOption = document.createElement("option"); defaultOption.value = ""; defaultOption.textContent = "Use connector default"; paymenterProfileViewEl.appendChild(defaultOption); getSelectablePaymenterProfiles().forEach(function (profile) { const id = trimString(profile && profile.id); if (!id) { return; } const option = document.createElement("option"); option.value = id; option.textContent = id + (trimString(profile.baseUrl) ? " (" + trimString(profile.baseUrl) + ")" : ""); paymenterProfileViewEl.appendChild(option); }); paymenterProfileViewEl.value = Array.from( paymenterProfileViewEl.options ).some(function (opt) { return trimString(opt.value) === current; }) ? current : ""; } renderRuntimeDiagnostics(); if (instanceConnectorProfileEl) { const current = trimString(instanceConnectorProfileEl.value); instanceConnectorProfileEl.innerHTML = ""; const none = document.createElement("option"); none.value = ""; none.textContent = "No profile target"; instanceConnectorProfileEl.appendChild(none); getSelectableConnectorProfiles().forEach(function (profile) { const id = trimString(profile && profile.id); if (!id) { return; } const option = document.createElement("option"); option.value = id; option.textContent = id + (trimString(profile.publicBaseUrl) ? " (" + trimString(profile.publicBaseUrl) + ")" : ""); instanceConnectorProfileEl.appendChild(option); }); instanceConnectorProfileEl.value = Array.from( instanceConnectorProfileEl.options ).some(function (opt) { return opt.value === current; }) ? current : ""; } if (activeInstanceDetailData) { populateInstanceConnectorControls( resolveDetailInstance(activeInstanceDetailData) ); } } function trimString(value) { return String(value || "").trim(); } function parseCsv(value) { return trimString(value) .split(",") .map(function (entry) { return trimString(entry); }) .filter(Boolean); } function toIdString(value) { const parsed = Number.parseInt(String(value || "").trim(), 10); return Number.isFinite(parsed) && parsed > 0 ? String(parsed) : ""; } function mapToRows(productMap, labelMap, summaryMap, priceMap) { const rows = []; const keys = new Set(); Object.keys(productMap || {}).forEach(function (key) { keys.add(key); }); Object.keys(labelMap || {}).forEach(function (key) { keys.add(key); }); Object.keys(summaryMap || {}).forEach(function (key) { keys.add(key); }); Object.keys(priceMap || {}).forEach(function (key) { keys.add(key); }); keys.forEach(function (code) { rows.push({ code: trimString(code), productId: toIdString(productMap && productMap[code]), label: trimString(labelMap && labelMap[code]), summary: trimString(summaryMap && summaryMap[code]), price: trimString(priceMap && priceMap[code]), }); }); return rows; } function packageSummaryMapToRows(summaryMap) { const rows = []; Object.keys(summaryMap || {}).forEach(function (code) { rows.push({ code: trimString(code), summary: trimString(summaryMap[code]), }); }); return rows; } function getDefaultPackageMeta(code) { const normalized = trimString(code); for (let index = 0; index < defaultPackageRows.length; index += 1) { if (trimString(defaultPackageRows[index].code) === normalized) { return defaultPackageRows[index]; } } return null; } function packageMapToRows(packageProductMap, packagePriceMap) { const source = packageProductMap && typeof packageProductMap === "object" ? packageProductMap : {}; const priceSource = packagePriceMap && typeof packagePriceMap === "object" ? packagePriceMap : {}; const keys = new Set(); Object.keys(source).forEach(function (code) { keys.add(code); }); Object.keys(priceSource).forEach(function (code) { keys.add(code); }); return Array.from(keys).map(function (code) { return { code: trimString(code), productId: toIdString(source[code]), summary: "", price: trimString(priceSource[code]), }; }); } function ensureDefaultPackageRows(rows, packageSummaryRows) { const list = Array.isArray(rows) ? rows : []; const summaryList = Array.isArray(packageSummaryRows) ? packageSummaryRows : []; const existing = new Map(); const summaries = new Map(); summaryList.forEach(function (row) { const code = trimString(row && row.code); if (!code || summaries.has(code)) { return; } summaries.set(code, trimString(row && row.summary)); }); list.forEach(function (row) { const code = trimString(row && row.code); if (!code || existing.has(code)) { return; } existing.set(code, { code: code, productId: toIdString(row.productId), summary: trimString(row.summary) || summaries.get(code) || "", price: trimString(row.price), }); }); const ordered = []; existing.forEach(function (value, key) { const defaults = getDefaultPackageMeta(key); if (!trimString(value.summary)) { value.summary = summaries.get(key) || trimString(defaults && defaults.summary); } ordered.push(value); }); return ordered; } function audienceMapToRows(audienceCatalogs) { return Object.keys(audienceCatalogs || {}).map(function (key) { const entry = audienceCatalogs[key] || {}; return { key: trimString(key), packages: Array.isArray(entry.packages) ? entry.packages .map(function (item) { return trimString(item); }) .filter(Boolean) : [], addons: Array.isArray(entry.addons) ? entry.addons .map(function (item) { return trimString(item); }) .filter(Boolean) : [], oneTime: Array.isArray(entry.oneTime) ? entry.oneTime .map(function (item) { return trimString(item); }) .filter(Boolean) : [], extraOfferCodes: Array.isArray(entry.extraOfferCodes) ? entry.extraOfferCodes .map(function (item) { return trimString(item); }) .filter(Boolean) : [], }; }); } function createAudienceRow(key) { return { key: trimString(key), packages: [], addons: [], oneTime: [], extraOfferCodes: [], }; } function getDefaultAudienceMeta(key) { return ( DEFAULT_AUDIENCES.find(function (entry) { return trimString(entry.key) === trimString(key); }) || null ); } function isDefaultAudienceKey(key) { return Boolean(getDefaultAudienceMeta(key)); } function ensureDefaultAudienceRows(rows) { const list = Array.isArray(rows) ? rows : []; const existingMap = new Map(); list.forEach(function (row) { const key = trimString(row && row.key); if (!key || existingMap.has(key)) { return; } existingMap.set(key, { key: key, packages: Array.isArray(row.packages) ? row.packages .map(function (item) { return trimString(item); }) .filter(Boolean) : [], addons: Array.isArray(row.addons) ? row.addons .map(function (item) { return trimString(item); }) .filter(Boolean) : [], oneTime: Array.isArray(row.oneTime) ? row.oneTime .map(function (item) { return trimString(item); }) .filter(Boolean) : [], extraOfferCodes: Array.isArray(row.extraOfferCodes) ? row.extraOfferCodes .map(function (item) { return trimString(item); }) .filter(Boolean) : [], }); }); const ordered = []; DEFAULT_AUDIENCES.forEach(function (entry) { if (existingMap.has(entry.key)) { ordered.push(existingMap.get(entry.key)); existingMap.delete(entry.key); return; } ordered.push(createAudienceRow(entry.key)); }); existingMap.forEach(function (value) { ordered.push(value); }); return ordered; } function getAudienceRowByKey(key) { const normalized = trimString(key); if (!normalized) { return null; } for ( let index = 0; index < catalogConfigState.audienceRows.length; index += 1 ) { const row = catalogConfigState.audienceRows[index]; if (trimString(row && row.key) === normalized) { return row; } } return null; } function getFirstAvailableAudienceKey() { const first = Array.isArray(catalogConfigState.audienceRows) && catalogConfigState.audienceRows.length ? catalogConfigState.audienceRows[0] : null; return trimString(first && first.key); } function setSelectedAudienceKey(key) { const normalized = trimString(key); const fallback = getFirstAvailableAudienceKey(); catalogConfigState.selectedAudienceKey = getAudienceRowByKey(normalized) ? normalized : fallback; } function toggleAudienceSelectionValue(audienceKey, field, value, checked) { const row = getAudienceRowByKey(audienceKey); const normalizedField = trimString(field); const normalizedValue = trimString(value); if ( !row || !normalizedValue || !Object.prototype.hasOwnProperty.call(row, normalizedField) ) { return; } const nextValues = Array.isArray(row[normalizedField]) ? row[normalizedField].slice() : []; const index = nextValues.indexOf(normalizedValue); if (checked) { if (index === -1) { nextValues.push(normalizedValue); } } else if (index !== -1) { nextValues.splice(index, 1); } row[normalizedField] = nextValues; } function getPackageAudienceOptions() { return ( Array.isArray(catalogConfigState.packageRows) ? catalogConfigState.packageRows : [] ) .map(function (row) { const code = trimString(row && row.code); if (!code) { return null; } const meta = getDefaultPackageMeta(code); const productId = toIdString(row.productId); const price = trimString(row.price); return { code: code, label: meta && meta.label ? meta.label : code, detail: (productId ? "Product ID " + productId : "Product ID not set yet") + (price ? " · " + price : ""), }; }) .filter(Boolean); } function resolveCatalogSummaryForPreview(entry, rows, fallbackSummary) { const code = trimString( entry && (entry.code || entry.tier || entry.packageTier) ); const directSummary = trimString(entry && entry.summary); if (directSummary) { return directSummary; } const row = (Array.isArray(rows) ? rows : []).find(function (candidate) { return trimString(candidate && candidate.code) === code; }); if (row && trimString(row.summary)) { return trimString(row.summary); } if (trimString(fallbackSummary)) { return trimString(fallbackSummary); } return "No summary configured."; } function resolvePackageSummaryForPreview(entry) { const code = trimString( entry && (entry.code || entry.tier || entry.packageTier) ); const meta = getDefaultPackageMeta(code); return resolveCatalogSummaryForPreview( entry, catalogConfigState.packageRows, meta && meta.summary ? meta.summary : "" ); } function resolveAddonSummaryForPreview(entry) { return resolveCatalogSummaryForPreview( entry, catalogConfigState.addonRows, "" ); } function resolveOneTimeSummaryForPreview(entry) { return resolveCatalogSummaryForPreview( entry, catalogConfigState.oneTimeRows, "" ); } function resolveCatalogPriceForPreview(entry, rows) { const directPrice = trimString(entry && entry.price); if (directPrice) { return directPrice; } const code = trimString( entry && (entry.code || entry.tier || entry.packageTier) ); const row = (Array.isArray(rows) ? rows : []).find(function (candidate) { return trimString(candidate && candidate.code) === code; }); return trimString(row && row.price); } function findCatalogPreviewEntryForRow(kind, row) { const code = trimString(row && row.code); const productId = toIdString(row && row.productId); if (kind === "package") { const subscriptions = Array.isArray( connectorCatalogPreview && connectorCatalogPreview.subscriptions ) ? connectorCatalogPreview.subscriptions : []; return ( subscriptions.find(function (entry) { const entryProductId = toIdString(entry && entry.productId); const entryCode = trimString( entry && (entry.code || entry.tier || entry.packageTier) ); if (productId && entryProductId === productId) { return true; } return Boolean(code && entryCode === code); }) || null ); } const addons = Array.isArray( connectorCatalogPreview && connectorCatalogPreview.addons ) ? connectorCatalogPreview.addons : []; return ( addons.find(function (entry) { const entryProductId = toIdString(entry && entry.productId); const entryCode = trimString(entry && entry.code); const monthly = isMonthlyAddonPreviewEntry(entry); if (kind === "addon" && !monthly) { return false; } if (kind === "one_time" && monthly) { return false; } if (productId && entryProductId === productId) { return true; } return Boolean(code && entryCode === code); }) || null ); } function prettifyCatalogSourceLabel(value) { const normalized = trimString(value).toLowerCase(); if (!normalized) { return ""; } switch (normalized) { case "paymenter_catalog": return "Live Paymenter catalog"; case "config_price_map": return "Configured fallback price"; case "mapped_product_id": return "Mapped product lookup"; case "mapped_config": return "Configured fallback mapping"; case "configured_fallback": return "Configured fallback"; case "warning_fallback": return "Configured fallback"; case "unavailable": return "Unavailable"; default: return normalized.replace(/[_-]+/g, " "); } } function getCatalogPricingClarity(kind, row) { const entry = findCatalogPreviewEntryForRow(kind, row); const fallbackPrice = trimString(row && row.price); if (!entry) { return { level: fallbackPrice ? "fallback" : "missing", statusLabel: fallbackPrice ? "Configured fallback active" : "Paymenter product data unavailable", title: "", price: fallbackPrice, priceSource: fallbackPrice ? "Configured fallback price" : "Unavailable", origin: "", hasFallback: Boolean(fallbackPrice), detail: fallbackPrice ? "Paymenter does not expose product data for this row, so the configured fallback is shown by design." : "Paymenter does not expose product data for this row yet, and no fallback price is saved yet.", }; } const title = trimString( entry.name || entry.label || entry.code || entry.tier ); const price = trimString(entry.price); const priceSourceKey = trimString(entry.priceSource || "").toLowerCase(); const priceSource = prettifyCatalogSourceLabel(priceSourceKey || ""); const origin = prettifyCatalogSourceLabel(entry.source || ""); const isFallbackSource = priceSourceKey === "config_price_map" || priceSourceKey === "mapped_config"; const isLiveSource = priceSourceKey === "paymenter_catalog" || priceSourceKey === "mapped_product_id" || (!isFallbackSource && price !== ""); if (isFallbackSource) { return { level: "fallback", statusLabel: fallbackPrice ? "Configured fallback in use" : "Configured fallback active", title: title, price: price || fallbackPrice, priceSource: priceSource || "Configured fallback price", origin: origin, hasFallback: Boolean(fallbackPrice), detail: fallbackPrice ? "Paymenter does not expose product data for this row, so the configured fallback is shown by design." : "Paymenter does not expose product data for this row, so the configured fallback price is active.", }; } if (isLiveSource && price) { return { level: "live", statusLabel: "Live Paymenter price active", title: title, price: price, priceSource: priceSource, origin: origin, hasFallback: Boolean(fallbackPrice), detail: fallbackPrice ? "A fallback override is saved, but the live Paymenter price is currently taking precedence." : "This row is currently resolving a live Paymenter price.", }; } if (fallbackPrice) { return { level: "fallback", statusLabel: "Configured fallback in use", title: title, price: fallbackPrice, priceSource: "Configured fallback price", origin: origin, hasFallback: true, detail: "Paymenter returned this row without a usable price, so the configured fallback is shown.", }; } if (price) { return { level: "live", statusLabel: "Live Paymenter price active", title: title, price: price, priceSource: priceSource || "Live Paymenter price", origin: origin, hasFallback: Boolean(fallbackPrice), detail: fallbackPrice ? "A fallback override is saved, but the live Paymenter price is currently taking precedence." : "This row is currently resolving a live Paymenter price.", }; } return { level: "missing", statusLabel: "Paymenter price unavailable", title: title, price: "", priceSource: priceSource || "Unavailable", origin: origin, hasFallback: false, detail: "Paymenter returned the row, but it did not include a usable price and no fallback price is saved yet.", }; } function buildCatalogLivePriceHelperText(kind, row) { const clarity = getCatalogPricingClarity(kind, row); const fallbackNote = "Configured fallback is expected when Paymenter does not expose product data for this row."; let text = clarity.statusLabel; if (clarity.title) { text += " | Catalog item: " + clarity.title; } if (clarity.price) { text += " | Price: " + clarity.price; } if (clarity.priceSource) { text += " | Price source: " + clarity.priceSource; } if (clarity.origin) { text += " | Origin: " + clarity.origin; } if (clarity.detail) { text += ". " + clarity.detail; } if (clarity.level === "fallback" || clarity.level === "missing") { return text + " " + fallbackNote; } return text; } function buildCatalogLivePriceHelperElement(kind, row) { const clarity = getCatalogPricingClarity(kind, row); const wrap = document.createElement("div"); wrap.className = "chd-config-row__pricing chd-config-row__pricing--" + clarity.level; const chips = document.createElement("div"); chips.className = "chd-config-row__pricing-chips"; const primary = document.createElement("span"); primary.className = "chd-config-row__pricing-chip chd-config-row__pricing-chip--" + clarity.level; primary.textContent = clarity.statusLabel; chips.appendChild(primary); if (clarity.hasFallback) { const fallbackChip = document.createElement("span"); fallbackChip.className = "chd-config-row__pricing-chip chd-config-row__pricing-chip--neutral"; fallbackChip.textContent = "Configured fallback"; chips.appendChild(fallbackChip); } if (clarity.priceSource) { const sourceChip = document.createElement("span"); sourceChip.className = "chd-config-row__pricing-chip chd-config-row__pricing-chip--neutral"; sourceChip.textContent = clarity.priceSource; chips.appendChild(sourceChip); } const detail = document.createElement("div"); detail.className = "chd-note chd-config-row__helper"; detail.textContent = buildCatalogLivePriceHelperText(kind, row); wrap.appendChild(chips); wrap.appendChild(detail); return wrap; } function buildAudienceOptionList(rows, emptyLabel) { if (!Array.isArray(rows) || !rows.length) { return []; } return rows .map(function (row) { const code = trimString(row && row.code); if (!code) { return null; } const label = trimString(row.label) || code; const productId = toIdString(row.productId); const price = trimString(row.price); return { code: code, label: label, detail: (productId ? "Product ID " + productId : emptyLabel) + (price ? " · " + price : ""), }; }) .filter(Boolean); } function extraOffersToRows(extraOffers) { const list = Array.isArray(extraOffers) ? extraOffers : []; return list.map(function (entry) { return { code: trimString(entry.code), label: trimString(entry.label), url: trimString(entry.url), description: trimString(entry.description), billingType: trimString(entry.billingType || "one_time") === "monthly" ? "monthly" : "one_time", audiences: Array.isArray(entry.audiences) ? entry.audiences.join(", ") : "", }; }); } function renderPackageRows() { if (!packageListEl) { return; } packageListEl.innerHTML = ""; if (!catalogConfigState.packageRows.length) { packageListEl.textContent = "No package mappings configured."; return; } catalogConfigState.packageRows.forEach(function (row, index) { const wrap = document.createElement("div"); wrap.className = "chd-config-row chd-config-row--product"; const codeInput = document.createElement("input"); codeInput.placeholder = "Package code"; codeInput.value = trimString(row.code); codeInput.addEventListener("input", function () { catalogConfigState.packageRows[index].code = trimString( codeInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); const productInput = document.createElement("input"); productInput.type = "number"; productInput.min = "1"; productInput.step = "1"; productInput.placeholder = "Product ID"; productInput.value = toIdString(row.productId); productInput.addEventListener("input", function () { catalogConfigState.packageRows[index].productId = toIdString( productInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); const priceInput = document.createElement("input"); priceInput.placeholder = "Fallback override price (optional)"; priceInput.value = trimString(row.price); priceInput.addEventListener("input", function () { catalogConfigState.packageRows[index].price = trimString( priceInput.value ); renderAudiencePreview(); }); const label = document.createElement("div"); label.className = "chd-note chd-config-row__helper"; const meta = getDefaultPackageMeta(row.code); label.textContent = meta && meta.label ? meta.label + " · Fallback price is used only when Paymenter data does not provide one." : "Custom package code · Fallback price is used only when Paymenter data does not provide one."; const liveHelper = buildCatalogLivePriceHelperElement("package", row); const summaryInput = document.createElement("textarea"); summaryInput.className = "chd-config-row__summary"; summaryInput.rows = 4; summaryInput.placeholder = "Summary / bullets shown to users. Use one line per bullet if you want multiline output."; summaryInput.value = trimString( row.summary || (meta && meta.summary) || "" ); summaryInput.addEventListener("input", function () { catalogConfigState.packageRows[index].summary = trimString( summaryInput.value ); renderAudiencePreview(); }); const removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.className = "button"; removeBtn.textContent = "Remove"; removeBtn.addEventListener("click", function () { catalogConfigState.packageRows.splice(index, 1); renderPackageRows(); renderAudienceEditor(); renderAudiencePreview(); }); wrap.appendChild(codeInput); wrap.appendChild(productInput); wrap.appendChild(priceInput); wrap.appendChild(label); wrap.appendChild(liveHelper); wrap.appendChild(removeBtn); wrap.appendChild(summaryInput); packageListEl.appendChild(wrap); }); } function renderAddonRows() { if (!addonListEl) { return; } addonListEl.innerHTML = ""; if (!catalogConfigState.addonRows.length) { addonListEl.textContent = "No monthly add-ons configured."; return; } catalogConfigState.addonRows.forEach(function (row, index) { const wrap = document.createElement("div"); wrap.className = "chd-config-row chd-config-row--product"; const codeInput = document.createElement("input"); codeInput.placeholder = "Code (e.g. capacity_100gb_monthly_basic)"; codeInput.value = row.code; codeInput.addEventListener("input", function () { catalogConfigState.addonRows[index].code = trimString(codeInput.value); renderAudienceEditor(); renderAudiencePreview(); }); const productInput = document.createElement("input"); productInput.type = "number"; productInput.min = "1"; productInput.step = "1"; productInput.placeholder = "Product ID"; productInput.value = row.productId; productInput.addEventListener("input", function () { catalogConfigState.addonRows[index].productId = toIdString( productInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); const labelInput = document.createElement("input"); labelInput.placeholder = "Display label"; labelInput.value = row.label; labelInput.addEventListener("input", function () { catalogConfigState.addonRows[index].label = trimString( labelInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); const priceInput = document.createElement("input"); priceInput.placeholder = "Fallback override price (optional)"; priceInput.value = trimString(row.price); priceInput.addEventListener("input", function () { catalogConfigState.addonRows[index].price = trimString( priceInput.value ); renderAudiencePreview(); }); const summaryInput = document.createElement("textarea"); summaryInput.className = "chd-config-row__summary"; summaryInput.rows = 4; summaryInput.placeholder = "Summary / bullets shown to users. Use one line per bullet if you want multiline output."; summaryInput.value = trimString(row.summary); summaryInput.addEventListener("input", function () { catalogConfigState.addonRows[index].summary = trimString( summaryInput.value ); renderAudiencePreview(); }); const helper = document.createElement("div"); helper.className = "chd-note chd-config-row__helper"; helper.textContent = "This text appears in the Monthly Add-ons preview and user-facing catalog output. Fallback price is only used when Paymenter data does not provide one."; const liveHelper = buildCatalogLivePriceHelperElement("addon", row); const removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.className = "button"; removeBtn.textContent = "Remove"; removeBtn.addEventListener("click", function () { catalogConfigState.addonRows.splice(index, 1); renderAddonRows(); renderAudienceEditor(); renderAudiencePreview(); }); wrap.appendChild(codeInput); wrap.appendChild(productInput); wrap.appendChild(labelInput); wrap.appendChild(priceInput); wrap.appendChild(removeBtn); wrap.appendChild(summaryInput); wrap.appendChild(helper); wrap.appendChild(liveHelper); addonListEl.appendChild(wrap); }); } function renderOneTimeRows() { if (!oneTimeListEl) { return; } oneTimeListEl.innerHTML = ""; if (!catalogConfigState.oneTimeRows.length) { oneTimeListEl.textContent = "No one-time products configured."; return; } catalogConfigState.oneTimeRows.forEach(function (row, index) { const wrap = document.createElement("div"); wrap.className = "chd-config-row chd-config-row--product"; const codeInput = document.createElement("input"); codeInput.placeholder = "Code (e.g. credits_1000)"; codeInput.value = row.code; codeInput.addEventListener("input", function () { catalogConfigState.oneTimeRows[index].code = trimString( codeInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); const productInput = document.createElement("input"); productInput.type = "number"; productInput.min = "1"; productInput.step = "1"; productInput.placeholder = "Product ID"; productInput.value = row.productId; productInput.addEventListener("input", function () { catalogConfigState.oneTimeRows[index].productId = toIdString( productInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); const labelInput = document.createElement("input"); labelInput.placeholder = "Display label"; labelInput.value = row.label; labelInput.addEventListener("input", function () { catalogConfigState.oneTimeRows[index].label = trimString( labelInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); const priceInput = document.createElement("input"); priceInput.placeholder = "Fallback override price (optional)"; priceInput.value = trimString(row.price); priceInput.addEventListener("input", function () { catalogConfigState.oneTimeRows[index].price = trimString( priceInput.value ); renderAudiencePreview(); }); const summaryInput = document.createElement("textarea"); summaryInput.className = "chd-config-row__summary"; summaryInput.rows = 4; summaryInput.placeholder = "Summary / bullets shown to users. Use one line per bullet if you want multiline output."; summaryInput.value = trimString(row.summary); summaryInput.addEventListener("input", function () { catalogConfigState.oneTimeRows[index].summary = trimString( summaryInput.value ); renderAudiencePreview(); }); const helper = document.createElement("div"); helper.className = "chd-note chd-config-row__helper"; helper.textContent = "This text appears in the One-Time Products preview and user-facing catalog output. Fallback price is only used when Paymenter data does not provide one."; const liveHelper = buildCatalogLivePriceHelperElement("one_time", row); const removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.className = "button"; removeBtn.textContent = "Remove"; removeBtn.addEventListener("click", function () { catalogConfigState.oneTimeRows.splice(index, 1); renderOneTimeRows(); renderAudienceEditor(); renderAudiencePreview(); }); wrap.appendChild(codeInput); wrap.appendChild(productInput); wrap.appendChild(labelInput); wrap.appendChild(priceInput); wrap.appendChild(removeBtn); wrap.appendChild(summaryInput); wrap.appendChild(helper); wrap.appendChild(liveHelper); oneTimeListEl.appendChild(wrap); }); } function renderAudienceRows() { if (!audienceListEl) { return; } audienceListEl.innerHTML = ""; if (!catalogConfigState.audienceRows.length) { audienceListEl.textContent = "No audiences configured yet."; if (audienceSelectEl) { audienceSelectEl.innerHTML = ""; } if (audienceRemoveCurrentButton) { audienceRemoveCurrentButton.disabled = true; } updatePreviewAudienceSelect(); renderAudienceEditor(); return; } setSelectedAudienceKey(catalogConfigState.selectedAudienceKey); if (audienceSelectEl) { const current = trimString(catalogConfigState.selectedAudienceKey); audienceSelectEl.innerHTML = ""; catalogConfigState.audienceRows.forEach(function (row) { const key = trimString(row.key); if (!key) { return; } const option = document.createElement("option"); option.value = key; option.textContent = key; audienceSelectEl.appendChild(option); }); audienceSelectEl.value = current || getFirstAvailableAudienceKey(); } catalogConfigState.audienceRows.forEach(function (row) { const key = trimString(row.key); if (!key) { return; } const button = document.createElement("button"); button.type = "button"; button.className = "button " + (trimString(catalogConfigState.selectedAudienceKey) === key ? "button-primary" : ""); button.textContent = key; button.addEventListener("click", function () { setSelectedAudienceKey(key); if (previewAudienceEl) { previewAudienceEl.value = key; } renderAudienceRows(); renderAudiencePreview(); }); audienceListEl.appendChild(button); }); if (audienceRemoveCurrentButton) { const selectedKey = trimString(catalogConfigState.selectedAudienceKey); audienceRemoveCurrentButton.disabled = !selectedKey || isDefaultAudienceKey(selectedKey); } updatePreviewAudienceSelect(); renderAudienceEditor(); } function renderAudienceEditorSection( parent, audienceKey, field, title, note, options, emptyText ) { const section = document.createElement("section"); section.className = "chd-audience-editor-section"; const heading = document.createElement("h5"); heading.textContent = title; section.appendChild(heading); const noteEl = document.createElement("div"); noteEl.className = "chd-note"; noteEl.textContent = note; section.appendChild(noteEl); if (!Array.isArray(options) || !options.length) { const empty = document.createElement("div"); empty.className = "chd-note"; empty.textContent = emptyText; section.appendChild(empty); parent.appendChild(section); return; } const selectedRow = getAudienceRowByKey(audienceKey); const selectedValues = new Set( Array.isArray(selectedRow && selectedRow[field]) ? selectedRow[field] : [] ); const optionWrap = document.createElement("div"); optionWrap.className = "chd-audience-option-list"; options.forEach(function (optionData) { const label = document.createElement("label"); label.className = "chd-audience-option"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = selectedValues.has(optionData.code); checkbox.addEventListener("change", function () { toggleAudienceSelectionValue( audienceKey, field, optionData.code, checkbox.checked ); renderAudienceEditor(); renderAudiencePreview(); }); const copy = document.createElement("div"); copy.className = "chd-audience-option-copy"; const titleEl = document.createElement("strong"); titleEl.textContent = optionData.label; const detailEl = document.createElement("span"); detailEl.textContent = optionData.code + (optionData.detail ? " · " + optionData.detail : ""); copy.appendChild(titleEl); copy.appendChild(detailEl); label.appendChild(checkbox); label.appendChild(copy); optionWrap.appendChild(label); }); section.appendChild(optionWrap); parent.appendChild(section); } function renderAudienceEditor() { if (!audienceEditorEl) { return; } audienceEditorEl.innerHTML = ""; const selectedKey = trimString(catalogConfigState.selectedAudienceKey); const selectedAudience = getAudienceRowByKey(selectedKey); if (!selectedAudience) { audienceEditorEl.textContent = "Select or create an audience to configure its visible catalog items."; return; } const hero = document.createElement("div"); hero.className = "chd-audience-editor-hero"; const title = document.createElement("h5"); title.textContent = selectedKey; hero.appendChild(title); const meta = getDefaultAudienceMeta(selectedKey); const description = document.createElement("div"); description.className = "chd-note"; description.textContent = meta && meta.description ? meta.description : "Custom audience key for catalog targeting."; hero.appendChild(description); if (selectedKey === "NuQloud-team") { const teamNote = document.createElement("div"); teamNote.className = "chd-note chd-note--warning"; teamNote.textContent = "Team member email/QORT roster tracking should live on order or instance records, not in catalog visibility config. For now, keep that data in fulfillment notes and operations tracking."; hero.appendChild(teamNote); } audienceEditorEl.appendChild(hero); renderAudienceEditorSection( audienceEditorEl, selectedKey, "packages", "Packages", "Choose which subscription package codes this audience should see.", getPackageAudienceOptions(), "No package codes are available yet." ); renderAudienceEditorSection( audienceEditorEl, selectedKey, "addons", "Monthly Add-ons", "Choose which recurring add-ons this audience should see.", buildAudienceOptionList( catalogConfigState.addonRows, "Product ID not set yet" ), "Add global monthly add-ons above before assigning them to this audience." ); renderAudienceEditorSection( audienceEditorEl, selectedKey, "oneTime", "One-Time Products", "Choose which one-time products this audience should see.", buildAudienceOptionList( catalogConfigState.oneTimeRows, "Product ID not set yet" ), "Add global one-time products above before assigning them to this audience." ); renderAudienceEditorSection( audienceEditorEl, selectedKey, "extraOfferCodes", "Extra Offers", "Choose which extra CHD offers should be shown to this audience.", (Array.isArray(catalogConfigState.extraOffers) ? catalogConfigState.extraOffers : [] ) .map(function (row) { const code = trimString(row && row.code); if (!code) { return null; } return { code: code, label: trimString(row.label) || code, detail: trimString(row.billingType || "one_time") === "monthly" ? "Monthly offer" : "One-time offer", }; }) .filter(Boolean), "Add global extra offers above before assigning them to this audience." ); } function renderExtraOffersRows() { if (!extraOffersListEl) { return; } extraOffersListEl.innerHTML = ""; if (!catalogConfigState.extraOffers.length) { extraOffersListEl.textContent = "No extra offers configured."; return; } catalogConfigState.extraOffers.forEach(function (row, index) { const wrap = document.createElement("div"); wrap.className = "chd-config-row chd-config-row--offer"; const codeInput = document.createElement("input"); codeInput.placeholder = "Offer code"; codeInput.value = row.code; codeInput.addEventListener("input", function () { catalogConfigState.extraOffers[index].code = trimString( codeInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); const labelInput = document.createElement("input"); labelInput.placeholder = "Label"; labelInput.value = row.label; labelInput.addEventListener("input", function () { catalogConfigState.extraOffers[index].label = trimString( labelInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); const urlInput = document.createElement("input"); urlInput.placeholder = "URL"; urlInput.value = row.url; urlInput.addEventListener("input", function () { catalogConfigState.extraOffers[index].url = trimString(urlInput.value); renderAudienceEditor(); renderAudiencePreview(); }); const billingTypeSelect = document.createElement("select"); [ { value: "one_time", label: "One-time" }, { value: "monthly", label: "Monthly" }, ].forEach(function (optionData) { const option = document.createElement("option"); option.value = optionData.value; option.textContent = optionData.label; if (trimString(row.billingType || "one_time") === optionData.value) { option.selected = true; } billingTypeSelect.appendChild(option); }); billingTypeSelect.addEventListener("change", function () { catalogConfigState.extraOffers[index].billingType = trimString( billingTypeSelect.value || "one_time" ); renderAudienceEditor(); renderAudiencePreview(); }); const removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.className = "button"; removeBtn.textContent = "Remove"; removeBtn.addEventListener("click", function () { catalogConfigState.extraOffers.splice(index, 1); renderExtraOffersRows(); renderAudienceEditor(); renderAudiencePreview(); }); wrap.appendChild(codeInput); wrap.appendChild(labelInput); wrap.appendChild(urlInput); wrap.appendChild(billingTypeSelect); wrap.appendChild(removeBtn); extraOffersListEl.appendChild(wrap); const descriptionInput = document.createElement("input"); descriptionInput.placeholder = "Description"; descriptionInput.value = row.description; descriptionInput.addEventListener("input", function () { catalogConfigState.extraOffers[index].description = trimString( descriptionInput.value ); renderAudienceEditor(); renderAudiencePreview(); }); descriptionInput.className = "chd-config-row"; extraOffersListEl.appendChild(descriptionInput); }); } function updatePreviewAudienceSelect() { if (!previewAudienceEl) { return; } const current = trimString(previewAudienceEl.value) || trimString(catalogConfigState.selectedAudienceKey); previewAudienceEl.innerHTML = ""; const allOption = document.createElement("option"); allOption.value = ""; allOption.textContent = "Default (no audience key)"; previewAudienceEl.appendChild(allOption); catalogConfigState.audienceRows.forEach(function (entry) { const key = trimString(entry.key); if (!key) { return; } const option = document.createElement("option"); option.value = key; option.textContent = key; previewAudienceEl.appendChild(option); }); previewAudienceEl.value = current && Array.from(previewAudienceEl.options).some(function (opt) { return opt.value === current; }) ? current : ""; } function buildConfigFromForm() { persistCurrentCatalogScopeState(); const packageProductMap = {}; const packageSummaryMap = {}; const packagePriceMap = {}; (Array.isArray(catalogConfigState.packageRows) ? catalogConfigState.packageRows : [] ).forEach(function (row) { const code = trimString(row && row.code); const productId = toIdString(row && row.productId); const summary = trimString(row && row.summary); const price = trimString(row && row.price); if (code && summary) { packageSummaryMap[code] = summary; } if (code && price) { packagePriceMap[code] = price; } if (!code || !productId) { return; } packageProductMap[code] = Number.parseInt(productId, 10); }); const addonProductMap = {}; const addonLabelMap = {}; const addonSummaryMap = {}; const addonPriceMap = {}; catalogConfigState.addonRows.forEach(function (row) { const code = trimString(row.code); const productId = toIdString(row.productId); const label = trimString(row.label); const summary = trimString(row.summary); const price = trimString(row.price); if (!code) { return; } if (productId) { addonProductMap[code] = Number.parseInt(productId, 10); } if (label) { addonLabelMap[code] = label; } if (summary) { addonSummaryMap[code] = summary; } if (price) { addonPriceMap[code] = price; } }); const oneTimeProductMap = {}; const oneTimeLabelMap = {}; const oneTimeSummaryMap = {}; const oneTimePriceMap = {}; catalogConfigState.oneTimeRows.forEach(function (row) { const code = trimString(row.code); const productId = toIdString(row.productId); const label = trimString(row.label); const summary = trimString(row.summary); const price = trimString(row.price); if (!code) { return; } if (productId) { oneTimeProductMap[code] = Number.parseInt(productId, 10); } if (label) { oneTimeLabelMap[code] = label; } if (summary) { oneTimeSummaryMap[code] = summary; } if (price) { oneTimePriceMap[code] = price; } }); const audienceCatalogs = {}; ensureDefaultAudienceRows(catalogConfigState.audienceRows).forEach( function (row) { const key = trimString(row.key); if (!key) { return; } audienceCatalogs[key] = { packages: Array.isArray(row.packages) ? row.packages .map(function (item) { return trimString(item); }) .filter(Boolean) : [], addons: Array.isArray(row.addons) ? row.addons .map(function (item) { return trimString(item); }) .filter(Boolean) : [], oneTime: Array.isArray(row.oneTime) ? row.oneTime .map(function (item) { return trimString(item); }) .filter(Boolean) : [], extraOfferCodes: Array.isArray(row.extraOfferCodes) ? row.extraOfferCodes .map(function (item) { return trimString(item); }) .filter(Boolean) : [], }; } ); const extraOffers = catalogConfigState.extraOffers .map(function (row) { const code = trimString(row.code); const label = trimString(row.label); const url = trimString(row.url); if (!code || !label || !url) { return null; } return { code: code, label: label, url: url, description: trimString(row.description), billingType: trimString(row.billingType || "one_time") === "monthly" ? "monthly" : "one_time", audiences: parseCsv(row.audiences), }; }) .filter(Boolean); const profileConfigs = {}; Object.keys(catalogConfigState.profileConfigs || {}).forEach(function ( profileId ) { const normalized = trimString(profileId); if (!normalized) { return; } const scope = cloneCatalogProfileState( catalogConfigState.profileConfigs[normalized] ); const scopePackageProductMap = {}; const scopePackageSummaryMap = {}; const scopePackagePriceMap = {}; (Array.isArray(scope.packageRows) ? scope.packageRows : []).forEach( function (row) { const code = trimString(row.code); const productId = toIdString(row.productId); const summary = trimString(row.summary); const price = trimString(row.price); if (!code) { return; } if (productId) { scopePackageProductMap[code] = Number.parseInt(productId, 10); } if (summary) { scopePackageSummaryMap[code] = summary; } if (price) { scopePackagePriceMap[code] = price; } } ); const scopeAddonProductMap = {}; const scopeAddonLabelMap = {}; const scopeAddonSummaryMap = {}; const scopeAddonPriceMap = {}; (Array.isArray(scope.addonRows) ? scope.addonRows : []).forEach(function ( row ) { const code = trimString(row.code); const productId = toIdString(row.productId); const label = trimString(row.label); const summary = trimString(row.summary); const price = trimString(row.price); if (!code) { return; } if (productId) { scopeAddonProductMap[code] = Number.parseInt(productId, 10); } if (label) { scopeAddonLabelMap[code] = label; } if (summary) { scopeAddonSummaryMap[code] = summary; } if (price) { scopeAddonPriceMap[code] = price; } }); const scopeOneTimeProductMap = {}; const scopeOneTimeLabelMap = {}; const scopeOneTimeSummaryMap = {}; const scopeOneTimePriceMap = {}; (Array.isArray(scope.oneTimeRows) ? scope.oneTimeRows : []).forEach( function (row) { const code = trimString(row.code); const productId = toIdString(row.productId); const label = trimString(row.label); const summary = trimString(row.summary); const price = trimString(row.price); if (!code) { return; } if (productId) { scopeOneTimeProductMap[code] = Number.parseInt(productId, 10); } if (label) { scopeOneTimeLabelMap[code] = label; } if (summary) { scopeOneTimeSummaryMap[code] = summary; } if (price) { scopeOneTimePriceMap[code] = price; } } ); const scopeAudienceCatalogs = {}; ensureDefaultAudienceRows(scope.audienceRows).forEach(function (row) { const key = trimString(row.key); if (!key) { return; } scopeAudienceCatalogs[key] = { packages: Array.isArray(row.packages) ? row.packages .map(function (item) { return trimString(item); }) .filter(Boolean) : [], addons: Array.isArray(row.addons) ? row.addons .map(function (item) { return trimString(item); }) .filter(Boolean) : [], oneTime: Array.isArray(row.oneTime) ? row.oneTime .map(function (item) { return trimString(item); }) .filter(Boolean) : [], extraOfferCodes: Array.isArray(row.extraOfferCodes) ? row.extraOfferCodes .map(function (item) { return trimString(item); }) .filter(Boolean) : [], }; }); const scopeExtraOffers = ( Array.isArray(scope.extraOffers) ? scope.extraOffers : [] ) .map(function (row) { const code = trimString(row.code); const label = trimString(row.label); const url = trimString(row.url); if (!code || !label || !url) { return null; } return { code: code, label: label, url: url, description: trimString(row.description), billingType: trimString(row.billingType || "one_time") === "monthly" ? "monthly" : "one_time", audiences: parseCsv(row.audiences), }; }) .filter(Boolean); profileConfigs[normalized] = { packageProductMap: scopePackageProductMap, packageSummaryMap: scopePackageSummaryMap, packagePriceMap: scopePackagePriceMap, addonProductMap: scopeAddonProductMap, addonLabelMap: scopeAddonLabelMap, addonSummaryMap: scopeAddonSummaryMap, addonPriceMap: scopeAddonPriceMap, oneTimeProductMap: scopeOneTimeProductMap, oneTimeLabelMap: scopeOneTimeLabelMap, oneTimeSummaryMap: scopeOneTimeSummaryMap, oneTimePriceMap: scopeOneTimePriceMap, audienceCatalogs: scopeAudienceCatalogs, extraOffers: scopeExtraOffers, }; }); const useScopedConfigs = Object.keys(profileConfigs).length > 0; return { version: 1, runtime: { activePaymenterProfile: paymenterProfileSelectEl ? trimString(paymenterProfileSelectEl.value) : "", }, packageProductMap: useScopedConfigs ? {} : packageProductMap, packageSummaryMap: useScopedConfigs ? {} : packageSummaryMap, packagePriceMap: useScopedConfigs ? {} : packagePriceMap, addonProductMap: useScopedConfigs ? {} : addonProductMap, addonLabelMap: useScopedConfigs ? {} : addonLabelMap, addonSummaryMap: useScopedConfigs ? {} : addonSummaryMap, addonPriceMap: useScopedConfigs ? {} : addonPriceMap, oneTimeProductMap: useScopedConfigs ? {} : oneTimeProductMap, oneTimeLabelMap: useScopedConfigs ? {} : oneTimeLabelMap, oneTimeSummaryMap: useScopedConfigs ? {} : oneTimeSummaryMap, oneTimePriceMap: useScopedConfigs ? {} : oneTimePriceMap, audienceCatalogs: useScopedConfigs ? {} : audienceCatalogs, extraOffers: useScopedConfigs ? [] : extraOffers, profileConfigs: profileConfigs, }; } function applyAudiencePreviewRules(catalog, config, audienceKey) { const rules = config && config.audienceCatalogs && config.audienceCatalogs[audienceKey] ? config.audienceCatalogs[audienceKey] : null; const subscriptions = Array.isArray(catalog && catalog.subscriptions) ? catalog.subscriptions : []; const addons = Array.isArray(catalog && catalog.addons) ? catalog.addons : []; const extraOffers = Array.isArray(config && config.extraOffers) ? config.extraOffers : []; if (!rules) { return { subscriptions: subscriptions, addons: addons, extraOffers: audienceKey ? extraOffers.filter(function (offer) { return ( !Array.isArray(offer.audiences) || offer.audiences.length === 0 || offer.audiences.indexOf(audienceKey) !== -1 ); }) : extraOffers, }; } const packageSet = new Set( Array.isArray(rules.packages) ? rules.packages : [] ); const addonSet = new Set(Array.isArray(rules.addons) ? rules.addons : []); const oneTimeSet = new Set( Array.isArray(rules.oneTime) ? rules.oneTime : [] ); const offersSet = new Set( Array.isArray(rules.extraOfferCodes) ? rules.extraOfferCodes : [] ); return { subscriptions: subscriptions.filter(function (entry) { const code = trimString(entry.code || entry.tier || entry.packageTier); return code ? packageSet.has(code) : false; }), addons: addons.filter(function (entry) { const code = trimString(entry.code || entry.id || entry.productId); if (!code) { return false; } if (isMonthlyAddonPreviewEntry(entry)) { return addonSet.has(code); } return oneTimeSet.has(code); }), extraOffers: extraOffers.filter(function (offer) { return offersSet.has(trimString(offer.code)); }), }; } function renderAudiencePreview() { if (!catalogPreviewEl) { return; } const config = buildConfigFromForm(); const audienceKey = previewAudienceEl ? trimString(previewAudienceEl.value) : ""; const preview = applyAudiencePreviewRules( connectorCatalogPreview, config, audienceKey ); const normalizedPreview = { subscriptions: (Array.isArray(preview.subscriptions) ? preview.subscriptions : [] ).map(function (entry) { return entry && typeof entry === "object" ? { ...entry } : entry; }), addons: (Array.isArray(preview.addons) ? preview.addons : []).map( function (entry) { return entry && typeof entry === "object" ? { ...entry } : entry; } ), extraOffers: (Array.isArray(preview.extraOffers) ? preview.extraOffers : [] ).map(function (entry) { return entry && typeof entry === "object" ? { ...entry } : entry; }), }; const payload = { explanation: { audienceKey: audienceKey || "(default)", description: audienceKey ? "Preview for this audience key. Only mapped items for that audience are shown." : "Default preview without audience filtering.", }, whatUserSees: normalizedPreview, }; catalogPreviewEl.textContent = JSON.stringify(payload, null, 2); renderAudiencePreviewSurface(payload); } function buildPreviewCard(title, summary, meta) { const card = document.createElement("div"); card.className = "chd-preview-card"; const titleEl = document.createElement("div"); titleEl.className = "chd-preview-card-title"; titleEl.textContent = title; const summaryEl = document.createElement("div"); summaryEl.className = "chd-preview-card-meta chd-preview-card-meta--summary"; summaryEl.textContent = summary; const metaEl = document.createElement("div"); metaEl.className = "chd-preview-card-meta"; metaEl.textContent = meta; card.appendChild(titleEl); card.appendChild(summaryEl); card.appendChild(metaEl); return card; } function renderAudiencePreviewSurface(payload) { if (!catalogPreviewRenderEl) { return; } const userView = payload && payload.whatUserSees ? payload.whatUserSees : {}; const subscriptions = Array.isArray(userView.subscriptions) ? userView.subscriptions : []; const allAddons = Array.isArray(userView.addons) ? userView.addons : []; const offers = Array.isArray(userView.extraOffers) ? userView.extraOffers : []; const monthlyAddons = allAddons.filter(function (entry) { return isMonthlyAddonPreviewEntry(entry); }); const oneTimeProducts = allAddons.filter(function (entry) { if (isMonthlyAddonPreviewEntry(entry)) { return false; } const purchaseType = trimString(entry.purchaseType || "").toLowerCase(); const purchaseInterval = trimString( entry.purchaseInterval || "" ).toLowerCase(); return ( purchaseType === "one_time" || purchaseInterval === "one_time" || (!purchaseType && purchaseInterval !== "monthly") ); }); catalogPreviewRenderEl.innerHTML = ""; const explanation = document.createElement("div"); explanation.className = "chd-preview-card-meta"; explanation.textContent = payload && payload.explanation && payload.explanation.description ? payload.explanation.description : "Preview data unavailable."; catalogPreviewRenderEl.appendChild(explanation); const pricingNote = document.createElement("div"); pricingNote.className = "chd-preview-card-meta"; pricingNote.textContent = "Pricing, currency, and checkout paths are pulled from Paymenter when mapped product IDs resolve. When Paymenter does not expose product info for a row, the configured fallback is the expected display."; catalogPreviewRenderEl.appendChild(pricingNote); const buildSection = function (heading, entries, mapper, emptyText) { const section = document.createElement("section"); section.className = "chd-preview-section"; const h = document.createElement("h5"); h.textContent = heading; section.appendChild(h); if (!entries.length) { const empty = document.createElement("div"); empty.className = "chd-preview-card-meta"; empty.textContent = emptyText; section.appendChild(empty); catalogPreviewRenderEl.appendChild(section); return; } const grid = document.createElement("div"); grid.className = "chd-preview-grid"; entries.forEach(function (entry) { grid.appendChild(mapper(entry)); }); section.appendChild(grid); catalogPreviewRenderEl.appendChild(section); }; buildSection( "Subscription Packages", subscriptions, function (entry) { const title = trimString( entry.name || entry.label || entry.code || entry.tier || "Package" ); const summary = resolvePackageSummaryForPreview(entry); const price = resolveCatalogPriceForPreview( entry, catalogConfigState.packageRows ); const source = trimString(entry.source || ""); const sourceLabel = prettifyCatalogSourceLabel(source); const meta = "Code: " + trimString(entry.code || entry.tier || "") + " | Product ID: " + trimString(entry.productId || "") + (price ? " | " + price : "") + (sourceLabel ? " | source: " + sourceLabel : ""); return buildPreviewCard(title, summary, meta); }, "No packages for this audience." ); buildSection( "Monthly Add-ons", monthlyAddons, function (entry) { const title = trimString( entry.name || entry.label || entry.code || "Monthly Add-on" ); const summary = resolveAddonSummaryForPreview(entry); const price = resolveCatalogPriceForPreview( entry, catalogConfigState.addonRows ); const source = trimString(entry.source || ""); const sourceLabel = prettifyCatalogSourceLabel(source); const meta = "Code: " + trimString(entry.code || "") + " | Product ID: " + trimString(entry.productId || "") + (price ? " | " + price : "") + (sourceLabel ? " | source: " + sourceLabel : ""); return buildPreviewCard(title, summary, meta); }, "No monthly add-ons for this audience." ); buildSection( "One-Time Products", oneTimeProducts, function (entry) { const title = trimString( entry.name || entry.label || entry.code || "One-time Product" ); const summary = resolveOneTimeSummaryForPreview(entry); const price = resolveCatalogPriceForPreview( entry, catalogConfigState.oneTimeRows ); const source = trimString(entry.source || ""); const sourceLabel = prettifyCatalogSourceLabel(source); const meta = "Code: " + trimString(entry.code || "") + " | Product ID: " + trimString(entry.productId || "") + (price ? " | " + price : "") + (sourceLabel ? " | source: " + sourceLabel : ""); return buildPreviewCard(title, summary, meta); }, "No one-time products for this audience." ); buildSection( "Extra CHD Offers", offers, function (entry) { const title = trimString(entry.label || entry.code || "Offer"); const summary = trimString( entry.description || "No description configured." ); const billingType = trimString(entry.billingType || "one_time") === "monthly" ? "monthly" : "one-time"; const source = trimString(entry.source || ""); const sourceLabel = prettifyCatalogSourceLabel(source); const meta = "Code: " + trimString(entry.code || "") + " | Billing: " + billingType + " | URL: " + trimString(entry.url || "") + (sourceLabel ? " | source: " + sourceLabel : ""); return buildPreviewCard(title, summary, meta); }, "No extra offers for this audience." ); } function renderCatalogConfigState() { persistCurrentCatalogScopeState(); catalogConfigState.packageRows = ensureDefaultPackageRows( catalogConfigState.packageRows ); catalogConfigState.audienceRows = ensureDefaultAudienceRows( catalogConfigState.audienceRows ); setSelectedAudienceKey(catalogConfigState.selectedAudienceKey); renderRuntimeControls(); renderPackageRows(); renderAddonRows(); renderOneTimeRows(); renderAudienceRows(); renderExtraOffersRows(); renderAudiencePreview(); } async function loadConnectorCatalogConfig() { if (!catalogConfigStatusEl) { return; } if (!connectorCatalogConfigUrl) { catalogConfigStatusEl.textContent = "Connector catalog config endpoint is not configured."; return; } catalogConfigStatusEl.textContent = "Loading..."; try { const payload = await requestJson(connectorCatalogConfigUrl, { method: "GET", headers: getRequestHeaders("application/json"), credentials: "same-origin", }); applyConnectorPackageMeta( payload && payload.data && typeof payload.data === "object" ? payload.data : {} ); applyConnectorRuntimeMeta( payload && payload.data && typeof payload.data === "object" ? payload.data : {} ); const configValue = normalizeCatalogConfigPayload(payload.data || {}); const nextProfileConfigs = buildProfileConfigsFromConfigValue(configValue); const desiredScope = trimString(catalogConfigState.selectedCatalogScopeProfile) || trimString( configValue.runtime && configValue.runtime.activePaymenterProfile ) || trimString(catalogConfigState.runtime.activePaymenterProfile); catalogConfigState = { selectedCatalogScopeProfile: desiredScope, profileConfigs: nextProfileConfigs, runtime: catalogConfigState.runtime, packageRows: [], addonRows: [], oneTimeRows: [], audienceRows: [], extraOffers: [], selectedAudienceKey: "", }; applyCatalogScopeState(desiredScope); connectorCatalogPreview = payload && payload.data && payload.data.catalogPreview && typeof payload.data.catalogPreview === "object" ? payload.data.catalogPreview : { subscriptions: [], addons: [] }; renderCatalogConfigState(); catalogConfigStatusEl.textContent = JSON.stringify( payload.data || {}, null, 2 ); void loadPaymenterClients(); void loadPaymenterOrders(); void loadPaymenterInvoices(); void loadPaymenterInvoiceItems(); if (trimString(activeInstanceId)) { void loadPaymenterSnapshotForActiveInstance(); } } catch (error) { catalogConfigStatusEl.textContent = "Failed to load connector catalog config: " + (error && error.message ? error.message : String(error)); } } async function saveConnectorCatalogConfig() { if (!catalogConfigStatusEl) { return; } if (!saveConnectorCatalogConfigUrl) { catalogConfigStatusEl.textContent = "Connector catalog config save endpoint is not configured."; return; } const parsed = buildConfigFromForm(); catalogConfigStatusEl.textContent = "Saving..."; try { const payload = await requestJson(saveConnectorCatalogConfigUrl, { method: "POST", headers: getRequestHeaders("application/json"), credentials: "same-origin", body: JSON.stringify({ config: parsed, }), }); applyConnectorPackageMeta( payload && payload.data && typeof payload.data === "object" ? payload.data : {} ); applyConnectorRuntimeMeta( payload && payload.data && typeof payload.data === "object" ? payload.data : {} ); const configValue = normalizeCatalogConfigPayload(payload.data || {}); const nextProfileConfigs = buildProfileConfigsFromConfigValue(configValue); const desiredScope = trimString(catalogConfigState.selectedCatalogScopeProfile) || trimString( configValue.runtime && configValue.runtime.activePaymenterProfile ) || trimString(catalogConfigState.runtime.activePaymenterProfile); catalogConfigState = { selectedCatalogScopeProfile: desiredScope, profileConfigs: nextProfileConfigs, runtime: catalogConfigState.runtime, packageRows: [], addonRows: [], oneTimeRows: [], audienceRows: [], extraOffers: [], selectedAudienceKey: "", }; applyCatalogScopeState(desiredScope); connectorCatalogPreview = payload && payload.data && payload.data.catalogPreview && typeof payload.data.catalogPreview === "object" ? payload.data.catalogPreview : connectorCatalogPreview; renderCatalogConfigState(); catalogConfigStatusEl.textContent = JSON.stringify( payload.data || {}, null, 2 ); void loadPaymenterClients(); void loadPaymenterOrders(); void loadPaymenterInvoices(); void loadPaymenterInvoiceItems(); if (trimString(activeInstanceId)) { void loadPaymenterSnapshotForActiveInstance(); } } catch (error) { catalogConfigStatusEl.textContent = "Failed to save connector catalog config: " + (error && error.message ? error.message : String(error)); } } if (refreshButton) { refreshButton.addEventListener("click", function () { void loadOverview(); }); } if (connectorRoutingSaveButton) { connectorRoutingSaveButton.addEventListener("click", function () { void saveConnectorRoutingConfig(); }); } if (connectorAdminTokenSaveButton) { connectorAdminTokenSaveButton.addEventListener("click", function () { void saveConnectorAdminToken(); }); } if (billingAutoFulfillmentSaveButton) { billingAutoFulfillmentSaveButton.addEventListener("click", function () { void saveBillingAutoFulfillment(); }); } if (billingAutoFulfillmentRunButton) { billingAutoFulfillmentRunButton.addEventListener("click", function () { void runBillingAutoFulfillment(); }); } if (billingAutoFulfillmentBackfillButton) { billingAutoFulfillmentBackfillButton.addEventListener("click", function () { void backfillBillingAutoFulfillment(); }); } if (connectorRefreshButton) { connectorRefreshButton.addEventListener("click", function () { void loadConnectorSummary(); }); } if (instancesRefreshButton) { instancesRefreshButton.addEventListener("click", function () { void loadConnectorInstances(); }); } if (instanceLoadButton) { instanceLoadButton.addEventListener("click", function () { const selectedId = instanceSelectEl ? String(instanceSelectEl.value || "").trim() : ""; void loadConnectorInstanceDetail(selectedId); }); } if (instanceSelectEl) { instanceSelectEl.addEventListener("change", function () { const selectedId = String(instanceSelectEl.value || "").trim(); void loadConnectorInstanceDetail(selectedId); }); } if (instanceConnectorProfileEl) { instanceConnectorProfileEl.addEventListener("change", function () { const selectedProfile = trimString(instanceConnectorProfileEl.value); const profileEntry = findSelectableConnectorProfile(selectedProfile); if ( instanceConnectorUrlEl && profileEntry && trimString(instanceConnectorUrlEl.value) === "" ) { instanceConnectorUrlEl.value = trimString(profileEntry.publicBaseUrl); } }); } if (instanceConnectorSaveButton) { instanceConnectorSaveButton.addEventListener("click", function () { void saveInstanceConnectorTarget(); }); } if (instanceMetadataSaveButton) { instanceMetadataSaveButton.addEventListener("click", function () { void saveInstanceMetadata(); }); } if (instanceRemoveButton) { instanceRemoveButton.addEventListener("click", function () { const instanceId = trimString(activeInstanceId); if (!instanceId) { if (instanceConnectorStatusEl) { instanceConnectorStatusEl.textContent = "Load an instance before removing it from the active configuration."; } return; } if ( !window.confirm( "Remove " + instanceId + " from the active connector configuration?" ) ) { return; } void saveConnectorInstanceState(instanceId, "remove").catch(function ( error ) { if (instanceConnectorStatusEl) { instanceConnectorStatusEl.textContent = "Failed to update instance state: " + (error && error.message ? error.message : String(error)); } }); }); } if (paymenterProfileViewEl) { paymenterProfileViewEl.addEventListener("change", function () { void loadPaymenterClients(); void loadPaymenterOrders(); void loadPaymenterInvoices(); void loadPaymenterInvoiceItems(); void loadPaymenterSnapshotForActiveInstance(); }); } if (paymenterClientsRefreshButton) { paymenterClientsRefreshButton.addEventListener("click", function () { void loadPaymenterClients(); }); } if (paymenterOrdersRefreshButton) { paymenterOrdersRefreshButton.addEventListener("click", function () { void loadPaymenterOrders(); }); } if (paymenterSnapshotRefreshButton) { paymenterSnapshotRefreshButton.addEventListener("click", function () { void loadPaymenterSnapshotForActiveInstance(); }); } if (paymenterBackupsRefreshButton) { paymenterBackupsRefreshButton.addEventListener("click", function () { void loadPaymenterBackups(); }); } if (paymenterInvoicesRefreshButton) { paymenterInvoicesRefreshButton.addEventListener("click", function () { void loadPaymenterInvoices(); }); } if (paymenterInvoiceItemsRefreshButton) { paymenterInvoiceItemsRefreshButton.addEventListener("click", function () { void loadPaymenterInvoiceItems(); }); } if (paymenterSyncDryRunButton) { paymenterSyncDryRunButton.addEventListener("click", function () { void runPaymenterSourceSync(true).catch(function (error) { setPaymenterStatus( "Dry run failed: " + (error && error.message ? error.message : String(error)) ); }); }); } if (paymenterSyncRunButton) { paymenterSyncRunButton.addEventListener("click", function () { const scopeLabel = trimString(activeInstanceId) ? "instance " + trimString(activeInstanceId) : "all active connector instances"; if ( !window.confirm( "Run Paymenter source-of-truth sync for " + scopeLabel + "?" ) ) { return; } void runPaymenterSourceSync(false).catch(function (error) { setPaymenterStatus( "Source-of-truth sync failed: " + (error && error.message ? error.message : String(error)) ); }); }); } if (catalogConfigRefreshButton) { catalogConfigRefreshButton.addEventListener("click", function () { void loadConnectorCatalogConfig(); }); } if (catalogConfigSaveButton) { catalogConfigSaveButton.addEventListener("click", function () { void saveConnectorCatalogConfig(); }); } if (catalogScopeProfileSelectEl) { catalogScopeProfileSelectEl.addEventListener("change", function () { persistCurrentCatalogScopeState(); applyCatalogScopeState(trimString(catalogScopeProfileSelectEl.value)); renderCatalogConfigState(); }); } if (packageAddButton) { packageAddButton.addEventListener("click", function () { catalogConfigState.packageRows.push({ code: "", productId: "", price: "", summary: "", }); renderPackageRows(); renderAudienceEditor(); renderAudiencePreview(); }); } if (addonAddButton) { addonAddButton.addEventListener("click", function () { catalogConfigState.addonRows.push({ code: "", productId: "", label: "", price: "", summary: "", }); renderAddonRows(); renderAudienceEditor(); renderAudiencePreview(); }); } if (oneTimeAddButton) { oneTimeAddButton.addEventListener("click", function () { catalogConfigState.oneTimeRows.push({ code: "", productId: "", label: "", price: "", summary: "", }); renderOneTimeRows(); renderAudienceEditor(); renderAudiencePreview(); }); } if (audienceAddButton) { audienceAddButton.addEventListener("click", function () { const requestedKey = trimString( audienceCreateNameEl ? audienceCreateNameEl.value : "" ); if (!requestedKey) { if (audienceManageStatusEl) { audienceManageStatusEl.textContent = "Enter an audience name before creating it."; } return; } if (getAudienceRowByKey(requestedKey)) { if (audienceManageStatusEl) { audienceManageStatusEl.textContent = "Audience already exists: " + requestedKey; } setSelectedAudienceKey(requestedKey); renderAudienceRows(); renderAudiencePreview(); return; } catalogConfigState.audienceRows.push(createAudienceRow(requestedKey)); setSelectedAudienceKey(requestedKey); if (previewAudienceEl) { previewAudienceEl.value = requestedKey; } if (audienceCreateNameEl) { audienceCreateNameEl.value = ""; } if (audienceManageStatusEl) { audienceManageStatusEl.textContent = "Created audience: " + requestedKey; } renderAudienceRows(); renderAudiencePreview(); }); } if (audienceRemoveCurrentButton) { audienceRemoveCurrentButton.addEventListener("click", function () { const selectedKey = trimString(catalogConfigState.selectedAudienceKey); if (!selectedKey || isDefaultAudienceKey(selectedKey)) { if (audienceManageStatusEl) { audienceManageStatusEl.textContent = selectedKey ? "Default audiences stay available by default and cannot be removed here." : "Select an audience first."; } return; } catalogConfigState.audienceRows = catalogConfigState.audienceRows.filter( function (row) { return trimString(row && row.key) !== selectedKey; } ); setSelectedAudienceKey(""); if (previewAudienceEl) { previewAudienceEl.value = trimString( catalogConfigState.selectedAudienceKey ); } if (audienceManageStatusEl) { audienceManageStatusEl.textContent = "Removed audience: " + selectedKey; } renderAudienceRows(); renderAudiencePreview(); }); } if (extraOffersAddButton) { extraOffersAddButton.addEventListener("click", function () { catalogConfigState.extraOffers.push({ code: "", label: "", url: "", description: "", billingType: "one_time", audiences: "", }); renderExtraOffersRows(); renderAudienceEditor(); renderAudiencePreview(); }); } if (audienceSelectEl) { audienceSelectEl.addEventListener("change", function () { setSelectedAudienceKey(audienceSelectEl.value); if (previewAudienceEl) { previewAudienceEl.value = trimString( catalogConfigState.selectedAudienceKey ); } if (audienceManageStatusEl) { audienceManageStatusEl.textContent = ""; } renderAudienceRows(); renderAudiencePreview(); }); } if (previewAudienceEl) { previewAudienceEl.addEventListener("change", function () { renderAudiencePreview(); }); } if (previewRefreshButton) { previewRefreshButton.addEventListener("click", function () { renderAudiencePreview(); }); } if (operationsRefreshButton) { operationsRefreshButton.addEventListener("click", function () { if (!activeInstanceId) { window.alert("Select an instance first."); return; } void loadInstanceOperations(activeInstanceId, activeInstanceDetailData); }); } if (operationsFormEl) { operationsFormEl.addEventListener("submit", function (event) { event.preventDefault(); const instanceId = cleanString(activeInstanceId); if (!instanceId) { window.alert("Select an instance before adding a task."); return; } const title = cleanString( operationsTitleInputEl && operationsTitleInputEl.value ); if (!title) { window.alert("Task title is required."); return; } const selectedOption = operationsUserSelectEl && operationsUserSelectEl.selectedOptions.length > 0 ? operationsUserSelectEl.selectedOptions[0] : null; const task = { source: "manual", taskType: cleanString(operationsTypeSelectEl && operationsTypeSelectEl.value) || "manual", title: title, status: "pending", nextcloudUserId: selectedOption ? cleanString(selectedOption.dataset.nextcloudUserId) : "", paymenterCustomerId: selectedOption ? cleanString(selectedOption.dataset.paymenterCustomerId) : "", userLabel: selectedOption ? cleanString(selectedOption.dataset.userLabel) : "", productCode: cleanString( operationsCodeInputEl && operationsCodeInputEl.value ), orderRef: cleanString( operationsCodeInputEl && operationsCodeInputEl.value ), notes: cleanString( operationsNotesInputEl && operationsNotesInputEl.value ), }; void saveInstanceOperationTask(instanceId, task) .then(function () { if (operationsTitleInputEl) { operationsTitleInputEl.value = ""; } if (operationsCodeInputEl) { operationsCodeInputEl.value = ""; } if (operationsNotesInputEl) { operationsNotesInputEl.value = ""; } return loadInstanceOperations(instanceId, activeInstanceDetailData); }) .catch(function (error) { window.alert( "Failed to save operation task: " + (error && error.message ? error.message : String(error)) ); }); }); } Object.keys(infoPanels).forEach(function (panelKey) { bindInfoPanelControls(infoPanels[panelKey]); }); setInstanceDetailIdle("Select an instance to view details."); updateChdcOpenUrl(); void loadOverview(); void loadConnectorRoutingConfig(); void loadConnectorSummary(); void loadConnectorInstances().then(function () { if (instanceSelectEl && instanceSelectEl.value) { return loadConnectorInstanceDetail(instanceSelectEl.value); } if (connectorInstancesCache.length > 0) { const first = connectorInstancesCache[0]; const firstId = first && typeof first === "object" ? String(first.instanceId || "").trim() : ""; if (firstId) { return loadConnectorInstanceDetail(firstId); } } return Promise.resolve(); }); void loadConnectorCatalogConfig(); void loadPaymenterBackups(); })();