Files

10868 lines
348 KiB
JavaScript

(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();
})();