// retoor import { Http } from "./Http.js"; import { GatewayChartManager } from "./GatewayChartManager.js"; const AUTO_REFRESH_MS = 30000; function formatMs(value) { if (value === null || value === undefined) return "-"; return `${Math.round(value)}ms`; } function formatPct(success, total) { if (!total) return "-"; return `${((success / total) * 100).toFixed(1)}%`; } export class GatewayStats { constructor(root) { this.root = root; this.charts = new GatewayChartManager(); this.rangeSelect = root.querySelector("#gw-stats-range"); this.modelSelect = root.querySelector("#gw-model-detail-select"); this.modelEmptyState = root.querySelector("#gw-model-detail-empty-state"); this.modelCharts = root.querySelector("#gw-model-detail-charts"); this.timer = null; } start() { if (!this.rangeSelect) return; this.rangeSelect.addEventListener("change", () => this.refresh()); if (this.modelSelect) { this.modelSelect.addEventListener("change", () => this.refreshModelDetail()); } this.loadModelOptions(); this.refresh(); this.timer = window.setInterval(() => this.refresh(), AUTO_REFRESH_MS); } stop() { if (this.timer) window.clearInterval(this.timer); } get range() { return this.rangeSelect ? this.rangeSelect.value : "24h"; } async loadModelOptions() { let data; try { data = await Http.getJson("/admin/gateway/stats/models"); } catch (err) { return; } if (!this.modelSelect) return; for (const entry of data.models || []) { const option = document.createElement("option"); option.value = `${entry.provider}|${entry.model}`; option.textContent = `${entry.model} (${entry.provider})`; this.modelSelect.appendChild(option); } } async refresh() { let data; try { data = await Http.getJson(`/admin/gateway/stats/data?range=${encodeURIComponent(this.range)}`); } catch (err) { return; } this.renderTiles(data); this.renderCharts(data); this.renderModelTable(data.per_model || []); this.renderFailuresTable(data.recent_failures || []); } renderTiles(data) { const totalRequests = (data.totals.success || 0) + (data.totals.error || 0); this.setText("#gw-stat-total-requests", totalRequests); this.setText("#gw-stat-success-rate", formatPct(data.totals.success || 0, totalRequests)); this.setText("#gw-stat-models-tracked", (data.per_model || []).length); this.setText( "#gw-stat-generated-at", data.generated_at ? new Date(data.generated_at * 1000).toLocaleTimeString() : "-" ); } setText(selector, value) { const el = this.root.querySelector(selector); if (el) el.textContent = value; } renderCharts(data) { this.charts.renderTimeseries("gw-chart-timeseries", data.timeseries || []); this.charts.renderTotals("gw-chart-totals", data.totals || {}); this.charts.renderPerModelBar("gw-chart-per-model", data.per_model || [], "total_requests", "Requests"); this.charts.renderEndpointBreakdown("gw-chart-endpoints", data.per_endpoint || []); this.charts.renderBucketBar( "gw-chart-status-codes", data.status_codes || [], "status_code", "count", "Requests", (code) => this.charts.statusCodeColor(code) ); this.charts.renderDoughnutFromCounts( "gw-chart-streaming", ["Streamed", "Non-streamed"], [data.streaming_split?.streamed || 0, data.streaming_split?.non_streamed || 0] ); this.charts.renderHorizontalBar( "gw-chart-failure-reasons", (data.failure_reasons || []).map((r) => r.reason || "unknown"), (data.failure_reasons || []).map((r) => r.count), "Failures" ); this.charts.renderBucketBar( "gw-chart-hourly", data.hourly_distribution || [], "hour", "count", "Requests" ); this.charts.renderBucketBar( "gw-chart-latency-hist", data.latency_histogram || [], "label", "count", "Requests" ); this.charts.renderBucketBar( "gw-chart-tps-hist", data.tokens_per_second_histogram || [], "label", "count", "Requests" ); const withWeight = (data.per_model || []).map((row) => ({ ...row, weight: row.health ? row.health.weight : null, })); this.charts.renderPerModelBar("gw-chart-weight", withWeight, "weight", "Weight"); this.charts.renderStackedBar( "gw-chart-tokens", (data.per_model || []).slice(0, 10).map((row) => `${row.model} (${row.provider})`), [ { label: "Prompt", data: (data.per_model || []).slice(0, 10).map((row) => row.prompt_tokens || 0), backgroundColor: this.charts.colors.accent, }, { label: "Completion", data: (data.per_model || []).slice(0, 10).map((row) => row.completion_tokens || 0), backgroundColor: this.charts.colors.warning, }, ] ); } renderModelTable(perModel) { const body = this.root.querySelector("#gw-stats-model-table-body"); if (!body) return; if (!perModel.length) { body.innerHTML = 'No requests in this range.'; return; } body.innerHTML = ""; for (const row of perModel) { const health = row.health || {}; const tr = document.createElement("tr"); tr.innerHTML = ` ${row.model} ${row.provider} ${row.total_requests} ${formatPct(row.success_requests, row.total_requests)} ${formatMs(row.avg_latency_ms)} ${row.avg_tokens_per_second ?? "-"} ${health.weight ?? "-"} ${health.circuit_open ? "open" : "closed"} ${row.prompt_tokens} ${row.completion_tokens} `; body.appendChild(tr); } } renderFailuresTable(failures) { const body = this.root.querySelector("#gw-stats-failures-table-body"); if (!body) return; if (!failures.length) { body.innerHTML = 'No failures in this range.'; return; } body.innerHTML = ""; for (const row of failures) { const tr = document.createElement("tr"); tr.innerHTML = ` ${row.created_at ? new Date(row.created_at).toLocaleString() : "-"} ${row.model || "-"} ${row.provider || "-"} ${row.endpoint || "-"} ${row.status_code ?? "-"} ${row.reason || "-"} ${row.fallback_used_route || "no fallback"} `; body.appendChild(tr); } } async refreshModelDetail() { const raw = this.modelSelect ? this.modelSelect.value : ""; if (!raw) { if (this.modelEmptyState) this.modelEmptyState.hidden = false; if (this.modelCharts) this.modelCharts.hidden = true; return; } const [provider, model] = raw.split("|"); let data; try { data = await Http.getJson( `/admin/gateway/stats/model/${encodeURIComponent(provider)}/${encodeURIComponent(model)}?range=${encodeURIComponent(this.range)}` ); } catch (err) { return; } if (this.modelEmptyState) this.modelEmptyState.hidden = true; if (this.modelCharts) this.modelCharts.hidden = false; const summary = data.summary || {}; this.setText("#gw-model-detail-requests", summary.total_requests ?? "-"); this.setText( "#gw-model-detail-success-rate", formatPct(summary.success_requests || 0, summary.total_requests || 0) ); this.setText("#gw-model-detail-latency", formatMs(summary.avg_latency_ms)); this.setText("#gw-model-detail-tps", summary.avg_tokens_per_second ?? "-"); this.charts.renderTimeseries("gw-chart-model-timeseries", data.timeseries || []); this.charts.renderBucketBar( "gw-chart-model-latency-hist", data.latency_histogram || [], "label", "count", "Requests" ); } }