const state = { requests: [], selectedID: null, activeTab: "preview", query: "", streamStatus: "connecting", }; const app = document.getElementById("app"); function init() { render(); connectStream(); } function connectStream() { const es = new EventSource("/stream"); es.onopen = () => { state.streamStatus = "connected"; render(); }; es.onerror = () => { state.streamStatus = "disconnected"; render(); }; es.onmessage = (event) => { const record = JSON.parse(event.data); const foundIdx = state.requests.findIndex((req) => req.ID === record.ID); if (foundIdx >= 0) { state.requests[foundIdx] = record; } else { state.requests.unshift(record); } render(); }; } function render() { const selected = getSelected(); app.innerHTML = `
${renderHeader()}
${renderRequestList()} ${renderInspector(selected, false)}
${selected ? renderMobileSheet(selected) : ""}
`; bindEvents(); renderPreview(selected); } function renderHeader() { return `

Conduit Monitor

Live tunnel traffic inspector

${state.streamStatus}
`; } function renderRequestList() { const filtered = filteredRequests(); return ` `; } function renderRequestRow(req) { const selected = req.ID === state.selectedID; const url = parseURL(req.URL); return ` `; } function renderInspector(selected, mobile) { if (!selected) { return ` `; } const wrapperClass = mobile ? "flex h-full flex-col bg-slate-950" : "hidden min-h-0 flex-col bg-slate-950 lg:flex"; return `
${renderInspectorHeader(selected, mobile)} ${renderTabs()}
${renderActiveTab(selected)}
`; } function renderMobileSheet(selected) { return `
${renderInspector(selected, true)}
`; } function renderInspectorHeader(req, mobile) { const url = parseURL(req.URL); return `
${mobile ? `` : ""}
${escapeHTML(req.Method || "-")} ${req.Status || "pending"} ${formatTime(req.Time)}

${escapeHTML(url.path)}

${escapeHTML(req.URL || "")}

`; } function renderTabs() { return ` `; } function renderActiveTab(req) { if (state.activeTab === "overview") return renderOverview(req); if (state.activeTab === "request") return renderMessage(req, "Request"); if (state.activeTab === "response") return renderMessage(req, "Response"); return `
`; } function renderOverview(req) { return `
${summaryCard("Method", req.Method || "-")} ${summaryCard("Status", req.Status || "pending")} ${summaryCard("Source", req.SourceAddr || "-")} ${summaryCard("Request Body", bodySummary(req, "Request"))} ${summaryCard("Response Body", bodySummary(req, "Response"))} ${summaryCard("Content Type", req.ResponseBodyType || req.RequestBodyType || "-")}
`; } function renderMessage(req, prefix) { const headers = req[`${prefix}Headers`] || {}; const body = decodeBody(req[`${prefix}Body`]); return `

Headers

${escapeHTML(formatHeaders(headers) || "No headers")}

Body

${escapeHTML(bodySummary(req, prefix))}

${escapeHTML(formatBody(body, req[`${prefix}BodyType`]))}
`; } function renderPreview(selected) { const roots = document.querySelectorAll("[data-preview-root]"); if (roots.length === 0 || !selected) return; roots.forEach((root) => renderPreviewInto(root, selected)); } function renderPreviewInto(root, selected) { const contentType = selected.ResponseBodyType || ""; const body = selected.ResponseBody; if (!selected.ResponseBodyCaptured || !body) { root.innerHTML = renderPreviewEmpty(selected); return; } if (isImage(contentType)) { root.innerHTML = `
Response preview
`; return; } const decoded = decodeBody(body); if (isHTML(contentType)) { root.innerHTML = ``; root.querySelector("iframe").srcdoc = decoded; return; } root.innerHTML = `
${escapeHTML(formatBody(decoded, contentType))}
`; } function renderPreviewEmpty(req) { const reason = req.ResponseBodySkipped || "No captured response body is available."; return `

Nothing to preview

${escapeHTML(reason)}

${escapeHTML(bodySummary(req, "Response"))}

`; } function bindEvents() { document.querySelectorAll("[data-select]").forEach((el) => { el.addEventListener("click", () => { state.selectedID = el.dataset.select; state.activeTab = "preview"; render(); }); }); document.querySelectorAll("[data-tab]").forEach((el) => { el.addEventListener("click", () => { state.activeTab = el.dataset.tab; render(); }); }); document.querySelectorAll("[data-action='clear']").forEach((el) => { el.addEventListener("click", () => { state.requests = []; state.selectedID = null; render(); }); }); document.querySelectorAll("[data-action='close']").forEach((el) => { el.addEventListener("click", () => { state.selectedID = null; render(); }); }); document.querySelectorAll("[data-copy]").forEach((el) => { el.addEventListener("click", () => navigator.clipboard?.writeText(el.dataset.copy || "")); }); const search = document.querySelector("[data-input='query']"); if (search) { search.addEventListener("input", (event) => { state.query = event.target.value; render(); document.querySelector("[data-input='query']")?.focus(); }); } } function filteredRequests() { const query = state.query.trim().toLowerCase(); if (!query) return state.requests; return state.requests.filter((req) => [ req.URL, req.Method, String(req.Status || "pending"), req.SourceAddr, req.RequestBodyType, req.ResponseBodyType, ].some((value) => String(value || "").toLowerCase().includes(query))); } function getSelected() { return state.requests.find((req) => req.ID === state.selectedID) || null; } function parseURL(raw) { try { const parsed = new URL(raw, window.location.origin); return { host: parsed.host, path: `${parsed.pathname}${parsed.search}` || raw }; } catch { return { host: "", path: raw || "-" }; } } function decodeBody(base64Data) { if (!base64Data) return ""; const binary = atob(base64Data); const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); return new TextDecoder().decode(bytes); } function formatBody(body, contentType = "") { if (isJSON(contentType)) { try { return JSON.stringify(JSON.parse(body), null, 2); } catch { return body; } } return body || "No body"; } function formatHeaders(headers) { return Object.entries(headers || {}) .map(([key, values]) => `${key}: ${Array.isArray(values) ? values.join(", ") : values}`) .join("\n"); } function bodySummary(req, prefix) { const size = req[`${prefix}BodySize`]; const captured = req[`${prefix}BodyCaptured`]; const truncated = req[`${prefix}BodyTruncated`]; const skipped = req[`${prefix}BodySkipped`]; if (captured) return `${formatBytes(size)} captured${truncated ? " (truncated)" : ""}`; if (skipped) return skipped; if (size > 0) return `${formatBytes(size)} not captured`; return "No body"; } function summaryCard(label, value) { return `
${escapeHTML(label)}
${escapeHTML(String(value))}
`; } function renderEmptyList() { return `
No requests yet. Start sending traffic through your tunnel.
`; } function isJSON(contentType) { return /(^|\/)json($|;)|\+json($|;)/i.test(contentType || ""); } function isHTML(contentType) { return /text\/html/i.test(contentType || ""); } function isImage(contentType) { return /^image\/(png|jpe?g|gif|webp|svg\+xml)/i.test(contentType || ""); } function methodClass(method) { return { GET: "bg-emerald-500/15 text-emerald-300", POST: "bg-blue-500/15 text-blue-300", PUT: "bg-amber-500/15 text-amber-300", PATCH: "bg-purple-500/15 text-purple-300", DELETE: "bg-red-500/15 text-red-300", }[method] || "bg-slate-700 text-slate-200"; } function statusClass(status) { if (!status) return "bg-slate-700 text-slate-300"; if (status < 300) return "bg-emerald-500/15 text-emerald-300"; if (status < 400) return "bg-blue-500/15 text-blue-300"; if (status < 500) return "bg-amber-500/15 text-amber-300"; return "bg-red-500/15 text-red-300"; } function streamStatusClass() { if (state.streamStatus === "connected") return "bg-emerald-500/15 text-emerald-300"; if (state.streamStatus === "connecting") return "bg-amber-500/15 text-amber-300"; return "bg-red-500/15 text-red-300"; } function formatTime(time) { if (!time) return "-"; return new Date(time).toLocaleTimeString(); } function formatBytes(bytes) { if (!bytes || bytes < 0) return "unknown size"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`; return `${(bytes / 1024 / 1024).toFixed(1)} MiB`; } function escapeHTML(value) { return String(value ?? "").replace(/[&<>'"]/g, (char) => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """, })[char]); } function escapeAttr(value) { return escapeHTML(value).replace(/`/g, "`"); } init();