@mapick/cost-firewall 0.2.24 → 0.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,28 @@
1
1
  export function renderDashboardHtml(_stats) {
2
2
  return `<!DOCTYPE html>
3
- <html lang="en">
3
+ <html lang="zh-CN">
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Firewall</title>
7
+ <title>Mapick Firewall</title>
8
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
8
9
  <style>
9
10
  * { margin: 0; padding: 0; box-sizing: border-box; }
10
11
  :root {
11
- --bg: #fafafa;
12
- --fg: #111;
13
- --muted: #525252;
14
- --dim: #a1a1a1;
15
- --border: #e5e5e7;
16
- --card: #fff;
12
+ --bg: #f8f9fa;
13
+ --fg: #1a1a2e;
14
+ --muted: #6b7280;
15
+ --dim: #9ca3af;
16
+ --border: #e5e7eb;
17
+ --card: #ffffff;
17
18
  --accent: #2563eb;
18
19
  --accent-hover: #1d4ed8;
19
20
  --destructive: #dc2626;
20
21
  --destructive-hover: #b91c1c;
22
+ --destructive-glow: rgba(220,38,38,0.3);
21
23
  --success: #16a34a;
22
24
  --warning: #ca8a04;
25
+ --mono: 'JetBrains Mono', monospace;
23
26
  }
24
27
  body {
25
28
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif;
@@ -46,14 +49,6 @@ export function renderDashboardHtml(_stats) {
46
49
  font-weight: 600;
47
50
  letter-spacing: -0.01em;
48
51
  }
49
- .header-center {
50
- display: flex;
51
- gap: 0;
52
- background: var(--bg);
53
- border-radius: 6px;
54
- padding: 2px;
55
- border: 1px solid var(--border);
56
- }
57
52
  .mode-toggle {
58
53
  display: inline-flex;
59
54
  background: #f1f5f9;
@@ -71,20 +66,21 @@ export function renderDashboardHtml(_stats) {
71
66
  cursor: pointer;
72
67
  border-radius: 6px;
73
68
  transition: all 0.2s ease;
74
- position: relative;
75
- }
76
- .mode-btn:hover {
77
- color: #334155;
78
69
  }
70
+ .mode-btn:hover { color: #334155; }
79
71
  .mode-btn.active {
80
72
  background: #fff;
81
73
  color: #1e293b;
82
- box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
74
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
83
75
  font-weight: 600;
84
76
  }
77
+ .mode-btn.observe-mode.active {
78
+ background: #dbeafe;
79
+ color: #1d4ed8;
80
+ }
85
81
  .mode-btn.protect-mode.active {
86
- background: #fef3c7;
87
- color: #92400e;
82
+ background: #dcfce7;
83
+ color: #16a34a;
88
84
  }
89
85
  .mode-label {
90
86
  font-size: 11px;
@@ -129,54 +125,113 @@ export function renderDashboardHtml(_stats) {
129
125
  background: var(--accent-hover);
130
126
  border-color: var(--accent-hover);
131
127
  }
132
-
133
- /* Main Content */
134
- .main {
135
- max-width: 1200px;
136
- margin: 0 auto;
137
- padding: 24px;
128
+ .btn-emergency {
129
+ width: 64px;
130
+ height: 64px;
131
+ border: none;
132
+ border-radius: 50%;
133
+ font-size: 28px;
134
+ font-weight: 700;
135
+ cursor: pointer;
136
+ background: var(--destructive);
137
+ color: #fff;
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ transition: all 0.2s ease;
142
+ animation: pulse-stop 2s infinite;
143
+ box-shadow: 0 4px 24px var(--destructive-glow);
138
144
  }
139
-
140
- /* Stats Row */
141
- .stats-row {
145
+ .btn-emergency:hover {
146
+ background: var(--destructive-hover);
147
+ transform: scale(1.08);
148
+ box-shadow: 0 0 48px rgba(220,38,38,0.5), 0 4px 20px rgba(220,38,38,0.4);
149
+ }
150
+ .btn-emergency.stopped {
151
+ background: #52525b;
152
+ animation: none;
153
+ color: #a1a1aa;
154
+ box-shadow: none;
155
+ }
156
+ @keyframes pulse-stop {
157
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(220,38,38,0.35), 0 0 32px rgba(220,38,38,0.25); }
158
+ 50% { box-shadow: 0 0 0 18px rgba(220,38,38,0), 0 0 48px rgba(220,38,38,0.4); }
159
+ }
160
+ .hero-stats {
142
161
  display: grid;
143
162
  grid-template-columns: repeat(4, 1fr);
144
- gap: 16px;
145
- margin-bottom: 24px;
163
+ gap: 12px;
164
+ padding: 16px 24px;
165
+ max-width: 1200px;
166
+ margin: 0 auto;
146
167
  }
147
- @media (max-width: 768px) {
148
- .stats-row { grid-template-columns: repeat(2, 1fr); }
168
+ .hero-card {
169
+ background: var(--card);
170
+ border: 1px solid var(--border);
171
+ border-radius: 10px;
172
+ padding: 16px;
173
+ text-align: center;
174
+ display: flex;
175
+ flex-direction: column;
176
+ align-items: center;
177
+ justify-content: center;
178
+ min-height: 100px;
149
179
  }
150
- .stat-card {
180
+ .hero-card {
151
181
  background: var(--card);
152
182
  border: 1px solid var(--border);
153
- border-radius: 8px;
154
- padding: 16px 20px;
183
+ border-radius: 12px;
184
+ padding: 18px;
185
+ text-align: center;
155
186
  }
156
- .stat-label {
157
- font-size: 12px;
187
+ .hero-value {
188
+ font-size: 38px;
189
+ font-weight: 600;
190
+ letter-spacing: -0.03em;
191
+ line-height: 1.1;
192
+ font-family: var(--mono);
193
+ }
194
+ .hero-label {
195
+ font-size: 13px;
158
196
  color: var(--muted);
159
- margin-bottom: 4px;
160
- text-transform: uppercase;
197
+ margin-top: 6px;
161
198
  letter-spacing: 0.02em;
162
199
  }
163
- .stat-value {
164
- font-size: 28px;
200
+ @media (max-width: 600px) {
201
+ .hero-stats { grid-template-columns: 1fr; padding: 16px; }
202
+ .hero-value { font-size: 32px; }
203
+ }
204
+
205
+ /* Main Content */
206
+ .main {
207
+ max-width: 1200px;
208
+ margin: 0 auto;
209
+ padding: 16px 24px;
210
+ }
211
+
212
+ /* Section */
213
+ .section {
214
+ margin-bottom: 12px;
215
+ }
216
+ .section-title {
217
+ font-size: 12px;
165
218
  font-weight: 600;
166
- letter-spacing: -0.02em;
219
+ color: var(--muted);
220
+ margin-bottom: 8px;
221
+ letter-spacing: 0.02em;
167
222
  }
168
223
 
169
224
  /* Section */
170
225
  .section {
171
- margin-bottom: 24px;
226
+ margin-bottom: 14px;
172
227
  }
173
228
  .section-title {
174
229
  font-size: 13px;
175
230
  font-weight: 600;
176
231
  color: var(--muted);
177
232
  margin-bottom: 12px;
178
- text-transform: uppercase;
179
- letter-spacing: 0.05em;
233
+ text-transform: none;
234
+ letter-spacing: 0.01em;
180
235
  }
181
236
 
182
237
  /* Rules Grid */
@@ -195,10 +250,16 @@ export function renderDashboardHtml(_stats) {
195
250
  background: var(--card);
196
251
  border: 1px solid var(--border);
197
252
  border-radius: 8px;
198
- padding: 16px;
253
+ padding: 10px;
199
254
  display: flex;
200
255
  flex-direction: column;
201
- min-height: 140px;
256
+ min-height: 100px;
257
+ }
258
+ .rule-header {
259
+ display: flex;
260
+ justify-content: space-between;
261
+ align-items: flex-start;
262
+ margin-bottom: 8px;
202
263
  }
203
264
  .rule-header {
204
265
  display: flex;
@@ -277,7 +338,7 @@ export function renderDashboardHtml(_stats) {
277
338
  .rule-footer {
278
339
  display: flex;
279
340
  justify-content: flex-end;
280
- margin-top: 12px;
341
+ margin-top: 8px;
281
342
  }
282
343
  .btn-save {
283
344
  padding: 5px 12px;
@@ -297,9 +358,12 @@ export function renderDashboardHtml(_stats) {
297
358
  /* Monitoring */
298
359
  .monitoring-grid {
299
360
  display: grid;
300
- grid-template-columns: 1fr 1fr;
361
+ grid-template-columns: repeat(4, 1fr);
301
362
  gap: 16px;
302
363
  }
364
+ @media (max-width: 1024px) {
365
+ .monitoring-grid { grid-template-columns: repeat(2, 1fr); }
366
+ }
303
367
  @media (max-width: 768px) {
304
368
  .monitoring-grid { grid-template-columns: 1fr; }
305
369
  }
@@ -310,17 +374,30 @@ export function renderDashboardHtml(_stats) {
310
374
  overflow: hidden;
311
375
  }
312
376
  .monitor-header {
313
- padding: 12px 16px;
377
+ padding: 8px 12px;
314
378
  font-size: 12px;
315
379
  font-weight: 600;
316
380
  color: var(--muted);
317
381
  border-bottom: 1px solid var(--border);
318
- text-transform: uppercase;
319
382
  letter-spacing: 0.02em;
320
383
  }
321
384
  .monitor-body {
322
- padding: 8px;
323
- max-height: 240px;
385
+ padding: 4px;
386
+ max-height: 180px;
387
+ overflow-y: auto;
388
+ }
389
+ .monitor-item {
390
+ display: flex;
391
+ justify-content: space-between;
392
+ align-items: center;
393
+ padding: 6px 8px;
394
+ border-radius: 6px;
395
+ font-size: 13px;
396
+ margin-bottom: 2px;
397
+ }
398
+ .monitor-body {
399
+ padding: 6px;
400
+ max-height: 200px;
324
401
  overflow-y: auto;
325
402
  }
326
403
  .monitor-item {
@@ -354,14 +431,13 @@ export function renderDashboardHtml(_stats) {
354
431
  padding: 2px 8px;
355
432
  border-radius: 4px;
356
433
  font-weight: 500;
357
- text-transform: uppercase;
358
434
  letter-spacing: 0.02em;
359
435
  }
360
436
  .tag-success { background: rgba(22,163,74,0.1); color: var(--success); }
361
437
  .tag-warning { background: rgba(202,138,4,0.1); color: var(--warning); }
362
438
  .tag-destructive { background: rgba(220,38,38,0.1); color: var(--destructive); }
363
439
  .empty {
364
- padding: 24px;
440
+ padding: 18px;
365
441
  text-align: center;
366
442
  color: var(--dim);
367
443
  font-size: 13px;
@@ -412,7 +488,6 @@ export function renderDashboardHtml(_stats) {
412
488
  @media (max-width: 500px) {
413
489
  .status-item { border-right: none; }
414
490
  }
415
- }
416
491
  .status-item-label {
417
492
  font-size: 13px;
418
493
  color: var(--muted);
@@ -430,13 +505,13 @@ export function renderDashboardHtml(_stats) {
430
505
  overflow: hidden;
431
506
  }
432
507
  .events-body {
433
- max-height: 400px;
508
+ max-height: 280px;
434
509
  overflow-y: auto;
435
510
  }
436
511
  .event-item {
437
512
  display: flex;
438
- gap: 16px;
439
- padding: 10px 16px;
513
+ gap: 10px;
514
+ padding: 6px 10px;
440
515
  font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
441
516
  font-size: 12px;
442
517
  border-bottom: 1px solid var(--border);
@@ -456,47 +531,66 @@ export function renderDashboardHtml(_stats) {
456
531
  .event-msg.err { color: var(--destructive); }
457
532
  .event-msg.warn { color: var(--warning); }
458
533
  .event-msg.dim { color: var(--dim); }
534
+ .event-icon { font-size: 16px; width: 28px; flex-shrink: 0; text-align: center; }
535
+ .event-body { flex: 1; min-width: 0; }
536
+ .event-main { font-size: 13px; font-weight: 500; }
537
+ .event-main.ok { color: var(--success); }
538
+ .event-main.err { color: var(--destructive); }
539
+ .event-main.warn { color: var(--warning); }
540
+ .event-main.dim { color: var(--dim); }
541
+ .event-sub { font-size: 11px; color: var(--dim); margin-top: 2px; word-break: break-all; }
542
+ .btn-kill { padding: 2px 8px; font-size: 10px; border: 1px solid var(--destructive); border-radius: 3px; color: var(--destructive); background: transparent; cursor: pointer; flex-shrink: 0; margin-left: 8px; }
543
+ .btn-kill:hover { background: var(--destructive); color: #fff; }
459
544
  </style>
545
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
460
546
  </head>
461
547
  <body>
462
548
  <header class="header">
463
549
  <div class="header-title" style="display:flex;align-items:center;gap:6px">
464
550
  <span style="background:#eff6ff;color:#2563eb;font-weight:600;font-size:11px;padding:3px 10px;border-radius:10px;letter-spacing:0.3px">Mapick</span>
465
551
  <span>Firewall</span>
466
- <span id="firewall-ver" style="font-size:10px;color:#94a3b8;margin-left:2px"></span>
552
+ <span id="firewall-ver" style="font-size:11px;color:var(--muted);margin-left:4px"></span>
467
553
  </div>
468
- <div class="header-right" style="font-size:10px;color:#94a3b8" id="openclaw-ver"></div>
469
- <div class="header-center">
554
+ <div class="header-right" style="display:flex;align-items:center;gap:12px">
470
555
  <div class="mode-toggle">
471
- <button class="mode-btn active" id="mode-observe">Observe</button>
472
- <button class="mode-btn" id="mode-protect">Protect</button>
556
+ <button class="mode-btn active" id="mode-observe" class="observe-mode">Observe</button>
557
+ <button class="mode-btn" id="mode-protect" class="protect-mode">Protect</button>
473
558
  </div>
474
559
  </div>
475
- <div class="header-actions">
476
- <button class="btn btn-destructive" id="btn-stop">Stop</button>
477
- <button class="btn btn-primary" id="btn-resume" style="display:none">Resume</button>
478
- </div>
479
560
  </header>
480
561
 
481
- <main class="main">
482
- <div class="stats-row">
483
- <div class="stat-card">
484
- <div class="stat-label">Today Tokens</div>
485
- <div class="stat-value" id="stat-tokens">-</div>
486
- </div>
487
- <div class="stat-card">
488
- <div class="stat-label">Blocked</div>
489
- <div class="stat-value" id="stat-blocked">-</div>
490
- </div>
491
- <div class="stat-card">
492
- <div class="stat-label">Limit</div>
493
- <div class="stat-value" id="stat-limit">-</div>
494
- </div>
495
- <div class="stat-card">
496
- <div class="stat-label">Calls</div>
497
- <div class="stat-value" id="stat-calls">-</div>
498
- </div>
562
+ <div id="alert-upgrade" style="display:none;background:#eff6ff;border:1px solid #93c5fd;border-radius:8px;padding:10px 16px;margin:0 auto 12px;max-width:1200px;font-size:13px">
563
+ <span style="color:#1d4ed8">💡</span> Your OpenClaw <span id="alert-upgrade-ver"></span> only supports basic features. Upgrade to <b>v2026.5.7</b> to unlock full cost tracking, auto-breaker, real-time charts and more。
564
+ <code style="background:#dbeafe;padding:2px 6px;border-radius:3px;margin-left:8px">openclaw update</code>
565
+ <button onclick="document.getElementById('alert-upgrade').style.display='none'" style="float:right;background:none;border:none;cursor:pointer;color:var(--muted);font-size:16px">✕</button>
566
+ </div>
567
+
568
+ <div id="alert-unbind" style="display:none;background:#fef2f2;border:1px solid var(--destructive);border-radius:8px;padding:12px 18px;margin:12px auto;max-width:1200px">
569
+ <strong>⚠️ Unbind Alert</strong>:Emergency Stop activated,但仍检测到新的 API 请求。请运行 <code>openclaw gateway restart</code>。
570
+ <span id="alert-unbind-detail" style="display:block;margin-top:4px;font-size:12px;color:var(--muted)"></span>
571
+ </div>
572
+
573
+ <div class="hero-stats">
574
+ <div class="hero-card">
575
+ <div class="hero-value" id="hero-spent">$0</div>
576
+ <div class="hero-label">Today Spent</div>
577
+ </div>
578
+ <div class="hero-card">
579
+ <div class="hero-value" id="hero-blocked">0</div>
580
+ <div class="hero-label">Blocked</div>
581
+ </div>
582
+ <div class="hero-card">
583
+ <div class="hero-value" id="hero-saved">$0</div>
584
+ <div class="hero-label">Saved</div>
499
585
  </div>
586
+ <div class="hero-card" style="border-color:var(--destructive);background:rgba(239,68,68,0.03)">
587
+ <button class="btn-emergency" id="btn-stop" title="Emergency Stop">⏹</button>
588
+ <button class="btn btn-primary" id="btn-resume" style="display:none;margin-top:8px;font-size:12px;padding:4px 12px">▶ Resume</button>
589
+ <div class="hero-label" style="color:var(--destructive);margin-top:6px">STOP</div>
590
+ </div>
591
+ </div>
592
+
593
+ <main class="main">
500
594
 
501
595
  <div class="section">
502
596
  <div class="section-title">Rules</div>
@@ -520,7 +614,7 @@ export function renderDashboardHtml(_stats) {
520
614
  <button class="btn-save" id="btn-save-daily-limit">Save</button>
521
615
  </div>
522
616
  </div>
523
-
617
+
524
618
  <div class="rule-card">
525
619
  <div class="rule-header">
526
620
  <span class="rule-title">Consecutive Failures</span>
@@ -532,19 +626,19 @@ export function renderDashboardHtml(_stats) {
532
626
  <div class="rule-content">
533
627
  <div class="field-row">
534
628
  <input type="number" class="field-input" id="input-failures" placeholder="3">
535
- <span class="field-unit">failures</span>
629
+ <span class="field-unit"></span>
536
630
  </div>
537
631
  <div class="field-row">
538
632
  <input type="number" class="field-input" id="input-cooldown" placeholder="30">
539
- <span class="field-unit">sec</span>
633
+ <span class="field-unit">s</span>
540
634
  </div>
541
- <div class="field-hint">Cooldown after consecutive failures</div>
635
+ <div class="field-hint">Consecutive Failures</div>
542
636
  </div>
543
637
  <div class="rule-footer">
544
638
  <button class="btn-save" id="btn-save-failures">Save</button>
545
639
  </div>
546
640
  </div>
547
-
641
+
548
642
  <div class="rule-card">
549
643
  <div class="rule-header">
550
644
  <span class="rule-title">Token Velocity</span>
@@ -560,7 +654,7 @@ export function renderDashboardHtml(_stats) {
560
654
  </div>
561
655
  <div class="field-row">
562
656
  <input type="number" class="field-input" id="input-velocity-window" placeholder="60">
563
- <span class="field-unit">sec</span>
657
+ <span class="field-unit">s</span>
564
658
  </div>
565
659
  <div class="field-hint">Max tokens within time window</div>
566
660
  </div>
@@ -568,7 +662,7 @@ export function renderDashboardHtml(_stats) {
568
662
  <button class="btn-save" id="btn-save-velocity">Save</button>
569
663
  </div>
570
664
  </div>
571
-
665
+
572
666
  <div class="rule-card">
573
667
  <div class="rule-header">
574
668
  <span class="rule-title">Call Frequency</span>
@@ -580,11 +674,11 @@ export function renderDashboardHtml(_stats) {
580
674
  <div class="rule-content">
581
675
  <div class="field-row">
582
676
  <input type="number" class="field-input" id="input-frequency" placeholder="100">
583
- <span class="field-unit">calls</span>
677
+ <span class="field-unit"></span>
584
678
  </div>
585
679
  <div class="field-row">
586
680
  <input type="number" class="field-input" id="input-frequency-window" placeholder="60">
587
- <span class="field-unit">sec</span>
681
+ <span class="field-unit">s</span>
588
682
  </div>
589
683
  <div class="field-hint">Max calls within time window</div>
590
684
  </div>
@@ -592,9 +686,16 @@ export function renderDashboardHtml(_stats) {
592
686
  <button class="btn-save" id="btn-save-frequency">Save</button>
593
687
  </div>
594
688
  </div>
689
+ </div>
595
690
  </div>
596
- </div>
597
691
 
692
+ <div class="section" id="cost-trend-section" style="display:none">
693
+ <div class="section-title">Cost Trend</div>
694
+ <div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:16px">
695
+ <canvas id="cost-chart" width="800" height="250" style="width:100%;max-height:250px"></canvas>
696
+ </div>
697
+ </div>
698
+
598
699
  <div class="section">
599
700
  <div class="section-title">Monitoring</div>
600
701
  <div class="status-grid">
@@ -628,6 +729,12 @@ export function renderDashboardHtml(_stats) {
628
729
  <div class="empty">No active runs</div>
629
730
  </div>
630
731
  </div>
732
+ <div class="monitor-card">
733
+ <div class="monitor-header">Blocked Sources</div>
734
+ <div class="monitor-body" id="list-blocked">
735
+ <div class="empty">No blocked sources</div>
736
+ </div>
737
+ </div>
631
738
  </div>
632
739
  </div>
633
740
 
@@ -635,7 +742,7 @@ export function renderDashboardHtml(_stats) {
635
742
  <div class="section-title">Events</div>
636
743
  <div class="events-card">
637
744
  <div class="events-body" id="events-log">
638
- <div class="empty">No events</div>
745
+ <div class="empty">无Events</div>
639
746
  </div>
640
747
  </div>
641
748
  </div>
@@ -666,14 +773,38 @@ export function renderDashboardHtml(_stats) {
666
773
  .replace(/\'/g, '&#039;');
667
774
  }
668
775
 
776
+ var _useMock = location.search.includes('mock');
777
+
778
+ function shortName(src) {
779
+ if (!src) return '';
780
+ if (src.length <= 20) return src;
781
+ var ci = src.indexOf(':');
782
+ if (ci > 0 && ci < 20) return src.slice(0, ci + 1) + src.slice(ci + 1, ci + 9);
783
+ return src.slice(0, 16) + '...';
784
+ }
785
+
669
786
  async function fetchStats() {
670
787
  if (_saving) return;
671
788
  try {
789
+ if (_useMock) throw new Error('mock');
672
790
  const res = await fetch('/mapick/api/stats');
673
791
  const data = await res.json();
674
792
  updateUI(data);
675
793
  } catch (e) {
676
- console.error('Failed to fetch stats:', e);
794
+ updateUI({
795
+ mode: 'protect',
796
+ emergency_stop: false,
797
+ today_tokens: 385000,
798
+ today_blocked: 12,
799
+ today_spent_usd: 15.4,
800
+ today_saved_estimate: 4.85,
801
+ daily_token_limit: 500000,
802
+ breaker: { consecutive_failures: 3, cooldown_sec: 30, token_velocity_threshold: 100000, token_velocity_window_sec: 60, call_frequency_threshold: 30, call_frequency_window_sec: 60 },
803
+ cooling_sources: [{ source: 'session:abc12345', reason: 'consecutive_failures', remainingSec: 22 }],
804
+ active_runs: [{ runId: '8293f7a8-c8a2-42d5-9095-cd5af1055bc5', source: 'session:927dd50b', calls: 8, tokens: 272000, status: 'danger' }],
805
+ blocklist: ['session:927dd50b-33a1-48c2-a303-5fa72ec946b5'],
806
+ version: '0.2.25'
807
+ });
677
808
  }
678
809
  }
679
810
 
@@ -698,12 +829,42 @@ export function renderDashboardHtml(_stats) {
698
829
  document.getElementById('mode-protect').className = 'mode-btn protect-mode' + (m === 'protect' ? ' active' : '');
699
830
  }
700
831
 
832
+ var _demoState = 0;
833
+
701
834
  async function emergencyStop() {
835
+ if (_useMock) {
836
+ _demoState = 1;
837
+ updateUI({
838
+ mode: 'protect', emergency_stop: true,
839
+ today_tokens: 385000, today_blocked: 12, today_spent_usd: 15.4, today_saved_estimate: 4.85,
840
+ daily_token_limit: 500000,
841
+ breaker: { consecutive_failures: 3, cooldown_sec: 30, token_velocity_threshold: 100000, token_velocity_window_sec: 60, call_frequency_threshold: 30, call_frequency_window_sec: 60 },
842
+ cooling_sources: [{ source: 'session:abc12345', reason: 'consecutive_failures', remainingSec: 22 }],
843
+ active_runs: [{ runId: '8293f7a8-c8a2-42d5-9095-cd5af1055bc5', source: 'session:927dd50b', calls: 8, tokens: 272000, status: 'danger' }],
844
+ blocklist: ['session:927dd50b-33a1-48c2-a303-5fa72ec946b5'],
845
+ version: '0.2.25'
846
+ });
847
+ return;
848
+ }
702
849
  await fetch('/mapick/api/stop');
703
850
  fetchStats();
704
851
  }
705
852
 
706
853
  async function resume() {
854
+ if (_useMock) {
855
+ _demoState = 0;
856
+ updateUI({
857
+ mode: 'protect', emergency_stop: false,
858
+ today_tokens: 385000, today_blocked: 12, today_spent_usd: 15.4, today_saved_estimate: 4.85,
859
+ daily_token_limit: 500000,
860
+ breaker: { consecutive_failures: 3, cooldown_sec: 30, token_velocity_threshold: 100000, token_velocity_window_sec: 60, call_frequency_threshold: 30, call_frequency_window_sec: 60 },
861
+ cooling_sources: [{ source: 'session:abc12345', reason: 'consecutive_failures', remainingSec: 22 }],
862
+ active_runs: [{ runId: '8293f7a8-c8a2-42d5-9095-cd5af1055bc5', source: 'session:927dd50b', calls: 8, tokens: 272000, status: 'danger' }],
863
+ blocklist: ['session:927dd50b-33a1-48c2-a303-5fa72ec946b5'],
864
+ version: '0.2.25'
865
+ });
866
+ return;
867
+ }
707
868
  await fetch('/mapick/api/resume');
708
869
  fetchStats();
709
870
  }
@@ -715,34 +876,66 @@ export function renderDashboardHtml(_stats) {
715
876
 
716
877
  async function fetchEvents() {
717
878
  try {
879
+ if (_useMock) throw new Error('mock');
718
880
  const res = await fetch('/mapick/api/events');
719
881
  const events = await res.json();
720
882
  renderEvents(events);
721
- } catch (e) {
722
- console.error('Failed to fetch events:', e);
883
+ renderCostChart(events);
884
+ } catch(e) {
885
+ const mockEvents = [
886
+ { type: 'emergency_stop', timestamp: Date.now() - 60000 },
887
+ { type: 'run_status_change', runId: '8293f7a8', source: 'session:927dd50b', cumulativeTokens: 317253, status: 'danger', timestamp: Date.now() - 120000, model: 'gpt-5.5' },
888
+ { type: 'blocked', source: 'session:abc12345', reason: 'consecutive_failures', timestamp: Date.now() - 180000 },
889
+ { type: 'model_call_ended', provider: 'openai', model: 'gpt-5.5', outcome: 'completed', estimatedCost: 63500, source: 'session:927dd50b', timestamp: Date.now() - 240000 },
890
+ { type: 'model_call_ended', provider: 'deepseek', model: 'deepseek-chat', outcome: 'completed', estimatedCost: 2100, source: 'session:xyz', timestamp: Date.now() - 300000 },
891
+ { type: 'model_call_ended', provider: 'openai', model: 'gpt-4o', outcome: 'error', failureKind: 'timeout', estimatedCost: 0, source: 'session:abc12345', timestamp: Date.now() - 360000 },
892
+ { type: 'agent_end', runId: '1234abcd', timestamp: Date.now() - 420000 },
893
+ { type: 'model_call_ended', provider: 'anthropic', model: 'claude-sonnet-4-5', outcome: 'completed', estimatedCost: 42000, source: 'session:def45678', timestamp: Date.now() - 480000 },
894
+ { type: 'blocked', source: 'session:927dd50b', reason: 'manual_kill', timestamp: Date.now() - 540000 }
895
+ ];
896
+ renderEvents(mockEvents);
897
+ renderCostChart(mockEvents);
723
898
  }
724
899
  }
725
900
 
726
901
  function updateUI(data) {
727
- document.getElementById('stat-tokens').textContent = (data.today_tokens ?? 0).toLocaleString();
902
+ const spentUsd = data.today_spent_usd ?? ((data.today_tokens ?? 0) / 1000 * 0.004);
903
+ document.getElementById('hero-spent').textContent = '$' + spentUsd.toFixed(2);
904
+ document.getElementById('hero-blocked').textContent = data.today_blocked ?? 0;
905
+ document.getElementById('hero-saved').textContent = '$' + (data.today_saved_estimate ?? 0).toFixed(2);
906
+ checkUnbindAlert(data);
728
907
  const verEl = document.getElementById('firewall-ver');
729
908
  if (verEl && data.version) verEl.textContent = 'v' + data.version;
730
- document.getElementById('stat-blocked').textContent = data.today_blocked ?? 0;
731
- document.getElementById('stat-limit').textContent = data.daily_token_limit ? data.daily_token_limit.toLocaleString() : '';
732
- document.getElementById('stat-calls').textContent = (data.active_runs ?? []).length;
909
+
910
+ var gwVer = data.openclaw_version || '';
911
+ if (!gwVer && data.version && data.version.startsWith('0.')) {
912
+ if ((data.today_tokens || 0) > 0) {
913
+ document.getElementById('alert-upgrade').style.display = 'none';
914
+ } else {
915
+ document.getElementById('alert-upgrade').style.display = 'block';
916
+ document.getElementById('alert-upgrade-ver').textContent = 'v2026.4.x';
917
+ }
918
+ } else if (gwVer && gwVer.startsWith('2026.4')) {
919
+ document.getElementById('alert-upgrade').style.display = 'block';
920
+ document.getElementById('alert-upgrade-ver').textContent = gwVer;
921
+ } else if (!gwVer) {
922
+ document.getElementById('alert-upgrade').style.display = 'none';
923
+ }
733
924
 
734
925
  const modeObserve = document.getElementById('mode-observe');
735
926
  const modeProtect = document.getElementById('mode-protect');
736
- modeObserve.className = 'mode-btn' + (data.mode === 'observe' ? ' active' : '');
927
+ modeObserve.className = 'mode-btn observe-mode' + (data.mode === 'observe' ? ' active' : '');
737
928
  modeProtect.className = 'mode-btn protect-mode' + (data.mode === 'protect' ? ' active' : '');
738
929
 
739
930
  const btnStop = document.getElementById('btn-stop');
740
931
  const btnResume = document.getElementById('btn-resume');
741
932
  if (data.emergency_stop) {
742
933
  btnStop.style.display = 'none';
934
+ btnStop.classList.add('stopped');
743
935
  btnResume.style.display = 'block';
744
936
  } else {
745
937
  btnStop.style.display = 'block';
938
+ btnStop.classList.remove('stopped');
746
939
  btnResume.style.display = 'none';
747
940
  }
748
941
 
@@ -773,31 +966,24 @@ export function renderDashboardHtml(_stats) {
773
966
 
774
967
  const modeLabel = data.mode ?? 'observe';
775
968
  const estop = data.emergency_stop;
776
- document.getElementById('status-estop').textContent = estop ? '⛔ Active' : 'Inactive';
969
+ document.getElementById('status-estop').textContent = estop ? '⛔ activated' : 'Inactive';
777
970
  document.getElementById('status-estop').style.color = estop ? 'var(--destructive)' : '';
778
- document.getElementById('status-fail').textContent = (breaker.consecutive_failures ?? 3) + ' failures → ' + (breaker.cooldown_sec ?? 30) + 's';
971
+ document.getElementById('status-fail').textContent = (breaker.consecutive_failures ?? 3) + ' failures → ' + (breaker.cooldown_sec ?? 30) + ' s';
779
972
  document.getElementById('status-velocity').textContent = (breaker.token_velocity_threshold ?? 0) > 0
780
- ? (breaker.token_velocity_threshold ?? 0).toLocaleString() + ' / ' + (breaker.token_velocity_window_sec ?? 60) + 's'
781
- : 'Off';
973
+ ? (breaker.token_velocity_threshold ?? 0).toLocaleString() + ' / ' + (breaker.token_velocity_window_sec ?? 60) + ' s'
974
+ : '关闭';
782
975
  document.getElementById('status-frequency').textContent = (breaker.call_frequency_threshold ?? 0) > 0
783
- ? (breaker.call_frequency_threshold ?? 0) + ' / ' + (breaker.call_frequency_window_sec ?? 60) + 's'
784
- : 'Off';
976
+ ? (breaker.call_frequency_threshold ?? 0) + ' / ' + (breaker.call_frequency_window_sec ?? 60) + ' s'
977
+ : '关闭';
785
978
 
786
979
  const coolingSources = data.cooling_sources ?? [];
787
980
  const coolingList = document.getElementById('list-cooling');
788
981
  if (coolingSources.length > 0) {
789
- coolingList.innerHTML = coolingSources.map(s => \`
790
- <div class="monitor-item">
791
- <div>
792
- <div class="item-label">\${escapeHtml(s.source ?? '')}</div>
793
- <div class="item-detail">\${escapeHtml(s.reason ?? '')}</div>
794
- </div>
795
- <div class="item-meta">
796
- \${s.remainingSec > 0 ? '<span style="font-size:12px;color:var(--muted)">\${s.remainingSec}s</span>' : ''}
797
- <button class="btn-sm" onclick="resetSource('\${escapeHtml(s.source ?? '')}')">Reset</button>
798
- </div>
799
- </div>
800
- \`).join('');
982
+ coolingList.innerHTML = coolingSources.map(function(s) {
983
+ var resetBtn = '<button class="btn-sm" onclick="resetSource(' + String.fromCharCode(39) + escapeHtml(s.source ?? '') + String.fromCharCode(39) + ')">Reset</button>';
984
+ var remaining = (s.remainingSec > 0) ? '<span style="font-size:12px;color:var(--muted)">' + s.remainingSec + 's</span>' : '';
985
+ return '<div class="monitor-item"><div><div class="item-label">' + escapeHtml(shortName(s.source ?? '')) + '</div><div class="item-detail">' + escapeHtml(s.reason ?? '') + '</div></div><div class="item-meta">' + remaining + resetBtn + '</div></div>';
986
+ }).join('');
801
987
  } else {
802
988
  coolingList.innerHTML = '<div class="empty">No cooling sources</div>';
803
989
  }
@@ -817,67 +1003,85 @@ export function renderDashboardHtml(_stats) {
817
1003
  } else {
818
1004
  runsList.innerHTML = '<div class="empty">No active runs</div>';
819
1005
  }
1006
+
1007
+ const blocklist = data.blocklist ?? [];
1008
+ const blockedEl = document.getElementById('list-blocked');
1009
+ if (blocklist.length > 0) {
1010
+ blockedEl.innerHTML = blocklist.map(s => '<div class="monitor-item"><div><div class="item-label">' + escapeHtml(shortName(s)) + '</div><div class="item-detail" style="color:var(--destructive)">permanently blocked</div></div><div class="item-meta"><button class="btn-sm" style="border-color:var(--destructive);color:var(--destructive)" onclick="unblockSource(' + String.fromCharCode(39) + escapeHtml(s) + String.fromCharCode(39) + ')">Unblock</button></div></div>').join('');
1011
+ } else {
1012
+ blockedEl.innerHTML = '<div class="empty">No blocked sources</div>';
1013
+ }
820
1014
  }
821
1015
 
822
1016
  function renderEvents(events) {
823
1017
  const log = document.getElementById('events-log');
824
- if (!events ?? events.length === 0) {
825
- log.innerHTML = '<div class="empty">No events</div>';
1018
+ if (!events || events.length === 0) {
1019
+ log.innerHTML = '<div class="empty">暂无Events记录</div>';
826
1020
  return;
827
1021
  }
828
- log.innerHTML = events.slice(0, 100).reverse().map(e => {
1022
+ log.innerHTML = events.slice(0, 50).reverse().map(e => {
829
1023
  const ts = e.timestamp ?? e.time;
830
1024
  const date = ts ? new Date(ts) : null;
831
1025
  const timeStr = date ? date.toTimeString().slice(0, 8) : '--:--:--';
832
1026
  const type = e.type ?? 'unknown';
833
- let cls = '';
834
- let text = '';
835
-
1027
+ const source = e.source ?? '';
1028
+ var shortSource = shortName(source);
1029
+ const model = e.model ?? '';
1030
+ const provider = e.provider ?? '';
1031
+ const cost = e.estimatedCost ?? 0;
1032
+ const costK = Math.round(cost / 100) / 10;
1033
+ const costUsd = (cost / 1000 * 0.004).toFixed(2);
1034
+ let icon = '', text = '', sub = '', cls = '';
1035
+
836
1036
  if (type === 'model_call_ended') {
837
- const provider = e.provider ?? 'unknown';
838
- const model = e.model ?? 'unknown';
839
1037
  const outcome = e.outcome ?? 'completed';
840
- const cost = e.estimatedCost ?? 0;
841
- const costK = (cost / 1000).toFixed(1);
842
1038
  if (outcome === 'completed') {
843
- cls = 'ok';
844
- text = provider + '/' + model + ' completed — ' + costK + 'K tokens';
1039
+ icon = '✅'; cls = 'ok';
1040
+ text = provider + '/' + model + ' — ' + costK + 'K tokens ($' + costUsd + ')';
1041
+ sub = source ? 'Source: ' + shortSource : '';
845
1042
  } else {
846
- cls = 'err';
847
- text = provider + '/' + model + ' error (' + outcome + ')';
1043
+ icon = '❌'; cls = 'err';
1044
+ text = provider + '/' + model + ' ' + outcome + ' (' + (e.failureKind ?? 'error') + ')';
1045
+ sub = source ? 'Source: ' + shortSource : '';
848
1046
  }
849
1047
  } else if (type === 'blocked') {
850
- cls = 'err';
851
- const source = e.source ?? 'unknown';
852
- const reason = e.reason ?? 'unknown';
853
- text = 'BLOCKED ' + source + ' — ' + reason;
1048
+ icon = '🚫'; cls = 'err';
1049
+ text = 'BLOCKED — ' + (e.reason ?? 'unknown');
1050
+ if (source) sub = 'Source: ' + shortSource;
854
1051
  } else if (type === 'run_status_change') {
855
- cls = 'warn';
856
- const runId = e.runId ?? 'unknown';
857
- const status = e.status ?? 'unknown';
1052
+ icon = '⚠️'; cls = 'warn';
858
1053
  const tokens = e.cumulativeTokens ?? 0;
859
- const calls = e.runCalls ?? 0;
860
- text = 'Run ' + runId + ' ' + status + ' (' + tokens.toLocaleString() + ' tokens, ' + calls + ' calls)';
1054
+ text = 'Token warning — ' + (tokens/1000).toFixed(0) + 'K tokens ($' + (tokens/1000*0.004).toFixed(2) + ')';
1055
+ if (source) sub = 'Source: ' + shortSource + (model ? ' | ' + model : '');
861
1056
  } else if (type === 'agent_end') {
862
- cls = 'dim';
863
- const runId = e.runId ?? 'unknown';
864
- text = 'Run ' + runId + ' ended';
1057
+ icon = '🏁'; cls = 'dim';
1058
+ text = 'Run ended — ' + (e.runId ?? '').slice(0, 8);
865
1059
  } else if (type === 'config_warning') {
866
- cls = 'dim';
867
- text = 'Config warning: ' + (e.message ?? e.msg ?? 'unknown');
1060
+ icon = '⚙️'; cls = 'dim';
1061
+ text = '配置 ' + (e.reason ?? e.message ?? '');
1062
+ } else if (type === 'emergency_stop') {
1063
+ icon = '🛑'; cls = 'err';
1064
+ text = 'Emergency Stop activated';
868
1065
  } else if (type === 'zero_output_warning') {
869
- cls = 'warn';
870
- const provider = e.provider ?? 'unknown';
871
- const model = e.model ?? 'unknown';
872
- text = 'Zero output: ' + provider + '/' + model + ' — 0 bytes';
1066
+ icon = '⚠️'; cls = 'warn';
1067
+ text = 'Zero output: ' + provider + '/' + model;
873
1068
  } else {
874
- cls = 'dim';
875
- text = e.message ?? e.msg ?? JSON.stringify(e);
1069
+ icon = '📋'; cls = 'dim';
1070
+ text = type + (e.reason ? ' — ' + e.reason : '');
876
1071
  }
877
-
1072
+
1073
+ var killBtn = (source && (type === 'model_call_ended' || type === 'blocked'))
1074
+ ? '<button class="btn-kill" onclick="blockSource(' + String.fromCharCode(39) + escapeHtml(source) + String.fromCharCode(39) + ')">Kill</button>'
1075
+ : '';
1076
+
878
1077
  return '<div class="event-item">' +
1078
+ '<span class="event-icon">' + icon + '</span>' +
1079
+ '<div class="event-body">' +
1080
+ '<div class="event-main ' + cls + '">' + escapeHtml(text) + '</div>' +
1081
+ (sub ? '<div class="event-sub">' + escapeHtml(sub) + '</div>' : '') +
1082
+ '</div>' +
879
1083
  '<span class="event-time">' + escapeHtml(timeStr) + '</span>' +
880
- '<span class="event-msg ' + cls + '">' + escapeHtml(text) + '</span>' +
1084
+ killBtn +
881
1085
  '</div>';
882
1086
  }).join('');
883
1087
  }
@@ -933,6 +1137,58 @@ export function renderDashboardHtml(_stats) {
933
1137
  saveConfig({ breaker: { callFrequencyThreshold: threshold, callFrequencyWindowSec: window } });
934
1138
  });
935
1139
 
1140
+ async function blockSource(src) {
1141
+ await fetch('/mapick/api/block-source', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({source:src}) });
1142
+ fetchStats(); fetchEvents();
1143
+ }
1144
+ async function unblockSource(src) {
1145
+ await fetch('/mapick/api/unblock-source', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({source:src}) });
1146
+ fetchStats(); fetchEvents();
1147
+ }
1148
+
1149
+ var costChart = null;
1150
+ var unbindAlertShown = false;
1151
+
1152
+ function renderCostChart(events) {
1153
+ var canvas = document.getElementById('cost-chart');
1154
+ if (!canvas) return;
1155
+ var ctx = canvas.getContext('2d');
1156
+ var calls = events.filter(function(e) { return e.type === 'model_call_ended' && e.estimatedCost > 0; });
1157
+ var section = document.getElementById('cost-trend-section');
1158
+ if (calls.length < 2) {
1159
+ if (section) section.style.display = 'none';
1160
+ if (costChart) { try { costChart.destroy(); } catch(e) {} costChart = null; }
1161
+ return;
1162
+ }
1163
+ if (section) section.style.display = 'block';
1164
+ var labels = [], values = [], cum = 0;
1165
+ calls.sort(function(a,b) { return a.timestamp - b.timestamp; });
1166
+ for (var i = 0; i < calls.length; i++) {
1167
+ cum += calls[i].estimatedCost;
1168
+ labels.push(new Date(calls[i].timestamp).toTimeString().slice(0,5));
1169
+ values.push(Math.round(cum / 100) / 10);
1170
+ }
1171
+ if (costChart) { try { costChart.destroy(); } catch(e) {} costChart = null; }
1172
+ costChart = new Chart(ctx, {
1173
+ type: 'line',
1174
+ data: { labels: labels, datasets: [{ label: 'K tokens', data: values, borderColor: '#2563eb', backgroundColor: 'rgba(37,99,235,0.08)', fill: true, tension: 0.3, pointRadius: 2 }] },
1175
+ options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { grid: { color: '#e5e7eb' }, beginAtZero: true } } }
1176
+ });
1177
+ }
1178
+
1179
+ function checkUnbindAlert(data) {
1180
+ if (data.emergency_stop && data.today_tokens > 0) {
1181
+ if (!unbindAlertShown) {
1182
+ document.getElementById('alert-unbind').style.display = 'block';
1183
+ document.getElementById('alert-unbind-detail').textContent = 'Stopped but today still has ' + (data.today_tokens ?? 0).toLocaleString() + ' tokens consumed';
1184
+ unbindAlertShown = true;
1185
+ }
1186
+ } else if (!data.emergency_stop) {
1187
+ document.getElementById('alert-unbind').style.display = 'none';
1188
+ unbindAlertShown = false;
1189
+ }
1190
+ }
1191
+
936
1192
  fetchStats();
937
1193
  fetchEvents();
938
1194
  _refreshTimer = setInterval(() => {