@mandujs/core 0.20.7 → 0.20.9
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.
- package/package.json +2 -1
- package/src/config/mandu.ts +2 -0
- package/src/kitchen/kitchen-handler.ts +81 -4
- package/src/kitchen/kitchen-ui.ts +1311 -906
- package/src/observability/index.ts +10 -0
- package/src/observability/sqlite-store.ts +254 -0
- package/src/runtime/server.ts +132 -4
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Kitchen UI - Inline HTML/CSS/JS for the dev dashboard.
|
|
3
|
-
*
|
|
4
|
-
* Phase 1 MVP: Single-page dashboard with three panels.
|
|
5
|
-
* No build step required — pure inline vanilla JS.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export function renderKitchenHTML(): string {
|
|
9
|
-
return `<!DOCTYPE html>
|
|
10
|
-
<html lang="en">
|
|
11
|
-
<head>
|
|
12
|
-
<meta charset="UTF-8">
|
|
13
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
14
|
-
<title>Mandu Kitchen</title>
|
|
15
|
-
<style>${CSS}</style>
|
|
16
|
-
</head>
|
|
1
|
+
/**
|
|
2
|
+
* Kitchen UI - Inline HTML/CSS/JS for the dev dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 MVP: Single-page dashboard with three panels.
|
|
5
|
+
* No build step required — pure inline vanilla JS.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function renderKitchenHTML(): string {
|
|
9
|
+
return `<!DOCTYPE html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="UTF-8">
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
14
|
+
<title>Mandu Kitchen</title>
|
|
15
|
+
<style>${CSS}</style>
|
|
16
|
+
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div class="app-shell">
|
|
19
19
|
<header class="hero">
|
|
@@ -63,6 +63,10 @@ export function renderKitchenHTML(): string {
|
|
|
63
63
|
<button class="tab" data-panel="guard">Guard</button>
|
|
64
64
|
<button class="tab" data-panel="preview">Preview</button>
|
|
65
65
|
<button class="tab" data-panel="contracts">Contracts</button>
|
|
66
|
+
<button class="tab" data-panel="requests">Requests</button>
|
|
67
|
+
<button class="tab" data-panel="mcp-activity">MCP</button>
|
|
68
|
+
<button class="tab" data-panel="cache">Cache</button>
|
|
69
|
+
<button class="tab" data-panel="metrics">Metrics</button>
|
|
66
70
|
</nav>
|
|
67
71
|
|
|
68
72
|
<main class="panels">
|
|
@@ -155,6 +159,59 @@ export function renderKitchenHTML(): string {
|
|
|
155
159
|
</div>
|
|
156
160
|
</div>
|
|
157
161
|
</section>
|
|
162
|
+
|
|
163
|
+
<section id="panel-requests" class="panel">
|
|
164
|
+
<div class="panel-header">
|
|
165
|
+
<div>
|
|
166
|
+
<h2>Requests</h2>
|
|
167
|
+
<p class="panel-subtitle">Recent HTTP requests. Click a row to see correlation-linked events.</p>
|
|
168
|
+
</div>
|
|
169
|
+
<button id="refresh-requests" class="btn-sm">Refresh</button>
|
|
170
|
+
</div>
|
|
171
|
+
<div id="requests-list" class="requests-list">
|
|
172
|
+
<div class="empty-state">Loading requests...</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div id="requests-detail" class="requests-detail" style="display:none;"></div>
|
|
175
|
+
</section>
|
|
176
|
+
|
|
177
|
+
<section id="panel-mcp-activity" class="panel">
|
|
178
|
+
<div class="panel-header">
|
|
179
|
+
<div>
|
|
180
|
+
<h2>MCP Activity</h2>
|
|
181
|
+
<p class="panel-subtitle">Recent MCP tool calls from the EventBus, grouped by correlation.</p>
|
|
182
|
+
</div>
|
|
183
|
+
<button id="refresh-mcp" class="btn-sm">Refresh</button>
|
|
184
|
+
</div>
|
|
185
|
+
<div id="mcp-list" class="mcp-list">
|
|
186
|
+
<div class="empty-state">Loading MCP activity...</div>
|
|
187
|
+
</div>
|
|
188
|
+
</section>
|
|
189
|
+
|
|
190
|
+
<section id="panel-cache" class="panel">
|
|
191
|
+
<div class="panel-header">
|
|
192
|
+
<div>
|
|
193
|
+
<h2>Cache</h2>
|
|
194
|
+
<p class="panel-subtitle">ISR/SWR cache store statistics.</p>
|
|
195
|
+
</div>
|
|
196
|
+
<button id="refresh-cache" class="btn-sm">Refresh</button>
|
|
197
|
+
</div>
|
|
198
|
+
<div id="cache-content" class="cache-content">
|
|
199
|
+
<div class="empty-state">Loading cache stats...</div>
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
|
|
203
|
+
<section id="panel-metrics" class="panel">
|
|
204
|
+
<div class="panel-header">
|
|
205
|
+
<div>
|
|
206
|
+
<h2>Metrics</h2>
|
|
207
|
+
<p class="panel-subtitle">Rolling 5-minute window over all observability events.</p>
|
|
208
|
+
</div>
|
|
209
|
+
<button id="refresh-metrics" class="btn-sm">Refresh</button>
|
|
210
|
+
</div>
|
|
211
|
+
<div id="metrics-content" class="metrics-content">
|
|
212
|
+
<div class="empty-state">Loading metrics...</div>
|
|
213
|
+
</div>
|
|
214
|
+
</section>
|
|
158
215
|
</main>
|
|
159
216
|
|
|
160
217
|
<div id="debug-bar" class="debug-bar"></div>
|
|
@@ -163,413 +220,413 @@ export function renderKitchenHTML(): string {
|
|
|
163
220
|
<script>${JS}</script>
|
|
164
221
|
</body>
|
|
165
222
|
</html>`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ─── CSS ─────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
const CSS = /* css */ `
|
|
171
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
172
|
-
|
|
173
|
-
body {
|
|
174
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
175
|
-
background: #0f1117;
|
|
176
|
-
color: #e4e4e7;
|
|
177
|
-
min-height: 100vh;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.header {
|
|
181
|
-
display: flex;
|
|
182
|
-
align-items: center;
|
|
183
|
-
justify-content: space-between;
|
|
184
|
-
padding: 12px 20px;
|
|
185
|
-
background: #18181b;
|
|
186
|
-
border-bottom: 1px solid #27272a;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.logo {
|
|
190
|
-
display: flex;
|
|
191
|
-
align-items: center;
|
|
192
|
-
gap: 8px;
|
|
193
|
-
font-size: 18px;
|
|
194
|
-
font-weight: 600;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
.logo-icon { font-size: 24px; }
|
|
198
|
-
|
|
199
|
-
.status {
|
|
200
|
-
display: flex;
|
|
201
|
-
align-items: center;
|
|
202
|
-
gap: 6px;
|
|
203
|
-
font-size: 13px;
|
|
204
|
-
color: #a1a1aa;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.status-dot {
|
|
208
|
-
width: 8px;
|
|
209
|
-
height: 8px;
|
|
210
|
-
border-radius: 50%;
|
|
211
|
-
transition: background 0.3s;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
.status-dot.connected { background: #22c55e; }
|
|
215
|
-
.status-dot.disconnected { background: #ef4444; }
|
|
216
|
-
.status-dot.connecting { background: #eab308; }
|
|
217
|
-
|
|
218
|
-
.tabs {
|
|
219
|
-
display: flex;
|
|
220
|
-
gap: 0;
|
|
221
|
-
background: #18181b;
|
|
222
|
-
border-bottom: 1px solid #27272a;
|
|
223
|
-
padding: 0 20px;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
.tab {
|
|
227
|
-
padding: 10px 20px;
|
|
228
|
-
background: none;
|
|
229
|
-
border: none;
|
|
230
|
-
color: #71717a;
|
|
231
|
-
font-size: 14px;
|
|
232
|
-
cursor: pointer;
|
|
233
|
-
border-bottom: 2px solid transparent;
|
|
234
|
-
transition: all 0.2s;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.tab:hover { color: #e4e4e7; }
|
|
238
|
-
.tab.active {
|
|
239
|
-
color: #a78bfa;
|
|
240
|
-
border-bottom-color: #a78bfa;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
.panels { padding: 16px 20px; }
|
|
244
|
-
|
|
245
|
-
.panel { display: none; }
|
|
246
|
-
.panel.active { display: block; }
|
|
247
|
-
|
|
248
|
-
.panel-header {
|
|
249
|
-
display: flex;
|
|
250
|
-
align-items: center;
|
|
251
|
-
justify-content: space-between;
|
|
252
|
-
margin-bottom: 12px;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
.panel-header h2 {
|
|
256
|
-
font-size: 16px;
|
|
257
|
-
font-weight: 600;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.btn-sm {
|
|
261
|
-
padding: 4px 12px;
|
|
262
|
-
background: #27272a;
|
|
263
|
-
border: 1px solid #3f3f46;
|
|
264
|
-
border-radius: 6px;
|
|
265
|
-
color: #e4e4e7;
|
|
266
|
-
font-size: 12px;
|
|
267
|
-
cursor: pointer;
|
|
268
|
-
transition: background 0.2s;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
.btn-sm:hover { background: #3f3f46; }
|
|
272
|
-
.btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
273
|
-
|
|
274
|
-
.empty-state {
|
|
275
|
-
padding: 40px 20px;
|
|
276
|
-
text-align: center;
|
|
277
|
-
color: #52525b;
|
|
278
|
-
font-size: 14px;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/* Activity */
|
|
282
|
-
.activity-list {
|
|
283
|
-
max-height: calc(100vh - 180px);
|
|
284
|
-
overflow-y: auto;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
.activity-item {
|
|
288
|
-
padding: 8px 12px;
|
|
289
|
-
border-bottom: 1px solid #1e1e22;
|
|
290
|
-
font-size: 13px;
|
|
291
|
-
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
|
|
292
|
-
display: flex;
|
|
293
|
-
gap: 10px;
|
|
294
|
-
align-items: flex-start;
|
|
295
|
-
animation: fadeIn 0.3s ease;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
@keyframes fadeIn {
|
|
299
|
-
from { opacity: 0; transform: translateY(-4px); }
|
|
300
|
-
to { opacity: 1; transform: translateY(0); }
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.activity-time {
|
|
304
|
-
color: #52525b;
|
|
305
|
-
white-space: nowrap;
|
|
306
|
-
flex-shrink: 0;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
.activity-tool {
|
|
310
|
-
color: #a78bfa;
|
|
311
|
-
font-weight: 500;
|
|
312
|
-
flex-shrink: 0;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
.activity-detail {
|
|
316
|
-
color: #a1a1aa;
|
|
317
|
-
overflow: hidden;
|
|
318
|
-
text-overflow: ellipsis;
|
|
319
|
-
white-space: nowrap;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/* Routes */
|
|
323
|
-
.summary {
|
|
324
|
-
display: flex;
|
|
325
|
-
gap: 12px;
|
|
326
|
-
font-size: 12px;
|
|
327
|
-
color: #a1a1aa;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
.summary-item {
|
|
331
|
-
display: flex;
|
|
332
|
-
align-items: center;
|
|
333
|
-
gap: 4px;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.summary-count {
|
|
337
|
-
font-weight: 600;
|
|
338
|
-
color: #e4e4e7;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
.route-item {
|
|
342
|
-
display: flex;
|
|
343
|
-
align-items: center;
|
|
344
|
-
gap: 12px;
|
|
345
|
-
padding: 10px 12px;
|
|
346
|
-
border-bottom: 1px solid #1e1e22;
|
|
347
|
-
font-size: 13px;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
.route-kind {
|
|
351
|
-
display: inline-block;
|
|
352
|
-
padding: 2px 8px;
|
|
353
|
-
border-radius: 4px;
|
|
354
|
-
font-size: 11px;
|
|
355
|
-
font-weight: 600;
|
|
356
|
-
text-transform: uppercase;
|
|
357
|
-
flex-shrink: 0;
|
|
358
|
-
min-width: 44px;
|
|
359
|
-
text-align: center;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
.route-kind.page { background: #1e3a5f; color: #60a5fa; }
|
|
363
|
-
.route-kind.api { background: #1a3c34; color: #4ade80; }
|
|
364
|
-
|
|
365
|
-
.route-pattern {
|
|
366
|
-
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
|
|
367
|
-
color: #e4e4e7;
|
|
368
|
-
flex: 1;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
.route-badges {
|
|
372
|
-
display: flex;
|
|
373
|
-
gap: 4px;
|
|
374
|
-
flex-shrink: 0;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
.badge {
|
|
378
|
-
padding: 1px 6px;
|
|
379
|
-
border-radius: 3px;
|
|
380
|
-
font-size: 10px;
|
|
381
|
-
background: #27272a;
|
|
382
|
-
color: #a1a1aa;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/* Guard */
|
|
386
|
-
.guard-status {
|
|
387
|
-
margin-bottom: 12px;
|
|
388
|
-
font-size: 13px;
|
|
389
|
-
color: #a1a1aa;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
.guard-summary {
|
|
393
|
-
display: flex;
|
|
394
|
-
gap: 16px;
|
|
395
|
-
padding: 12px;
|
|
396
|
-
background: #18181b;
|
|
397
|
-
border-radius: 8px;
|
|
398
|
-
margin-bottom: 12px;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.guard-stat {
|
|
402
|
-
text-align: center;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
.guard-stat-value {
|
|
406
|
-
font-size: 24px;
|
|
407
|
-
font-weight: 700;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
.guard-stat-label {
|
|
411
|
-
font-size: 11px;
|
|
412
|
-
color: #71717a;
|
|
413
|
-
text-transform: uppercase;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
.sev-error { color: #ef4444; }
|
|
417
|
-
.sev-warning { color: #eab308; }
|
|
418
|
-
.sev-info { color: #3b82f6; }
|
|
419
|
-
|
|
420
|
-
.violation-item {
|
|
421
|
-
padding: 8px 12px;
|
|
422
|
-
border-bottom: 1px solid #1e1e22;
|
|
423
|
-
font-size: 13px;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
.violation-file {
|
|
427
|
-
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
|
|
428
|
-
color: #a78bfa;
|
|
429
|
-
margin-bottom: 2px;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
.violation-msg {
|
|
433
|
-
color: #a1a1aa;
|
|
434
|
-
font-size: 12px;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
.violation-sev {
|
|
438
|
-
display: inline-block;
|
|
439
|
-
padding: 1px 6px;
|
|
440
|
-
border-radius: 3px;
|
|
441
|
-
font-size: 10px;
|
|
442
|
-
font-weight: 600;
|
|
443
|
-
margin-right: 4px;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
.violation-sev.error { background: #3b1111; color: #ef4444; }
|
|
447
|
-
.violation-sev.warning { background: #3b2f11; color: #eab308; }
|
|
448
|
-
.violation-sev.info { background: #112840; color: #3b82f6; }
|
|
449
|
-
|
|
450
|
-
/* Preview */
|
|
451
|
-
.preview-list { max-height: 40vh; overflow-y: auto; }
|
|
452
|
-
.preview-diff { max-height: 50vh; overflow-y: auto; padding: 8px; }
|
|
453
|
-
|
|
454
|
-
.change-item {
|
|
455
|
-
display: flex;
|
|
456
|
-
align-items: center;
|
|
457
|
-
gap: 10px;
|
|
458
|
-
padding: 8px 12px;
|
|
459
|
-
border-bottom: 1px solid #1e1e22;
|
|
460
|
-
font-size: 13px;
|
|
461
|
-
cursor: pointer;
|
|
462
|
-
transition: background 0.15s;
|
|
463
|
-
}
|
|
464
|
-
.change-item:hover { background: #27272a; }
|
|
465
|
-
.change-icon { flex-shrink: 0; }
|
|
466
|
-
.change-path {
|
|
467
|
-
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
|
|
468
|
-
color: #e4e4e7;
|
|
469
|
-
flex: 1;
|
|
470
|
-
overflow: hidden;
|
|
471
|
-
text-overflow: ellipsis;
|
|
472
|
-
white-space: nowrap;
|
|
473
|
-
}
|
|
474
|
-
.change-status {
|
|
475
|
-
font-size: 10px;
|
|
476
|
-
padding: 1px 6px;
|
|
477
|
-
border-radius: 3px;
|
|
478
|
-
text-transform: uppercase;
|
|
479
|
-
font-weight: 600;
|
|
480
|
-
flex-shrink: 0;
|
|
481
|
-
}
|
|
482
|
-
.change-status.added { background: #1a3c34; color: #4ade80; }
|
|
483
|
-
.change-status.modified { background: #1e3a5f; color: #60a5fa; }
|
|
484
|
-
.change-status.deleted { background: #3b1111; color: #ef4444; }
|
|
485
|
-
.change-status.untracked { background: #3b2f11; color: #eab308; }
|
|
486
|
-
.change-status.renamed { background: #2a1a3c; color: #a78bfa; }
|
|
487
|
-
|
|
488
|
-
.diff-header {
|
|
489
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
490
|
-
padding: 8px 12px; background: #18181b; border-radius: 6px 6px 0 0;
|
|
491
|
-
border-bottom: 1px solid #27272a;
|
|
492
|
-
}
|
|
493
|
-
.diff-file { font-family: monospace; color: #a78bfa; font-size: 13px; }
|
|
494
|
-
.diff-stats { font-size: 12px; }
|
|
495
|
-
.diff-add { color: #4ade80; margin-right: 8px; }
|
|
496
|
-
.diff-del { color: #ef4444; }
|
|
497
|
-
.diff-hunk-header { padding: 4px 12px; background: #112840; color: #3b82f6; font-size: 12px; font-family: monospace; }
|
|
498
|
-
.diff-line { display: flex; font-family: monospace; font-size: 12px; line-height: 20px; }
|
|
499
|
-
.diff-line-num { width: 40px; text-align: right; padding: 0 4px; color: #52525b; user-select: none; flex-shrink: 0; }
|
|
500
|
-
.diff-line-content { flex: 1; padding: 0 8px; white-space: pre; overflow: hidden; text-overflow: ellipsis; }
|
|
501
|
-
.diff-line.add { background: rgba(74,222,128,0.08); }
|
|
502
|
-
.diff-line.add .diff-line-content::before { content: '+'; color: #4ade80; }
|
|
503
|
-
.diff-line.remove { background: rgba(239,68,68,0.08); }
|
|
504
|
-
.diff-line.remove .diff-line-content::before { content: '-'; color: #ef4444; }
|
|
505
|
-
.diff-line.context .diff-line-content::before { content: ' '; }
|
|
506
|
-
|
|
507
|
-
/* Contracts */
|
|
508
|
-
.contracts-layout { display: flex; gap: 12px; height: calc(100vh - 180px); }
|
|
509
|
-
.contracts-list { width: 300px; flex-shrink: 0; overflow-y: auto; border-right: 1px solid #27272a; padding-right: 12px; }
|
|
510
|
-
.contracts-detail { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
|
|
511
|
-
|
|
512
|
-
.contract-item {
|
|
513
|
-
display: flex; align-items: center; gap: 8px;
|
|
514
|
-
padding: 8px 12px; border-bottom: 1px solid #1e1e22;
|
|
515
|
-
cursor: pointer; transition: background 0.15s; font-size: 13px;
|
|
516
|
-
}
|
|
517
|
-
.contract-item:hover { background: #27272a; }
|
|
518
|
-
.contract-item.selected { background: #27272a; border-left: 2px solid #a78bfa; }
|
|
519
|
-
|
|
520
|
-
.method-badge {
|
|
521
|
-
display: inline-block; padding: 1px 6px; border-radius: 3px;
|
|
522
|
-
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
523
|
-
flex-shrink: 0; min-width: 36px; text-align: center;
|
|
524
|
-
}
|
|
525
|
-
.method-badge.get { background: #1a3c34; color: #4ade80; }
|
|
526
|
-
.method-badge.post { background: #1e3a5f; color: #60a5fa; }
|
|
527
|
-
.method-badge.put { background: #3b2f11; color: #eab308; }
|
|
528
|
-
.method-badge.patch { background: #2a1a3c; color: #a78bfa; }
|
|
529
|
-
.method-badge.delete { background: #3b1111; color: #ef4444; }
|
|
530
|
-
|
|
531
|
-
.contract-pattern { font-family: monospace; color: #e4e4e7; }
|
|
532
|
-
|
|
533
|
-
.contract-schema {
|
|
534
|
-
background: #18181b; border-radius: 8px; padding: 12px;
|
|
535
|
-
font-family: monospace; font-size: 12px; white-space: pre-wrap;
|
|
536
|
-
max-height: 40vh; overflow-y: auto;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
.contract-playground { background: #18181b; border-radius: 8px; padding: 12px; }
|
|
540
|
-
.contract-playground h3 { font-size: 14px; margin-bottom: 8px; }
|
|
541
|
-
|
|
542
|
-
.playground-controls { display: flex; gap: 8px; margin-bottom: 8px; }
|
|
543
|
-
.select-sm {
|
|
544
|
-
padding: 4px 8px; background: #27272a; border: 1px solid #3f3f46;
|
|
545
|
-
border-radius: 6px; color: #e4e4e7; font-size: 12px;
|
|
546
|
-
}
|
|
547
|
-
.playground-inputs { display: flex; flex-direction: column; gap: 6px; }
|
|
548
|
-
.playground-inputs label { font-size: 11px; color: #71717a; display: flex; flex-direction: column; gap: 2px; }
|
|
549
|
-
.playground-inputs textarea {
|
|
550
|
-
background: #27272a; border: 1px solid #3f3f46; border-radius: 4px;
|
|
551
|
-
color: #e4e4e7; font-family: monospace; font-size: 12px; padding: 6px;
|
|
552
|
-
resize: vertical;
|
|
553
|
-
}
|
|
554
|
-
.validate-result { margin-top: 8px; padding: 8px; border-radius: 4px; font-size: 12px; font-family: monospace; }
|
|
555
|
-
.validate-result.success { background: #1a3c34; color: #4ade80; }
|
|
556
|
-
.validate-result.error { background: #3b1111; color: #ef4444; }
|
|
557
|
-
|
|
558
|
-
.debug-bar {
|
|
559
|
-
position: fixed;
|
|
560
|
-
bottom: 0;
|
|
561
|
-
left: 0;
|
|
562
|
-
right: 0;
|
|
563
|
-
padding: 4px 12px;
|
|
564
|
-
background: #1a1a2e;
|
|
565
|
-
border-top: 1px solid #27272a;
|
|
566
|
-
font-size: 11px;
|
|
567
|
-
font-family: monospace;
|
|
568
|
-
color: #71717a;
|
|
569
|
-
max-height: 60px;
|
|
570
|
-
overflow-y: auto;
|
|
571
|
-
}
|
|
572
|
-
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── CSS ─────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
const CSS = /* css */ `
|
|
228
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
229
|
+
|
|
230
|
+
body {
|
|
231
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
232
|
+
background: #0f1117;
|
|
233
|
+
color: #e4e4e7;
|
|
234
|
+
min-height: 100vh;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.header {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
justify-content: space-between;
|
|
241
|
+
padding: 12px 20px;
|
|
242
|
+
background: #18181b;
|
|
243
|
+
border-bottom: 1px solid #27272a;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.logo {
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
gap: 8px;
|
|
250
|
+
font-size: 18px;
|
|
251
|
+
font-weight: 600;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.logo-icon { font-size: 24px; }
|
|
255
|
+
|
|
256
|
+
.status {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: 6px;
|
|
260
|
+
font-size: 13px;
|
|
261
|
+
color: #a1a1aa;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.status-dot {
|
|
265
|
+
width: 8px;
|
|
266
|
+
height: 8px;
|
|
267
|
+
border-radius: 50%;
|
|
268
|
+
transition: background 0.3s;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.status-dot.connected { background: #22c55e; }
|
|
272
|
+
.status-dot.disconnected { background: #ef4444; }
|
|
273
|
+
.status-dot.connecting { background: #eab308; }
|
|
274
|
+
|
|
275
|
+
.tabs {
|
|
276
|
+
display: flex;
|
|
277
|
+
gap: 0;
|
|
278
|
+
background: #18181b;
|
|
279
|
+
border-bottom: 1px solid #27272a;
|
|
280
|
+
padding: 0 20px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.tab {
|
|
284
|
+
padding: 10px 20px;
|
|
285
|
+
background: none;
|
|
286
|
+
border: none;
|
|
287
|
+
color: #71717a;
|
|
288
|
+
font-size: 14px;
|
|
289
|
+
cursor: pointer;
|
|
290
|
+
border-bottom: 2px solid transparent;
|
|
291
|
+
transition: all 0.2s;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.tab:hover { color: #e4e4e7; }
|
|
295
|
+
.tab.active {
|
|
296
|
+
color: #a78bfa;
|
|
297
|
+
border-bottom-color: #a78bfa;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.panels { padding: 16px 20px; }
|
|
301
|
+
|
|
302
|
+
.panel { display: none; }
|
|
303
|
+
.panel.active { display: block; }
|
|
304
|
+
|
|
305
|
+
.panel-header {
|
|
306
|
+
display: flex;
|
|
307
|
+
align-items: center;
|
|
308
|
+
justify-content: space-between;
|
|
309
|
+
margin-bottom: 12px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.panel-header h2 {
|
|
313
|
+
font-size: 16px;
|
|
314
|
+
font-weight: 600;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.btn-sm {
|
|
318
|
+
padding: 4px 12px;
|
|
319
|
+
background: #27272a;
|
|
320
|
+
border: 1px solid #3f3f46;
|
|
321
|
+
border-radius: 6px;
|
|
322
|
+
color: #e4e4e7;
|
|
323
|
+
font-size: 12px;
|
|
324
|
+
cursor: pointer;
|
|
325
|
+
transition: background 0.2s;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.btn-sm:hover { background: #3f3f46; }
|
|
329
|
+
.btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
330
|
+
|
|
331
|
+
.empty-state {
|
|
332
|
+
padding: 40px 20px;
|
|
333
|
+
text-align: center;
|
|
334
|
+
color: #52525b;
|
|
335
|
+
font-size: 14px;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Activity */
|
|
339
|
+
.activity-list {
|
|
340
|
+
max-height: calc(100vh - 180px);
|
|
341
|
+
overflow-y: auto;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.activity-item {
|
|
345
|
+
padding: 8px 12px;
|
|
346
|
+
border-bottom: 1px solid #1e1e22;
|
|
347
|
+
font-size: 13px;
|
|
348
|
+
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
|
|
349
|
+
display: flex;
|
|
350
|
+
gap: 10px;
|
|
351
|
+
align-items: flex-start;
|
|
352
|
+
animation: fadeIn 0.3s ease;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@keyframes fadeIn {
|
|
356
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
357
|
+
to { opacity: 1; transform: translateY(0); }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.activity-time {
|
|
361
|
+
color: #52525b;
|
|
362
|
+
white-space: nowrap;
|
|
363
|
+
flex-shrink: 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.activity-tool {
|
|
367
|
+
color: #a78bfa;
|
|
368
|
+
font-weight: 500;
|
|
369
|
+
flex-shrink: 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.activity-detail {
|
|
373
|
+
color: #a1a1aa;
|
|
374
|
+
overflow: hidden;
|
|
375
|
+
text-overflow: ellipsis;
|
|
376
|
+
white-space: nowrap;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* Routes */
|
|
380
|
+
.summary {
|
|
381
|
+
display: flex;
|
|
382
|
+
gap: 12px;
|
|
383
|
+
font-size: 12px;
|
|
384
|
+
color: #a1a1aa;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.summary-item {
|
|
388
|
+
display: flex;
|
|
389
|
+
align-items: center;
|
|
390
|
+
gap: 4px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.summary-count {
|
|
394
|
+
font-weight: 600;
|
|
395
|
+
color: #e4e4e7;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.route-item {
|
|
399
|
+
display: flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
gap: 12px;
|
|
402
|
+
padding: 10px 12px;
|
|
403
|
+
border-bottom: 1px solid #1e1e22;
|
|
404
|
+
font-size: 13px;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.route-kind {
|
|
408
|
+
display: inline-block;
|
|
409
|
+
padding: 2px 8px;
|
|
410
|
+
border-radius: 4px;
|
|
411
|
+
font-size: 11px;
|
|
412
|
+
font-weight: 600;
|
|
413
|
+
text-transform: uppercase;
|
|
414
|
+
flex-shrink: 0;
|
|
415
|
+
min-width: 44px;
|
|
416
|
+
text-align: center;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.route-kind.page { background: #1e3a5f; color: #60a5fa; }
|
|
420
|
+
.route-kind.api { background: #1a3c34; color: #4ade80; }
|
|
421
|
+
|
|
422
|
+
.route-pattern {
|
|
423
|
+
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
|
|
424
|
+
color: #e4e4e7;
|
|
425
|
+
flex: 1;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.route-badges {
|
|
429
|
+
display: flex;
|
|
430
|
+
gap: 4px;
|
|
431
|
+
flex-shrink: 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.badge {
|
|
435
|
+
padding: 1px 6px;
|
|
436
|
+
border-radius: 3px;
|
|
437
|
+
font-size: 10px;
|
|
438
|
+
background: #27272a;
|
|
439
|
+
color: #a1a1aa;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* Guard */
|
|
443
|
+
.guard-status {
|
|
444
|
+
margin-bottom: 12px;
|
|
445
|
+
font-size: 13px;
|
|
446
|
+
color: #a1a1aa;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.guard-summary {
|
|
450
|
+
display: flex;
|
|
451
|
+
gap: 16px;
|
|
452
|
+
padding: 12px;
|
|
453
|
+
background: #18181b;
|
|
454
|
+
border-radius: 8px;
|
|
455
|
+
margin-bottom: 12px;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.guard-stat {
|
|
459
|
+
text-align: center;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.guard-stat-value {
|
|
463
|
+
font-size: 24px;
|
|
464
|
+
font-weight: 700;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.guard-stat-label {
|
|
468
|
+
font-size: 11px;
|
|
469
|
+
color: #71717a;
|
|
470
|
+
text-transform: uppercase;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.sev-error { color: #ef4444; }
|
|
474
|
+
.sev-warning { color: #eab308; }
|
|
475
|
+
.sev-info { color: #3b82f6; }
|
|
476
|
+
|
|
477
|
+
.violation-item {
|
|
478
|
+
padding: 8px 12px;
|
|
479
|
+
border-bottom: 1px solid #1e1e22;
|
|
480
|
+
font-size: 13px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.violation-file {
|
|
484
|
+
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
|
|
485
|
+
color: #a78bfa;
|
|
486
|
+
margin-bottom: 2px;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.violation-msg {
|
|
490
|
+
color: #a1a1aa;
|
|
491
|
+
font-size: 12px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.violation-sev {
|
|
495
|
+
display: inline-block;
|
|
496
|
+
padding: 1px 6px;
|
|
497
|
+
border-radius: 3px;
|
|
498
|
+
font-size: 10px;
|
|
499
|
+
font-weight: 600;
|
|
500
|
+
margin-right: 4px;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.violation-sev.error { background: #3b1111; color: #ef4444; }
|
|
504
|
+
.violation-sev.warning { background: #3b2f11; color: #eab308; }
|
|
505
|
+
.violation-sev.info { background: #112840; color: #3b82f6; }
|
|
506
|
+
|
|
507
|
+
/* Preview */
|
|
508
|
+
.preview-list { max-height: 40vh; overflow-y: auto; }
|
|
509
|
+
.preview-diff { max-height: 50vh; overflow-y: auto; padding: 8px; }
|
|
510
|
+
|
|
511
|
+
.change-item {
|
|
512
|
+
display: flex;
|
|
513
|
+
align-items: center;
|
|
514
|
+
gap: 10px;
|
|
515
|
+
padding: 8px 12px;
|
|
516
|
+
border-bottom: 1px solid #1e1e22;
|
|
517
|
+
font-size: 13px;
|
|
518
|
+
cursor: pointer;
|
|
519
|
+
transition: background 0.15s;
|
|
520
|
+
}
|
|
521
|
+
.change-item:hover { background: #27272a; }
|
|
522
|
+
.change-icon { flex-shrink: 0; }
|
|
523
|
+
.change-path {
|
|
524
|
+
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
|
|
525
|
+
color: #e4e4e7;
|
|
526
|
+
flex: 1;
|
|
527
|
+
overflow: hidden;
|
|
528
|
+
text-overflow: ellipsis;
|
|
529
|
+
white-space: nowrap;
|
|
530
|
+
}
|
|
531
|
+
.change-status {
|
|
532
|
+
font-size: 10px;
|
|
533
|
+
padding: 1px 6px;
|
|
534
|
+
border-radius: 3px;
|
|
535
|
+
text-transform: uppercase;
|
|
536
|
+
font-weight: 600;
|
|
537
|
+
flex-shrink: 0;
|
|
538
|
+
}
|
|
539
|
+
.change-status.added { background: #1a3c34; color: #4ade80; }
|
|
540
|
+
.change-status.modified { background: #1e3a5f; color: #60a5fa; }
|
|
541
|
+
.change-status.deleted { background: #3b1111; color: #ef4444; }
|
|
542
|
+
.change-status.untracked { background: #3b2f11; color: #eab308; }
|
|
543
|
+
.change-status.renamed { background: #2a1a3c; color: #a78bfa; }
|
|
544
|
+
|
|
545
|
+
.diff-header {
|
|
546
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
547
|
+
padding: 8px 12px; background: #18181b; border-radius: 6px 6px 0 0;
|
|
548
|
+
border-bottom: 1px solid #27272a;
|
|
549
|
+
}
|
|
550
|
+
.diff-file { font-family: monospace; color: #a78bfa; font-size: 13px; }
|
|
551
|
+
.diff-stats { font-size: 12px; }
|
|
552
|
+
.diff-add { color: #4ade80; margin-right: 8px; }
|
|
553
|
+
.diff-del { color: #ef4444; }
|
|
554
|
+
.diff-hunk-header { padding: 4px 12px; background: #112840; color: #3b82f6; font-size: 12px; font-family: monospace; }
|
|
555
|
+
.diff-line { display: flex; font-family: monospace; font-size: 12px; line-height: 20px; }
|
|
556
|
+
.diff-line-num { width: 40px; text-align: right; padding: 0 4px; color: #52525b; user-select: none; flex-shrink: 0; }
|
|
557
|
+
.diff-line-content { flex: 1; padding: 0 8px; white-space: pre; overflow: hidden; text-overflow: ellipsis; }
|
|
558
|
+
.diff-line.add { background: rgba(74,222,128,0.08); }
|
|
559
|
+
.diff-line.add .diff-line-content::before { content: '+'; color: #4ade80; }
|
|
560
|
+
.diff-line.remove { background: rgba(239,68,68,0.08); }
|
|
561
|
+
.diff-line.remove .diff-line-content::before { content: '-'; color: #ef4444; }
|
|
562
|
+
.diff-line.context .diff-line-content::before { content: ' '; }
|
|
563
|
+
|
|
564
|
+
/* Contracts */
|
|
565
|
+
.contracts-layout { display: flex; gap: 12px; height: calc(100vh - 180px); }
|
|
566
|
+
.contracts-list { width: 300px; flex-shrink: 0; overflow-y: auto; border-right: 1px solid #27272a; padding-right: 12px; }
|
|
567
|
+
.contracts-detail { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
|
|
568
|
+
|
|
569
|
+
.contract-item {
|
|
570
|
+
display: flex; align-items: center; gap: 8px;
|
|
571
|
+
padding: 8px 12px; border-bottom: 1px solid #1e1e22;
|
|
572
|
+
cursor: pointer; transition: background 0.15s; font-size: 13px;
|
|
573
|
+
}
|
|
574
|
+
.contract-item:hover { background: #27272a; }
|
|
575
|
+
.contract-item.selected { background: #27272a; border-left: 2px solid #a78bfa; }
|
|
576
|
+
|
|
577
|
+
.method-badge {
|
|
578
|
+
display: inline-block; padding: 1px 6px; border-radius: 3px;
|
|
579
|
+
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
580
|
+
flex-shrink: 0; min-width: 36px; text-align: center;
|
|
581
|
+
}
|
|
582
|
+
.method-badge.get { background: #1a3c34; color: #4ade80; }
|
|
583
|
+
.method-badge.post { background: #1e3a5f; color: #60a5fa; }
|
|
584
|
+
.method-badge.put { background: #3b2f11; color: #eab308; }
|
|
585
|
+
.method-badge.patch { background: #2a1a3c; color: #a78bfa; }
|
|
586
|
+
.method-badge.delete { background: #3b1111; color: #ef4444; }
|
|
587
|
+
|
|
588
|
+
.contract-pattern { font-family: monospace; color: #e4e4e7; }
|
|
589
|
+
|
|
590
|
+
.contract-schema {
|
|
591
|
+
background: #18181b; border-radius: 8px; padding: 12px;
|
|
592
|
+
font-family: monospace; font-size: 12px; white-space: pre-wrap;
|
|
593
|
+
max-height: 40vh; overflow-y: auto;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.contract-playground { background: #18181b; border-radius: 8px; padding: 12px; }
|
|
597
|
+
.contract-playground h3 { font-size: 14px; margin-bottom: 8px; }
|
|
598
|
+
|
|
599
|
+
.playground-controls { display: flex; gap: 8px; margin-bottom: 8px; }
|
|
600
|
+
.select-sm {
|
|
601
|
+
padding: 4px 8px; background: #27272a; border: 1px solid #3f3f46;
|
|
602
|
+
border-radius: 6px; color: #e4e4e7; font-size: 12px;
|
|
603
|
+
}
|
|
604
|
+
.playground-inputs { display: flex; flex-direction: column; gap: 6px; }
|
|
605
|
+
.playground-inputs label { font-size: 11px; color: #71717a; display: flex; flex-direction: column; gap: 2px; }
|
|
606
|
+
.playground-inputs textarea {
|
|
607
|
+
background: #27272a; border: 1px solid #3f3f46; border-radius: 4px;
|
|
608
|
+
color: #e4e4e7; font-family: monospace; font-size: 12px; padding: 6px;
|
|
609
|
+
resize: vertical;
|
|
610
|
+
}
|
|
611
|
+
.validate-result { margin-top: 8px; padding: 8px; border-radius: 4px; font-size: 12px; font-family: monospace; }
|
|
612
|
+
.validate-result.success { background: #1a3c34; color: #4ade80; }
|
|
613
|
+
.validate-result.error { background: #3b1111; color: #ef4444; }
|
|
614
|
+
|
|
615
|
+
.debug-bar {
|
|
616
|
+
position: fixed;
|
|
617
|
+
bottom: 0;
|
|
618
|
+
left: 0;
|
|
619
|
+
right: 0;
|
|
620
|
+
padding: 4px 12px;
|
|
621
|
+
background: #1a1a2e;
|
|
622
|
+
border-top: 1px solid #27272a;
|
|
623
|
+
font-size: 11px;
|
|
624
|
+
font-family: monospace;
|
|
625
|
+
color: #71717a;
|
|
626
|
+
max-height: 60px;
|
|
627
|
+
overflow-y: auto;
|
|
628
|
+
}
|
|
629
|
+
|
|
573
630
|
.debug-bar .err { color: #ef4444; }
|
|
574
631
|
.debug-bar .ok { color: #22c55e; }
|
|
575
632
|
|
|
@@ -1131,6 +1188,114 @@ const CSS = /* css */ `
|
|
|
1131
1188
|
box-shadow: 0 12px 28px rgba(30, 42, 58, 0.22);
|
|
1132
1189
|
}
|
|
1133
1190
|
|
|
1191
|
+
.requests-list, .mcp-list, .cache-content, .metrics-content {
|
|
1192
|
+
display: flex;
|
|
1193
|
+
flex-direction: column;
|
|
1194
|
+
gap: 6px;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
.req-row {
|
|
1198
|
+
display: grid;
|
|
1199
|
+
grid-template-columns: 70px 60px 1fr 90px 90px;
|
|
1200
|
+
gap: 10px;
|
|
1201
|
+
padding: 10px 12px;
|
|
1202
|
+
border-radius: 12px;
|
|
1203
|
+
background: rgba(255, 255, 255, 0.55);
|
|
1204
|
+
border: 1px solid var(--line);
|
|
1205
|
+
cursor: pointer;
|
|
1206
|
+
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
|
|
1207
|
+
font-size: 12px;
|
|
1208
|
+
align-items: center;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
.req-row:hover { background: rgba(184, 106, 18, 0.08); }
|
|
1212
|
+
.req-row.selected { background: rgba(184, 106, 18, 0.12); border-color: rgba(184, 106, 18, 0.28); }
|
|
1213
|
+
|
|
1214
|
+
.req-method {
|
|
1215
|
+
font-weight: 700;
|
|
1216
|
+
text-transform: uppercase;
|
|
1217
|
+
font-size: 11px;
|
|
1218
|
+
letter-spacing: 0.06em;
|
|
1219
|
+
}
|
|
1220
|
+
.req-method.get { color: #2563eb; }
|
|
1221
|
+
.req-method.post { color: #16a34a; }
|
|
1222
|
+
.req-method.put, .req-method.patch { color: #d97706; }
|
|
1223
|
+
.req-method.delete { color: #dc2626; }
|
|
1224
|
+
|
|
1225
|
+
.req-status { font-weight: 700; }
|
|
1226
|
+
.req-status.s2 { color: var(--success); }
|
|
1227
|
+
.req-status.s3 { color: #2563eb; }
|
|
1228
|
+
.req-status.s4 { color: #d97706; }
|
|
1229
|
+
.req-status.s5 { color: var(--danger); }
|
|
1230
|
+
|
|
1231
|
+
.req-path { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1232
|
+
.req-dur, .req-cache { color: var(--muted); text-align: right; }
|
|
1233
|
+
|
|
1234
|
+
.requests-detail {
|
|
1235
|
+
margin-top: 14px;
|
|
1236
|
+
padding: 14px;
|
|
1237
|
+
background: rgba(255, 255, 255, 0.78);
|
|
1238
|
+
border: 1px solid var(--line);
|
|
1239
|
+
border-radius: 14px;
|
|
1240
|
+
max-height: 420px;
|
|
1241
|
+
overflow: auto;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.corr-event {
|
|
1245
|
+
padding: 8px 10px;
|
|
1246
|
+
border-bottom: 1px solid rgba(220, 207, 186, 0.6);
|
|
1247
|
+
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
|
|
1248
|
+
font-size: 12px;
|
|
1249
|
+
}
|
|
1250
|
+
.corr-event:last-child { border-bottom: none; }
|
|
1251
|
+
.corr-type {
|
|
1252
|
+
display: inline-block;
|
|
1253
|
+
padding: 2px 6px;
|
|
1254
|
+
border-radius: 6px;
|
|
1255
|
+
font-size: 10px;
|
|
1256
|
+
font-weight: 700;
|
|
1257
|
+
margin-right: 8px;
|
|
1258
|
+
background: rgba(30, 42, 58, 0.08);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.mcp-group {
|
|
1262
|
+
border: 1px solid var(--line);
|
|
1263
|
+
border-radius: 12px;
|
|
1264
|
+
padding: 10px 12px;
|
|
1265
|
+
background: rgba(255, 255, 255, 0.55);
|
|
1266
|
+
}
|
|
1267
|
+
.mcp-group-header {
|
|
1268
|
+
font-size: 11px;
|
|
1269
|
+
color: var(--muted);
|
|
1270
|
+
margin-bottom: 6px;
|
|
1271
|
+
}
|
|
1272
|
+
.mcp-event {
|
|
1273
|
+
display: grid;
|
|
1274
|
+
grid-template-columns: 90px 22px 1fr 70px;
|
|
1275
|
+
gap: 8px;
|
|
1276
|
+
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
|
|
1277
|
+
font-size: 12px;
|
|
1278
|
+
padding: 4px 0;
|
|
1279
|
+
align-items: center;
|
|
1280
|
+
}
|
|
1281
|
+
.mcp-status-ok { color: var(--success); font-weight: 700; }
|
|
1282
|
+
.mcp-status-err { color: var(--danger); font-weight: 700; }
|
|
1283
|
+
|
|
1284
|
+
.cache-grid, .metrics-grid {
|
|
1285
|
+
display: grid;
|
|
1286
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
1287
|
+
gap: 12px;
|
|
1288
|
+
}
|
|
1289
|
+
.stat-card {
|
|
1290
|
+
padding: 14px;
|
|
1291
|
+
border-radius: 14px;
|
|
1292
|
+
background: rgba(255, 255, 255, 0.78);
|
|
1293
|
+
border: 1px solid var(--line);
|
|
1294
|
+
}
|
|
1295
|
+
.stat-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
1296
|
+
.stat-value { font-size: 22px; font-weight: 700; margin-top: 4px; color: var(--ink); }
|
|
1297
|
+
.stat-sub { font-size: 11px; color: var(--muted); margin-top: 4px; }
|
|
1298
|
+
|
|
1134
1299
|
@media (max-width: 1040px) {
|
|
1135
1300
|
body {
|
|
1136
1301
|
padding: 18px;
|
|
@@ -1186,22 +1351,22 @@ const CSS = /* css */ `
|
|
|
1186
1351
|
}
|
|
1187
1352
|
}
|
|
1188
1353
|
`;
|
|
1189
|
-
|
|
1190
|
-
// ─── JavaScript ──────────────────────────────────
|
|
1191
|
-
|
|
1192
|
-
const JS = /* js */ `
|
|
1193
|
-
(function() {
|
|
1194
|
-
var dbg = document.getElementById('debug-bar');
|
|
1195
|
-
function log(msg, cls) {
|
|
1196
|
-
if (!dbg) return;
|
|
1197
|
-
var s = document.createElement('span');
|
|
1198
|
-
s.className = cls || '';
|
|
1199
|
-
s.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg + ' ';
|
|
1200
|
-
dbg.appendChild(s);
|
|
1201
|
-
dbg.scrollTop = dbg.scrollHeight;
|
|
1202
|
-
console.log('[Kitchen]', msg);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1354
|
+
|
|
1355
|
+
// ─── JavaScript ──────────────────────────────────
|
|
1356
|
+
|
|
1357
|
+
const JS = /* js */ `
|
|
1358
|
+
(function() {
|
|
1359
|
+
var dbg = document.getElementById('debug-bar');
|
|
1360
|
+
function log(msg, cls) {
|
|
1361
|
+
if (!dbg) return;
|
|
1362
|
+
var s = document.createElement('span');
|
|
1363
|
+
s.className = cls || '';
|
|
1364
|
+
s.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg + ' ';
|
|
1365
|
+
dbg.appendChild(s);
|
|
1366
|
+
dbg.scrollTop = dbg.scrollHeight;
|
|
1367
|
+
console.log('[Kitchen]', msg);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1205
1370
|
function escapeHtml(str) {
|
|
1206
1371
|
if (!str) return '';
|
|
1207
1372
|
var d = document.createElement('div');
|
|
@@ -1216,140 +1381,140 @@ const JS = /* js */ `
|
|
|
1216
1381
|
}
|
|
1217
1382
|
|
|
1218
1383
|
try { log('JS loaded', 'ok'); } catch(e) {}
|
|
1219
|
-
|
|
1220
|
-
// Tab switching
|
|
1221
|
-
var tabs = document.querySelectorAll('.tab');
|
|
1222
|
-
for (var i = 0; i < tabs.length; i++) {
|
|
1223
|
-
tabs[i].addEventListener('click', function() {
|
|
1224
|
-
var all = document.querySelectorAll('.tab');
|
|
1225
|
-
var panels = document.querySelectorAll('.panel');
|
|
1226
|
-
for (var j = 0; j < all.length; j++) all[j].classList.remove('active');
|
|
1227
|
-
for (var j = 0; j < panels.length; j++) panels[j].classList.remove('active');
|
|
1228
|
-
this.classList.add('active');
|
|
1229
|
-
var p = document.getElementById('panel-' + this.getAttribute('data-panel'));
|
|
1230
|
-
if (p) p.classList.add('active');
|
|
1231
|
-
});
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
// ─── SSE Activity Stream ─────────────────────
|
|
1235
|
-
var statusDot = document.getElementById('sse-status');
|
|
1236
|
-
var statusLabel = document.getElementById('sse-label');
|
|
1237
|
-
var activityList = document.getElementById('activity-list');
|
|
1238
|
-
var activityCount = 0;
|
|
1239
|
-
var MAX_ITEMS = 200;
|
|
1240
|
-
var sseRetryCount = 0;
|
|
1241
|
-
|
|
1242
|
-
function connectSSE() {
|
|
1243
|
-
statusDot.className = 'status-dot connecting';
|
|
1244
|
-
statusLabel.textContent = 'Connecting...';
|
|
1245
|
-
log('SSE connecting...');
|
|
1246
|
-
|
|
1247
|
-
var es;
|
|
1248
|
-
try {
|
|
1249
|
-
es = new EventSource('/__kitchen/sse/activity');
|
|
1250
|
-
} catch(e) {
|
|
1251
|
-
log('SSE EventSource failed: ' + e.message, 'err');
|
|
1252
|
-
statusDot.className = 'status-dot disconnected';
|
|
1253
|
-
statusLabel.textContent = 'Failed';
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
es.onopen = function() {
|
|
1258
|
-
statusDot.className = 'status-dot connected';
|
|
1259
|
-
statusLabel.textContent = 'Connected';
|
|
1260
|
-
sseRetryCount = 0;
|
|
1261
|
-
log('SSE connected', 'ok');
|
|
1262
|
-
};
|
|
1263
|
-
|
|
1264
|
-
es.onmessage = function(e) {
|
|
1265
|
-
try {
|
|
1266
|
-
var data = JSON.parse(e.data);
|
|
1267
|
-
if (data.type === 'connected') {
|
|
1268
|
-
log('SSE welcome: ' + data.clientId, 'ok');
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
if (data.type === 'heartbeat') return;
|
|
1272
|
-
appendActivity(data);
|
|
1273
|
-
} catch(err) {
|
|
1274
|
-
log('SSE parse error: ' + err.message, 'err');
|
|
1275
|
-
}
|
|
1276
|
-
};
|
|
1277
|
-
|
|
1278
|
-
es.onerror = function(evt) {
|
|
1279
|
-
log('SSE error (readyState=' + es.readyState + ')', 'err');
|
|
1280
|
-
statusDot.className = 'status-dot disconnected';
|
|
1281
|
-
statusLabel.textContent = 'Disconnected';
|
|
1282
|
-
es.close();
|
|
1283
|
-
sseRetryCount++;
|
|
1284
|
-
var delay = Math.min(3000 * sseRetryCount, 15000);
|
|
1285
|
-
log('SSE retry in ' + (delay/1000) + 's');
|
|
1286
|
-
setTimeout(connectSSE, delay);
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1384
|
+
|
|
1385
|
+
// Tab switching
|
|
1386
|
+
var tabs = document.querySelectorAll('.tab');
|
|
1387
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
1388
|
+
tabs[i].addEventListener('click', function() {
|
|
1389
|
+
var all = document.querySelectorAll('.tab');
|
|
1390
|
+
var panels = document.querySelectorAll('.panel');
|
|
1391
|
+
for (var j = 0; j < all.length; j++) all[j].classList.remove('active');
|
|
1392
|
+
for (var j = 0; j < panels.length; j++) panels[j].classList.remove('active');
|
|
1393
|
+
this.classList.add('active');
|
|
1394
|
+
var p = document.getElementById('panel-' + this.getAttribute('data-panel'));
|
|
1395
|
+
if (p) p.classList.add('active');
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// ─── SSE Activity Stream ─────────────────────
|
|
1400
|
+
var statusDot = document.getElementById('sse-status');
|
|
1401
|
+
var statusLabel = document.getElementById('sse-label');
|
|
1402
|
+
var activityList = document.getElementById('activity-list');
|
|
1403
|
+
var activityCount = 0;
|
|
1404
|
+
var MAX_ITEMS = 200;
|
|
1405
|
+
var sseRetryCount = 0;
|
|
1406
|
+
|
|
1407
|
+
function connectSSE() {
|
|
1408
|
+
statusDot.className = 'status-dot connecting';
|
|
1409
|
+
statusLabel.textContent = 'Connecting...';
|
|
1410
|
+
log('SSE connecting...');
|
|
1411
|
+
|
|
1412
|
+
var es;
|
|
1413
|
+
try {
|
|
1414
|
+
es = new EventSource('/__kitchen/sse/activity');
|
|
1415
|
+
} catch(e) {
|
|
1416
|
+
log('SSE EventSource failed: ' + e.message, 'err');
|
|
1417
|
+
statusDot.className = 'status-dot disconnected';
|
|
1418
|
+
statusLabel.textContent = 'Failed';
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
es.onopen = function() {
|
|
1423
|
+
statusDot.className = 'status-dot connected';
|
|
1424
|
+
statusLabel.textContent = 'Connected';
|
|
1425
|
+
sseRetryCount = 0;
|
|
1426
|
+
log('SSE connected', 'ok');
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
es.onmessage = function(e) {
|
|
1430
|
+
try {
|
|
1431
|
+
var data = JSON.parse(e.data);
|
|
1432
|
+
if (data.type === 'connected') {
|
|
1433
|
+
log('SSE welcome: ' + data.clientId, 'ok');
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (data.type === 'heartbeat') return;
|
|
1437
|
+
appendActivity(data);
|
|
1438
|
+
} catch(err) {
|
|
1439
|
+
log('SSE parse error: ' + err.message, 'err');
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
es.onerror = function(evt) {
|
|
1444
|
+
log('SSE error (readyState=' + es.readyState + ')', 'err');
|
|
1445
|
+
statusDot.className = 'status-dot disconnected';
|
|
1446
|
+
statusLabel.textContent = 'Disconnected';
|
|
1447
|
+
es.close();
|
|
1448
|
+
sseRetryCount++;
|
|
1449
|
+
var delay = Math.min(3000 * sseRetryCount, 15000);
|
|
1450
|
+
log('SSE retry in ' + (delay/1000) + 's');
|
|
1451
|
+
setTimeout(connectSSE, delay);
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1290
1455
|
function appendActivity(data) {
|
|
1291
1456
|
if (activityCount === 0) {
|
|
1292
1457
|
activityList.innerHTML = '';
|
|
1293
1458
|
}
|
|
1294
1459
|
activityCount++;
|
|
1295
1460
|
setMetric('metric-activity', activityCount);
|
|
1296
|
-
|
|
1297
|
-
var item = document.createElement('div');
|
|
1298
|
-
item.className = 'activity-item';
|
|
1299
|
-
|
|
1300
|
-
var ts = data.ts || data.timestamp || new Date().toISOString();
|
|
1301
|
-
var time = new Date(ts).toLocaleTimeString();
|
|
1302
|
-
var tool = data.tool || data.type || 'event';
|
|
1303
|
-
var detail = data.description || data.message || data.resource || JSON.stringify(data).substring(0, 120);
|
|
1304
|
-
|
|
1305
|
-
item.innerHTML =
|
|
1306
|
-
'<span class="activity-time">' + escapeHtml(time) + '</span>' +
|
|
1307
|
-
'<span class="activity-tool">' + escapeHtml(tool) + '</span>' +
|
|
1308
|
-
'<span class="activity-detail">' + escapeHtml(detail) + '</span>';
|
|
1309
|
-
|
|
1310
|
-
activityList.insertBefore(item, activityList.firstChild);
|
|
1311
|
-
|
|
1312
|
-
while (activityList.children.length > MAX_ITEMS) {
|
|
1313
|
-
activityList.removeChild(activityList.lastChild);
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1461
|
+
|
|
1462
|
+
var item = document.createElement('div');
|
|
1463
|
+
item.className = 'activity-item';
|
|
1464
|
+
|
|
1465
|
+
var ts = data.ts || data.timestamp || new Date().toISOString();
|
|
1466
|
+
var time = new Date(ts).toLocaleTimeString();
|
|
1467
|
+
var tool = data.tool || data.type || 'event';
|
|
1468
|
+
var detail = data.description || data.message || data.resource || JSON.stringify(data).substring(0, 120);
|
|
1469
|
+
|
|
1470
|
+
item.innerHTML =
|
|
1471
|
+
'<span class="activity-time">' + escapeHtml(time) + '</span>' +
|
|
1472
|
+
'<span class="activity-tool">' + escapeHtml(tool) + '</span>' +
|
|
1473
|
+
'<span class="activity-detail">' + escapeHtml(detail) + '</span>';
|
|
1474
|
+
|
|
1475
|
+
activityList.insertBefore(item, activityList.firstChild);
|
|
1476
|
+
|
|
1477
|
+
while (activityList.children.length > MAX_ITEMS) {
|
|
1478
|
+
activityList.removeChild(activityList.lastChild);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1317
1482
|
document.getElementById('clear-activity').addEventListener('click', function() {
|
|
1318
1483
|
activityList.innerHTML = '<div class="empty-state">Waiting for MCP activity...</div>';
|
|
1319
1484
|
activityCount = 0;
|
|
1320
1485
|
setMetric('metric-activity', 0);
|
|
1321
1486
|
});
|
|
1322
|
-
|
|
1323
|
-
connectSSE();
|
|
1324
|
-
|
|
1325
|
-
// ─── Routes ──────────────────────────────────
|
|
1326
|
-
function loadRoutes() {
|
|
1327
|
-
log('Fetching routes...');
|
|
1328
|
-
var xhr = new XMLHttpRequest();
|
|
1329
|
-
xhr.open('GET', '/__kitchen/api/routes', true);
|
|
1330
|
-
xhr.onload = function() {
|
|
1331
|
-
if (xhr.status === 200) {
|
|
1332
|
-
try {
|
|
1333
|
-
var data = JSON.parse(xhr.responseText);
|
|
1334
|
-
log('Routes loaded: ' + data.summary.total + ' routes', 'ok');
|
|
1335
|
-
renderRoutes(data);
|
|
1336
|
-
} catch(e) {
|
|
1337
|
-
log('Routes parse error: ' + e.message, 'err');
|
|
1338
|
-
}
|
|
1339
|
-
} else {
|
|
1340
|
-
log('Routes HTTP ' + xhr.status, 'err');
|
|
1341
|
-
document.getElementById('routes-list').innerHTML =
|
|
1342
|
-
'<div class="empty-state">Failed to load routes (HTTP ' + xhr.status + ')</div>';
|
|
1343
|
-
}
|
|
1344
|
-
};
|
|
1345
|
-
xhr.onerror = function() {
|
|
1346
|
-
log('Routes network error', 'err');
|
|
1347
|
-
document.getElementById('routes-list').innerHTML =
|
|
1348
|
-
'<div class="empty-state">Network error loading routes.</div>';
|
|
1349
|
-
};
|
|
1350
|
-
xhr.send();
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1487
|
+
|
|
1488
|
+
connectSSE();
|
|
1489
|
+
|
|
1490
|
+
// ─── Routes ──────────────────────────────────
|
|
1491
|
+
function loadRoutes() {
|
|
1492
|
+
log('Fetching routes...');
|
|
1493
|
+
var xhr = new XMLHttpRequest();
|
|
1494
|
+
xhr.open('GET', '/__kitchen/api/routes', true);
|
|
1495
|
+
xhr.onload = function() {
|
|
1496
|
+
if (xhr.status === 200) {
|
|
1497
|
+
try {
|
|
1498
|
+
var data = JSON.parse(xhr.responseText);
|
|
1499
|
+
log('Routes loaded: ' + data.summary.total + ' routes', 'ok');
|
|
1500
|
+
renderRoutes(data);
|
|
1501
|
+
} catch(e) {
|
|
1502
|
+
log('Routes parse error: ' + e.message, 'err');
|
|
1503
|
+
}
|
|
1504
|
+
} else {
|
|
1505
|
+
log('Routes HTTP ' + xhr.status, 'err');
|
|
1506
|
+
document.getElementById('routes-list').innerHTML =
|
|
1507
|
+
'<div class="empty-state">Failed to load routes (HTTP ' + xhr.status + ')</div>';
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
xhr.onerror = function() {
|
|
1511
|
+
log('Routes network error', 'err');
|
|
1512
|
+
document.getElementById('routes-list').innerHTML =
|
|
1513
|
+
'<div class="empty-state">Network error loading routes.</div>';
|
|
1514
|
+
};
|
|
1515
|
+
xhr.send();
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1353
1518
|
function renderRoutes(data) {
|
|
1354
1519
|
var summaryEl = document.getElementById('routes-summary');
|
|
1355
1520
|
var listEl = document.getElementById('routes-list');
|
|
@@ -1357,93 +1522,93 @@ const JS = /* js */ `
|
|
|
1357
1522
|
setMetric('metric-routes', s.total);
|
|
1358
1523
|
|
|
1359
1524
|
summaryEl.innerHTML =
|
|
1360
|
-
'<span class="summary-item"><span class="summary-count">' + s.total + '</span> total</span>' +
|
|
1361
|
-
'<span class="summary-item"><span class="summary-count">' + s.pages + '</span> pages</span>' +
|
|
1362
|
-
'<span class="summary-item"><span class="summary-count">' + s.apis + '</span> APIs</span>' +
|
|
1363
|
-
'<span class="summary-item"><span class="summary-count">' + s.withIslands + '</span> islands</span>';
|
|
1364
|
-
|
|
1365
|
-
if (!data.routes.length) {
|
|
1366
|
-
listEl.innerHTML = '<div class="empty-state">No routes found.</div>';
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
var html = '';
|
|
1371
|
-
for (var i = 0; i < data.routes.length; i++) {
|
|
1372
|
-
var r = data.routes[i];
|
|
1373
|
-
var badges = '';
|
|
1374
|
-
if (r.hasSlot) badges += '<span class="badge">slot</span>';
|
|
1375
|
-
if (r.hasContract) badges += '<span class="badge">contract</span>';
|
|
1376
|
-
if (r.hasClient) badges += '<span class="badge">island</span>';
|
|
1377
|
-
if (r.hasLayout) badges += '<span class="badge">layout</span>';
|
|
1378
|
-
if (r.hydration && r.hydration !== 'none') badges += '<span class="badge">' + escapeHtml(r.hydration) + '</span>';
|
|
1379
|
-
|
|
1380
|
-
html += '<div class="route-item">' +
|
|
1381
|
-
'<span class="route-kind ' + r.kind + '">' + r.kind + '</span>' +
|
|
1382
|
-
'<span class="route-pattern">' + escapeHtml(r.pattern) + '</span>' +
|
|
1383
|
-
'<span class="route-badges">' + badges + '</span>' +
|
|
1384
|
-
'</div>';
|
|
1385
|
-
}
|
|
1386
|
-
listEl.innerHTML = html;
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
loadRoutes();
|
|
1390
|
-
|
|
1391
|
-
// ─── Guard ───────────────────────────────────
|
|
1392
|
-
var scanBtn = document.getElementById('scan-guard');
|
|
1393
|
-
var guardStatusEl = document.getElementById('guard-status');
|
|
1394
|
-
var guardListEl = document.getElementById('guard-list');
|
|
1395
|
-
|
|
1396
|
-
function loadGuardStatus() {
|
|
1397
|
-
log('Fetching guard status...');
|
|
1398
|
-
var xhr = new XMLHttpRequest();
|
|
1399
|
-
xhr.open('GET', '/__kitchen/api/guard', true);
|
|
1400
|
-
xhr.onload = function() {
|
|
1401
|
-
if (xhr.status === 200) {
|
|
1402
|
-
try {
|
|
1403
|
-
var data = JSON.parse(xhr.responseText);
|
|
1404
|
-
log('Guard: ' + (data.enabled ? 'enabled (' + data.preset + ')' : 'disabled'), 'ok');
|
|
1405
|
-
renderGuardData(data);
|
|
1406
|
-
} catch(e) {
|
|
1407
|
-
log('Guard parse error: ' + e.message, 'err');
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
};
|
|
1411
|
-
xhr.onerror = function() { log('Guard network error', 'err'); };
|
|
1412
|
-
xhr.send();
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
scanBtn.addEventListener('click', function() {
|
|
1416
|
-
scanBtn.disabled = true;
|
|
1417
|
-
scanBtn.textContent = 'Scanning...';
|
|
1418
|
-
log('Guard scan started...');
|
|
1419
|
-
|
|
1420
|
-
var xhr = new XMLHttpRequest();
|
|
1421
|
-
xhr.open('POST', '/__kitchen/api/guard/scan', true);
|
|
1422
|
-
xhr.onload = function() {
|
|
1423
|
-
scanBtn.disabled = false;
|
|
1424
|
-
scanBtn.textContent = 'Scan';
|
|
1425
|
-
if (xhr.status === 200) {
|
|
1426
|
-
try {
|
|
1427
|
-
var data = JSON.parse(xhr.responseText);
|
|
1428
|
-
log('Guard scan done: ' + (data.report ? data.report.totalViolations + ' violations' : 'no report'), 'ok');
|
|
1429
|
-
renderGuardData(data);
|
|
1430
|
-
} catch(e) {
|
|
1431
|
-
log('Guard scan parse error: ' + e.message, 'err');
|
|
1432
|
-
}
|
|
1433
|
-
} else {
|
|
1434
|
-
log('Guard scan HTTP ' + xhr.status, 'err');
|
|
1435
|
-
guardStatusEl.textContent = 'Scan failed.';
|
|
1436
|
-
}
|
|
1437
|
-
};
|
|
1438
|
-
xhr.onerror = function() {
|
|
1439
|
-
scanBtn.disabled = false;
|
|
1440
|
-
scanBtn.textContent = 'Scan';
|
|
1441
|
-
log('Guard scan network error', 'err');
|
|
1442
|
-
guardStatusEl.textContent = 'Scan failed.';
|
|
1443
|
-
};
|
|
1444
|
-
xhr.send();
|
|
1445
|
-
});
|
|
1446
|
-
|
|
1525
|
+
'<span class="summary-item"><span class="summary-count">' + s.total + '</span> total</span>' +
|
|
1526
|
+
'<span class="summary-item"><span class="summary-count">' + s.pages + '</span> pages</span>' +
|
|
1527
|
+
'<span class="summary-item"><span class="summary-count">' + s.apis + '</span> APIs</span>' +
|
|
1528
|
+
'<span class="summary-item"><span class="summary-count">' + s.withIslands + '</span> islands</span>';
|
|
1529
|
+
|
|
1530
|
+
if (!data.routes.length) {
|
|
1531
|
+
listEl.innerHTML = '<div class="empty-state">No routes found.</div>';
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
var html = '';
|
|
1536
|
+
for (var i = 0; i < data.routes.length; i++) {
|
|
1537
|
+
var r = data.routes[i];
|
|
1538
|
+
var badges = '';
|
|
1539
|
+
if (r.hasSlot) badges += '<span class="badge">slot</span>';
|
|
1540
|
+
if (r.hasContract) badges += '<span class="badge">contract</span>';
|
|
1541
|
+
if (r.hasClient) badges += '<span class="badge">island</span>';
|
|
1542
|
+
if (r.hasLayout) badges += '<span class="badge">layout</span>';
|
|
1543
|
+
if (r.hydration && r.hydration !== 'none') badges += '<span class="badge">' + escapeHtml(r.hydration) + '</span>';
|
|
1544
|
+
|
|
1545
|
+
html += '<div class="route-item">' +
|
|
1546
|
+
'<span class="route-kind ' + r.kind + '">' + r.kind + '</span>' +
|
|
1547
|
+
'<span class="route-pattern">' + escapeHtml(r.pattern) + '</span>' +
|
|
1548
|
+
'<span class="route-badges">' + badges + '</span>' +
|
|
1549
|
+
'</div>';
|
|
1550
|
+
}
|
|
1551
|
+
listEl.innerHTML = html;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
loadRoutes();
|
|
1555
|
+
|
|
1556
|
+
// ─── Guard ───────────────────────────────────
|
|
1557
|
+
var scanBtn = document.getElementById('scan-guard');
|
|
1558
|
+
var guardStatusEl = document.getElementById('guard-status');
|
|
1559
|
+
var guardListEl = document.getElementById('guard-list');
|
|
1560
|
+
|
|
1561
|
+
function loadGuardStatus() {
|
|
1562
|
+
log('Fetching guard status...');
|
|
1563
|
+
var xhr = new XMLHttpRequest();
|
|
1564
|
+
xhr.open('GET', '/__kitchen/api/guard', true);
|
|
1565
|
+
xhr.onload = function() {
|
|
1566
|
+
if (xhr.status === 200) {
|
|
1567
|
+
try {
|
|
1568
|
+
var data = JSON.parse(xhr.responseText);
|
|
1569
|
+
log('Guard: ' + (data.enabled ? 'enabled (' + data.preset + ')' : 'disabled'), 'ok');
|
|
1570
|
+
renderGuardData(data);
|
|
1571
|
+
} catch(e) {
|
|
1572
|
+
log('Guard parse error: ' + e.message, 'err');
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
xhr.onerror = function() { log('Guard network error', 'err'); };
|
|
1577
|
+
xhr.send();
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
scanBtn.addEventListener('click', function() {
|
|
1581
|
+
scanBtn.disabled = true;
|
|
1582
|
+
scanBtn.textContent = 'Scanning...';
|
|
1583
|
+
log('Guard scan started...');
|
|
1584
|
+
|
|
1585
|
+
var xhr = new XMLHttpRequest();
|
|
1586
|
+
xhr.open('POST', '/__kitchen/api/guard/scan', true);
|
|
1587
|
+
xhr.onload = function() {
|
|
1588
|
+
scanBtn.disabled = false;
|
|
1589
|
+
scanBtn.textContent = 'Scan';
|
|
1590
|
+
if (xhr.status === 200) {
|
|
1591
|
+
try {
|
|
1592
|
+
var data = JSON.parse(xhr.responseText);
|
|
1593
|
+
log('Guard scan done: ' + (data.report ? data.report.totalViolations + ' violations' : 'no report'), 'ok');
|
|
1594
|
+
renderGuardData(data);
|
|
1595
|
+
} catch(e) {
|
|
1596
|
+
log('Guard scan parse error: ' + e.message, 'err');
|
|
1597
|
+
}
|
|
1598
|
+
} else {
|
|
1599
|
+
log('Guard scan HTTP ' + xhr.status, 'err');
|
|
1600
|
+
guardStatusEl.textContent = 'Scan failed.';
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
xhr.onerror = function() {
|
|
1604
|
+
scanBtn.disabled = false;
|
|
1605
|
+
scanBtn.textContent = 'Scan';
|
|
1606
|
+
log('Guard scan network error', 'err');
|
|
1607
|
+
guardStatusEl.textContent = 'Scan failed.';
|
|
1608
|
+
};
|
|
1609
|
+
xhr.send();
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1447
1612
|
function renderGuardData(data) {
|
|
1448
1613
|
if (!data.enabled) {
|
|
1449
1614
|
guardStatusEl.textContent = 'Guard is not configured for this project.';
|
|
@@ -1463,270 +1628,510 @@ const JS = /* js */ `
|
|
|
1463
1628
|
var r = data.report;
|
|
1464
1629
|
setMetric('metric-guard', r.totalViolations);
|
|
1465
1630
|
var summaryHtml = '<div class="guard-summary">' +
|
|
1466
|
-
'<div class="guard-stat"><div class="guard-stat-value">' + r.totalViolations + '</div><div class="guard-stat-label">Total</div></div>' +
|
|
1467
|
-
'<div class="guard-stat"><div class="guard-stat-value sev-error">' + (r.bySeverity.error || 0) + '</div><div class="guard-stat-label">Errors</div></div>' +
|
|
1468
|
-
'<div class="guard-stat"><div class="guard-stat-value sev-warning">' + (r.bySeverity.warning || 0) + '</div><div class="guard-stat-label">Warnings</div></div>' +
|
|
1469
|
-
'<div class="guard-stat"><div class="guard-stat-value sev-info">' + (r.bySeverity.info || 0) + '</div><div class="guard-stat-label">Info</div></div>' +
|
|
1470
|
-
'</div>';
|
|
1471
|
-
|
|
1472
|
-
if (!r.violations.length) {
|
|
1473
|
-
guardListEl.innerHTML = summaryHtml + '<div class="empty-state">No violations found!</div>';
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
var violHtml = '';
|
|
1478
|
-
var list = r.violations.length > 100 ? r.violations.slice(0, 100) : r.violations;
|
|
1479
|
-
for (var i = 0; i < list.length; i++) {
|
|
1480
|
-
var v = list[i];
|
|
1481
|
-
violHtml += '<div class="violation-item">' +
|
|
1482
|
-
'<div class="violation-file">' +
|
|
1483
|
-
'<span class="violation-sev ' + v.severity + '">' + v.severity + '</span>' +
|
|
1484
|
-
escapeHtml(v.filePath) + ':' + v.line +
|
|
1485
|
-
'</div>' +
|
|
1486
|
-
'<div class="violation-msg">' +
|
|
1487
|
-
escapeHtml(v.fromLayer) + ' → ' + escapeHtml(v.toLayer) + ': ' + escapeHtml(v.ruleDescription) +
|
|
1488
|
-
'</div>' +
|
|
1489
|
-
'</div>';
|
|
1490
|
-
}
|
|
1491
|
-
guardListEl.innerHTML = summaryHtml + violHtml;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
loadGuardStatus();
|
|
1495
|
-
|
|
1496
|
-
// ─── Preview ──────────────────────────────────
|
|
1497
|
-
var previewListEl = document.getElementById('preview-list');
|
|
1498
|
-
var previewDiffEl = document.getElementById('preview-diff');
|
|
1499
|
-
var refreshChangesBtn = document.getElementById('refresh-changes');
|
|
1500
|
-
|
|
1501
|
-
function loadFileChanges() {
|
|
1502
|
-
log('Fetching file changes...');
|
|
1503
|
-
var xhr = new XMLHttpRequest();
|
|
1504
|
-
xhr.open('GET', '/__kitchen/api/file/changes', true);
|
|
1505
|
-
xhr.onload = function() {
|
|
1506
|
-
if (xhr.status === 200) {
|
|
1507
|
-
try {
|
|
1508
|
-
var data = JSON.parse(xhr.responseText);
|
|
1509
|
-
log('Changes loaded: ' + data.changes.length, 'ok');
|
|
1510
|
-
renderFileChanges(data.changes);
|
|
1511
|
-
} catch(e) {
|
|
1512
|
-
log('Changes parse error: ' + e.message, 'err');
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
};
|
|
1516
|
-
xhr.onerror = function() { log('Changes network error', 'err'); };
|
|
1517
|
-
xhr.send();
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1631
|
+
'<div class="guard-stat"><div class="guard-stat-value">' + r.totalViolations + '</div><div class="guard-stat-label">Total</div></div>' +
|
|
1632
|
+
'<div class="guard-stat"><div class="guard-stat-value sev-error">' + (r.bySeverity.error || 0) + '</div><div class="guard-stat-label">Errors</div></div>' +
|
|
1633
|
+
'<div class="guard-stat"><div class="guard-stat-value sev-warning">' + (r.bySeverity.warning || 0) + '</div><div class="guard-stat-label">Warnings</div></div>' +
|
|
1634
|
+
'<div class="guard-stat"><div class="guard-stat-value sev-info">' + (r.bySeverity.info || 0) + '</div><div class="guard-stat-label">Info</div></div>' +
|
|
1635
|
+
'</div>';
|
|
1636
|
+
|
|
1637
|
+
if (!r.violations.length) {
|
|
1638
|
+
guardListEl.innerHTML = summaryHtml + '<div class="empty-state">No violations found!</div>';
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
var violHtml = '';
|
|
1643
|
+
var list = r.violations.length > 100 ? r.violations.slice(0, 100) : r.violations;
|
|
1644
|
+
for (var i = 0; i < list.length; i++) {
|
|
1645
|
+
var v = list[i];
|
|
1646
|
+
violHtml += '<div class="violation-item">' +
|
|
1647
|
+
'<div class="violation-file">' +
|
|
1648
|
+
'<span class="violation-sev ' + v.severity + '">' + v.severity + '</span>' +
|
|
1649
|
+
escapeHtml(v.filePath) + ':' + v.line +
|
|
1650
|
+
'</div>' +
|
|
1651
|
+
'<div class="violation-msg">' +
|
|
1652
|
+
escapeHtml(v.fromLayer) + ' → ' + escapeHtml(v.toLayer) + ': ' + escapeHtml(v.ruleDescription) +
|
|
1653
|
+
'</div>' +
|
|
1654
|
+
'</div>';
|
|
1655
|
+
}
|
|
1656
|
+
guardListEl.innerHTML = summaryHtml + violHtml;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
loadGuardStatus();
|
|
1660
|
+
|
|
1661
|
+
// ─── Preview ──────────────────────────────────
|
|
1662
|
+
var previewListEl = document.getElementById('preview-list');
|
|
1663
|
+
var previewDiffEl = document.getElementById('preview-diff');
|
|
1664
|
+
var refreshChangesBtn = document.getElementById('refresh-changes');
|
|
1665
|
+
|
|
1666
|
+
function loadFileChanges() {
|
|
1667
|
+
log('Fetching file changes...');
|
|
1668
|
+
var xhr = new XMLHttpRequest();
|
|
1669
|
+
xhr.open('GET', '/__kitchen/api/file/changes', true);
|
|
1670
|
+
xhr.onload = function() {
|
|
1671
|
+
if (xhr.status === 200) {
|
|
1672
|
+
try {
|
|
1673
|
+
var data = JSON.parse(xhr.responseText);
|
|
1674
|
+
log('Changes loaded: ' + data.changes.length, 'ok');
|
|
1675
|
+
renderFileChanges(data.changes);
|
|
1676
|
+
} catch(e) {
|
|
1677
|
+
log('Changes parse error: ' + e.message, 'err');
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
xhr.onerror = function() { log('Changes network error', 'err'); };
|
|
1682
|
+
xhr.send();
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1520
1685
|
function renderFileChanges(changes) {
|
|
1521
1686
|
setMetric('metric-changes', changes.length);
|
|
1522
1687
|
if (!changes.length) {
|
|
1523
1688
|
previewListEl.innerHTML = '<div class="empty-state">No file changes detected.</div>';
|
|
1524
1689
|
return;
|
|
1525
|
-
}
|
|
1526
|
-
var html = '';
|
|
1527
|
-
var icons = { added: '+', modified: '~', deleted: '-', untracked: '?', renamed: 'R' };
|
|
1528
|
-
for (var i = 0; i < changes.length; i++) {
|
|
1529
|
-
var c = changes[i];
|
|
1530
|
-
html += '<div class="change-item" data-path="' + escapeHtml(c.filePath) + '">' +
|
|
1531
|
-
'<span class="change-icon">' + (icons[c.status] || '?') + '</span>' +
|
|
1532
|
-
'<span class="change-path">' + escapeHtml(c.filePath) + '</span>' +
|
|
1533
|
-
'<span class="change-status ' + c.status + '">' + c.status + '</span>' +
|
|
1534
|
-
'</div>';
|
|
1535
|
-
}
|
|
1536
|
-
previewListEl.innerHTML = html;
|
|
1537
|
-
|
|
1538
|
-
// Attach click handlers
|
|
1539
|
-
var items = previewListEl.querySelectorAll('.change-item');
|
|
1540
|
-
for (var j = 0; j < items.length; j++) {
|
|
1541
|
-
items[j].addEventListener('click', function() {
|
|
1542
|
-
var p = this.getAttribute('data-path');
|
|
1543
|
-
loadFileDiff(p);
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
function loadFileDiff(filePath) {
|
|
1549
|
-
log('Fetching diff for ' + filePath);
|
|
1550
|
-
var xhr = new XMLHttpRequest();
|
|
1551
|
-
xhr.open('GET', '/__kitchen/api/file/diff?path=' + encodeURIComponent(filePath), true);
|
|
1552
|
-
xhr.onload = function() {
|
|
1553
|
-
if (xhr.status === 200) {
|
|
1554
|
-
try {
|
|
1555
|
-
var diff = JSON.parse(xhr.responseText);
|
|
1556
|
-
renderDiff(diff);
|
|
1557
|
-
} catch(e) {
|
|
1558
|
-
log('Diff parse error: ' + e.message, 'err');
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
};
|
|
1562
|
-
xhr.onerror = function() { log('Diff network error', 'err'); };
|
|
1563
|
-
xhr.send();
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
function renderDiff(diff) {
|
|
1567
|
-
if (!diff.hunks || !diff.hunks.length) {
|
|
1568
|
-
previewDiffEl.innerHTML = '<div class="empty-state">No diff available.</div>';
|
|
1569
|
-
previewDiffEl.style.display = 'block';
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
var html = '<div class="diff-header">' +
|
|
1573
|
-
'<span class="diff-file">' + escapeHtml(diff.filePath) + '</span>' +
|
|
1574
|
-
'<span class="diff-stats"><span class="diff-add">+' + diff.additions + '</span><span class="diff-del">-' + diff.deletions + '</span></span>' +
|
|
1575
|
-
'<button class="btn-sm" onclick="document.getElementById(\'preview-diff\').style.display=\'none\'">Close</button>' +
|
|
1576
|
-
'</div>';
|
|
1577
|
-
for (var h = 0; h < diff.hunks.length; h++) {
|
|
1578
|
-
var hunk = diff.hunks[h];
|
|
1579
|
-
html += '<div class="diff-hunk-header">' + escapeHtml(hunk.header) + '</div>';
|
|
1580
|
-
for (var l = 0; l < hunk.lines.length; l++) {
|
|
1581
|
-
var line = hunk.lines[l];
|
|
1582
|
-
var cls = line.type === 'add' ? 'add' : line.type === 'remove' ? 'remove' : 'context';
|
|
1583
|
-
html += '<div class="diff-line ' + cls + '">' +
|
|
1584
|
-
'<span class="diff-line-num">' + (line.oldLine || '') + '</span>' +
|
|
1585
|
-
'<span class="diff-line-num">' + (line.newLine || '') + '</span>' +
|
|
1586
|
-
'<span class="diff-line-content">' + escapeHtml(line.content) + '</span>' +
|
|
1587
|
-
'</div>';
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
previewDiffEl.innerHTML = html;
|
|
1591
|
-
previewDiffEl.style.display = 'block';
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
refreshChangesBtn.addEventListener('click', loadFileChanges);
|
|
1595
|
-
loadFileChanges();
|
|
1596
|
-
|
|
1597
|
-
// ─── Contracts ─────────────────────────────────
|
|
1598
|
-
var contractsListEl = document.getElementById('contracts-list');
|
|
1599
|
-
var contractSchemaEl = document.getElementById('contract-schema');
|
|
1600
|
-
var validateBtn = document.getElementById('validate-btn');
|
|
1601
|
-
var validateResultEl = document.getElementById('validate-result');
|
|
1602
|
-
var exportJsonBtn = document.getElementById('export-openapi-json');
|
|
1603
|
-
var exportYamlBtn = document.getElementById('export-openapi-yaml');
|
|
1604
|
-
var selectedContractId = null;
|
|
1605
|
-
|
|
1606
|
-
function loadContracts() {
|
|
1607
|
-
log('Fetching contracts...');
|
|
1608
|
-
var xhr = new XMLHttpRequest();
|
|
1609
|
-
xhr.open('GET', '/__kitchen/api/contracts', true);
|
|
1610
|
-
xhr.onload = function() {
|
|
1611
|
-
if (xhr.status === 200) {
|
|
1612
|
-
try {
|
|
1613
|
-
var data = JSON.parse(xhr.responseText);
|
|
1614
|
-
log('Contracts loaded: ' + data.contracts.length, 'ok');
|
|
1615
|
-
renderContractsList(data.contracts);
|
|
1616
|
-
} catch(e) {
|
|
1617
|
-
log('Contracts parse error: ' + e.message, 'err');
|
|
1618
|
-
contractsListEl.innerHTML = '<div class="empty-state">Failed to parse contracts.</div>';
|
|
1619
|
-
}
|
|
1620
|
-
} else if (xhr.status === 404) {
|
|
1621
|
-
contractsListEl.innerHTML = '<div class="empty-state">Contracts API not available.</div>';
|
|
1622
|
-
}
|
|
1623
|
-
};
|
|
1624
|
-
xhr.onerror = function() {
|
|
1625
|
-
log('Contracts network error', 'err');
|
|
1626
|
-
contractsListEl.innerHTML = '<div class="empty-state">Network error.</div>';
|
|
1627
|
-
};
|
|
1628
|
-
xhr.send();
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1690
|
+
}
|
|
1691
|
+
var html = '';
|
|
1692
|
+
var icons = { added: '+', modified: '~', deleted: '-', untracked: '?', renamed: 'R' };
|
|
1693
|
+
for (var i = 0; i < changes.length; i++) {
|
|
1694
|
+
var c = changes[i];
|
|
1695
|
+
html += '<div class="change-item" data-path="' + escapeHtml(c.filePath) + '">' +
|
|
1696
|
+
'<span class="change-icon">' + (icons[c.status] || '?') + '</span>' +
|
|
1697
|
+
'<span class="change-path">' + escapeHtml(c.filePath) + '</span>' +
|
|
1698
|
+
'<span class="change-status ' + c.status + '">' + c.status + '</span>' +
|
|
1699
|
+
'</div>';
|
|
1700
|
+
}
|
|
1701
|
+
previewListEl.innerHTML = html;
|
|
1702
|
+
|
|
1703
|
+
// Attach click handlers
|
|
1704
|
+
var items = previewListEl.querySelectorAll('.change-item');
|
|
1705
|
+
for (var j = 0; j < items.length; j++) {
|
|
1706
|
+
items[j].addEventListener('click', function() {
|
|
1707
|
+
var p = this.getAttribute('data-path');
|
|
1708
|
+
loadFileDiff(p);
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function loadFileDiff(filePath) {
|
|
1714
|
+
log('Fetching diff for ' + filePath);
|
|
1715
|
+
var xhr = new XMLHttpRequest();
|
|
1716
|
+
xhr.open('GET', '/__kitchen/api/file/diff?path=' + encodeURIComponent(filePath), true);
|
|
1717
|
+
xhr.onload = function() {
|
|
1718
|
+
if (xhr.status === 200) {
|
|
1719
|
+
try {
|
|
1720
|
+
var diff = JSON.parse(xhr.responseText);
|
|
1721
|
+
renderDiff(diff);
|
|
1722
|
+
} catch(e) {
|
|
1723
|
+
log('Diff parse error: ' + e.message, 'err');
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
xhr.onerror = function() { log('Diff network error', 'err'); };
|
|
1728
|
+
xhr.send();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function renderDiff(diff) {
|
|
1732
|
+
if (!diff.hunks || !diff.hunks.length) {
|
|
1733
|
+
previewDiffEl.innerHTML = '<div class="empty-state">No diff available.</div>';
|
|
1734
|
+
previewDiffEl.style.display = 'block';
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
var html = '<div class="diff-header">' +
|
|
1738
|
+
'<span class="diff-file">' + escapeHtml(diff.filePath) + '</span>' +
|
|
1739
|
+
'<span class="diff-stats"><span class="diff-add">+' + diff.additions + '</span><span class="diff-del">-' + diff.deletions + '</span></span>' +
|
|
1740
|
+
'<button class="btn-sm" onclick="document.getElementById(\'preview-diff\').style.display=\'none\'">Close</button>' +
|
|
1741
|
+
'</div>';
|
|
1742
|
+
for (var h = 0; h < diff.hunks.length; h++) {
|
|
1743
|
+
var hunk = diff.hunks[h];
|
|
1744
|
+
html += '<div class="diff-hunk-header">' + escapeHtml(hunk.header) + '</div>';
|
|
1745
|
+
for (var l = 0; l < hunk.lines.length; l++) {
|
|
1746
|
+
var line = hunk.lines[l];
|
|
1747
|
+
var cls = line.type === 'add' ? 'add' : line.type === 'remove' ? 'remove' : 'context';
|
|
1748
|
+
html += '<div class="diff-line ' + cls + '">' +
|
|
1749
|
+
'<span class="diff-line-num">' + (line.oldLine || '') + '</span>' +
|
|
1750
|
+
'<span class="diff-line-num">' + (line.newLine || '') + '</span>' +
|
|
1751
|
+
'<span class="diff-line-content">' + escapeHtml(line.content) + '</span>' +
|
|
1752
|
+
'</div>';
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
previewDiffEl.innerHTML = html;
|
|
1756
|
+
previewDiffEl.style.display = 'block';
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
refreshChangesBtn.addEventListener('click', loadFileChanges);
|
|
1760
|
+
loadFileChanges();
|
|
1761
|
+
|
|
1762
|
+
// ─── Contracts ─────────────────────────────────
|
|
1763
|
+
var contractsListEl = document.getElementById('contracts-list');
|
|
1764
|
+
var contractSchemaEl = document.getElementById('contract-schema');
|
|
1765
|
+
var validateBtn = document.getElementById('validate-btn');
|
|
1766
|
+
var validateResultEl = document.getElementById('validate-result');
|
|
1767
|
+
var exportJsonBtn = document.getElementById('export-openapi-json');
|
|
1768
|
+
var exportYamlBtn = document.getElementById('export-openapi-yaml');
|
|
1769
|
+
var selectedContractId = null;
|
|
1770
|
+
|
|
1771
|
+
function loadContracts() {
|
|
1772
|
+
log('Fetching contracts...');
|
|
1773
|
+
var xhr = new XMLHttpRequest();
|
|
1774
|
+
xhr.open('GET', '/__kitchen/api/contracts', true);
|
|
1775
|
+
xhr.onload = function() {
|
|
1776
|
+
if (xhr.status === 200) {
|
|
1777
|
+
try {
|
|
1778
|
+
var data = JSON.parse(xhr.responseText);
|
|
1779
|
+
log('Contracts loaded: ' + data.contracts.length, 'ok');
|
|
1780
|
+
renderContractsList(data.contracts);
|
|
1781
|
+
} catch(e) {
|
|
1782
|
+
log('Contracts parse error: ' + e.message, 'err');
|
|
1783
|
+
contractsListEl.innerHTML = '<div class="empty-state">Failed to parse contracts.</div>';
|
|
1784
|
+
}
|
|
1785
|
+
} else if (xhr.status === 404) {
|
|
1786
|
+
contractsListEl.innerHTML = '<div class="empty-state">Contracts API not available.</div>';
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
xhr.onerror = function() {
|
|
1790
|
+
log('Contracts network error', 'err');
|
|
1791
|
+
contractsListEl.innerHTML = '<div class="empty-state">Network error.</div>';
|
|
1792
|
+
};
|
|
1793
|
+
xhr.send();
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1631
1796
|
function renderContractsList(contracts) {
|
|
1632
1797
|
setMetric('metric-contracts', contracts.length);
|
|
1633
1798
|
if (!contracts.length) {
|
|
1634
1799
|
contractsListEl.innerHTML = '<div class="empty-state">No contracts found.</div>';
|
|
1635
1800
|
return;
|
|
1636
|
-
}
|
|
1637
|
-
var html = '';
|
|
1638
|
-
for (var i = 0; i < contracts.length; i++) {
|
|
1639
|
-
var c = contracts[i];
|
|
1640
|
-
var methods = (c.methods || []).map(function(m) {
|
|
1641
|
-
return '<span class="method-badge ' + m.toLowerCase() + '">' + m + '</span>';
|
|
1642
|
-
}).join('');
|
|
1643
|
-
html += '<div class="contract-item" data-id="' + escapeHtml(c.id) + '">' +
|
|
1644
|
-
methods +
|
|
1645
|
-
'<span class="contract-pattern">' + escapeHtml(c.pattern) + '</span>' +
|
|
1646
|
-
'</div>';
|
|
1647
|
-
}
|
|
1648
|
-
contractsListEl.innerHTML = html;
|
|
1649
|
-
|
|
1650
|
-
var items = contractsListEl.querySelectorAll('.contract-item');
|
|
1651
|
-
for (var j = 0; j < items.length; j++) {
|
|
1652
|
-
items[j].addEventListener('click', function() {
|
|
1653
|
-
var id = this.getAttribute('data-id');
|
|
1654
|
-
selectedContractId = id;
|
|
1655
|
-
var all = contractsListEl.querySelectorAll('.contract-item');
|
|
1656
|
-
for (var k = 0; k < all.length; k++) all[k].classList.remove('selected');
|
|
1657
|
-
this.classList.add('selected');
|
|
1658
|
-
loadContractDetail(id);
|
|
1659
|
-
});
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
function loadContractDetail(id) {
|
|
1664
|
-
var xhr = new XMLHttpRequest();
|
|
1665
|
-
xhr.open('GET', '/__kitchen/api/contracts/' + encodeURIComponent(id), true);
|
|
1666
|
-
xhr.onload = function() {
|
|
1667
|
-
if (xhr.status === 200) {
|
|
1668
|
-
try {
|
|
1669
|
-
var data = JSON.parse(xhr.responseText);
|
|
1670
|
-
contractSchemaEl.textContent = JSON.stringify(data, null, 2);
|
|
1671
|
-
} catch(e) {
|
|
1672
|
-
contractSchemaEl.textContent = 'Parse error';
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
};
|
|
1676
|
-
xhr.send();
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
validateBtn.addEventListener('click', function() {
|
|
1680
|
-
if (!selectedContractId) {
|
|
1681
|
-
validateResultEl.className = 'validate-result error';
|
|
1682
|
-
validateResultEl.textContent = 'Select a contract first.';
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
var method = document.getElementById('validate-method').value;
|
|
1686
|
-
var input = {};
|
|
1687
|
-
try {
|
|
1688
|
-
var q = document.getElementById('validate-query').value.trim();
|
|
1689
|
-
var b = document.getElementById('validate-body').value.trim();
|
|
1690
|
-
var p = document.getElementById('validate-params').value.trim();
|
|
1691
|
-
if (q) input.query = JSON.parse(q);
|
|
1692
|
-
if (b) input.body = JSON.parse(b);
|
|
1693
|
-
if (p) input.params = JSON.parse(p);
|
|
1694
|
-
} catch(e) {
|
|
1695
|
-
validateResultEl.className = 'validate-result error';
|
|
1696
|
-
validateResultEl.textContent = 'Invalid JSON: ' + e.message;
|
|
1697
|
-
return;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
var xhr = new XMLHttpRequest();
|
|
1701
|
-
xhr.open('POST', '/__kitchen/api/contracts/validate', true);
|
|
1702
|
-
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
1703
|
-
xhr.onload = function() {
|
|
1704
|
-
try {
|
|
1705
|
-
var result = JSON.parse(xhr.responseText);
|
|
1706
|
-
if (result.valid) {
|
|
1707
|
-
validateResultEl.className = 'validate-result success';
|
|
1708
|
-
validateResultEl.textContent = 'Validation passed!';
|
|
1709
|
-
} else {
|
|
1710
|
-
validateResultEl.className = 'validate-result error';
|
|
1711
|
-
validateResultEl.textContent = JSON.stringify(result.errors || result, null, 2);
|
|
1712
|
-
}
|
|
1713
|
-
} catch(e) {
|
|
1714
|
-
validateResultEl.className = 'validate-result error';
|
|
1715
|
-
validateResultEl.textContent = 'Response parse error';
|
|
1716
|
-
}
|
|
1717
|
-
};
|
|
1718
|
-
xhr.send(JSON.stringify({ contractId: selectedContractId, method: method, input: input }));
|
|
1719
|
-
});
|
|
1720
|
-
|
|
1721
|
-
exportJsonBtn.addEventListener('click', function() {
|
|
1722
|
-
window.open('/__kitchen/api/contracts/openapi', '_blank');
|
|
1723
|
-
});
|
|
1724
|
-
|
|
1725
|
-
exportYamlBtn.addEventListener('click', function() {
|
|
1726
|
-
window.open('/__kitchen/api/contracts/openapi.yaml', '_blank');
|
|
1727
|
-
});
|
|
1728
|
-
|
|
1729
|
-
loadContracts();
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1801
|
+
}
|
|
1802
|
+
var html = '';
|
|
1803
|
+
for (var i = 0; i < contracts.length; i++) {
|
|
1804
|
+
var c = contracts[i];
|
|
1805
|
+
var methods = (c.methods || []).map(function(m) {
|
|
1806
|
+
return '<span class="method-badge ' + m.toLowerCase() + '">' + m + '</span>';
|
|
1807
|
+
}).join('');
|
|
1808
|
+
html += '<div class="contract-item" data-id="' + escapeHtml(c.id) + '">' +
|
|
1809
|
+
methods +
|
|
1810
|
+
'<span class="contract-pattern">' + escapeHtml(c.pattern) + '</span>' +
|
|
1811
|
+
'</div>';
|
|
1812
|
+
}
|
|
1813
|
+
contractsListEl.innerHTML = html;
|
|
1814
|
+
|
|
1815
|
+
var items = contractsListEl.querySelectorAll('.contract-item');
|
|
1816
|
+
for (var j = 0; j < items.length; j++) {
|
|
1817
|
+
items[j].addEventListener('click', function() {
|
|
1818
|
+
var id = this.getAttribute('data-id');
|
|
1819
|
+
selectedContractId = id;
|
|
1820
|
+
var all = contractsListEl.querySelectorAll('.contract-item');
|
|
1821
|
+
for (var k = 0; k < all.length; k++) all[k].classList.remove('selected');
|
|
1822
|
+
this.classList.add('selected');
|
|
1823
|
+
loadContractDetail(id);
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function loadContractDetail(id) {
|
|
1829
|
+
var xhr = new XMLHttpRequest();
|
|
1830
|
+
xhr.open('GET', '/__kitchen/api/contracts/' + encodeURIComponent(id), true);
|
|
1831
|
+
xhr.onload = function() {
|
|
1832
|
+
if (xhr.status === 200) {
|
|
1833
|
+
try {
|
|
1834
|
+
var data = JSON.parse(xhr.responseText);
|
|
1835
|
+
contractSchemaEl.textContent = JSON.stringify(data, null, 2);
|
|
1836
|
+
} catch(e) {
|
|
1837
|
+
contractSchemaEl.textContent = 'Parse error';
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
xhr.send();
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
validateBtn.addEventListener('click', function() {
|
|
1845
|
+
if (!selectedContractId) {
|
|
1846
|
+
validateResultEl.className = 'validate-result error';
|
|
1847
|
+
validateResultEl.textContent = 'Select a contract first.';
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
var method = document.getElementById('validate-method').value;
|
|
1851
|
+
var input = {};
|
|
1852
|
+
try {
|
|
1853
|
+
var q = document.getElementById('validate-query').value.trim();
|
|
1854
|
+
var b = document.getElementById('validate-body').value.trim();
|
|
1855
|
+
var p = document.getElementById('validate-params').value.trim();
|
|
1856
|
+
if (q) input.query = JSON.parse(q);
|
|
1857
|
+
if (b) input.body = JSON.parse(b);
|
|
1858
|
+
if (p) input.params = JSON.parse(p);
|
|
1859
|
+
} catch(e) {
|
|
1860
|
+
validateResultEl.className = 'validate-result error';
|
|
1861
|
+
validateResultEl.textContent = 'Invalid JSON: ' + e.message;
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
var xhr = new XMLHttpRequest();
|
|
1866
|
+
xhr.open('POST', '/__kitchen/api/contracts/validate', true);
|
|
1867
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
1868
|
+
xhr.onload = function() {
|
|
1869
|
+
try {
|
|
1870
|
+
var result = JSON.parse(xhr.responseText);
|
|
1871
|
+
if (result.valid) {
|
|
1872
|
+
validateResultEl.className = 'validate-result success';
|
|
1873
|
+
validateResultEl.textContent = 'Validation passed!';
|
|
1874
|
+
} else {
|
|
1875
|
+
validateResultEl.className = 'validate-result error';
|
|
1876
|
+
validateResultEl.textContent = JSON.stringify(result.errors || result, null, 2);
|
|
1877
|
+
}
|
|
1878
|
+
} catch(e) {
|
|
1879
|
+
validateResultEl.className = 'validate-result error';
|
|
1880
|
+
validateResultEl.textContent = 'Response parse error';
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
xhr.send(JSON.stringify({ contractId: selectedContractId, method: method, input: input }));
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
exportJsonBtn.addEventListener('click', function() {
|
|
1887
|
+
window.open('/__kitchen/api/contracts/openapi', '_blank');
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
exportYamlBtn.addEventListener('click', function() {
|
|
1891
|
+
window.open('/__kitchen/api/contracts/openapi.yaml', '_blank');
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
loadContracts();
|
|
1895
|
+
|
|
1896
|
+
// ─── Requests Tab ─────────────────────────────
|
|
1897
|
+
var requestsListEl = document.getElementById('requests-list');
|
|
1898
|
+
var requestsDetailEl = document.getElementById('requests-detail');
|
|
1899
|
+
|
|
1900
|
+
function fetchJson(url, cb) {
|
|
1901
|
+
var xhr = new XMLHttpRequest();
|
|
1902
|
+
xhr.open('GET', url, true);
|
|
1903
|
+
xhr.onload = function() {
|
|
1904
|
+
if (xhr.status === 200) {
|
|
1905
|
+
try { cb(null, JSON.parse(xhr.responseText)); }
|
|
1906
|
+
catch(e) { cb(e); }
|
|
1907
|
+
} else { cb(new Error('HTTP ' + xhr.status)); }
|
|
1908
|
+
};
|
|
1909
|
+
xhr.onerror = function() { cb(new Error('network')); };
|
|
1910
|
+
xhr.send();
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function statusClass(s) {
|
|
1914
|
+
if (!s) return '';
|
|
1915
|
+
if (s >= 500) return 's5';
|
|
1916
|
+
if (s >= 400) return 's4';
|
|
1917
|
+
if (s >= 300) return 's3';
|
|
1918
|
+
return 's2';
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
function loadRequests() {
|
|
1922
|
+
requestsListEl.innerHTML = '<div class="empty-state">Loading requests...</div>';
|
|
1923
|
+
fetchJson('/__kitchen/api/requests?limit=100', function(err, data) {
|
|
1924
|
+
if (err) {
|
|
1925
|
+
requestsListEl.innerHTML = '<div class="empty-state">Failed: ' + escapeHtml(err.message) + '</div>';
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
var reqs = data.requests || [];
|
|
1929
|
+
if (!reqs.length) {
|
|
1930
|
+
requestsListEl.innerHTML = '<div class="empty-state">No requests yet.</div>';
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
var html = '';
|
|
1934
|
+
for (var i = 0; i < reqs.length; i++) {
|
|
1935
|
+
var r = reqs[i];
|
|
1936
|
+
// EventBus event shape vs legacy ring buffer shape
|
|
1937
|
+
var method = (r.data && r.data.method) || r.method || '?';
|
|
1938
|
+
var pathStr = (r.data && (r.data.path || r.data.url)) || r.path || r.message || '';
|
|
1939
|
+
var status = (r.data && r.data.status) || r.status || 0;
|
|
1940
|
+
var dur = r.duration || (r.data && r.data.duration) || 0;
|
|
1941
|
+
var cache = (r.data && (r.data.cacheStatus || r.data.cache)) || r.cacheStatus || '';
|
|
1942
|
+
var corr = r.correlationId || '';
|
|
1943
|
+
html += '<div class="req-row" data-corr="' + escapeHtml(corr) + '">' +
|
|
1944
|
+
'<span class="req-method ' + String(method).toLowerCase() + '">' + escapeHtml(method) + '</span>' +
|
|
1945
|
+
'<span class="req-status ' + statusClass(status) + '">' + escapeHtml(status || '-') + '</span>' +
|
|
1946
|
+
'<span class="req-path">' + escapeHtml(pathStr) + '</span>' +
|
|
1947
|
+
'<span class="req-dur">' + (dur ? Math.round(dur) + 'ms' : '-') + '</span>' +
|
|
1948
|
+
'<span class="req-cache">' + escapeHtml(cache || '-') + '</span>' +
|
|
1949
|
+
'</div>';
|
|
1950
|
+
}
|
|
1951
|
+
requestsListEl.innerHTML = html;
|
|
1952
|
+
|
|
1953
|
+
var rows = requestsListEl.querySelectorAll('.req-row');
|
|
1954
|
+
for (var j = 0; j < rows.length; j++) {
|
|
1955
|
+
rows[j].addEventListener('click', function() {
|
|
1956
|
+
var all = requestsListEl.querySelectorAll('.req-row');
|
|
1957
|
+
for (var k = 0; k < all.length; k++) all[k].classList.remove('selected');
|
|
1958
|
+
this.classList.add('selected');
|
|
1959
|
+
var cid = this.getAttribute('data-corr');
|
|
1960
|
+
if (cid) loadCorrelation(cid);
|
|
1961
|
+
else {
|
|
1962
|
+
requestsDetailEl.innerHTML = '<div class="empty-state">No correlation ID on this request.</div>';
|
|
1963
|
+
requestsDetailEl.style.display = 'block';
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function loadCorrelation(cid) {
|
|
1971
|
+
requestsDetailEl.innerHTML = '<div class="empty-state">Loading correlated events...</div>';
|
|
1972
|
+
requestsDetailEl.style.display = 'block';
|
|
1973
|
+
fetchJson('/__kitchen/api/correlation?id=' + encodeURIComponent(cid), function(err, data) {
|
|
1974
|
+
if (err) {
|
|
1975
|
+
requestsDetailEl.innerHTML = '<div class="empty-state">Failed: ' + escapeHtml(err.message) + '</div>';
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
var events = data.events || [];
|
|
1979
|
+
if (!events.length) {
|
|
1980
|
+
requestsDetailEl.innerHTML = '<div class="empty-state">No events for correlation ' + escapeHtml(cid) + '</div>';
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
var html = '<div style="font-size:11px;color:var(--muted);margin-bottom:8px;">correlation: ' + escapeHtml(cid) + '</div>';
|
|
1984
|
+
for (var i = 0; i < events.length; i++) {
|
|
1985
|
+
var e = events[i];
|
|
1986
|
+
var t = new Date(e.timestamp).toLocaleTimeString();
|
|
1987
|
+
html += '<div class="corr-event">' +
|
|
1988
|
+
'<span class="corr-type">' + escapeHtml(e.type) + '</span>' +
|
|
1989
|
+
'<span>' + escapeHtml(t) + ' </span>' +
|
|
1990
|
+
'<span>' + escapeHtml(e.message || '') + '</span>' +
|
|
1991
|
+
(e.duration ? ' <span style="color:var(--muted)">(' + Math.round(e.duration) + 'ms)</span>' : '') +
|
|
1992
|
+
'</div>';
|
|
1993
|
+
}
|
|
1994
|
+
requestsDetailEl.innerHTML = html;
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
document.getElementById('refresh-requests').addEventListener('click', loadRequests);
|
|
1999
|
+
|
|
2000
|
+
// ─── MCP Activity Tab ─────────────────────────
|
|
2001
|
+
var mcpListEl = document.getElementById('mcp-list');
|
|
2002
|
+
|
|
2003
|
+
function loadMcpActivity() {
|
|
2004
|
+
mcpListEl.innerHTML = '<div class="empty-state">Loading MCP activity...</div>';
|
|
2005
|
+
fetchJson('/__kitchen/api/activity?limit=100', function(err, data) {
|
|
2006
|
+
if (err) {
|
|
2007
|
+
mcpListEl.innerHTML = '<div class="empty-state">Failed: ' + escapeHtml(err.message) + '</div>';
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
var events = data.events || [];
|
|
2011
|
+
if (!events.length) {
|
|
2012
|
+
mcpListEl.innerHTML = '<div class="empty-state">No MCP activity yet.</div>';
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// Group by correlationId (fall back to individual items)
|
|
2017
|
+
var groups = {};
|
|
2018
|
+
var order = [];
|
|
2019
|
+
for (var i = 0; i < events.length; i++) {
|
|
2020
|
+
var e = events[i];
|
|
2021
|
+
var cid = e.correlationId || ('_' + (e.id || i));
|
|
2022
|
+
if (!groups[cid]) { groups[cid] = []; order.push(cid); }
|
|
2023
|
+
groups[cid].push(e);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
var html = '';
|
|
2027
|
+
for (var g = 0; g < order.length; g++) {
|
|
2028
|
+
var cid = order[g];
|
|
2029
|
+
var items = groups[cid];
|
|
2030
|
+
var header = cid.charAt(0) === '_' ? 'ungrouped' : 'correlation: ' + cid;
|
|
2031
|
+
html += '<div class="mcp-group"><div class="mcp-group-header">' + escapeHtml(header) + '</div>';
|
|
2032
|
+
for (var k = 0; k < items.length; k++) {
|
|
2033
|
+
var ev = items[k];
|
|
2034
|
+
var ts = ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString() : (ev.ts || '');
|
|
2035
|
+
var tool = (ev.data && (ev.data.tool || ev.data.name)) || ev.tool || ev.source || 'mcp';
|
|
2036
|
+
var isErr = ev.severity === 'error';
|
|
2037
|
+
var icon = isErr ? '<span class="mcp-status-err">X</span>' : '<span class="mcp-status-ok">OK</span>';
|
|
2038
|
+
var dur = ev.duration ? Math.round(ev.duration) + 'ms' : '-';
|
|
2039
|
+
html += '<div class="mcp-event">' +
|
|
2040
|
+
'<span>' + escapeHtml(ts) + '</span>' +
|
|
2041
|
+
icon +
|
|
2042
|
+
'<span>' + escapeHtml(tool) + ' ' + escapeHtml(ev.message || '') + '</span>' +
|
|
2043
|
+
'<span style="text-align:right;color:var(--muted)">' + dur + '</span>' +
|
|
2044
|
+
'</div>';
|
|
2045
|
+
}
|
|
2046
|
+
html += '</div>';
|
|
2047
|
+
}
|
|
2048
|
+
mcpListEl.innerHTML = html;
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
document.getElementById('refresh-mcp').addEventListener('click', loadMcpActivity);
|
|
2053
|
+
|
|
2054
|
+
// ─── Cache Tab ────────────────────────────────
|
|
2055
|
+
var cacheContentEl = document.getElementById('cache-content');
|
|
2056
|
+
|
|
2057
|
+
function loadCacheStats() {
|
|
2058
|
+
cacheContentEl.innerHTML = '<div class="empty-state">Loading cache stats...</div>';
|
|
2059
|
+
fetchJson('/__kitchen/api/cache-stats', function(err, data) {
|
|
2060
|
+
if (err) {
|
|
2061
|
+
cacheContentEl.innerHTML = '<div class="empty-state">Failed: ' + escapeHtml(err.message) + '</div>';
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
if (!data.enabled) {
|
|
2065
|
+
cacheContentEl.innerHTML = '<div class="empty-state">Cache store is not enabled.</div>';
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
var s = data.stats || {};
|
|
2069
|
+
var entries = s.entries != null ? s.entries : data.size;
|
|
2070
|
+
var hits = s.hits || 0;
|
|
2071
|
+
var misses = s.misses || 0;
|
|
2072
|
+
var total = hits + misses;
|
|
2073
|
+
var hitRate = total > 0 ? ((hits / total) * 100).toFixed(1) + '%' : '-';
|
|
2074
|
+
var stale = s.stale != null ? s.stale : (s.staleCount != null ? s.staleCount : '-');
|
|
2075
|
+
var tags = s.tags != null ? (Array.isArray(s.tags) ? s.tags.length : s.tags) : '-';
|
|
2076
|
+
|
|
2077
|
+
var html = '<div class="cache-grid">' +
|
|
2078
|
+
'<div class="stat-card"><div class="stat-label">Entries</div><div class="stat-value">' + escapeHtml(entries) + '</div></div>' +
|
|
2079
|
+
'<div class="stat-card"><div class="stat-label">Hit Rate</div><div class="stat-value">' + escapeHtml(hitRate) + '</div><div class="stat-sub">' + hits + ' hit / ' + misses + ' miss</div></div>' +
|
|
2080
|
+
'<div class="stat-card"><div class="stat-label">Stale</div><div class="stat-value">' + escapeHtml(stale) + '</div></div>' +
|
|
2081
|
+
'<div class="stat-card"><div class="stat-label">Tags</div><div class="stat-value">' + escapeHtml(tags) + '</div></div>' +
|
|
2082
|
+
'</div>';
|
|
2083
|
+
cacheContentEl.innerHTML = html;
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
document.getElementById('refresh-cache').addEventListener('click', loadCacheStats);
|
|
2088
|
+
|
|
2089
|
+
// ─── Metrics Tab ──────────────────────────────
|
|
2090
|
+
var metricsContentEl = document.getElementById('metrics-content');
|
|
2091
|
+
|
|
2092
|
+
function loadMetrics() {
|
|
2093
|
+
metricsContentEl.innerHTML = '<div class="empty-state">Loading metrics...</div>';
|
|
2094
|
+
fetchJson('/__kitchen/api/metrics?window=5m', function(err, data) {
|
|
2095
|
+
if (err) {
|
|
2096
|
+
metricsContentEl.innerHTML = '<div class="empty-state">Failed: ' + escapeHtml(err.message) + '</div>';
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
var http = data.http || {};
|
|
2100
|
+
var mcp = data.mcp || {};
|
|
2101
|
+
var errRate = ((data.errorRate || 0) * 100).toFixed(2) + '%';
|
|
2102
|
+
var html = '<div class="metrics-grid">' +
|
|
2103
|
+
'<div class="stat-card"><div class="stat-label">HTTP Requests (5m)</div><div class="stat-value">' + (http.count || 0) + '</div></div>' +
|
|
2104
|
+
'<div class="stat-card"><div class="stat-label">TTFB p50</div><div class="stat-value">' + Math.round(http.p50 || 0) + 'ms</div></div>' +
|
|
2105
|
+
'<div class="stat-card"><div class="stat-label">TTFB p95</div><div class="stat-value">' + Math.round(http.p95 || 0) + 'ms</div></div>' +
|
|
2106
|
+
'<div class="stat-card"><div class="stat-label">TTFB p99</div><div class="stat-value">' + Math.round(http.p99 || 0) + 'ms</div></div>' +
|
|
2107
|
+
'<div class="stat-card"><div class="stat-label">MCP Calls</div><div class="stat-value">' + (mcp.count || 0) + '</div><div class="stat-sub">' + (mcp.errors || 0) + ' errors</div></div>' +
|
|
2108
|
+
'<div class="stat-card"><div class="stat-label">MCP Avg Duration</div><div class="stat-value">' + Math.round(mcp.avgDuration || 0) + 'ms</div></div>' +
|
|
2109
|
+
'<div class="stat-card"><div class="stat-label">Error Rate</div><div class="stat-value">' + errRate + '</div></div>' +
|
|
2110
|
+
'<div class="stat-card"><div class="stat-label">Total Events</div><div class="stat-value">' + (data.totalEvents || 0) + '</div></div>' +
|
|
2111
|
+
'</div>';
|
|
2112
|
+
metricsContentEl.innerHTML = html;
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
document.getElementById('refresh-metrics').addEventListener('click', loadMetrics);
|
|
2117
|
+
|
|
2118
|
+
// Lazy-load new tab data when clicked (avoids loading everything up front)
|
|
2119
|
+
var tabLoaders = {
|
|
2120
|
+
'requests': loadRequests,
|
|
2121
|
+
'mcp-activity': loadMcpActivity,
|
|
2122
|
+
'cache': loadCacheStats,
|
|
2123
|
+
'metrics': loadMetrics
|
|
2124
|
+
};
|
|
2125
|
+
var loadedTabs = {};
|
|
2126
|
+
for (var t = 0; t < tabs.length; t++) {
|
|
2127
|
+
tabs[t].addEventListener('click', function() {
|
|
2128
|
+
var name = this.getAttribute('data-panel');
|
|
2129
|
+
if (tabLoaders[name] && !loadedTabs[name]) {
|
|
2130
|
+
loadedTabs[name] = true;
|
|
2131
|
+
tabLoaders[name]();
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
})();
|
|
2137
|
+
`;
|