cacodex commited on
Commit
c0aaca1
·
verified ·
1 Parent(s): 843ad3a

Upload 12 files

Browse files
Files changed (2) hide show
  1. static/public.js +148 -147
  2. static/style.css +33 -15
static/public.js CHANGED
@@ -1,148 +1,149 @@
1
- const overviewCards = document.getElementById("overview-cards");
2
- const dashboardUpdated = document.getElementById("dashboard-updated");
3
- const timelineHeader = document.getElementById("timeline-header");
4
- const healthGrid = document.getElementById("health-grid");
5
- const dashboardEmpty = document.getElementById("dashboard-empty");
6
- const refreshDashboardBtn = document.getElementById("refresh-dashboard");
7
-
8
- const dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
9
- hour: "2-digit",
10
- minute: "2-digit",
11
- hour12: false,
12
- });
13
-
14
- const SUMMARY_WIDTH = 280;
15
- const CELL_SIZE = 28;
16
- const CELL_GAP = 8;
17
-
18
- function formatDateTime(value) {
19
- if (!value) return "--";
20
- const date = new Date(value);
21
- if (Number.isNaN(date.getTime())) return "--";
22
- return dateTimeFormatter.format(date);
23
- }
24
-
25
- function rateMeta(rate) {
26
- if (rate === null || rate === undefined) return { label: "暂无数据", className: "status-cyan", colorClass: "cell-empty" };
27
- if (rate >= 95) return { label: "优秀", className: "status-cyan", colorClass: "cell-cyan" };
28
- if (rate >= 80) return { label: "良好", className: "status-green", colorClass: "cell-green" };
29
- if (rate >= 50) return { label: "告警", className: "status-orange", colorClass: "cell-orange" };
30
- return { label: "异常", className: "status-red", colorClass: "cell-red" };
31
- }
32
-
33
- function createSummaryCard(label, value, detail = "") {
34
- const card = document.createElement("article");
35
- card.className = "summary-card";
36
- card.innerHTML = `<span>${label}</span><strong>${value}</strong><p>${detail}</p>`;
37
- return card;
38
- }
39
-
40
- function applyTimelineGrid(pointsCount) {
41
- const headerColumns = `${SUMMARY_WIDTH}px repeat(${pointsCount}, ${CELL_SIZE}px)`;
42
- timelineHeader.style.gridTemplateColumns = headerColumns;
43
- }
44
-
45
- function renderOverview(data) {
46
- overviewCards.innerHTML = "";
47
- const averageHealth = data.average_health === null || data.average_health === undefined ? "--" : `${data.average_health.toFixed(2)}%`;
48
- const totalCalls = data.total_requests ?? 0;
49
- const healthyModels = (data.models || []).filter((model) => (model.latest_success_rate ?? 0) >= 95).length;
50
- const displayedBuckets = data.models?.[0]?.points?.length || 0;
51
-
52
- overviewCards.appendChild(createSummaryCard("总调用次数", totalCalls, "统计来自网关累计成功与失败请求"));
53
- overviewCards.appendChild(createSummaryCard("平均健康度", averageHealth, "按监控模型最近 10 分钟成功率平均值计算"));
54
- overviewCards.appendChild(createSummaryCard("高健康模型数", healthyModels, "最近一档成功率达到 95% 以上的模型数量"));
55
- overviewCards.appendChild(createSummaryCard("统计窗口", `${displayedBuckets * (data.bucket_minutes || 10)} 分钟`, `当前按 ${data.bucket_minutes || 10} 分钟粒度滚动统计`));
56
- dashboardUpdated.textContent = formatDateTime(data.generated_at);
57
- }
58
-
59
- function renderTimelineHeader(models) {
60
- timelineHeader.innerHTML = "";
61
- const points = models?.[0]?.points || [];
62
- applyTimelineGrid(points.length);
63
-
64
- const emptyCell = document.createElement("div");
65
- emptyCell.className = "timeline-label timeline-left-placeholder";
66
- timelineHeader.appendChild(emptyCell);
67
-
68
- points.forEach((point) => {
69
- const cell = document.createElement("div");
70
- cell.className = "timeline-label";
71
- cell.textContent = point.label || formatDateTime(point.bucket_start);
72
- timelineHeader.appendChild(cell);
73
- });
74
- }
75
-
76
- function renderHealthRows(models) {
77
- healthGrid.innerHTML = "";
78
- if (!models || models.length === 0) {
79
- dashboardEmpty.textContent = "当前没有可展示的模型健康数据,请确认 MODEL_LIST 配置并等待请求进入统计。";
80
- return;
81
- }
82
- dashboardEmpty.textContent = "";
83
- renderTimelineHeader(models);
84
-
85
- models.forEach((model) => {
86
- const latestRate = model.latest_success_rate === null || model.latest_success_rate === undefined ? "--" : `${model.latest_success_rate.toFixed(2)}%`;
87
- const latestMeta = rateMeta(model.latest_success_rate);
88
-
89
- const row = document.createElement("article");
90
- row.className = "health-row-card";
91
- row.style.gridTemplateColumns = `${SUMMARY_WIDTH}px max-content`;
92
-
93
- const summary = document.createElement("div");
94
- summary.className = "health-row-summary";
95
- summary.innerHTML = `
96
- <div class="health-row-accent ${latestMeta.className}"></div>
97
- <div class="health-row-copy">
98
- <h3 title="${model.model_id}">${model.model_id}</h3>
99
- <div class="health-meta-inline">
100
- <span class="health-rate-pill ${latestMeta.className}">${latestRate}</span>
101
- <span class="health-rate-pill health-call-pill">调用 ${model.total_calls ?? 0} 次</span>
102
- </div>
103
- </div>
104
- `;
105
-
106
- const cells = document.createElement("div");
107
- cells.className = "health-cells";
108
- cells.style.gridTemplateColumns = `repeat(${model.points?.length || 0}, ${CELL_SIZE}px)`;
109
- (model.points || []).forEach((point) => {
110
- const meta = rateMeta(point.success_rate);
111
- const cell = document.createElement("div");
112
- cell.className = `health-cell ${meta.colorClass}`;
113
- cell.title = `${point.label} 成功 ${point.success_count}/${point.total_count}`;
114
- cells.appendChild(cell);
115
- });
116
-
117
- row.appendChild(summary);
118
- row.appendChild(cells);
119
- healthGrid.appendChild(row);
120
- });
121
- }
122
-
123
- async function loadDashboard() {
124
- const response = await fetch("/api/dashboard", { headers: { Accept: "application/json" } });
125
- if (!response.ok) {
126
- throw new Error("健康看板数据加载失败");
127
- }
128
- const payload = await response.json();
129
- renderOverview(payload);
130
- renderHealthRows(payload.models || []);
131
- }
132
-
133
- async function refreshDashboard() {
134
- try {
135
- await loadDashboard();
136
- } catch (error) {
137
- dashboardEmpty.textContent = error.message;
138
- timelineHeader.innerHTML = "";
139
- healthGrid.innerHTML = "";
140
- }
141
- }
142
-
143
- refreshDashboardBtn?.addEventListener("click", refreshDashboard);
144
-
145
- window.addEventListener("DOMContentLoaded", () => {
146
- refreshDashboard();
147
- window.setInterval(refreshDashboard, 60 * 1000);
 
148
  });
 
1
+ const overviewCards = document.getElementById("overview-cards");
2
+ const dashboardUpdated = document.getElementById("dashboard-updated");
3
+ const timelineHeader = document.getElementById("timeline-header");
4
+ const healthGrid = document.getElementById("health-grid");
5
+ const dashboardEmpty = document.getElementById("dashboard-empty");
6
+ const refreshDashboardBtn = document.getElementById("refresh-dashboard");
7
+
8
+ const dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
9
+ hour: "2-digit",
10
+ minute: "2-digit",
11
+ hour12: false,
12
+ });
13
+
14
+ const SUMMARY_WIDTH = 360;
15
+ const CELL_SIZE = 42;
16
+ const CELL_GAP = 8;
17
+
18
+ function formatDateTime(value) {
19
+ if (!value) return "--";
20
+ const date = new Date(value);
21
+ if (Number.isNaN(date.getTime())) return "--";
22
+ return dateTimeFormatter.format(date);
23
+ }
24
+
25
+ function rateMeta(rate) {
26
+ if (rate === null || rate === undefined) return { label: "暂无数据", className: "status-cyan", colorClass: "cell-empty" };
27
+ if (rate >= 95) return { label: "优秀", className: "status-cyan", colorClass: "cell-cyan" };
28
+ if (rate >= 80) return { label: "良好", className: "status-green", colorClass: "cell-green" };
29
+ if (rate >= 50) return { label: "告警", className: "status-orange", colorClass: "cell-orange" };
30
+ return { label: "异常", className: "status-red", colorClass: "cell-red" };
31
+ }
32
+
33
+ function createSummaryCard(label, value, detail = "") {
34
+ const card = document.createElement("article");
35
+ card.className = "summary-card";
36
+ card.innerHTML = `<span>${label}</span><strong>${value}</strong><p>${detail}</p>`;
37
+ return card;
38
+ }
39
+
40
+ function applyTimelineGrid(pointsCount) {
41
+ const headerColumns = `minmax(${SUMMARY_WIDTH}px, 1fr) repeat(${pointsCount}, ${CELL_SIZE}px)`;
42
+ timelineHeader.style.gridTemplateColumns = headerColumns;
43
+ }
44
+
45
+ function renderOverview(data) {
46
+ overviewCards.innerHTML = "";
47
+ const averageHealth = data.average_health === null || data.average_health === undefined ? "--" : `${data.average_health.toFixed(2)}%`;
48
+ const totalCalls = data.total_requests ?? 0;
49
+ const healthyModels = (data.models || []).filter((model) => (model.latest_success_rate ?? 0) >= 95).length;
50
+ const displayedBuckets = data.models?.[0]?.points?.length || 0;
51
+
52
+ overviewCards.appendChild(createSummaryCard("总调用次数", totalCalls, "统计来自网关累计成功与失败请求"));
53
+ overviewCards.appendChild(createSummaryCard("平均健康度", averageHealth, "按监控模型最近 10 分钟成功率平均值计算"));
54
+ overviewCards.appendChild(createSummaryCard("高健康模型数", healthyModels, "最近一档成功率达到 95% 以上的模型数量"));
55
+ overviewCards.appendChild(createSummaryCard("统计窗口", `${displayedBuckets * (data.bucket_minutes || 10)} 分钟`, `当前按 ${data.bucket_minutes || 10} 分钟粒度滚动统计`));
56
+ dashboardUpdated.textContent = formatDateTime(data.generated_at);
57
+ }
58
+
59
+ function renderTimelineHeader(models) {
60
+ timelineHeader.innerHTML = "";
61
+ const points = models?.[0]?.points || [];
62
+ applyTimelineGrid(points.length);
63
+
64
+ const emptyCell = document.createElement("div");
65
+ emptyCell.className = "timeline-label timeline-left-placeholder";
66
+ timelineHeader.appendChild(emptyCell);
67
+
68
+ points.forEach((point) => {
69
+ const cell = document.createElement("div");
70
+ cell.className = "timeline-label";
71
+ cell.textContent = point.label || formatDateTime(point.bucket_start);
72
+ timelineHeader.appendChild(cell);
73
+ });
74
+ }
75
+
76
+ function renderHealthRows(models) {
77
+ healthGrid.innerHTML = "";
78
+ if (!models || models.length === 0) {
79
+ dashboardEmpty.textContent = "当前没有可展示的模型健康数据,请确认 MODEL_LIST 配置并等待请求进入统计。";
80
+ return;
81
+ }
82
+ dashboardEmpty.textContent = "";
83
+ renderTimelineHeader(models);
84
+
85
+ models.forEach((model) => {
86
+ const latestRate = model.latest_success_rate === null || model.latest_success_rate === undefined ? "--" : `${model.latest_success_rate.toFixed(2)}%`;
87
+ const latestMeta = rateMeta(model.latest_success_rate);
88
+
89
+ const row = document.createElement("article");
90
+ row.className = "health-row-card";
91
+ row.style.gridTemplateColumns = `minmax(${SUMMARY_WIDTH}px, 1fr) max-content`;
92
+
93
+ const summary = document.createElement("div");
94
+ summary.className = "health-row-summary";
95
+ summary.innerHTML = `
96
+ <div class="health-row-accent ${latestMeta.className}"></div>
97
+ <div class="health-row-copy">
98
+ <h3 title="${model.model_id}">${model.model_id}</h3>
99
+ <div class="health-meta-inline">
100
+ <span class="health-rate-pill ${latestMeta.className}">${latestRate}</span>
101
+ <span class="health-rate-pill health-call-pill">调用 ${model.total_calls ?? 0} 次</span>
102
+ </div>
103
+ </div>
104
+ `;
105
+
106
+ const cells = document.createElement("div");
107
+ cells.className = "health-cells";
108
+ cells.style.gridTemplateColumns = `repeat(${model.points?.length || 0}, ${CELL_SIZE}px)`;
109
+ cells.style.justifySelf = "end";
110
+ (model.points || []).forEach((point) => {
111
+ const meta = rateMeta(point.success_rate);
112
+ const cell = document.createElement("div");
113
+ cell.className = `health-cell ${meta.colorClass}`;
114
+ cell.title = `${point.label} 成功 ${point.success_count}/${point.total_count}`;
115
+ cells.appendChild(cell);
116
+ });
117
+
118
+ row.appendChild(summary);
119
+ row.appendChild(cells);
120
+ healthGrid.appendChild(row);
121
+ });
122
+ }
123
+
124
+ async function loadDashboard() {
125
+ const response = await fetch("/api/dashboard", { headers: { Accept: "application/json" } });
126
+ if (!response.ok) {
127
+ throw new Error("健康看板数据加载失败");
128
+ }
129
+ const payload = await response.json();
130
+ renderOverview(payload);
131
+ renderHealthRows(payload.models || []);
132
+ }
133
+
134
+ async function refreshDashboard() {
135
+ try {
136
+ await loadDashboard();
137
+ } catch (error) {
138
+ dashboardEmpty.textContent = error.message;
139
+ timelineHeader.innerHTML = "";
140
+ healthGrid.innerHTML = "";
141
+ }
142
+ }
143
+
144
+ refreshDashboardBtn?.addEventListener("click", refreshDashboard);
145
+
146
+ window.addEventListener("DOMContentLoaded", () => {
147
+ refreshDashboard();
148
+ window.setInterval(refreshDashboard, 60 * 1000);
149
  });
static/style.css CHANGED
@@ -223,10 +223,12 @@ body {
223
  .timeline-label {
224
  text-align: center;
225
  font-size: 12px;
 
226
  }
227
 
228
  .timeline-left-placeholder {
229
  text-align: left;
 
230
  }
231
 
232
  .health-list {
@@ -238,11 +240,10 @@ body {
238
 
239
  .health-row-card {
240
  display: grid;
241
- gap: 14px;
242
  align-items: center;
243
- padding: 14px 0;
244
  border-bottom: 1px solid var(--line);
245
- min-width: max-content;
246
  }
247
 
248
  .health-row-card:last-child {
@@ -252,28 +253,33 @@ body {
252
  .health-row-summary {
253
  display: flex;
254
  align-items: center;
255
- gap: 12px;
256
  min-width: 0;
 
257
  }
258
 
259
  .health-row-accent {
260
- width: 16px;
261
- min-width: 16px;
262
- height: 74px;
263
  border-radius: 999px;
264
  background: var(--cyan);
265
  }
266
 
267
  .health-row-copy {
268
  min-width: 0;
 
269
  }
270
 
271
  .health-row-copy h3 {
272
  margin: 0;
273
- font-size: 21px;
274
- white-space: nowrap;
275
- overflow: hidden;
276
- text-overflow: ellipsis;
 
 
 
277
  }
278
 
279
  .health-rate-pill {
@@ -298,20 +304,22 @@ body {
298
  .health-call-pill {
299
  color: var(--cyan-deep);
300
  background: rgba(87, 200, 223, 0.14);
 
301
  }
302
 
303
  .health-cells {
304
  display: grid;
305
  gap: 8px;
306
  align-items: center;
307
- justify-content: start;
308
  width: max-content;
 
309
  }
310
 
311
  .health-cell {
312
- width: 28px;
313
- height: 28px;
314
- border-radius: 9px;
315
  border: 3px solid transparent;
316
  background: var(--surface-muted);
317
  }
@@ -529,4 +537,14 @@ body {
529
  .health-row-copy h3 {
530
  font-size: 19px;
531
  }
 
 
 
 
 
 
 
 
 
 
532
  }
 
223
  .timeline-label {
224
  text-align: center;
225
  font-size: 12px;
226
+ width: 42px;
227
  }
228
 
229
  .timeline-left-placeholder {
230
  text-align: left;
231
+ width: auto;
232
  }
233
 
234
  .health-list {
 
240
 
241
  .health-row-card {
242
  display: grid;
243
+ gap: 20px;
244
  align-items: center;
245
+ padding: 16px 0;
246
  border-bottom: 1px solid var(--line);
 
247
  }
248
 
249
  .health-row-card:last-child {
 
253
  .health-row-summary {
254
  display: flex;
255
  align-items: center;
256
+ gap: 14px;
257
  min-width: 0;
258
+ padding-right: 16px;
259
  }
260
 
261
  .health-row-accent {
262
+ width: 18px;
263
+ min-width: 18px;
264
+ height: 84px;
265
  border-radius: 999px;
266
  background: var(--cyan);
267
  }
268
 
269
  .health-row-copy {
270
  min-width: 0;
271
+ max-width: 100%;
272
  }
273
 
274
  .health-row-copy h3 {
275
  margin: 0;
276
+ font-size: 22px;
277
+ line-height: 1.3;
278
+ white-space: normal;
279
+ overflow: visible;
280
+ text-overflow: clip;
281
+ overflow-wrap: anywhere;
282
+ word-break: break-word;
283
  }
284
 
285
  .health-rate-pill {
 
304
  .health-call-pill {
305
  color: var(--cyan-deep);
306
  background: rgba(87, 200, 223, 0.14);
307
+ border: 1px solid rgba(87, 200, 223, 0.18);
308
  }
309
 
310
  .health-cells {
311
  display: grid;
312
  gap: 8px;
313
  align-items: center;
314
+ justify-content: end;
315
  width: max-content;
316
+ margin-left: auto;
317
  }
318
 
319
  .health-cell {
320
+ width: 42px;
321
+ height: 42px;
322
+ border-radius: 12px;
323
  border: 3px solid transparent;
324
  background: var(--surface-muted);
325
  }
 
537
  .health-row-copy h3 {
538
  font-size: 19px;
539
  }
540
+
541
+ .health-cells {
542
+ gap: 6px;
543
+ }
544
+
545
+ .health-cell,
546
+ .timeline-label {
547
+ width: 34px;
548
+ height: 34px;
549
+ }
550
  }