@mandujs/core 0.41.2 → 0.43.0
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 +21 -4
- package/src/auth/__tests__/login.test.ts +420 -419
- package/src/auth/__tests__/reset.test.ts +296 -296
- package/src/brain/adapters/anthropic-oauth.ts +421 -420
- package/src/brain/adapters/index.ts +2 -1
- package/src/brain/adapters/ollama.ts +1 -1
- package/src/brain/adapters/openai-oauth.ts +534 -533
- package/src/brain/brain.ts +2 -1
- package/src/brain/redactor.ts +196 -196
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
- package/src/bundler/__tests__/cold-start.test.ts +504 -504
- package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
- package/src/bundler/__tests__/hdr.test.ts +1 -1
- package/src/bundler/analyzer.ts +958 -958
- package/src/bundler/build.ts +104 -14
- package/src/bundler/dev.ts +125 -0
- package/src/bundler/hmr-types.ts +1 -0
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
- package/src/bundler/plugins/index.ts +14 -0
- package/src/bundler/plugins/react-compiler-lint.ts +253 -0
- package/src/bundler/plugins/react-compiler.ts +162 -0
- package/src/bundler/types.ts +12 -0
- package/src/change/integrity.ts +2 -1
- package/src/client/index.ts +10 -0
- package/src/client/island.ts +38 -11
- package/src/client/router.ts +6 -1
- package/src/config/mandu.ts +57 -0
- package/src/config/validate.ts +42 -0
- package/src/content/collection.ts +844 -809
- package/src/content/content-layer.ts +316 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/digest.ts +133 -133
- package/src/content/generate-types.ts +168 -168
- package/src/content/index.ts +6 -1
- package/src/content/llms-txt.ts +277 -277
- package/src/contract/define.ts +474 -474
- package/src/contract/route-helpers.ts +2 -1
- package/src/contract/zod-utils.ts +158 -155
- package/src/db/index.ts +513 -513
- package/src/desktop/__tests__/smoke.test.ts +100 -100
- package/src/desktop/webview-fallback.ts +583 -583
- package/src/desktop/window.ts +3 -1
- package/src/dev-error-overlay/overlay-client.ts +300 -300
- package/src/devtools/ai/mcp-connector.ts +499 -498
- package/src/devtools/client/components/kitchen-root.tsx +7 -2
- package/src/email/resend.ts +163 -163
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/graph.ts +898 -898
- package/src/guard/index.ts +16 -0
- package/src/guard/statistics.ts +578 -578
- package/src/guard/tsgolint-bridge.ts +512 -0
- package/src/i18n/locale-resolver.ts +214 -214
- package/src/id/__tests__/id.test.ts +120 -120
- package/src/intent/index.ts +321 -321
- package/src/island/index.ts +39 -23
- package/src/kitchen/api/contract-api.ts +15 -8
- package/src/kitchen/kitchen-ui.ts +2137 -2137
- package/src/lockfile/index.ts +3 -2
- package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
- package/src/middleware/secure/index.ts +417 -417
- package/src/observability/event-bus.ts +2 -2
- package/src/observability/metrics.ts +334 -334
- package/src/observability/tracing.ts +694 -694
- package/src/openapi/generator.ts +1 -1
- package/src/perf/user-marks.ts +553 -553
- package/src/plugins/registry.ts +387 -387
- package/src/resource/ddl/diff.ts +392 -392
- package/src/resource/ddl/snapshot.ts +448 -447
- package/src/resource/generator-schema.ts +477 -476
- package/src/resource/parser.ts +4 -2
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.ts +422 -422
- package/src/runtime/fast-refresh-types.ts +126 -128
- package/src/runtime/image-handler.ts +206 -195
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +36 -19
- package/src/runtime/session-key.ts +328 -328
- package/src/scheduler/__tests__/scheduler.test.ts +514 -514
- package/src/seo/resolve/index.ts +353 -353
- package/src/spec/load.ts +1 -1
- package/src/testing/reporter.ts +676 -676
- package/src/testing/server.ts +196 -196
- package/src/testing/snapshot.ts +444 -444
- package/src/utils/__tests__/lru-cache.test.ts +186 -186
- package/src/utils/bun.ts +8 -8
|
@@ -1,2137 +1,2137 @@
|
|
|
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
|
-
<body>
|
|
18
|
-
<div class="app-shell">
|
|
19
|
-
<header class="hero">
|
|
20
|
-
<div class="hero-copy">
|
|
21
|
-
<div class="hero-kicker">Mandu Dev Console</div>
|
|
22
|
-
<div class="logo">
|
|
23
|
-
<span class="logo-text">Mandu Kitchen</span>
|
|
24
|
-
<span class="logo-badge">live</span>
|
|
25
|
-
</div>
|
|
26
|
-
<p class="hero-subtitle">Routes, architecture, live activity, file changes, and contract checks for the current dev session.</p>
|
|
27
|
-
</div>
|
|
28
|
-
<div class="hero-side">
|
|
29
|
-
<a class="hero-link" href="/" target="_blank" rel="noreferrer">Open app</a>
|
|
30
|
-
<div class="status-pill">
|
|
31
|
-
<span id="sse-status" class="status-dot disconnected"></span>
|
|
32
|
-
<span id="sse-label">Connecting...</span>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
</header>
|
|
36
|
-
|
|
37
|
-
<section class="overview">
|
|
38
|
-
<div class="metric-card">
|
|
39
|
-
<span class="metric-label">Activity</span>
|
|
40
|
-
<strong id="metric-activity" class="metric-value">0</strong>
|
|
41
|
-
</div>
|
|
42
|
-
<div class="metric-card">
|
|
43
|
-
<span class="metric-label">Routes</span>
|
|
44
|
-
<strong id="metric-routes" class="metric-value">...</strong>
|
|
45
|
-
</div>
|
|
46
|
-
<div class="metric-card">
|
|
47
|
-
<span class="metric-label">Guard</span>
|
|
48
|
-
<strong id="metric-guard" class="metric-value">...</strong>
|
|
49
|
-
</div>
|
|
50
|
-
<div class="metric-card">
|
|
51
|
-
<span class="metric-label">Changes</span>
|
|
52
|
-
<strong id="metric-changes" class="metric-value">...</strong>
|
|
53
|
-
</div>
|
|
54
|
-
<div class="metric-card">
|
|
55
|
-
<span class="metric-label">Contracts</span>
|
|
56
|
-
<strong id="metric-contracts" class="metric-value">...</strong>
|
|
57
|
-
</div>
|
|
58
|
-
</section>
|
|
59
|
-
|
|
60
|
-
<nav class="tabs">
|
|
61
|
-
<button class="tab" data-panel="activity">Activity</button>
|
|
62
|
-
<button class="tab active" data-panel="routes">Routes</button>
|
|
63
|
-
<button class="tab" data-panel="guard">Guard</button>
|
|
64
|
-
<button class="tab" data-panel="preview">Preview</button>
|
|
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>
|
|
70
|
-
</nav>
|
|
71
|
-
|
|
72
|
-
<main class="panels">
|
|
73
|
-
<section id="panel-activity" class="panel">
|
|
74
|
-
<div class="panel-header">
|
|
75
|
-
<div>
|
|
76
|
-
<h2>Activity Stream</h2>
|
|
77
|
-
<p class="panel-subtitle">Recent Kitchen events and MCP activity.</p>
|
|
78
|
-
</div>
|
|
79
|
-
<button id="clear-activity" class="btn-sm">Clear</button>
|
|
80
|
-
</div>
|
|
81
|
-
<div id="activity-list" class="activity-list">
|
|
82
|
-
<div class="empty-state">Waiting for MCP activity...</div>
|
|
83
|
-
</div>
|
|
84
|
-
</section>
|
|
85
|
-
|
|
86
|
-
<section id="panel-routes" class="panel active">
|
|
87
|
-
<div class="panel-header">
|
|
88
|
-
<div>
|
|
89
|
-
<h2>Routes</h2>
|
|
90
|
-
<p class="panel-subtitle">Current filesystem routes, slots, contracts, and hydration hints.</p>
|
|
91
|
-
</div>
|
|
92
|
-
<div id="routes-summary" class="summary"></div>
|
|
93
|
-
</div>
|
|
94
|
-
<div id="routes-list" class="routes-list">
|
|
95
|
-
<div class="empty-state">Loading routes...</div>
|
|
96
|
-
</div>
|
|
97
|
-
</section>
|
|
98
|
-
|
|
99
|
-
<section id="panel-guard" class="panel">
|
|
100
|
-
<div class="panel-header">
|
|
101
|
-
<div>
|
|
102
|
-
<h2>Architecture Guard</h2>
|
|
103
|
-
<p class="panel-subtitle">Run a scan and inspect dependency rule violations.</p>
|
|
104
|
-
</div>
|
|
105
|
-
<button id="scan-guard" class="btn-sm">Scan</button>
|
|
106
|
-
</div>
|
|
107
|
-
<div id="guard-status" class="guard-status"></div>
|
|
108
|
-
<div id="guard-list" class="violations-list">
|
|
109
|
-
<div class="empty-state">Click "Scan" to check architecture rules.</div>
|
|
110
|
-
</div>
|
|
111
|
-
</section>
|
|
112
|
-
|
|
113
|
-
<section id="panel-preview" class="panel">
|
|
114
|
-
<div class="panel-header">
|
|
115
|
-
<div>
|
|
116
|
-
<h2>Preview</h2>
|
|
117
|
-
<p class="panel-subtitle">Inspect changed files and open diffs without leaving Kitchen.</p>
|
|
118
|
-
</div>
|
|
119
|
-
<button id="refresh-changes" class="btn-sm">Refresh</button>
|
|
120
|
-
</div>
|
|
121
|
-
<div id="preview-list" class="preview-list">
|
|
122
|
-
<div class="empty-state">Loading file changes...</div>
|
|
123
|
-
</div>
|
|
124
|
-
<div id="preview-diff" class="preview-diff" style="display:none;"></div>
|
|
125
|
-
</section>
|
|
126
|
-
|
|
127
|
-
<section id="panel-contracts" class="panel">
|
|
128
|
-
<div class="panel-header">
|
|
129
|
-
<div>
|
|
130
|
-
<h2>Contracts</h2>
|
|
131
|
-
<p class="panel-subtitle">Browse route contracts and validate payloads in place.</p>
|
|
132
|
-
</div>
|
|
133
|
-
<div class="panel-actions">
|
|
134
|
-
<button id="export-openapi-json" class="btn-sm">Export JSON</button>
|
|
135
|
-
<button id="export-openapi-yaml" class="btn-sm">Export YAML</button>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
<div class="contracts-layout">
|
|
139
|
-
<div id="contracts-list" class="contracts-list">
|
|
140
|
-
<div class="empty-state">Loading contracts...</div>
|
|
141
|
-
</div>
|
|
142
|
-
<div id="contracts-detail" class="contracts-detail">
|
|
143
|
-
<div id="contract-schema" class="contract-schema"></div>
|
|
144
|
-
<div id="contract-playground" class="contract-playground">
|
|
145
|
-
<h3>Validate</h3>
|
|
146
|
-
<div class="playground-controls">
|
|
147
|
-
<select id="validate-method" class="select-sm">
|
|
148
|
-
<option>GET</option><option>POST</option><option>PUT</option><option>PATCH</option><option>DELETE</option>
|
|
149
|
-
</select>
|
|
150
|
-
<button id="validate-btn" class="btn-sm">Validate</button>
|
|
151
|
-
</div>
|
|
152
|
-
<div class="playground-inputs">
|
|
153
|
-
<label>Query <textarea id="validate-query" rows="2" placeholder='{"key":"value"}'></textarea></label>
|
|
154
|
-
<label>Body <textarea id="validate-body" rows="3" placeholder='{"key":"value"}'></textarea></label>
|
|
155
|
-
<label>Params <textarea id="validate-params" rows="2" placeholder='{"id":"1"}'></textarea></label>
|
|
156
|
-
</div>
|
|
157
|
-
<div id="validate-result" class="validate-result"></div>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
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>
|
|
215
|
-
</main>
|
|
216
|
-
|
|
217
|
-
<div id="debug-bar" class="debug-bar"></div>
|
|
218
|
-
</div>
|
|
219
|
-
|
|
220
|
-
<script>${JS}</script>
|
|
221
|
-
</body>
|
|
222
|
-
</html>`;
|
|
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
|
-
|
|
630
|
-
.debug-bar .err { color: #ef4444; }
|
|
631
|
-
.debug-bar .ok { color: #22c55e; }
|
|
632
|
-
|
|
633
|
-
:root {
|
|
634
|
-
--bg: #f4efe6;
|
|
635
|
-
--bg-soft: rgba(255, 252, 246, 0.7);
|
|
636
|
-
--surface: #fffdfa;
|
|
637
|
-
--surface-strong: #f8f1e4;
|
|
638
|
-
--surface-alt: #f0e5d2;
|
|
639
|
-
--ink: #1e2a3a;
|
|
640
|
-
--muted: #677181;
|
|
641
|
-
--line: #dccfba;
|
|
642
|
-
--accent: #b86a12;
|
|
643
|
-
--accent-strong: #8d4f0e;
|
|
644
|
-
--accent-soft: rgba(184, 106, 18, 0.14);
|
|
645
|
-
--success: #177f56;
|
|
646
|
-
--danger: #bc3d3d;
|
|
647
|
-
--info: #2e66b8;
|
|
648
|
-
--warning: #ad7a12;
|
|
649
|
-
--shadow: 0 20px 50px rgba(49, 39, 23, 0.08);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
body {
|
|
653
|
-
font-family: "IBM Plex Sans", "Segoe UI Variable", "Segoe UI", sans-serif;
|
|
654
|
-
background:
|
|
655
|
-
radial-gradient(circle at top left, rgba(226, 186, 124, 0.35), transparent 30%),
|
|
656
|
-
radial-gradient(circle at top right, rgba(152, 191, 193, 0.22), transparent 24%),
|
|
657
|
-
linear-gradient(180deg, #f8f3ea 0%, #efe7d8 100%);
|
|
658
|
-
color: var(--ink);
|
|
659
|
-
padding: 24px;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
.app-shell {
|
|
663
|
-
width: min(1360px, 100%);
|
|
664
|
-
margin: 0 auto;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
.hero {
|
|
668
|
-
display: flex;
|
|
669
|
-
align-items: flex-start;
|
|
670
|
-
justify-content: space-between;
|
|
671
|
-
gap: 24px;
|
|
672
|
-
padding: 28px 30px;
|
|
673
|
-
background:
|
|
674
|
-
linear-gradient(135deg, rgba(255, 249, 240, 0.92), rgba(247, 238, 224, 0.9)),
|
|
675
|
-
linear-gradient(120deg, rgba(184, 106, 18, 0.08), transparent 60%);
|
|
676
|
-
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
677
|
-
border-radius: 28px;
|
|
678
|
-
box-shadow: var(--shadow);
|
|
679
|
-
margin-bottom: 18px;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
.hero-kicker {
|
|
683
|
-
display: inline-flex;
|
|
684
|
-
align-items: center;
|
|
685
|
-
padding: 6px 10px;
|
|
686
|
-
border-radius: 999px;
|
|
687
|
-
background: rgba(30, 42, 58, 0.08);
|
|
688
|
-
color: var(--muted);
|
|
689
|
-
font-size: 12px;
|
|
690
|
-
letter-spacing: 0.08em;
|
|
691
|
-
text-transform: uppercase;
|
|
692
|
-
margin-bottom: 14px;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
.logo {
|
|
696
|
-
gap: 10px;
|
|
697
|
-
font-size: 32px;
|
|
698
|
-
font-weight: 700;
|
|
699
|
-
letter-spacing: -0.03em;
|
|
700
|
-
color: var(--ink);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
.logo-badge {
|
|
704
|
-
display: inline-flex;
|
|
705
|
-
align-items: center;
|
|
706
|
-
padding: 4px 10px;
|
|
707
|
-
border-radius: 999px;
|
|
708
|
-
background: var(--accent-soft);
|
|
709
|
-
color: var(--accent-strong);
|
|
710
|
-
font-size: 12px;
|
|
711
|
-
font-weight: 700;
|
|
712
|
-
letter-spacing: 0.08em;
|
|
713
|
-
text-transform: uppercase;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
.hero-subtitle {
|
|
717
|
-
margin-top: 12px;
|
|
718
|
-
max-width: 720px;
|
|
719
|
-
color: var(--muted);
|
|
720
|
-
font-size: 15px;
|
|
721
|
-
line-height: 1.6;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
.hero-side {
|
|
725
|
-
display: flex;
|
|
726
|
-
flex-direction: column;
|
|
727
|
-
align-items: flex-end;
|
|
728
|
-
gap: 12px;
|
|
729
|
-
min-width: 190px;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
.hero-link,
|
|
733
|
-
.hero-link:visited {
|
|
734
|
-
color: var(--ink);
|
|
735
|
-
text-decoration: none;
|
|
736
|
-
font-size: 13px;
|
|
737
|
-
font-weight: 600;
|
|
738
|
-
padding: 10px 14px;
|
|
739
|
-
border: 1px solid var(--line);
|
|
740
|
-
border-radius: 999px;
|
|
741
|
-
background: rgba(255, 255, 255, 0.72);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
.hero-link:hover {
|
|
745
|
-
border-color: var(--accent);
|
|
746
|
-
color: var(--accent-strong);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
.status-pill {
|
|
750
|
-
display: inline-flex;
|
|
751
|
-
align-items: center;
|
|
752
|
-
gap: 8px;
|
|
753
|
-
padding: 10px 14px;
|
|
754
|
-
border-radius: 999px;
|
|
755
|
-
background: rgba(255, 255, 255, 0.78);
|
|
756
|
-
border: 1px solid var(--line);
|
|
757
|
-
color: var(--muted);
|
|
758
|
-
font-size: 13px;
|
|
759
|
-
font-weight: 600;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
.status-dot.connected { background: var(--success); }
|
|
763
|
-
.status-dot.disconnected { background: var(--danger); }
|
|
764
|
-
.status-dot.connecting { background: var(--warning); }
|
|
765
|
-
|
|
766
|
-
.overview {
|
|
767
|
-
display: grid;
|
|
768
|
-
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
769
|
-
gap: 14px;
|
|
770
|
-
margin-bottom: 18px;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
.metric-card {
|
|
774
|
-
padding: 16px 18px;
|
|
775
|
-
background: var(--bg-soft);
|
|
776
|
-
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
777
|
-
border-radius: 18px;
|
|
778
|
-
box-shadow: 0 8px 24px rgba(49, 39, 23, 0.05);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
.metric-label {
|
|
782
|
-
display: block;
|
|
783
|
-
font-size: 12px;
|
|
784
|
-
font-weight: 700;
|
|
785
|
-
letter-spacing: 0.08em;
|
|
786
|
-
text-transform: uppercase;
|
|
787
|
-
color: var(--muted);
|
|
788
|
-
margin-bottom: 10px;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
.metric-value {
|
|
792
|
-
font-size: 28px;
|
|
793
|
-
line-height: 1;
|
|
794
|
-
letter-spacing: -0.04em;
|
|
795
|
-
color: var(--ink);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
.tabs {
|
|
799
|
-
gap: 10px;
|
|
800
|
-
flex-wrap: wrap;
|
|
801
|
-
background: transparent;
|
|
802
|
-
border-bottom: none;
|
|
803
|
-
padding: 0 0 16px 0;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
.tab {
|
|
807
|
-
padding: 10px 14px;
|
|
808
|
-
border: 1px solid transparent;
|
|
809
|
-
border-radius: 999px;
|
|
810
|
-
color: var(--muted);
|
|
811
|
-
font-weight: 600;
|
|
812
|
-
background: rgba(255, 252, 246, 0.5);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
.tab:hover {
|
|
816
|
-
color: var(--ink);
|
|
817
|
-
background: rgba(255, 255, 255, 0.85);
|
|
818
|
-
border-color: var(--line);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
.tab.active {
|
|
822
|
-
color: var(--accent-strong);
|
|
823
|
-
border-bottom-color: transparent;
|
|
824
|
-
border-color: rgba(184, 106, 18, 0.24);
|
|
825
|
-
background: rgba(184, 106, 18, 0.14);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
.panels {
|
|
829
|
-
padding: 0;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
.panel {
|
|
833
|
-
display: none;
|
|
834
|
-
background: rgba(255, 253, 250, 0.88);
|
|
835
|
-
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
836
|
-
border-radius: 24px;
|
|
837
|
-
padding: 24px;
|
|
838
|
-
box-shadow: var(--shadow);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
.panel.active {
|
|
842
|
-
display: block;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
.panel-header {
|
|
846
|
-
align-items: flex-start;
|
|
847
|
-
margin-bottom: 18px;
|
|
848
|
-
gap: 12px;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
.panel-header h2 {
|
|
852
|
-
font-size: 22px;
|
|
853
|
-
letter-spacing: -0.03em;
|
|
854
|
-
color: var(--ink);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
.panel-subtitle {
|
|
858
|
-
margin-top: 6px;
|
|
859
|
-
font-size: 14px;
|
|
860
|
-
line-height: 1.5;
|
|
861
|
-
color: var(--muted);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
.panel-actions {
|
|
865
|
-
display: flex;
|
|
866
|
-
gap: 8px;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
.btn-sm {
|
|
870
|
-
padding: 8px 14px;
|
|
871
|
-
background: rgba(255, 255, 255, 0.9);
|
|
872
|
-
border: 1px solid var(--line);
|
|
873
|
-
border-radius: 999px;
|
|
874
|
-
color: var(--ink);
|
|
875
|
-
font-size: 12px;
|
|
876
|
-
font-weight: 700;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
.btn-sm:hover {
|
|
880
|
-
background: rgba(184, 106, 18, 0.12);
|
|
881
|
-
border-color: rgba(184, 106, 18, 0.28);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
.summary {
|
|
885
|
-
gap: 8px;
|
|
886
|
-
flex-wrap: wrap;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
.summary-item {
|
|
890
|
-
padding: 8px 12px;
|
|
891
|
-
border-radius: 999px;
|
|
892
|
-
background: rgba(184, 106, 18, 0.08);
|
|
893
|
-
color: var(--accent-strong);
|
|
894
|
-
font-weight: 600;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
.summary-count {
|
|
898
|
-
color: var(--ink);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
.activity-list,
|
|
902
|
-
.routes-list,
|
|
903
|
-
.violations-list,
|
|
904
|
-
.preview-list,
|
|
905
|
-
.contracts-list,
|
|
906
|
-
.contracts-detail,
|
|
907
|
-
.preview-diff {
|
|
908
|
-
background: rgba(255, 255, 255, 0.72);
|
|
909
|
-
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
910
|
-
border-radius: 18px;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
.activity-list,
|
|
914
|
-
.routes-list,
|
|
915
|
-
.violations-list,
|
|
916
|
-
.preview-list {
|
|
917
|
-
overflow: hidden;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
.activity-list {
|
|
921
|
-
max-height: calc(100vh - 320px);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
.activity-item,
|
|
925
|
-
.route-item,
|
|
926
|
-
.change-item,
|
|
927
|
-
.contract-item {
|
|
928
|
-
border-bottom: 1px solid rgba(220, 207, 186, 0.72);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
.activity-item:last-child,
|
|
932
|
-
.route-item:last-child,
|
|
933
|
-
.change-item:last-child,
|
|
934
|
-
.contract-item:last-child {
|
|
935
|
-
border-bottom: none;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
.activity-item {
|
|
939
|
-
padding: 12px 14px;
|
|
940
|
-
font-size: 12px;
|
|
941
|
-
color: var(--ink);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
.activity-time {
|
|
945
|
-
color: var(--muted);
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
.activity-tool {
|
|
949
|
-
color: var(--accent-strong);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
.activity-detail {
|
|
953
|
-
color: var(--ink);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
.route-item {
|
|
957
|
-
padding: 14px 16px;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
.route-kind.page {
|
|
961
|
-
background: rgba(46, 102, 184, 0.12);
|
|
962
|
-
color: var(--info);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
.route-kind.api {
|
|
966
|
-
background: rgba(23, 127, 86, 0.12);
|
|
967
|
-
color: var(--success);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
.route-pattern {
|
|
971
|
-
color: var(--ink);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
.badge {
|
|
975
|
-
background: rgba(30, 42, 58, 0.08);
|
|
976
|
-
color: var(--muted);
|
|
977
|
-
border: 1px solid rgba(220, 207, 186, 0.72);
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
.guard-status {
|
|
981
|
-
margin-bottom: 12px;
|
|
982
|
-
color: var(--muted);
|
|
983
|
-
font-size: 14px;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
.guard-summary {
|
|
987
|
-
display: grid;
|
|
988
|
-
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
989
|
-
gap: 12px;
|
|
990
|
-
padding: 0;
|
|
991
|
-
background: transparent;
|
|
992
|
-
margin-bottom: 16px;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
.guard-stat {
|
|
996
|
-
padding: 16px;
|
|
997
|
-
border-radius: 18px;
|
|
998
|
-
background: rgba(255, 255, 255, 0.72);
|
|
999
|
-
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
.guard-stat-value {
|
|
1003
|
-
font-size: 28px;
|
|
1004
|
-
font-weight: 700;
|
|
1005
|
-
letter-spacing: -0.04em;
|
|
1006
|
-
color: var(--ink);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
.guard-stat-label {
|
|
1010
|
-
color: var(--muted);
|
|
1011
|
-
font-size: 12px;
|
|
1012
|
-
letter-spacing: 0.08em;
|
|
1013
|
-
text-transform: uppercase;
|
|
1014
|
-
margin-top: 8px;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
.sev-error { color: var(--danger); }
|
|
1018
|
-
.sev-warning { color: var(--warning); }
|
|
1019
|
-
.sev-info { color: var(--info); }
|
|
1020
|
-
|
|
1021
|
-
.violation-item {
|
|
1022
|
-
padding: 14px 16px;
|
|
1023
|
-
border-bottom: 1px solid rgba(220, 207, 186, 0.72);
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
.violation-item:last-child {
|
|
1027
|
-
border-bottom: none;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
.violation-file {
|
|
1031
|
-
color: var(--ink);
|
|
1032
|
-
font-weight: 600;
|
|
1033
|
-
margin-bottom: 4px;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
.violation-msg {
|
|
1037
|
-
color: var(--muted);
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
.preview-list,
|
|
1041
|
-
.preview-diff {
|
|
1042
|
-
margin-bottom: 16px;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
.change-item {
|
|
1046
|
-
padding: 14px 16px;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
.change-item:hover,
|
|
1050
|
-
.contract-item:hover {
|
|
1051
|
-
background: rgba(184, 106, 18, 0.06);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
.change-icon {
|
|
1055
|
-
color: var(--accent-strong);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
.change-path {
|
|
1059
|
-
color: var(--ink);
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
.change-status {
|
|
1063
|
-
font-weight: 700;
|
|
1064
|
-
text-transform: uppercase;
|
|
1065
|
-
font-size: 11px;
|
|
1066
|
-
letter-spacing: 0.08em;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
.contracts-layout {
|
|
1070
|
-
grid-template-columns: minmax(280px, 0.9fr) minmax(420px, 1.6fr);
|
|
1071
|
-
gap: 16px;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
.contracts-list,
|
|
1075
|
-
.contracts-detail {
|
|
1076
|
-
padding: 8px;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
.contract-item {
|
|
1080
|
-
padding: 14px 14px;
|
|
1081
|
-
border-radius: 14px;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
.contract-item.selected {
|
|
1085
|
-
background: rgba(184, 106, 18, 0.12);
|
|
1086
|
-
border-color: rgba(184, 106, 18, 0.28);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
.method-badge {
|
|
1090
|
-
border-radius: 999px;
|
|
1091
|
-
padding: 4px 8px;
|
|
1092
|
-
font-size: 10px;
|
|
1093
|
-
font-weight: 800;
|
|
1094
|
-
letter-spacing: 0.08em;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
.contract-schema,
|
|
1098
|
-
.contract-playground {
|
|
1099
|
-
background: rgba(255, 255, 255, 0.78);
|
|
1100
|
-
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
1101
|
-
border-radius: 16px;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
.contract-schema {
|
|
1105
|
-
padding: 16px;
|
|
1106
|
-
max-height: 420px;
|
|
1107
|
-
overflow: auto;
|
|
1108
|
-
color: var(--ink);
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
.contract-playground {
|
|
1112
|
-
margin-top: 14px;
|
|
1113
|
-
padding: 16px;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
.playground-inputs label {
|
|
1117
|
-
color: var(--muted);
|
|
1118
|
-
font-size: 13px;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
textarea,
|
|
1122
|
-
.select-sm {
|
|
1123
|
-
background: rgba(255, 252, 246, 0.92);
|
|
1124
|
-
border: 1px solid var(--line);
|
|
1125
|
-
color: var(--ink);
|
|
1126
|
-
border-radius: 12px;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
textarea {
|
|
1130
|
-
width: 100%;
|
|
1131
|
-
margin-top: 6px;
|
|
1132
|
-
padding: 10px 12px;
|
|
1133
|
-
resize: vertical;
|
|
1134
|
-
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
.select-sm {
|
|
1138
|
-
padding: 8px 12px;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
.validate-result {
|
|
1142
|
-
margin-top: 12px;
|
|
1143
|
-
border-radius: 12px;
|
|
1144
|
-
padding: 12px 14px;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
.validate-result.success {
|
|
1148
|
-
background: rgba(23, 127, 86, 0.12);
|
|
1149
|
-
color: var(--success);
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
.validate-result.error {
|
|
1153
|
-
background: rgba(188, 61, 61, 0.12);
|
|
1154
|
-
color: var(--danger);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
.diff-header,
|
|
1158
|
-
.diff-hunk-header {
|
|
1159
|
-
background: transparent;
|
|
1160
|
-
color: var(--ink);
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
.diff-line-num {
|
|
1164
|
-
color: var(--muted);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
.diff-line.add {
|
|
1168
|
-
background: rgba(23, 127, 86, 0.08);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
.diff-line.remove {
|
|
1172
|
-
background: rgba(188, 61, 61, 0.08);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
.empty-state {
|
|
1176
|
-
padding: 48px 20px;
|
|
1177
|
-
color: var(--muted);
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
.debug-bar {
|
|
1181
|
-
position: sticky;
|
|
1182
|
-
bottom: 0;
|
|
1183
|
-
margin-top: 16px;
|
|
1184
|
-
border-radius: 16px;
|
|
1185
|
-
background: rgba(30, 42, 58, 0.94);
|
|
1186
|
-
border: 1px solid rgba(30, 42, 58, 0.94);
|
|
1187
|
-
color: rgba(255, 255, 255, 0.76);
|
|
1188
|
-
box-shadow: 0 12px 28px rgba(30, 42, 58, 0.22);
|
|
1189
|
-
}
|
|
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
|
-
|
|
1299
|
-
@media (max-width: 1040px) {
|
|
1300
|
-
body {
|
|
1301
|
-
padding: 18px;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
.hero {
|
|
1305
|
-
flex-direction: column;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
.hero-side {
|
|
1309
|
-
align-items: flex-start;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
.overview {
|
|
1313
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
.contracts-layout {
|
|
1317
|
-
grid-template-columns: 1fr;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
.guard-summary {
|
|
1321
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
@media (max-width: 720px) {
|
|
1326
|
-
body {
|
|
1327
|
-
padding: 12px;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
.hero,
|
|
1331
|
-
.panel {
|
|
1332
|
-
padding: 18px;
|
|
1333
|
-
border-radius: 20px;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
.overview {
|
|
1337
|
-
grid-template-columns: 1fr;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
.tabs {
|
|
1341
|
-
gap: 8px;
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
.tab {
|
|
1345
|
-
width: calc(50% - 4px);
|
|
1346
|
-
justify-content: center;
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
.panel-header {
|
|
1350
|
-
flex-direction: column;
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
`;
|
|
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
|
-
|
|
1370
|
-
function escapeHtml(str) {
|
|
1371
|
-
if (!str) return '';
|
|
1372
|
-
var d = document.createElement('div');
|
|
1373
|
-
d.appendChild(document.createTextNode(String(str)));
|
|
1374
|
-
return d.innerHTML;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function setMetric(id, value) {
|
|
1378
|
-
var el = document.getElementById(id);
|
|
1379
|
-
if (!el) return;
|
|
1380
|
-
el.textContent = String(value);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
try { log('JS loaded', 'ok'); } catch(e) {}
|
|
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
|
-
|
|
1455
|
-
function appendActivity(data) {
|
|
1456
|
-
if (activityCount === 0) {
|
|
1457
|
-
activityList.innerHTML = '';
|
|
1458
|
-
}
|
|
1459
|
-
activityCount++;
|
|
1460
|
-
setMetric('metric-activity', activityCount);
|
|
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
|
-
|
|
1482
|
-
document.getElementById('clear-activity').addEventListener('click', function() {
|
|
1483
|
-
activityList.innerHTML = '<div class="empty-state">Waiting for MCP activity...</div>';
|
|
1484
|
-
activityCount = 0;
|
|
1485
|
-
setMetric('metric-activity', 0);
|
|
1486
|
-
});
|
|
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
|
-
|
|
1518
|
-
function renderRoutes(data) {
|
|
1519
|
-
var summaryEl = document.getElementById('routes-summary');
|
|
1520
|
-
var listEl = document.getElementById('routes-list');
|
|
1521
|
-
var s = data.summary;
|
|
1522
|
-
setMetric('metric-routes', s.total);
|
|
1523
|
-
|
|
1524
|
-
summaryEl.innerHTML =
|
|
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
|
-
|
|
1612
|
-
function renderGuardData(data) {
|
|
1613
|
-
if (!data.enabled) {
|
|
1614
|
-
guardStatusEl.textContent = 'Guard is not configured for this project.';
|
|
1615
|
-
guardListEl.innerHTML = '';
|
|
1616
|
-
setMetric('metric-guard', 'off');
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
guardStatusEl.innerHTML = 'Preset: <strong>' + escapeHtml(data.preset) + '</strong>';
|
|
1621
|
-
|
|
1622
|
-
if (!data.report) {
|
|
1623
|
-
guardListEl.innerHTML = '<div class="empty-state">No scan results yet. Click "Scan" to check.</div>';
|
|
1624
|
-
setMetric('metric-guard', 'ready');
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
var r = data.report;
|
|
1629
|
-
setMetric('metric-guard', r.totalViolations);
|
|
1630
|
-
var summaryHtml = '<div class="guard-summary">' +
|
|
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
|
-
|
|
1685
|
-
function renderFileChanges(changes) {
|
|
1686
|
-
setMetric('metric-changes', changes.length);
|
|
1687
|
-
if (!changes.length) {
|
|
1688
|
-
previewListEl.innerHTML = '<div class="empty-state">No file changes detected.</div>';
|
|
1689
|
-
return;
|
|
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
|
-
|
|
1796
|
-
function renderContractsList(contracts) {
|
|
1797
|
-
setMetric('metric-contracts', contracts.length);
|
|
1798
|
-
if (!contracts.length) {
|
|
1799
|
-
contractsListEl.innerHTML = '<div class="empty-state">No contracts found.</div>';
|
|
1800
|
-
return;
|
|
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
|
-
`;
|
|
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
|
+
<body>
|
|
18
|
+
<div class="app-shell">
|
|
19
|
+
<header class="hero">
|
|
20
|
+
<div class="hero-copy">
|
|
21
|
+
<div class="hero-kicker">Mandu Dev Console</div>
|
|
22
|
+
<div class="logo">
|
|
23
|
+
<span class="logo-text">Mandu Kitchen</span>
|
|
24
|
+
<span class="logo-badge">live</span>
|
|
25
|
+
</div>
|
|
26
|
+
<p class="hero-subtitle">Routes, architecture, live activity, file changes, and contract checks for the current dev session.</p>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="hero-side">
|
|
29
|
+
<a class="hero-link" href="/" target="_blank" rel="noreferrer">Open app</a>
|
|
30
|
+
<div class="status-pill">
|
|
31
|
+
<span id="sse-status" class="status-dot disconnected"></span>
|
|
32
|
+
<span id="sse-label">Connecting...</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
<section class="overview">
|
|
38
|
+
<div class="metric-card">
|
|
39
|
+
<span class="metric-label">Activity</span>
|
|
40
|
+
<strong id="metric-activity" class="metric-value">0</strong>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="metric-card">
|
|
43
|
+
<span class="metric-label">Routes</span>
|
|
44
|
+
<strong id="metric-routes" class="metric-value">...</strong>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="metric-card">
|
|
47
|
+
<span class="metric-label">Guard</span>
|
|
48
|
+
<strong id="metric-guard" class="metric-value">...</strong>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="metric-card">
|
|
51
|
+
<span class="metric-label">Changes</span>
|
|
52
|
+
<strong id="metric-changes" class="metric-value">...</strong>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="metric-card">
|
|
55
|
+
<span class="metric-label">Contracts</span>
|
|
56
|
+
<strong id="metric-contracts" class="metric-value">...</strong>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<nav class="tabs">
|
|
61
|
+
<button class="tab" data-panel="activity">Activity</button>
|
|
62
|
+
<button class="tab active" data-panel="routes">Routes</button>
|
|
63
|
+
<button class="tab" data-panel="guard">Guard</button>
|
|
64
|
+
<button class="tab" data-panel="preview">Preview</button>
|
|
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>
|
|
70
|
+
</nav>
|
|
71
|
+
|
|
72
|
+
<main class="panels">
|
|
73
|
+
<section id="panel-activity" class="panel">
|
|
74
|
+
<div class="panel-header">
|
|
75
|
+
<div>
|
|
76
|
+
<h2>Activity Stream</h2>
|
|
77
|
+
<p class="panel-subtitle">Recent Kitchen events and MCP activity.</p>
|
|
78
|
+
</div>
|
|
79
|
+
<button id="clear-activity" class="btn-sm">Clear</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div id="activity-list" class="activity-list">
|
|
82
|
+
<div class="empty-state">Waiting for MCP activity...</div>
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<section id="panel-routes" class="panel active">
|
|
87
|
+
<div class="panel-header">
|
|
88
|
+
<div>
|
|
89
|
+
<h2>Routes</h2>
|
|
90
|
+
<p class="panel-subtitle">Current filesystem routes, slots, contracts, and hydration hints.</p>
|
|
91
|
+
</div>
|
|
92
|
+
<div id="routes-summary" class="summary"></div>
|
|
93
|
+
</div>
|
|
94
|
+
<div id="routes-list" class="routes-list">
|
|
95
|
+
<div class="empty-state">Loading routes...</div>
|
|
96
|
+
</div>
|
|
97
|
+
</section>
|
|
98
|
+
|
|
99
|
+
<section id="panel-guard" class="panel">
|
|
100
|
+
<div class="panel-header">
|
|
101
|
+
<div>
|
|
102
|
+
<h2>Architecture Guard</h2>
|
|
103
|
+
<p class="panel-subtitle">Run a scan and inspect dependency rule violations.</p>
|
|
104
|
+
</div>
|
|
105
|
+
<button id="scan-guard" class="btn-sm">Scan</button>
|
|
106
|
+
</div>
|
|
107
|
+
<div id="guard-status" class="guard-status"></div>
|
|
108
|
+
<div id="guard-list" class="violations-list">
|
|
109
|
+
<div class="empty-state">Click "Scan" to check architecture rules.</div>
|
|
110
|
+
</div>
|
|
111
|
+
</section>
|
|
112
|
+
|
|
113
|
+
<section id="panel-preview" class="panel">
|
|
114
|
+
<div class="panel-header">
|
|
115
|
+
<div>
|
|
116
|
+
<h2>Preview</h2>
|
|
117
|
+
<p class="panel-subtitle">Inspect changed files and open diffs without leaving Kitchen.</p>
|
|
118
|
+
</div>
|
|
119
|
+
<button id="refresh-changes" class="btn-sm">Refresh</button>
|
|
120
|
+
</div>
|
|
121
|
+
<div id="preview-list" class="preview-list">
|
|
122
|
+
<div class="empty-state">Loading file changes...</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div id="preview-diff" class="preview-diff" style="display:none;"></div>
|
|
125
|
+
</section>
|
|
126
|
+
|
|
127
|
+
<section id="panel-contracts" class="panel">
|
|
128
|
+
<div class="panel-header">
|
|
129
|
+
<div>
|
|
130
|
+
<h2>Contracts</h2>
|
|
131
|
+
<p class="panel-subtitle">Browse route contracts and validate payloads in place.</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="panel-actions">
|
|
134
|
+
<button id="export-openapi-json" class="btn-sm">Export JSON</button>
|
|
135
|
+
<button id="export-openapi-yaml" class="btn-sm">Export YAML</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="contracts-layout">
|
|
139
|
+
<div id="contracts-list" class="contracts-list">
|
|
140
|
+
<div class="empty-state">Loading contracts...</div>
|
|
141
|
+
</div>
|
|
142
|
+
<div id="contracts-detail" class="contracts-detail">
|
|
143
|
+
<div id="contract-schema" class="contract-schema"></div>
|
|
144
|
+
<div id="contract-playground" class="contract-playground">
|
|
145
|
+
<h3>Validate</h3>
|
|
146
|
+
<div class="playground-controls">
|
|
147
|
+
<select id="validate-method" class="select-sm">
|
|
148
|
+
<option>GET</option><option>POST</option><option>PUT</option><option>PATCH</option><option>DELETE</option>
|
|
149
|
+
</select>
|
|
150
|
+
<button id="validate-btn" class="btn-sm">Validate</button>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="playground-inputs">
|
|
153
|
+
<label>Query <textarea id="validate-query" rows="2" placeholder='{"key":"value"}'></textarea></label>
|
|
154
|
+
<label>Body <textarea id="validate-body" rows="3" placeholder='{"key":"value"}'></textarea></label>
|
|
155
|
+
<label>Params <textarea id="validate-params" rows="2" placeholder='{"id":"1"}'></textarea></label>
|
|
156
|
+
</div>
|
|
157
|
+
<div id="validate-result" class="validate-result"></div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
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>
|
|
215
|
+
</main>
|
|
216
|
+
|
|
217
|
+
<div id="debug-bar" class="debug-bar"></div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<script>${JS}</script>
|
|
221
|
+
</body>
|
|
222
|
+
</html>`;
|
|
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
|
+
|
|
630
|
+
.debug-bar .err { color: #ef4444; }
|
|
631
|
+
.debug-bar .ok { color: #22c55e; }
|
|
632
|
+
|
|
633
|
+
:root {
|
|
634
|
+
--bg: #f4efe6;
|
|
635
|
+
--bg-soft: rgba(255, 252, 246, 0.7);
|
|
636
|
+
--surface: #fffdfa;
|
|
637
|
+
--surface-strong: #f8f1e4;
|
|
638
|
+
--surface-alt: #f0e5d2;
|
|
639
|
+
--ink: #1e2a3a;
|
|
640
|
+
--muted: #677181;
|
|
641
|
+
--line: #dccfba;
|
|
642
|
+
--accent: #b86a12;
|
|
643
|
+
--accent-strong: #8d4f0e;
|
|
644
|
+
--accent-soft: rgba(184, 106, 18, 0.14);
|
|
645
|
+
--success: #177f56;
|
|
646
|
+
--danger: #bc3d3d;
|
|
647
|
+
--info: #2e66b8;
|
|
648
|
+
--warning: #ad7a12;
|
|
649
|
+
--shadow: 0 20px 50px rgba(49, 39, 23, 0.08);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
body {
|
|
653
|
+
font-family: "IBM Plex Sans", "Segoe UI Variable", "Segoe UI", sans-serif;
|
|
654
|
+
background:
|
|
655
|
+
radial-gradient(circle at top left, rgba(226, 186, 124, 0.35), transparent 30%),
|
|
656
|
+
radial-gradient(circle at top right, rgba(152, 191, 193, 0.22), transparent 24%),
|
|
657
|
+
linear-gradient(180deg, #f8f3ea 0%, #efe7d8 100%);
|
|
658
|
+
color: var(--ink);
|
|
659
|
+
padding: 24px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.app-shell {
|
|
663
|
+
width: min(1360px, 100%);
|
|
664
|
+
margin: 0 auto;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.hero {
|
|
668
|
+
display: flex;
|
|
669
|
+
align-items: flex-start;
|
|
670
|
+
justify-content: space-between;
|
|
671
|
+
gap: 24px;
|
|
672
|
+
padding: 28px 30px;
|
|
673
|
+
background:
|
|
674
|
+
linear-gradient(135deg, rgba(255, 249, 240, 0.92), rgba(247, 238, 224, 0.9)),
|
|
675
|
+
linear-gradient(120deg, rgba(184, 106, 18, 0.08), transparent 60%);
|
|
676
|
+
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
677
|
+
border-radius: 28px;
|
|
678
|
+
box-shadow: var(--shadow);
|
|
679
|
+
margin-bottom: 18px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.hero-kicker {
|
|
683
|
+
display: inline-flex;
|
|
684
|
+
align-items: center;
|
|
685
|
+
padding: 6px 10px;
|
|
686
|
+
border-radius: 999px;
|
|
687
|
+
background: rgba(30, 42, 58, 0.08);
|
|
688
|
+
color: var(--muted);
|
|
689
|
+
font-size: 12px;
|
|
690
|
+
letter-spacing: 0.08em;
|
|
691
|
+
text-transform: uppercase;
|
|
692
|
+
margin-bottom: 14px;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.logo {
|
|
696
|
+
gap: 10px;
|
|
697
|
+
font-size: 32px;
|
|
698
|
+
font-weight: 700;
|
|
699
|
+
letter-spacing: -0.03em;
|
|
700
|
+
color: var(--ink);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.logo-badge {
|
|
704
|
+
display: inline-flex;
|
|
705
|
+
align-items: center;
|
|
706
|
+
padding: 4px 10px;
|
|
707
|
+
border-radius: 999px;
|
|
708
|
+
background: var(--accent-soft);
|
|
709
|
+
color: var(--accent-strong);
|
|
710
|
+
font-size: 12px;
|
|
711
|
+
font-weight: 700;
|
|
712
|
+
letter-spacing: 0.08em;
|
|
713
|
+
text-transform: uppercase;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.hero-subtitle {
|
|
717
|
+
margin-top: 12px;
|
|
718
|
+
max-width: 720px;
|
|
719
|
+
color: var(--muted);
|
|
720
|
+
font-size: 15px;
|
|
721
|
+
line-height: 1.6;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.hero-side {
|
|
725
|
+
display: flex;
|
|
726
|
+
flex-direction: column;
|
|
727
|
+
align-items: flex-end;
|
|
728
|
+
gap: 12px;
|
|
729
|
+
min-width: 190px;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.hero-link,
|
|
733
|
+
.hero-link:visited {
|
|
734
|
+
color: var(--ink);
|
|
735
|
+
text-decoration: none;
|
|
736
|
+
font-size: 13px;
|
|
737
|
+
font-weight: 600;
|
|
738
|
+
padding: 10px 14px;
|
|
739
|
+
border: 1px solid var(--line);
|
|
740
|
+
border-radius: 999px;
|
|
741
|
+
background: rgba(255, 255, 255, 0.72);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.hero-link:hover {
|
|
745
|
+
border-color: var(--accent);
|
|
746
|
+
color: var(--accent-strong);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.status-pill {
|
|
750
|
+
display: inline-flex;
|
|
751
|
+
align-items: center;
|
|
752
|
+
gap: 8px;
|
|
753
|
+
padding: 10px 14px;
|
|
754
|
+
border-radius: 999px;
|
|
755
|
+
background: rgba(255, 255, 255, 0.78);
|
|
756
|
+
border: 1px solid var(--line);
|
|
757
|
+
color: var(--muted);
|
|
758
|
+
font-size: 13px;
|
|
759
|
+
font-weight: 600;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.status-dot.connected { background: var(--success); }
|
|
763
|
+
.status-dot.disconnected { background: var(--danger); }
|
|
764
|
+
.status-dot.connecting { background: var(--warning); }
|
|
765
|
+
|
|
766
|
+
.overview {
|
|
767
|
+
display: grid;
|
|
768
|
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
769
|
+
gap: 14px;
|
|
770
|
+
margin-bottom: 18px;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.metric-card {
|
|
774
|
+
padding: 16px 18px;
|
|
775
|
+
background: var(--bg-soft);
|
|
776
|
+
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
777
|
+
border-radius: 18px;
|
|
778
|
+
box-shadow: 0 8px 24px rgba(49, 39, 23, 0.05);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.metric-label {
|
|
782
|
+
display: block;
|
|
783
|
+
font-size: 12px;
|
|
784
|
+
font-weight: 700;
|
|
785
|
+
letter-spacing: 0.08em;
|
|
786
|
+
text-transform: uppercase;
|
|
787
|
+
color: var(--muted);
|
|
788
|
+
margin-bottom: 10px;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.metric-value {
|
|
792
|
+
font-size: 28px;
|
|
793
|
+
line-height: 1;
|
|
794
|
+
letter-spacing: -0.04em;
|
|
795
|
+
color: var(--ink);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.tabs {
|
|
799
|
+
gap: 10px;
|
|
800
|
+
flex-wrap: wrap;
|
|
801
|
+
background: transparent;
|
|
802
|
+
border-bottom: none;
|
|
803
|
+
padding: 0 0 16px 0;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.tab {
|
|
807
|
+
padding: 10px 14px;
|
|
808
|
+
border: 1px solid transparent;
|
|
809
|
+
border-radius: 999px;
|
|
810
|
+
color: var(--muted);
|
|
811
|
+
font-weight: 600;
|
|
812
|
+
background: rgba(255, 252, 246, 0.5);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.tab:hover {
|
|
816
|
+
color: var(--ink);
|
|
817
|
+
background: rgba(255, 255, 255, 0.85);
|
|
818
|
+
border-color: var(--line);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.tab.active {
|
|
822
|
+
color: var(--accent-strong);
|
|
823
|
+
border-bottom-color: transparent;
|
|
824
|
+
border-color: rgba(184, 106, 18, 0.24);
|
|
825
|
+
background: rgba(184, 106, 18, 0.14);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.panels {
|
|
829
|
+
padding: 0;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.panel {
|
|
833
|
+
display: none;
|
|
834
|
+
background: rgba(255, 253, 250, 0.88);
|
|
835
|
+
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
836
|
+
border-radius: 24px;
|
|
837
|
+
padding: 24px;
|
|
838
|
+
box-shadow: var(--shadow);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.panel.active {
|
|
842
|
+
display: block;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.panel-header {
|
|
846
|
+
align-items: flex-start;
|
|
847
|
+
margin-bottom: 18px;
|
|
848
|
+
gap: 12px;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.panel-header h2 {
|
|
852
|
+
font-size: 22px;
|
|
853
|
+
letter-spacing: -0.03em;
|
|
854
|
+
color: var(--ink);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.panel-subtitle {
|
|
858
|
+
margin-top: 6px;
|
|
859
|
+
font-size: 14px;
|
|
860
|
+
line-height: 1.5;
|
|
861
|
+
color: var(--muted);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.panel-actions {
|
|
865
|
+
display: flex;
|
|
866
|
+
gap: 8px;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.btn-sm {
|
|
870
|
+
padding: 8px 14px;
|
|
871
|
+
background: rgba(255, 255, 255, 0.9);
|
|
872
|
+
border: 1px solid var(--line);
|
|
873
|
+
border-radius: 999px;
|
|
874
|
+
color: var(--ink);
|
|
875
|
+
font-size: 12px;
|
|
876
|
+
font-weight: 700;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.btn-sm:hover {
|
|
880
|
+
background: rgba(184, 106, 18, 0.12);
|
|
881
|
+
border-color: rgba(184, 106, 18, 0.28);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.summary {
|
|
885
|
+
gap: 8px;
|
|
886
|
+
flex-wrap: wrap;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.summary-item {
|
|
890
|
+
padding: 8px 12px;
|
|
891
|
+
border-radius: 999px;
|
|
892
|
+
background: rgba(184, 106, 18, 0.08);
|
|
893
|
+
color: var(--accent-strong);
|
|
894
|
+
font-weight: 600;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
.summary-count {
|
|
898
|
+
color: var(--ink);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.activity-list,
|
|
902
|
+
.routes-list,
|
|
903
|
+
.violations-list,
|
|
904
|
+
.preview-list,
|
|
905
|
+
.contracts-list,
|
|
906
|
+
.contracts-detail,
|
|
907
|
+
.preview-diff {
|
|
908
|
+
background: rgba(255, 255, 255, 0.72);
|
|
909
|
+
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
910
|
+
border-radius: 18px;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.activity-list,
|
|
914
|
+
.routes-list,
|
|
915
|
+
.violations-list,
|
|
916
|
+
.preview-list {
|
|
917
|
+
overflow: hidden;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.activity-list {
|
|
921
|
+
max-height: calc(100vh - 320px);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.activity-item,
|
|
925
|
+
.route-item,
|
|
926
|
+
.change-item,
|
|
927
|
+
.contract-item {
|
|
928
|
+
border-bottom: 1px solid rgba(220, 207, 186, 0.72);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
.activity-item:last-child,
|
|
932
|
+
.route-item:last-child,
|
|
933
|
+
.change-item:last-child,
|
|
934
|
+
.contract-item:last-child {
|
|
935
|
+
border-bottom: none;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.activity-item {
|
|
939
|
+
padding: 12px 14px;
|
|
940
|
+
font-size: 12px;
|
|
941
|
+
color: var(--ink);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.activity-time {
|
|
945
|
+
color: var(--muted);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.activity-tool {
|
|
949
|
+
color: var(--accent-strong);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.activity-detail {
|
|
953
|
+
color: var(--ink);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.route-item {
|
|
957
|
+
padding: 14px 16px;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.route-kind.page {
|
|
961
|
+
background: rgba(46, 102, 184, 0.12);
|
|
962
|
+
color: var(--info);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.route-kind.api {
|
|
966
|
+
background: rgba(23, 127, 86, 0.12);
|
|
967
|
+
color: var(--success);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.route-pattern {
|
|
971
|
+
color: var(--ink);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.badge {
|
|
975
|
+
background: rgba(30, 42, 58, 0.08);
|
|
976
|
+
color: var(--muted);
|
|
977
|
+
border: 1px solid rgba(220, 207, 186, 0.72);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.guard-status {
|
|
981
|
+
margin-bottom: 12px;
|
|
982
|
+
color: var(--muted);
|
|
983
|
+
font-size: 14px;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.guard-summary {
|
|
987
|
+
display: grid;
|
|
988
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
989
|
+
gap: 12px;
|
|
990
|
+
padding: 0;
|
|
991
|
+
background: transparent;
|
|
992
|
+
margin-bottom: 16px;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
.guard-stat {
|
|
996
|
+
padding: 16px;
|
|
997
|
+
border-radius: 18px;
|
|
998
|
+
background: rgba(255, 255, 255, 0.72);
|
|
999
|
+
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.guard-stat-value {
|
|
1003
|
+
font-size: 28px;
|
|
1004
|
+
font-weight: 700;
|
|
1005
|
+
letter-spacing: -0.04em;
|
|
1006
|
+
color: var(--ink);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.guard-stat-label {
|
|
1010
|
+
color: var(--muted);
|
|
1011
|
+
font-size: 12px;
|
|
1012
|
+
letter-spacing: 0.08em;
|
|
1013
|
+
text-transform: uppercase;
|
|
1014
|
+
margin-top: 8px;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.sev-error { color: var(--danger); }
|
|
1018
|
+
.sev-warning { color: var(--warning); }
|
|
1019
|
+
.sev-info { color: var(--info); }
|
|
1020
|
+
|
|
1021
|
+
.violation-item {
|
|
1022
|
+
padding: 14px 16px;
|
|
1023
|
+
border-bottom: 1px solid rgba(220, 207, 186, 0.72);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.violation-item:last-child {
|
|
1027
|
+
border-bottom: none;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
.violation-file {
|
|
1031
|
+
color: var(--ink);
|
|
1032
|
+
font-weight: 600;
|
|
1033
|
+
margin-bottom: 4px;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.violation-msg {
|
|
1037
|
+
color: var(--muted);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.preview-list,
|
|
1041
|
+
.preview-diff {
|
|
1042
|
+
margin-bottom: 16px;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.change-item {
|
|
1046
|
+
padding: 14px 16px;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.change-item:hover,
|
|
1050
|
+
.contract-item:hover {
|
|
1051
|
+
background: rgba(184, 106, 18, 0.06);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.change-icon {
|
|
1055
|
+
color: var(--accent-strong);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.change-path {
|
|
1059
|
+
color: var(--ink);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.change-status {
|
|
1063
|
+
font-weight: 700;
|
|
1064
|
+
text-transform: uppercase;
|
|
1065
|
+
font-size: 11px;
|
|
1066
|
+
letter-spacing: 0.08em;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.contracts-layout {
|
|
1070
|
+
grid-template-columns: minmax(280px, 0.9fr) minmax(420px, 1.6fr);
|
|
1071
|
+
gap: 16px;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.contracts-list,
|
|
1075
|
+
.contracts-detail {
|
|
1076
|
+
padding: 8px;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.contract-item {
|
|
1080
|
+
padding: 14px 14px;
|
|
1081
|
+
border-radius: 14px;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.contract-item.selected {
|
|
1085
|
+
background: rgba(184, 106, 18, 0.12);
|
|
1086
|
+
border-color: rgba(184, 106, 18, 0.28);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.method-badge {
|
|
1090
|
+
border-radius: 999px;
|
|
1091
|
+
padding: 4px 8px;
|
|
1092
|
+
font-size: 10px;
|
|
1093
|
+
font-weight: 800;
|
|
1094
|
+
letter-spacing: 0.08em;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.contract-schema,
|
|
1098
|
+
.contract-playground {
|
|
1099
|
+
background: rgba(255, 255, 255, 0.78);
|
|
1100
|
+
border: 1px solid rgba(220, 207, 186, 0.9);
|
|
1101
|
+
border-radius: 16px;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
.contract-schema {
|
|
1105
|
+
padding: 16px;
|
|
1106
|
+
max-height: 420px;
|
|
1107
|
+
overflow: auto;
|
|
1108
|
+
color: var(--ink);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.contract-playground {
|
|
1112
|
+
margin-top: 14px;
|
|
1113
|
+
padding: 16px;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.playground-inputs label {
|
|
1117
|
+
color: var(--muted);
|
|
1118
|
+
font-size: 13px;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
textarea,
|
|
1122
|
+
.select-sm {
|
|
1123
|
+
background: rgba(255, 252, 246, 0.92);
|
|
1124
|
+
border: 1px solid var(--line);
|
|
1125
|
+
color: var(--ink);
|
|
1126
|
+
border-radius: 12px;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
textarea {
|
|
1130
|
+
width: 100%;
|
|
1131
|
+
margin-top: 6px;
|
|
1132
|
+
padding: 10px 12px;
|
|
1133
|
+
resize: vertical;
|
|
1134
|
+
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.select-sm {
|
|
1138
|
+
padding: 8px 12px;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.validate-result {
|
|
1142
|
+
margin-top: 12px;
|
|
1143
|
+
border-radius: 12px;
|
|
1144
|
+
padding: 12px 14px;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
.validate-result.success {
|
|
1148
|
+
background: rgba(23, 127, 86, 0.12);
|
|
1149
|
+
color: var(--success);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
.validate-result.error {
|
|
1153
|
+
background: rgba(188, 61, 61, 0.12);
|
|
1154
|
+
color: var(--danger);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.diff-header,
|
|
1158
|
+
.diff-hunk-header {
|
|
1159
|
+
background: transparent;
|
|
1160
|
+
color: var(--ink);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.diff-line-num {
|
|
1164
|
+
color: var(--muted);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.diff-line.add {
|
|
1168
|
+
background: rgba(23, 127, 86, 0.08);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.diff-line.remove {
|
|
1172
|
+
background: rgba(188, 61, 61, 0.08);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.empty-state {
|
|
1176
|
+
padding: 48px 20px;
|
|
1177
|
+
color: var(--muted);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.debug-bar {
|
|
1181
|
+
position: sticky;
|
|
1182
|
+
bottom: 0;
|
|
1183
|
+
margin-top: 16px;
|
|
1184
|
+
border-radius: 16px;
|
|
1185
|
+
background: rgba(30, 42, 58, 0.94);
|
|
1186
|
+
border: 1px solid rgba(30, 42, 58, 0.94);
|
|
1187
|
+
color: rgba(255, 255, 255, 0.76);
|
|
1188
|
+
box-shadow: 0 12px 28px rgba(30, 42, 58, 0.22);
|
|
1189
|
+
}
|
|
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
|
+
|
|
1299
|
+
@media (max-width: 1040px) {
|
|
1300
|
+
body {
|
|
1301
|
+
padding: 18px;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
.hero {
|
|
1305
|
+
flex-direction: column;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
.hero-side {
|
|
1309
|
+
align-items: flex-start;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
.overview {
|
|
1313
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
.contracts-layout {
|
|
1317
|
+
grid-template-columns: 1fr;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.guard-summary {
|
|
1321
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
@media (max-width: 720px) {
|
|
1326
|
+
body {
|
|
1327
|
+
padding: 12px;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
.hero,
|
|
1331
|
+
.panel {
|
|
1332
|
+
padding: 18px;
|
|
1333
|
+
border-radius: 20px;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
.overview {
|
|
1337
|
+
grid-template-columns: 1fr;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
.tabs {
|
|
1341
|
+
gap: 8px;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.tab {
|
|
1345
|
+
width: calc(50% - 4px);
|
|
1346
|
+
justify-content: center;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
.panel-header {
|
|
1350
|
+
flex-direction: column;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
`;
|
|
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
|
+
|
|
1370
|
+
function escapeHtml(str) {
|
|
1371
|
+
if (!str) return '';
|
|
1372
|
+
var d = document.createElement('div');
|
|
1373
|
+
d.appendChild(document.createTextNode(String(str)));
|
|
1374
|
+
return d.innerHTML;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function setMetric(id, value) {
|
|
1378
|
+
var el = document.getElementById(id);
|
|
1379
|
+
if (!el) return;
|
|
1380
|
+
el.textContent = String(value);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
try { log('JS loaded', 'ok'); } catch(e) {}
|
|
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
|
+
|
|
1455
|
+
function appendActivity(data) {
|
|
1456
|
+
if (activityCount === 0) {
|
|
1457
|
+
activityList.innerHTML = '';
|
|
1458
|
+
}
|
|
1459
|
+
activityCount++;
|
|
1460
|
+
setMetric('metric-activity', activityCount);
|
|
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
|
+
|
|
1482
|
+
document.getElementById('clear-activity').addEventListener('click', function() {
|
|
1483
|
+
activityList.innerHTML = '<div class="empty-state">Waiting for MCP activity...</div>';
|
|
1484
|
+
activityCount = 0;
|
|
1485
|
+
setMetric('metric-activity', 0);
|
|
1486
|
+
});
|
|
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
|
+
|
|
1518
|
+
function renderRoutes(data) {
|
|
1519
|
+
var summaryEl = document.getElementById('routes-summary');
|
|
1520
|
+
var listEl = document.getElementById('routes-list');
|
|
1521
|
+
var s = data.summary;
|
|
1522
|
+
setMetric('metric-routes', s.total);
|
|
1523
|
+
|
|
1524
|
+
summaryEl.innerHTML =
|
|
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
|
+
|
|
1612
|
+
function renderGuardData(data) {
|
|
1613
|
+
if (!data.enabled) {
|
|
1614
|
+
guardStatusEl.textContent = 'Guard is not configured for this project.';
|
|
1615
|
+
guardListEl.innerHTML = '';
|
|
1616
|
+
setMetric('metric-guard', 'off');
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
guardStatusEl.innerHTML = 'Preset: <strong>' + escapeHtml(data.preset) + '</strong>';
|
|
1621
|
+
|
|
1622
|
+
if (!data.report) {
|
|
1623
|
+
guardListEl.innerHTML = '<div class="empty-state">No scan results yet. Click "Scan" to check.</div>';
|
|
1624
|
+
setMetric('metric-guard', 'ready');
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
var r = data.report;
|
|
1629
|
+
setMetric('metric-guard', r.totalViolations);
|
|
1630
|
+
var summaryHtml = '<div class="guard-summary">' +
|
|
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
|
+
|
|
1685
|
+
function renderFileChanges(changes) {
|
|
1686
|
+
setMetric('metric-changes', changes.length);
|
|
1687
|
+
if (!changes.length) {
|
|
1688
|
+
previewListEl.innerHTML = '<div class="empty-state">No file changes detected.</div>';
|
|
1689
|
+
return;
|
|
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
|
+
|
|
1796
|
+
function renderContractsList(contracts) {
|
|
1797
|
+
setMetric('metric-contracts', contracts.length);
|
|
1798
|
+
if (!contracts.length) {
|
|
1799
|
+
contractsListEl.innerHTML = '<div class="empty-state">No contracts found.</div>';
|
|
1800
|
+
return;
|
|
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
|
+
`;
|