n2r-dev / static /public.js
cacodex's picture
Upload 16 files
ed58a14 verified
const overviewCards = document.getElementById("overview-cards");
const dashboardUpdated = document.getElementById("dashboard-updated");
const timelineHeader = document.getElementById("timeline-header");
const healthGrid = document.getElementById("health-grid");
const dashboardEmpty = document.getElementById("dashboard-empty");
const refreshDashboardBtn = document.getElementById("refresh-dashboard");
const DISPLAY_TIMEZONE = "Asia/Shanghai";
const dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: DISPLAY_TIMEZONE,
});
const SUMMARY_WIDTH = 360;
const CELL_SIZE = 42;
const CELL_GAP = 8;
function formatDateTime(value) {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return dateTimeFormatter.format(date);
}
function rateMeta(rate) {
if (rate === null || rate === undefined) return { label: "暂无数据", className: "status-cyan", colorClass: "cell-empty" };
if (rate >= 95) return { label: "优秀", className: "status-cyan", colorClass: "cell-cyan" };
if (rate >= 80) return { label: "良好", className: "status-green", colorClass: "cell-green" };
if (rate >= 50) return { label: "告警", className: "status-orange", colorClass: "cell-orange" };
return { label: "异常", className: "status-red", colorClass: "cell-red" };
}
function createSummaryCard(label, value, detail = "") {
const card = document.createElement("article");
card.className = "summary-card";
card.innerHTML = `<span>${label}</span><strong>${value}</strong><p>${detail}</p>`;
return card;
}
function applyTimelineGrid(pointsCount) {
const headerColumns = `minmax(${SUMMARY_WIDTH}px, 1fr) repeat(${pointsCount}, ${CELL_SIZE}px)`;
timelineHeader.style.gridTemplateColumns = headerColumns;
}
function renderOverview(data) {
overviewCards.innerHTML = "";
const averageHealth = data.average_health === null || data.average_health === undefined ? "--" : `${data.average_health.toFixed(2)}%`;
const totalCalls = data.total_requests ?? 0;
const healthyModels = (data.models || []).filter((model) => (model.latest_success_rate ?? 0) >= 95).length;
const displayedBuckets = data.models?.[0]?.points?.length || 0;
const healthWindowMinutes = data.health_window_minutes || 120;
overviewCards.appendChild(createSummaryCard("总调用次数", totalCalls, "统计来自网关累计成功与失败请求"));
overviewCards.appendChild(createSummaryCard("平均健康度", averageHealth, `按监控模型最近 ${healthWindowMinutes} 分钟滚动成功率平均值计算`));
overviewCards.appendChild(createSummaryCard("高健康模型数", healthyModels, `最近 ${healthWindowMinutes} 分钟滚动成功率达到 95% 以上的模型数量`));
overviewCards.appendChild(createSummaryCard("统计窗口", `${displayedBuckets * (data.bucket_minutes || 10)} 分钟`, `当前按 ${data.bucket_minutes || 10} 分钟粒度滚动统计`));
dashboardUpdated.textContent = formatDateTime(data.generated_at);
}
function renderTimelineHeader(models) {
timelineHeader.innerHTML = "";
const points = models?.[0]?.points || [];
applyTimelineGrid(points.length);
const emptyCell = document.createElement("div");
emptyCell.className = "timeline-label timeline-left-placeholder";
timelineHeader.appendChild(emptyCell);
points.forEach((point) => {
const cell = document.createElement("div");
cell.className = "timeline-label";
cell.textContent = point.label || formatDateTime(point.bucket_start);
timelineHeader.appendChild(cell);
});
}
function renderHealthRows(models) {
healthGrid.innerHTML = "";
if (!models || models.length === 0) {
dashboardEmpty.textContent = "当前没有可展示的模型健康数据,请确认 MODEL_LIST 配置并等待请求进入统计。";
return;
}
dashboardEmpty.textContent = "";
renderTimelineHeader(models);
models.forEach((model) => {
const latestRate = model.latest_success_rate === null || model.latest_success_rate === undefined ? "--" : `${model.latest_success_rate.toFixed(2)}%`;
const latestMeta = rateMeta(model.latest_success_rate);
const healthWindowMinutes = model.health_window_minutes || 120;
const row = document.createElement("article");
row.className = "health-row-card";
row.style.gridTemplateColumns = `minmax(${SUMMARY_WIDTH}px, 1fr) max-content`;
const summary = document.createElement("div");
summary.className = "health-row-summary";
summary.innerHTML = `
<div class="health-row-accent ${latestMeta.className}"></div>
<div class="health-row-copy">
<h3 title="${model.model_id}">${model.model_id}</h3>
<div class="health-meta-inline">
<span class="health-rate-pill ${latestMeta.className}" title="最近 ${healthWindowMinutes} 分钟滚动成功率">${latestRate}</span>
<span class="health-rate-pill health-call-pill">调用 ${model.total_calls ?? 0} 次</span>
</div>
</div>
`;
const cells = document.createElement("div");
cells.className = "health-cells";
cells.style.gridTemplateColumns = `repeat(${model.points?.length || 0}, ${CELL_SIZE}px)`;
cells.style.justifySelf = "end";
(model.points || []).forEach((point) => {
const meta = rateMeta(point.success_rate);
const cell = document.createElement("div");
cell.className = `health-cell ${meta.colorClass}`;
cell.title = `${point.label} 成功 ${point.success_count}/${point.total_count}`;
cells.appendChild(cell);
});
row.appendChild(summary);
row.appendChild(cells);
healthGrid.appendChild(row);
});
}
async function loadDashboard() {
const response = await fetch("/api/dashboard", { headers: { Accept: "application/json" } });
if (!response.ok) {
throw new Error("健康看板数据加载失败");
}
const payload = await response.json();
renderOverview(payload);
renderHealthRows(payload.models || []);
}
async function refreshDashboard() {
try {
await loadDashboard();
} catch (error) {
dashboardEmpty.textContent = error.message;
timelineHeader.innerHTML = "";
healthGrid.innerHTML = "";
}
}
refreshDashboardBtn?.addEventListener("click", refreshDashboard);
window.addEventListener("DOMContentLoaded", () => {
refreshDashboard();
window.setInterval(refreshDashboard, 60 * 1000);
});