File size: 6,540 Bytes
c0aaca1
 
 
 
 
 
 
2c073e0
 
c0aaca1
 
 
 
2c073e0
c0aaca1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed58a14
 
 
 
 
 
 
 
 
 
 
 
 
 
c0aaca1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed58a14
 
 
 
 
 
 
 
c0aaca1
 
 
 
ed58a14
 
 
 
 
 
 
 
 
c0aaca1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c74679b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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);
});