"use strict"; (() => { let loading = false, lastData = null, activeTab = "overview"; const chartPalette = ["var(--chart-accent)", "var(--chart-secondary)", "var(--chart-tertiary)", "var(--chart-quaternary)", "var(--chart-quinary)", "var(--chart-senary)", "var(--chart-septenary)", "var(--chart-octonary)"]; const scope = () => $("traffic-scope")?.value || "*"; const number = value => fmt(Number(value) || 0); function selectTab(name) { activeTab = name; document.querySelectorAll("[data-traffic-tab]").forEach(button => { const active = button.dataset.trafficTab === name; button.classList.toggle("active", active); button.setAttribute("aria-selected", String(active)); button.tabIndex = active ? 0 : -1; }); document.querySelectorAll("[data-traffic-page]").forEach(page => { page.hidden = page.dataset.trafficPage !== name; }); } function configure(data) { const selected = scope(), options = [el("option", t("logAllVhosts"))]; options[0].value = "*"; for (const vhost of data.vhosts || []) { const option = el("option", `${vhost.id} / ${(vhost.hosts || []).join(", ")}`); option.value = vhost.id; options.push(option); } $("traffic-scope").replaceChildren(...options); $("traffic-scope").value = options.some(option => option.value === selected) ? selected : "*"; } function chartEntries(entries, enabled = true) { return (enabled ? (entries || []) : []).filter(item => Number(item.count) > 0); } function renderDonut(id, entries, enabled = true) { const chart = $(`traffic-chart-${id}`), legend = $(`traffic-chart-${id}-legend`), totalNode = $(`traffic-chart-${id}-total`); if (!chart || !legend || !totalNode) return; const empty = $(`traffic-chart-${id}-empty`), values = chartEntries(entries, enabled); const total = values.reduce((sum, item) => sum + Number(item.count || 0), 0); totalNode.textContent = number(total); if (empty) empty.hidden = total > 0; if (!total) { chart.style.background = "var(--chart-surface)"; chart.setAttribute("aria-label", t("trafficNoData")); legend.replaceChildren(el("li", t("trafficNoData"), "fine")); return; } const shown = values.slice(0, 7).map(item => ({ value: item.value || "-", count: Number(item.count) || 0 })); if (values.length > shown.length) shown.push({ value: t("trafficOther"), count: values.slice(shown.length).reduce((sum, item) => sum + Number(item.count || 0), 0) }); let cursor = 0; chart.style.background = `conic-gradient(${shown.map((item, index) => { const start = cursor / total * 100; cursor += item.count; const end = cursor / total * 100; return `${chartPalette[index % chartPalette.length]} ${start.toFixed(4)}% ${end.toFixed(4)}%`; }).join(", ")})`; const heading = chart.closest(".traffic-chart-card")?.querySelector("h3")?.textContent || t("trafficDimensionsTitle"); chart.setAttribute("aria-label", `${heading}: ${shown.map(item => `${item.value} ${Math.round(item.count / total * 100)}%`).join(", ")}`); legend.replaceChildren(...shown.map((item, index) => { const row = el("li"), label = el("span"), swatch = el("i", undefined, "traffic-chart-swatch"); swatch.style.background = chartPalette[index % chartPalette.length]; const text = el("span", item.value || "-", "traffic-chart-label"); text.title = item.value || "-"; label.append(swatch, text); row.append(label, el("b", `${(item.count / total * 100).toFixed(1)}%`)); return row; })); } function renderBars(id, entries, enabled = true, formatter = number) { const chart = $(`traffic-chart-${id}`); if (!chart) return; const empty = $(`traffic-chart-${id}-empty`), values = chartEntries(entries, enabled).slice(0, 12); const total = values.reduce((sum, item) => sum + Number(item.count || 0), 0), peak = Math.max(1, ...values.map(item => Number(item.count) || 0)); if (empty) empty.hidden = values.length > 0; chart.replaceChildren(...values.map(item => { const count = Number(item.count) || 0, row = el("div", undefined, "traffic-bar-row"); const label = el("span", item.value || "-", "traffic-bar-label"); label.title = item.value || "-"; const track = el("span", undefined, "traffic-bar-track"), fill = el("i", undefined, "traffic-bar-fill"); fill.style.width = `${Math.max(1.5, count / peak * 100)}%`; track.append(fill); const value = el("span", formatter(count), "traffic-bar-value"); value.title = total ? `${(count / total * 100).toFixed(1)}%` : ""; row.append(label, track, value); return row; })); chart.setAttribute("aria-label", values.length ? values.map(item => `${item.value}: ${formatter(Number(item.count) || 0)}`).join(", ") : t("trafficNoData")); } function render(data) { lastData = data; configure(data); const analytics = data.analytics || {}, stats = analytics.data || {}, settings = data.settings || {}; $("traffic-requests").textContent = number(stats.requests); $("traffic-errors").textContent = number(stats.errors); const requests = Number(stats.requests) || 0, errors = Number(stats.errors) || 0; $("traffic-error-ratio").textContent = `${requests ? (errors / requests * 100).toFixed(2) : "0.00"}%`; $("traffic-response-bytes").textContent = fmtBytes(Number(stats.response_bytes) || 0); $("traffic-request-bytes").textContent = `${t("trafficRequestBytes")}: ${fmtBytes(Number(stats.request_bytes) || 0)}`; $("traffic-latency").textContent = stats.mean_duration_ms == null ? "-" : `${fmt(stats.mean_duration_ms, 2)} ms`; $("traffic-dropped").textContent = `${t("trafficDroppedSamples")}: ${number(analytics.queue_dropped)}`; const capture = [settings.capture_user_agent ? t("trafficUserAgentEnabled") : t("trafficUserAgentDisabled")]; capture.push(settings.capture_country ? (data.geoip?.loaded ? t("trafficCountryEnabled") : t("trafficCountryNoDb")) : t("trafficCountryDisabled")); $("traffic-capture-status").textContent = capture.join(" ยท "); renderDonut("countries", stats.countries, !!settings.capture_country); renderBars("user-agents", stats.user_agents, !!settings.capture_user_agent); renderBars("client-ips", stats.client_ips); renderBars("paths", stats.paths); renderBars("routes", stats.routes); renderDonut("methods", stats.methods); renderDonut("methods-detail", stats.methods); renderDonut("statuses", stats.statuses); renderDonut("statuses-detail", stats.statuses); renderDonut("cache", stats.cache); renderDonut("cache-detail", stats.cache); renderBars("volume", [ { value: t("trafficRequestBytes"), count: Number(stats.request_bytes) || 0 }, { value: t("trafficResponseBytes"), count: Number(stats.response_bytes) || 0 }, ], true, fmtBytes); $("traffic-panel-state").textContent = `${t("trafficUpdated")}: ${formatTime(new Date())}`; } async function loadTraffic({ signal, isCurrent }) { if (!authenticated || currentPanel !== "traffic" || loading) return; loading = true; try { const data = await api(`/api/traffic?vhost=${encodeURIComponent(scope())}`, { signal }); if (isCurrent()) render(data); } catch (error) { if (error?.name !== "AbortError" && isCurrent()) $("traffic-panel-state").textContent = error.message; } finally { loading = false; } } function refresh() { return rustProxyRefresh.manual("traffic"); } function reset() { lastData = null; if ($("traffic-panel-state")) $("traffic-panel-state").textContent = ""; document.querySelectorAll(".traffic-bar-chart").forEach(chart => chart.replaceChildren()); for (const id of ["countries", "methods", "methods-detail", "statuses", "statuses-detail", "cache", "cache-detail"]) { const chart = $(`traffic-chart-${id}`), legend = $(`traffic-chart-${id}-legend`), total = $(`traffic-chart-${id}-total`); if (chart) chart.style.background = "var(--chart-surface)"; legend?.replaceChildren(); if (total) total.textContent = "-"; } selectTab("overview"); } document.querySelectorAll("[data-traffic-tab]").forEach(button => button.addEventListener("click", () => selectTab(button.dataset.trafficTab))); $("traffic-refresh")?.addEventListener("click", () => void refresh()); $("traffic-scope")?.addEventListener("change", () => void refresh()); $("traffic-settings")?.addEventListener("click", () => { setPanel("settings"); window.rustProxySettings?.select("logging"); }); rustProxyRefresh.register("traffic", { interval: () => rustProxyRefresh.current().traffic_seconds, active: () => currentPanel === "traffic" && !!$("traffic-live")?.checked, run: loadTraffic }); $("traffic-live")?.addEventListener("change", () => rustProxyRefresh.sync()); selectTab(activeTab); window.rustProxyTrafficPanel = { refresh, reset, rerender: () => lastData && render(lastData), select: selectTab }; })();