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 `
↯
No request selected
Send traffic through a tunnel, then select a request to inspect headers, bodies, and previews.
`;
}
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 = ``;
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();