function createDebugContentView({ DEBUG_PANEL_ID, MODAL_ID, debugState, formatDebugBody, formatHeaderBlock, formatMetadataBlock, createCloseIcon, environment }) { const { document } = environment; const categories = [ ["all", "All"], ["authentication", "Authentication"], ["opportunities", "Opportunities"], ["products", "Products"], ["ai", "AI"], ["consumption", "Consumption"], ["installBase", "Installed Base"], ["serviceRequests", "Service Requests"], ["actions", "Actions"], ["other", "Other"] ]; let selectedCategory = "all"; const expandedIds = new Set(); let returnFocus = null; function getRequestCategory(entry) { if (/^request(?:CurrentUser|TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication"; if (entry.name === "requestOpportunities") return "opportunities"; if (entry.name === "requestOpptyProducts") return "products"; if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions"; if (entry.name === "requestGenerateAiSummary") return "ai"; if (entry.name === "requestInstallBase") return "installBase"; if (["requestSRInfo", "requestLookupTypePlatform", "requestInternalSrServices", "requestPostInternalServiceRequest"].includes(entry.name)) return "serviceRequests"; if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestResourceUser", "requestSrCatalog", "requestSrTokenCatalog", "requestPostServiceRequest"].includes(entry.name)) return "serviceRequests"; if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption"; return "other"; } function getChronologicalRequests() { return [...debugState.requestLog].sort((a, b) => (new Date(a.startedAt).getTime() || 0) - (new Date(b.startedAt).getTime() || 0) || a.id - b.id); } function toggleDebugPanel() { const modal = document.getElementById(MODAL_ID); if (!modal || modal.hidden) { return; } const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel) { return; } if (!panel.hidden) { closeDebugPanel(); return; } returnFocus = document.activeElement; panel.hidden = false; renderDebugPanel(); panel.querySelectorAll(".opportunities-extension-debug-close-button")[0]?.focus({ preventScroll: true }); } function closeDebugPanel() { const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel) return; panel.hidden = true; if (returnFocus?.isConnected) returnFocus.focus({ preventScroll: true }); returnFocus = null; } function renderDebugPanel() { const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel || panel.hidden) { return; } for (const item of panel.querySelectorAll("details[data-request-id]")) { const id = item.getAttribute("data-request-id"); if (item.open) expandedIds.add(id); else expandedIds.delete(id); } const focusedCategory = document.activeElement && document.activeElement.getAttribute("data-debug-category"); const focusedControl = document.activeElement && document.activeElement.getAttribute("data-debug-focus"); const listScrollTop = panel.querySelectorAll(".opportunities-extension-debug-list")[0]?.scrollTop || 0; const scrollTop = panel.scrollTop; panel.textContent = ""; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-title`); const focusTargets = new Map(); const panelHeader = document.createElement("header"); panelHeader.className = "opportunities-extension-debug-panel-header"; const heading = document.createElement("div"); const title = document.createElement("h2"); title.id = `${DEBUG_PANEL_ID}-title`; title.textContent = "Debug Requests"; const summary = document.createElement("p"); summary.textContent = `${debugState.requestLog.length} request${debugState.requestLog.length === 1 ? "" : "s"} since modal opened.`; const running = debugState.requestLog.filter(entry => entry.status === "pending").length; if (running) summary.textContent += ` ${running} running.`; heading.append(title, summary); const visibilityButton = document.createElement("button"); visibilityButton.type = "button"; visibilityButton.className = "opportunities-extension-icon-button opportunities-extension-debug-visibility-button"; visibilityButton.setAttribute("data-debug-focus", "visibility"); focusTargets.set("visibility", visibilityButton); visibilityButton.setAttribute("aria-pressed", String(debugState.debugSecretsVisible)); visibilityButton.setAttribute("aria-label", debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values"); visibilityButton.title = debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values"; visibilityButton.append(createDebugVisibilityIcon(debugState.debugSecretsVisible)); visibilityButton.addEventListener("click", () => { debugState.debugSecretsVisible = !debugState.debugSecretsVisible; renderDebugPanel(); }); const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "opportunities-extension-icon-button opportunities-extension-debug-close-button"; closeButton.title = "Close debug"; closeButton.setAttribute("aria-label", "Close debug"); closeButton.setAttribute("data-debug-focus", "close"); closeButton.append(createCloseIcon()); closeButton.addEventListener("click", closeDebugPanel); focusTargets.set("close", closeButton); const actions = document.createElement("div"); actions.className = "opportunities-extension-debug-actions"; actions.append(visibilityButton, closeButton); panelHeader.append(heading, actions); const entries = getChronologicalRequests(); const availableCategories = categories.filter(([key]) => key === "all" || entries.some(entry => getRequestCategory(entry) === key)); if (!availableCategories.some(([key]) => key === selectedCategory)) selectedCategory = "all"; const tabs = document.createElement("div"); tabs.className = "opportunities-extension-debug-tabs"; tabs.setAttribute("role", "tablist"); tabs.setAttribute("aria-label", "Request categories"); const tabButtons = []; availableCategories.forEach(([key, label], index) => { const categoryEntries = key === "all" ? entries : entries.filter(entry => getRequestCategory(entry) === key); const pending = categoryEntries.filter(entry => entry.status === "pending").length; const button = document.createElement("button"); button.type = "button"; button.id = `${DEBUG_PANEL_ID}-tab-${key}`; button.setAttribute("role", "tab"); button.setAttribute("data-debug-category", key); button.setAttribute("aria-selected", String(key === selectedCategory)); button.setAttribute("aria-controls", `${DEBUG_PANEL_ID}-requests`); button.tabIndex = key === selectedCategory ? 0 : -1; button.textContent = `${label} (${categoryEntries.length})${pending ? ` / ${pending} running` : ""}`; button.addEventListener("click", () => { selectedCategory = key; renderDebugPanel(); }); button.addEventListener("keydown", event => { let nextIndex; if (event.key === "ArrowRight") nextIndex = (index + 1) % availableCategories.length; else if (event.key === "ArrowLeft") nextIndex = (index + availableCategories.length - 1) % availableCategories.length; else if (event.key === "Home") nextIndex = 0; else if (event.key === "End") nextIndex = availableCategories.length - 1; else return; event.preventDefault(); selectedCategory = availableCategories[nextIndex][0]; renderDebugPanel(); }); tabs.append(button); tabButtons.push(button); }); const list = document.createElement("div"); list.className = "opportunities-extension-debug-list"; list.id = `${DEBUG_PANEL_ID}-requests`; list.setAttribute("role", "tabpanel"); list.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-tab-${selectedCategory}`); list.tabIndex = 0; list.setAttribute("data-debug-focus", "list"); focusTargets.set("list", list); const summaries = []; if (debugState.requestLog.length === 0) { const empty = document.createElement("div"); empty.className = "opportunities-extension-debug-empty"; empty.textContent = "No requests recorded yet."; list.append(empty); } entries.filter(entry => selectedCategory === "all" || getRequestCategory(entry) === selectedCategory).forEach((entry) => { const item = document.createElement("details"); item.className = `opportunities-extension-debug-item opportunities-extension-debug-item-${entry.status}`; item.setAttribute("data-request-id", String(entry.id)); item.open = expandedIds.has(String(entry.id)); const heading = document.createElement("summary"); heading.className = "opportunities-extension-debug-heading"; heading.setAttribute("data-debug-focus", `request-${entry.id}`); focusTargets.set(`request-${entry.id}`, heading); summaries.push(heading); const name = document.createElement("strong"); name.textContent = `${entry.id}. ${entry.name}`; if (entry.name === "requestOpptyProducts" && entry.metadata.optyNumber) { name.textContent += ` (${entry.metadata.optyNumber}, offset ${entry.metadata.offset})`; } const httpStatus = document.createElement("span"); httpStatus.className = "opportunities-extension-debug-http-status"; httpStatus.textContent = entry.httpStatus ? String(entry.httpStatus) : "pending"; httpStatus.setAttribute("data-http-class", entry.httpStatus ? String(Math.floor(entry.httpStatus / 100)) : "pending"); const status = document.createElement("span"); status.className = "opportunities-extension-debug-status"; status.textContent = entry.status === "pending" ? "running" : entry.status; const summaryMeta = document.createElement("span"); summaryMeta.className = "opportunities-extension-debug-summary-meta"; const startedTime = document.createElement("time"); startedTime.className = "opportunities-extension-debug-time"; startedTime.textContent = formatDebugTime(entry.startedAt); startedTime.title = `Started: ${entry.startedAt.toLocaleString()}`; const method = document.createElement("span"); method.className = "opportunities-extension-debug-method"; method.textContent = entry.method || "GET"; method.setAttribute("data-method", method.textContent.toUpperCase()); const duration = document.createElement("span"); duration.className = "opportunities-extension-debug-duration"; const hasDuration = typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs) && entry.durationMs >= 0; duration.textContent = hasDuration ? entry.durationMs < 1000 ? `${Math.round(entry.durationMs)} ms` : `${(entry.durationMs / 1000).toFixed(2)} s` : entry.status === "pending" ? "In progress" : "N/D"; duration.title = hasDuration ? `Request duration: ${entry.durationMs} ms` : "Request duration not yet available"; summaryMeta.append(startedTime, method, httpStatus, status, duration); heading.append(name, summaryMeta); const lines = [ `${entry.method} ${entry.url}`, `Started: ${formatDebugTime(entry.startedAt)}`, entry.completedAt ? `Completed: ${formatDebugTime(entry.completedAt)} (${entry.durationMs}ms)` : "Completed: pending", entry.httpStatus ? `HTTP: ${entry.httpStatus} ${entry.statusText}`.trim() : "", entry.error ? `Error: ${entry.error}` : "", `Response body truncated: ${entry.responseBodyTruncated ? "yes" : "no"}` ].filter(Boolean); const details = document.createElement("div"); details.className = "opportunities-extension-debug-details"; appendDebugBlock(details, "Request", lines.join("\n")); appendDebugBlock(details, "Request Metadata", formatMetadataBlock(entry.metadata)); appendDebugBlock(details, "Request Headers", formatHeaderBlock(entry.requestHeaders)); appendDebugBlock(details, "Request Body", formatDebugBody(entry.requestBody)); appendDebugBlock(details, "Response Headers", formatHeaderBlock(entry.responseHeaders)); appendDebugBlock(details, "Response Body", formatDebugBody(entry.responseBody, entry.name)); item.append(heading, details); list.append(item); }); panel.append(panelHeader, tabs, list); panel.onkeydown = event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); closeDebugPanel(); } else if (event.key === "Tab") { const controls = [visibilityButton, closeButton, ...tabButtons.filter(button => button.tabIndex === 0), list, ...summaries]; const index = controls.indexOf(document.activeElement); if (index < 0 || (event.shiftKey && index === 0) || (!event.shiftKey && index === controls.length - 1)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus({ preventScroll: true }); } } }; if (focusedCategory) tabButtons.find(button => button.getAttribute("data-debug-category") === selectedCategory).focus({ preventScroll: true }); else if (focusedControl) focusTargets.get(focusedControl)?.focus({ preventScroll: true }); list.scrollTop = listScrollTop; panel.scrollTop = scrollTop; } function createDebugVisibilityIcon(secretsVisible) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const eye = document.createElementNS("http://www.w3.org/2000/svg", "path"); const pupil = document.createElementNS("http://www.w3.org/2000/svg", "circle"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); eye.setAttribute("d", "M2.5 12s3.5-6.5 9.5-6.5 9.5 6.5 9.5 6.5-3.5 6.5-9.5 6.5S2.5 12 2.5 12Z"); pupil.setAttribute("cx", "12"); pupil.setAttribute("cy", "12"); pupil.setAttribute("r", "2.75"); [eye, pupil].forEach((shape) => { shape.setAttribute("fill", "none"); shape.setAttribute("stroke", "currentColor"); shape.setAttribute("stroke-linecap", "round"); shape.setAttribute("stroke-linejoin", "round"); shape.setAttribute("stroke-width", "1.7"); }); svg.append(eye, pupil); if (secretsVisible) { const slash = document.createElementNS("http://www.w3.org/2000/svg", "path"); slash.setAttribute("d", "M4 4l16 16"); slash.setAttribute("fill", "none"); slash.setAttribute("stroke", "currentColor"); slash.setAttribute("stroke-linecap", "round"); slash.setAttribute("stroke-width", "1.9"); svg.append(slash); } return svg; } function appendDebugBlock(container, label, value) { const block = document.createElement("section"); block.className = "opportunities-extension-debug-block"; block.setAttribute("data-direction", label.startsWith("Response") ? "response" : "request"); const heading = document.createElement("h3"); const content = document.createElement("pre"); heading.textContent = label; appendJsonTokens(content, value); block.append(heading, content); container.append(block); } function appendJsonTokens(content, value) { const text = String(value ?? ""); try { JSON.parse(text); } catch { content.textContent = text; return; } // Highlight only already-masked display text; never interpret response content as HTML. const tokens = [...text.matchAll(/"(?:[^"\\]|\\.)*"\s*:|"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\b(?:true|false|null)\b/g)]; let offset = 0; const append = (textValue, kind) => { if (!textValue) return; const span = document.createElement("span"); span.textContent = textValue; if (kind) span.className = `opportunities-extension-debug-json-${kind}`; content.append(span); }; tokens.forEach(token => { append(text.slice(offset, token.index)); const value = token[0]; const kind = value.startsWith('"') ? value.endsWith(":") ? "key" : "string" : /^(true|false|null)$/.test(value) ? "literal" : "number"; append(value, kind); offset = token.index + value.length; }); append(text.slice(offset)); } function formatDebugTime(date) { return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } return { toggleDebugPanel, closeDebugPanel, renderDebugPanel, createDebugVisibilityIcon, appendDebugBlock, formatDebugTime, getRequestCategory, getChronologicalRequests }; }