@mandujs/core 0.19.0 → 0.19.2

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.
Files changed (90) hide show
  1. package/README.ko.md +0 -14
  2. package/package.json +4 -1
  3. package/src/brain/architecture/analyzer.ts +4 -4
  4. package/src/brain/doctor/analyzer.ts +18 -14
  5. package/src/bundler/build.test.ts +127 -0
  6. package/src/bundler/build.ts +291 -113
  7. package/src/bundler/css.ts +20 -5
  8. package/src/bundler/dev.ts +55 -2
  9. package/src/bundler/prerender.ts +195 -0
  10. package/src/change/snapshot.ts +4 -23
  11. package/src/change/types.ts +2 -3
  12. package/src/client/Form.tsx +105 -0
  13. package/src/client/__tests__/use-sse.test.ts +153 -0
  14. package/src/client/hooks.ts +105 -6
  15. package/src/client/index.ts +35 -6
  16. package/src/client/router.ts +670 -433
  17. package/src/client/rpc.ts +140 -0
  18. package/src/client/runtime.ts +24 -21
  19. package/src/client/use-fetch.ts +239 -0
  20. package/src/client/use-head.ts +197 -0
  21. package/src/client/use-sse.ts +378 -0
  22. package/src/components/Image.tsx +162 -0
  23. package/src/config/mandu.ts +5 -0
  24. package/src/config/validate.ts +34 -0
  25. package/src/content/index.ts +5 -1
  26. package/src/devtools/client/catchers/error-catcher.ts +17 -0
  27. package/src/devtools/client/catchers/network-proxy.ts +390 -367
  28. package/src/devtools/client/components/kitchen-root.tsx +479 -467
  29. package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
  30. package/src/devtools/client/components/panel/guard-panel.tsx +374 -244
  31. package/src/devtools/client/components/panel/index.ts +45 -32
  32. package/src/devtools/client/components/panel/panel-container.tsx +332 -312
  33. package/src/devtools/client/components/panel/preview-panel.tsx +188 -0
  34. package/src/devtools/client/state-manager.ts +535 -478
  35. package/src/devtools/design-tokens.ts +265 -264
  36. package/src/devtools/types.ts +345 -319
  37. package/src/filling/filling.ts +336 -14
  38. package/src/filling/index.ts +5 -1
  39. package/src/filling/session.ts +216 -0
  40. package/src/filling/ws.ts +78 -0
  41. package/src/generator/generate.ts +2 -2
  42. package/src/guard/auto-correct.ts +0 -29
  43. package/src/guard/check.ts +14 -31
  44. package/src/guard/presets/index.ts +296 -294
  45. package/src/guard/rules.ts +15 -19
  46. package/src/guard/validator.ts +834 -834
  47. package/src/index.ts +5 -1
  48. package/src/island/index.ts +373 -304
  49. package/src/kitchen/api/contract-api.ts +225 -0
  50. package/src/kitchen/api/diff-parser.ts +108 -0
  51. package/src/kitchen/api/file-api.ts +273 -0
  52. package/src/kitchen/api/guard-api.ts +83 -0
  53. package/src/kitchen/api/guard-decisions.ts +100 -0
  54. package/src/kitchen/api/routes-api.ts +50 -0
  55. package/src/kitchen/index.ts +21 -0
  56. package/src/kitchen/kitchen-handler.ts +256 -0
  57. package/src/kitchen/kitchen-ui.ts +1732 -0
  58. package/src/kitchen/stream/activity-sse.ts +145 -0
  59. package/src/kitchen/stream/file-tailer.ts +99 -0
  60. package/src/middleware/compress.ts +62 -0
  61. package/src/middleware/cors.ts +47 -0
  62. package/src/middleware/index.ts +10 -0
  63. package/src/middleware/jwt.ts +134 -0
  64. package/src/middleware/logger.ts +58 -0
  65. package/src/middleware/timeout.ts +55 -0
  66. package/src/paths.ts +0 -4
  67. package/src/plugins/hooks.ts +64 -0
  68. package/src/plugins/index.ts +3 -0
  69. package/src/plugins/types.ts +5 -0
  70. package/src/report/build.ts +0 -6
  71. package/src/resource/__tests__/backward-compat.test.ts +0 -1
  72. package/src/router/fs-patterns.ts +11 -1
  73. package/src/router/fs-routes.ts +78 -14
  74. package/src/router/fs-scanner.ts +2 -2
  75. package/src/router/fs-types.ts +2 -1
  76. package/src/runtime/adapter-bun.ts +62 -0
  77. package/src/runtime/adapter.ts +47 -0
  78. package/src/runtime/cache.ts +310 -0
  79. package/src/runtime/handler.ts +65 -0
  80. package/src/runtime/image-handler.ts +195 -0
  81. package/src/runtime/index.ts +12 -0
  82. package/src/runtime/middleware.ts +263 -0
  83. package/src/runtime/server.ts +662 -83
  84. package/src/runtime/ssr.ts +55 -29
  85. package/src/runtime/streaming-ssr.ts +106 -82
  86. package/src/spec/index.ts +0 -1
  87. package/src/spec/schema.ts +1 -0
  88. package/src/testing/index.ts +144 -0
  89. package/src/watcher/watcher.ts +27 -1
  90. package/src/spec/lock.ts +0 -56
@@ -0,0 +1,1732 @@
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
+ </nav>
67
+
68
+ <main class="panels">
69
+ <section id="panel-activity" class="panel">
70
+ <div class="panel-header">
71
+ <div>
72
+ <h2>Activity Stream</h2>
73
+ <p class="panel-subtitle">Recent Kitchen events and MCP activity.</p>
74
+ </div>
75
+ <button id="clear-activity" class="btn-sm">Clear</button>
76
+ </div>
77
+ <div id="activity-list" class="activity-list">
78
+ <div class="empty-state">Waiting for MCP activity...</div>
79
+ </div>
80
+ </section>
81
+
82
+ <section id="panel-routes" class="panel active">
83
+ <div class="panel-header">
84
+ <div>
85
+ <h2>Routes</h2>
86
+ <p class="panel-subtitle">Current filesystem routes, slots, contracts, and hydration hints.</p>
87
+ </div>
88
+ <div id="routes-summary" class="summary"></div>
89
+ </div>
90
+ <div id="routes-list" class="routes-list">
91
+ <div class="empty-state">Loading routes...</div>
92
+ </div>
93
+ </section>
94
+
95
+ <section id="panel-guard" class="panel">
96
+ <div class="panel-header">
97
+ <div>
98
+ <h2>Architecture Guard</h2>
99
+ <p class="panel-subtitle">Run a scan and inspect dependency rule violations.</p>
100
+ </div>
101
+ <button id="scan-guard" class="btn-sm">Scan</button>
102
+ </div>
103
+ <div id="guard-status" class="guard-status"></div>
104
+ <div id="guard-list" class="violations-list">
105
+ <div class="empty-state">Click "Scan" to check architecture rules.</div>
106
+ </div>
107
+ </section>
108
+
109
+ <section id="panel-preview" class="panel">
110
+ <div class="panel-header">
111
+ <div>
112
+ <h2>Preview</h2>
113
+ <p class="panel-subtitle">Inspect changed files and open diffs without leaving Kitchen.</p>
114
+ </div>
115
+ <button id="refresh-changes" class="btn-sm">Refresh</button>
116
+ </div>
117
+ <div id="preview-list" class="preview-list">
118
+ <div class="empty-state">Loading file changes...</div>
119
+ </div>
120
+ <div id="preview-diff" class="preview-diff" style="display:none;"></div>
121
+ </section>
122
+
123
+ <section id="panel-contracts" class="panel">
124
+ <div class="panel-header">
125
+ <div>
126
+ <h2>Contracts</h2>
127
+ <p class="panel-subtitle">Browse route contracts and validate payloads in place.</p>
128
+ </div>
129
+ <div class="panel-actions">
130
+ <button id="export-openapi-json" class="btn-sm">Export JSON</button>
131
+ <button id="export-openapi-yaml" class="btn-sm">Export YAML</button>
132
+ </div>
133
+ </div>
134
+ <div class="contracts-layout">
135
+ <div id="contracts-list" class="contracts-list">
136
+ <div class="empty-state">Loading contracts...</div>
137
+ </div>
138
+ <div id="contracts-detail" class="contracts-detail">
139
+ <div id="contract-schema" class="contract-schema"></div>
140
+ <div id="contract-playground" class="contract-playground">
141
+ <h3>Validate</h3>
142
+ <div class="playground-controls">
143
+ <select id="validate-method" class="select-sm">
144
+ <option>GET</option><option>POST</option><option>PUT</option><option>PATCH</option><option>DELETE</option>
145
+ </select>
146
+ <button id="validate-btn" class="btn-sm">Validate</button>
147
+ </div>
148
+ <div class="playground-inputs">
149
+ <label>Query <textarea id="validate-query" rows="2" placeholder='{"key":"value"}'></textarea></label>
150
+ <label>Body <textarea id="validate-body" rows="3" placeholder='{"key":"value"}'></textarea></label>
151
+ <label>Params <textarea id="validate-params" rows="2" placeholder='{"id":"1"}'></textarea></label>
152
+ </div>
153
+ <div id="validate-result" class="validate-result"></div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </section>
158
+ </main>
159
+
160
+ <div id="debug-bar" class="debug-bar"></div>
161
+ </div>
162
+
163
+ <script>${JS}</script>
164
+ </body>
165
+ </html>`;
166
+ }
167
+
168
+ // ─── CSS ─────────────────────────────────────────
169
+
170
+ const CSS = /* css */ `
171
+ * { margin: 0; padding: 0; box-sizing: border-box; }
172
+
173
+ body {
174
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
175
+ background: #0f1117;
176
+ color: #e4e4e7;
177
+ min-height: 100vh;
178
+ }
179
+
180
+ .header {
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: space-between;
184
+ padding: 12px 20px;
185
+ background: #18181b;
186
+ border-bottom: 1px solid #27272a;
187
+ }
188
+
189
+ .logo {
190
+ display: flex;
191
+ align-items: center;
192
+ gap: 8px;
193
+ font-size: 18px;
194
+ font-weight: 600;
195
+ }
196
+
197
+ .logo-icon { font-size: 24px; }
198
+
199
+ .status {
200
+ display: flex;
201
+ align-items: center;
202
+ gap: 6px;
203
+ font-size: 13px;
204
+ color: #a1a1aa;
205
+ }
206
+
207
+ .status-dot {
208
+ width: 8px;
209
+ height: 8px;
210
+ border-radius: 50%;
211
+ transition: background 0.3s;
212
+ }
213
+
214
+ .status-dot.connected { background: #22c55e; }
215
+ .status-dot.disconnected { background: #ef4444; }
216
+ .status-dot.connecting { background: #eab308; }
217
+
218
+ .tabs {
219
+ display: flex;
220
+ gap: 0;
221
+ background: #18181b;
222
+ border-bottom: 1px solid #27272a;
223
+ padding: 0 20px;
224
+ }
225
+
226
+ .tab {
227
+ padding: 10px 20px;
228
+ background: none;
229
+ border: none;
230
+ color: #71717a;
231
+ font-size: 14px;
232
+ cursor: pointer;
233
+ border-bottom: 2px solid transparent;
234
+ transition: all 0.2s;
235
+ }
236
+
237
+ .tab:hover { color: #e4e4e7; }
238
+ .tab.active {
239
+ color: #a78bfa;
240
+ border-bottom-color: #a78bfa;
241
+ }
242
+
243
+ .panels { padding: 16px 20px; }
244
+
245
+ .panel { display: none; }
246
+ .panel.active { display: block; }
247
+
248
+ .panel-header {
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: space-between;
252
+ margin-bottom: 12px;
253
+ }
254
+
255
+ .panel-header h2 {
256
+ font-size: 16px;
257
+ font-weight: 600;
258
+ }
259
+
260
+ .btn-sm {
261
+ padding: 4px 12px;
262
+ background: #27272a;
263
+ border: 1px solid #3f3f46;
264
+ border-radius: 6px;
265
+ color: #e4e4e7;
266
+ font-size: 12px;
267
+ cursor: pointer;
268
+ transition: background 0.2s;
269
+ }
270
+
271
+ .btn-sm:hover { background: #3f3f46; }
272
+ .btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
273
+
274
+ .empty-state {
275
+ padding: 40px 20px;
276
+ text-align: center;
277
+ color: #52525b;
278
+ font-size: 14px;
279
+ }
280
+
281
+ /* Activity */
282
+ .activity-list {
283
+ max-height: calc(100vh - 180px);
284
+ overflow-y: auto;
285
+ }
286
+
287
+ .activity-item {
288
+ padding: 8px 12px;
289
+ border-bottom: 1px solid #1e1e22;
290
+ font-size: 13px;
291
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
292
+ display: flex;
293
+ gap: 10px;
294
+ align-items: flex-start;
295
+ animation: fadeIn 0.3s ease;
296
+ }
297
+
298
+ @keyframes fadeIn {
299
+ from { opacity: 0; transform: translateY(-4px); }
300
+ to { opacity: 1; transform: translateY(0); }
301
+ }
302
+
303
+ .activity-time {
304
+ color: #52525b;
305
+ white-space: nowrap;
306
+ flex-shrink: 0;
307
+ }
308
+
309
+ .activity-tool {
310
+ color: #a78bfa;
311
+ font-weight: 500;
312
+ flex-shrink: 0;
313
+ }
314
+
315
+ .activity-detail {
316
+ color: #a1a1aa;
317
+ overflow: hidden;
318
+ text-overflow: ellipsis;
319
+ white-space: nowrap;
320
+ }
321
+
322
+ /* Routes */
323
+ .summary {
324
+ display: flex;
325
+ gap: 12px;
326
+ font-size: 12px;
327
+ color: #a1a1aa;
328
+ }
329
+
330
+ .summary-item {
331
+ display: flex;
332
+ align-items: center;
333
+ gap: 4px;
334
+ }
335
+
336
+ .summary-count {
337
+ font-weight: 600;
338
+ color: #e4e4e7;
339
+ }
340
+
341
+ .route-item {
342
+ display: flex;
343
+ align-items: center;
344
+ gap: 12px;
345
+ padding: 10px 12px;
346
+ border-bottom: 1px solid #1e1e22;
347
+ font-size: 13px;
348
+ }
349
+
350
+ .route-kind {
351
+ display: inline-block;
352
+ padding: 2px 8px;
353
+ border-radius: 4px;
354
+ font-size: 11px;
355
+ font-weight: 600;
356
+ text-transform: uppercase;
357
+ flex-shrink: 0;
358
+ min-width: 44px;
359
+ text-align: center;
360
+ }
361
+
362
+ .route-kind.page { background: #1e3a5f; color: #60a5fa; }
363
+ .route-kind.api { background: #1a3c34; color: #4ade80; }
364
+
365
+ .route-pattern {
366
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
367
+ color: #e4e4e7;
368
+ flex: 1;
369
+ }
370
+
371
+ .route-badges {
372
+ display: flex;
373
+ gap: 4px;
374
+ flex-shrink: 0;
375
+ }
376
+
377
+ .badge {
378
+ padding: 1px 6px;
379
+ border-radius: 3px;
380
+ font-size: 10px;
381
+ background: #27272a;
382
+ color: #a1a1aa;
383
+ }
384
+
385
+ /* Guard */
386
+ .guard-status {
387
+ margin-bottom: 12px;
388
+ font-size: 13px;
389
+ color: #a1a1aa;
390
+ }
391
+
392
+ .guard-summary {
393
+ display: flex;
394
+ gap: 16px;
395
+ padding: 12px;
396
+ background: #18181b;
397
+ border-radius: 8px;
398
+ margin-bottom: 12px;
399
+ }
400
+
401
+ .guard-stat {
402
+ text-align: center;
403
+ }
404
+
405
+ .guard-stat-value {
406
+ font-size: 24px;
407
+ font-weight: 700;
408
+ }
409
+
410
+ .guard-stat-label {
411
+ font-size: 11px;
412
+ color: #71717a;
413
+ text-transform: uppercase;
414
+ }
415
+
416
+ .sev-error { color: #ef4444; }
417
+ .sev-warning { color: #eab308; }
418
+ .sev-info { color: #3b82f6; }
419
+
420
+ .violation-item {
421
+ padding: 8px 12px;
422
+ border-bottom: 1px solid #1e1e22;
423
+ font-size: 13px;
424
+ }
425
+
426
+ .violation-file {
427
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
428
+ color: #a78bfa;
429
+ margin-bottom: 2px;
430
+ }
431
+
432
+ .violation-msg {
433
+ color: #a1a1aa;
434
+ font-size: 12px;
435
+ }
436
+
437
+ .violation-sev {
438
+ display: inline-block;
439
+ padding: 1px 6px;
440
+ border-radius: 3px;
441
+ font-size: 10px;
442
+ font-weight: 600;
443
+ margin-right: 4px;
444
+ }
445
+
446
+ .violation-sev.error { background: #3b1111; color: #ef4444; }
447
+ .violation-sev.warning { background: #3b2f11; color: #eab308; }
448
+ .violation-sev.info { background: #112840; color: #3b82f6; }
449
+
450
+ /* Preview */
451
+ .preview-list { max-height: 40vh; overflow-y: auto; }
452
+ .preview-diff { max-height: 50vh; overflow-y: auto; padding: 8px; }
453
+
454
+ .change-item {
455
+ display: flex;
456
+ align-items: center;
457
+ gap: 10px;
458
+ padding: 8px 12px;
459
+ border-bottom: 1px solid #1e1e22;
460
+ font-size: 13px;
461
+ cursor: pointer;
462
+ transition: background 0.15s;
463
+ }
464
+ .change-item:hover { background: #27272a; }
465
+ .change-icon { flex-shrink: 0; }
466
+ .change-path {
467
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
468
+ color: #e4e4e7;
469
+ flex: 1;
470
+ overflow: hidden;
471
+ text-overflow: ellipsis;
472
+ white-space: nowrap;
473
+ }
474
+ .change-status {
475
+ font-size: 10px;
476
+ padding: 1px 6px;
477
+ border-radius: 3px;
478
+ text-transform: uppercase;
479
+ font-weight: 600;
480
+ flex-shrink: 0;
481
+ }
482
+ .change-status.added { background: #1a3c34; color: #4ade80; }
483
+ .change-status.modified { background: #1e3a5f; color: #60a5fa; }
484
+ .change-status.deleted { background: #3b1111; color: #ef4444; }
485
+ .change-status.untracked { background: #3b2f11; color: #eab308; }
486
+ .change-status.renamed { background: #2a1a3c; color: #a78bfa; }
487
+
488
+ .diff-header {
489
+ display: flex; align-items: center; justify-content: space-between;
490
+ padding: 8px 12px; background: #18181b; border-radius: 6px 6px 0 0;
491
+ border-bottom: 1px solid #27272a;
492
+ }
493
+ .diff-file { font-family: monospace; color: #a78bfa; font-size: 13px; }
494
+ .diff-stats { font-size: 12px; }
495
+ .diff-add { color: #4ade80; margin-right: 8px; }
496
+ .diff-del { color: #ef4444; }
497
+ .diff-hunk-header { padding: 4px 12px; background: #112840; color: #3b82f6; font-size: 12px; font-family: monospace; }
498
+ .diff-line { display: flex; font-family: monospace; font-size: 12px; line-height: 20px; }
499
+ .diff-line-num { width: 40px; text-align: right; padding: 0 4px; color: #52525b; user-select: none; flex-shrink: 0; }
500
+ .diff-line-content { flex: 1; padding: 0 8px; white-space: pre; overflow: hidden; text-overflow: ellipsis; }
501
+ .diff-line.add { background: rgba(74,222,128,0.08); }
502
+ .diff-line.add .diff-line-content::before { content: '+'; color: #4ade80; }
503
+ .diff-line.remove { background: rgba(239,68,68,0.08); }
504
+ .diff-line.remove .diff-line-content::before { content: '-'; color: #ef4444; }
505
+ .diff-line.context .diff-line-content::before { content: ' '; }
506
+
507
+ /* Contracts */
508
+ .contracts-layout { display: flex; gap: 12px; height: calc(100vh - 180px); }
509
+ .contracts-list { width: 300px; flex-shrink: 0; overflow-y: auto; border-right: 1px solid #27272a; padding-right: 12px; }
510
+ .contracts-detail { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
511
+
512
+ .contract-item {
513
+ display: flex; align-items: center; gap: 8px;
514
+ padding: 8px 12px; border-bottom: 1px solid #1e1e22;
515
+ cursor: pointer; transition: background 0.15s; font-size: 13px;
516
+ }
517
+ .contract-item:hover { background: #27272a; }
518
+ .contract-item.selected { background: #27272a; border-left: 2px solid #a78bfa; }
519
+
520
+ .method-badge {
521
+ display: inline-block; padding: 1px 6px; border-radius: 3px;
522
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
523
+ flex-shrink: 0; min-width: 36px; text-align: center;
524
+ }
525
+ .method-badge.get { background: #1a3c34; color: #4ade80; }
526
+ .method-badge.post { background: #1e3a5f; color: #60a5fa; }
527
+ .method-badge.put { background: #3b2f11; color: #eab308; }
528
+ .method-badge.patch { background: #2a1a3c; color: #a78bfa; }
529
+ .method-badge.delete { background: #3b1111; color: #ef4444; }
530
+
531
+ .contract-pattern { font-family: monospace; color: #e4e4e7; }
532
+
533
+ .contract-schema {
534
+ background: #18181b; border-radius: 8px; padding: 12px;
535
+ font-family: monospace; font-size: 12px; white-space: pre-wrap;
536
+ max-height: 40vh; overflow-y: auto;
537
+ }
538
+
539
+ .contract-playground { background: #18181b; border-radius: 8px; padding: 12px; }
540
+ .contract-playground h3 { font-size: 14px; margin-bottom: 8px; }
541
+
542
+ .playground-controls { display: flex; gap: 8px; margin-bottom: 8px; }
543
+ .select-sm {
544
+ padding: 4px 8px; background: #27272a; border: 1px solid #3f3f46;
545
+ border-radius: 6px; color: #e4e4e7; font-size: 12px;
546
+ }
547
+ .playground-inputs { display: flex; flex-direction: column; gap: 6px; }
548
+ .playground-inputs label { font-size: 11px; color: #71717a; display: flex; flex-direction: column; gap: 2px; }
549
+ .playground-inputs textarea {
550
+ background: #27272a; border: 1px solid #3f3f46; border-radius: 4px;
551
+ color: #e4e4e7; font-family: monospace; font-size: 12px; padding: 6px;
552
+ resize: vertical;
553
+ }
554
+ .validate-result { margin-top: 8px; padding: 8px; border-radius: 4px; font-size: 12px; font-family: monospace; }
555
+ .validate-result.success { background: #1a3c34; color: #4ade80; }
556
+ .validate-result.error { background: #3b1111; color: #ef4444; }
557
+
558
+ .debug-bar {
559
+ position: fixed;
560
+ bottom: 0;
561
+ left: 0;
562
+ right: 0;
563
+ padding: 4px 12px;
564
+ background: #1a1a2e;
565
+ border-top: 1px solid #27272a;
566
+ font-size: 11px;
567
+ font-family: monospace;
568
+ color: #71717a;
569
+ max-height: 60px;
570
+ overflow-y: auto;
571
+ }
572
+
573
+ .debug-bar .err { color: #ef4444; }
574
+ .debug-bar .ok { color: #22c55e; }
575
+
576
+ :root {
577
+ --bg: #f4efe6;
578
+ --bg-soft: rgba(255, 252, 246, 0.7);
579
+ --surface: #fffdfa;
580
+ --surface-strong: #f8f1e4;
581
+ --surface-alt: #f0e5d2;
582
+ --ink: #1e2a3a;
583
+ --muted: #677181;
584
+ --line: #dccfba;
585
+ --accent: #b86a12;
586
+ --accent-strong: #8d4f0e;
587
+ --accent-soft: rgba(184, 106, 18, 0.14);
588
+ --success: #177f56;
589
+ --danger: #bc3d3d;
590
+ --info: #2e66b8;
591
+ --warning: #ad7a12;
592
+ --shadow: 0 20px 50px rgba(49, 39, 23, 0.08);
593
+ }
594
+
595
+ body {
596
+ font-family: "IBM Plex Sans", "Segoe UI Variable", "Segoe UI", sans-serif;
597
+ background:
598
+ radial-gradient(circle at top left, rgba(226, 186, 124, 0.35), transparent 30%),
599
+ radial-gradient(circle at top right, rgba(152, 191, 193, 0.22), transparent 24%),
600
+ linear-gradient(180deg, #f8f3ea 0%, #efe7d8 100%);
601
+ color: var(--ink);
602
+ padding: 24px;
603
+ }
604
+
605
+ .app-shell {
606
+ width: min(1360px, 100%);
607
+ margin: 0 auto;
608
+ }
609
+
610
+ .hero {
611
+ display: flex;
612
+ align-items: flex-start;
613
+ justify-content: space-between;
614
+ gap: 24px;
615
+ padding: 28px 30px;
616
+ background:
617
+ linear-gradient(135deg, rgba(255, 249, 240, 0.92), rgba(247, 238, 224, 0.9)),
618
+ linear-gradient(120deg, rgba(184, 106, 18, 0.08), transparent 60%);
619
+ border: 1px solid rgba(220, 207, 186, 0.9);
620
+ border-radius: 28px;
621
+ box-shadow: var(--shadow);
622
+ margin-bottom: 18px;
623
+ }
624
+
625
+ .hero-kicker {
626
+ display: inline-flex;
627
+ align-items: center;
628
+ padding: 6px 10px;
629
+ border-radius: 999px;
630
+ background: rgba(30, 42, 58, 0.08);
631
+ color: var(--muted);
632
+ font-size: 12px;
633
+ letter-spacing: 0.08em;
634
+ text-transform: uppercase;
635
+ margin-bottom: 14px;
636
+ }
637
+
638
+ .logo {
639
+ gap: 10px;
640
+ font-size: 32px;
641
+ font-weight: 700;
642
+ letter-spacing: -0.03em;
643
+ color: var(--ink);
644
+ }
645
+
646
+ .logo-badge {
647
+ display: inline-flex;
648
+ align-items: center;
649
+ padding: 4px 10px;
650
+ border-radius: 999px;
651
+ background: var(--accent-soft);
652
+ color: var(--accent-strong);
653
+ font-size: 12px;
654
+ font-weight: 700;
655
+ letter-spacing: 0.08em;
656
+ text-transform: uppercase;
657
+ }
658
+
659
+ .hero-subtitle {
660
+ margin-top: 12px;
661
+ max-width: 720px;
662
+ color: var(--muted);
663
+ font-size: 15px;
664
+ line-height: 1.6;
665
+ }
666
+
667
+ .hero-side {
668
+ display: flex;
669
+ flex-direction: column;
670
+ align-items: flex-end;
671
+ gap: 12px;
672
+ min-width: 190px;
673
+ }
674
+
675
+ .hero-link,
676
+ .hero-link:visited {
677
+ color: var(--ink);
678
+ text-decoration: none;
679
+ font-size: 13px;
680
+ font-weight: 600;
681
+ padding: 10px 14px;
682
+ border: 1px solid var(--line);
683
+ border-radius: 999px;
684
+ background: rgba(255, 255, 255, 0.72);
685
+ }
686
+
687
+ .hero-link:hover {
688
+ border-color: var(--accent);
689
+ color: var(--accent-strong);
690
+ }
691
+
692
+ .status-pill {
693
+ display: inline-flex;
694
+ align-items: center;
695
+ gap: 8px;
696
+ padding: 10px 14px;
697
+ border-radius: 999px;
698
+ background: rgba(255, 255, 255, 0.78);
699
+ border: 1px solid var(--line);
700
+ color: var(--muted);
701
+ font-size: 13px;
702
+ font-weight: 600;
703
+ }
704
+
705
+ .status-dot.connected { background: var(--success); }
706
+ .status-dot.disconnected { background: var(--danger); }
707
+ .status-dot.connecting { background: var(--warning); }
708
+
709
+ .overview {
710
+ display: grid;
711
+ grid-template-columns: repeat(5, minmax(0, 1fr));
712
+ gap: 14px;
713
+ margin-bottom: 18px;
714
+ }
715
+
716
+ .metric-card {
717
+ padding: 16px 18px;
718
+ background: var(--bg-soft);
719
+ border: 1px solid rgba(220, 207, 186, 0.9);
720
+ border-radius: 18px;
721
+ box-shadow: 0 8px 24px rgba(49, 39, 23, 0.05);
722
+ }
723
+
724
+ .metric-label {
725
+ display: block;
726
+ font-size: 12px;
727
+ font-weight: 700;
728
+ letter-spacing: 0.08em;
729
+ text-transform: uppercase;
730
+ color: var(--muted);
731
+ margin-bottom: 10px;
732
+ }
733
+
734
+ .metric-value {
735
+ font-size: 28px;
736
+ line-height: 1;
737
+ letter-spacing: -0.04em;
738
+ color: var(--ink);
739
+ }
740
+
741
+ .tabs {
742
+ gap: 10px;
743
+ flex-wrap: wrap;
744
+ background: transparent;
745
+ border-bottom: none;
746
+ padding: 0 0 16px 0;
747
+ }
748
+
749
+ .tab {
750
+ padding: 10px 14px;
751
+ border: 1px solid transparent;
752
+ border-radius: 999px;
753
+ color: var(--muted);
754
+ font-weight: 600;
755
+ background: rgba(255, 252, 246, 0.5);
756
+ }
757
+
758
+ .tab:hover {
759
+ color: var(--ink);
760
+ background: rgba(255, 255, 255, 0.85);
761
+ border-color: var(--line);
762
+ }
763
+
764
+ .tab.active {
765
+ color: var(--accent-strong);
766
+ border-bottom-color: transparent;
767
+ border-color: rgba(184, 106, 18, 0.24);
768
+ background: rgba(184, 106, 18, 0.14);
769
+ }
770
+
771
+ .panels {
772
+ padding: 0;
773
+ }
774
+
775
+ .panel {
776
+ display: none;
777
+ background: rgba(255, 253, 250, 0.88);
778
+ border: 1px solid rgba(220, 207, 186, 0.9);
779
+ border-radius: 24px;
780
+ padding: 24px;
781
+ box-shadow: var(--shadow);
782
+ }
783
+
784
+ .panel.active {
785
+ display: block;
786
+ }
787
+
788
+ .panel-header {
789
+ align-items: flex-start;
790
+ margin-bottom: 18px;
791
+ gap: 12px;
792
+ }
793
+
794
+ .panel-header h2 {
795
+ font-size: 22px;
796
+ letter-spacing: -0.03em;
797
+ color: var(--ink);
798
+ }
799
+
800
+ .panel-subtitle {
801
+ margin-top: 6px;
802
+ font-size: 14px;
803
+ line-height: 1.5;
804
+ color: var(--muted);
805
+ }
806
+
807
+ .panel-actions {
808
+ display: flex;
809
+ gap: 8px;
810
+ }
811
+
812
+ .btn-sm {
813
+ padding: 8px 14px;
814
+ background: rgba(255, 255, 255, 0.9);
815
+ border: 1px solid var(--line);
816
+ border-radius: 999px;
817
+ color: var(--ink);
818
+ font-size: 12px;
819
+ font-weight: 700;
820
+ }
821
+
822
+ .btn-sm:hover {
823
+ background: rgba(184, 106, 18, 0.12);
824
+ border-color: rgba(184, 106, 18, 0.28);
825
+ }
826
+
827
+ .summary {
828
+ gap: 8px;
829
+ flex-wrap: wrap;
830
+ }
831
+
832
+ .summary-item {
833
+ padding: 8px 12px;
834
+ border-radius: 999px;
835
+ background: rgba(184, 106, 18, 0.08);
836
+ color: var(--accent-strong);
837
+ font-weight: 600;
838
+ }
839
+
840
+ .summary-count {
841
+ color: var(--ink);
842
+ }
843
+
844
+ .activity-list,
845
+ .routes-list,
846
+ .violations-list,
847
+ .preview-list,
848
+ .contracts-list,
849
+ .contracts-detail,
850
+ .preview-diff {
851
+ background: rgba(255, 255, 255, 0.72);
852
+ border: 1px solid rgba(220, 207, 186, 0.9);
853
+ border-radius: 18px;
854
+ }
855
+
856
+ .activity-list,
857
+ .routes-list,
858
+ .violations-list,
859
+ .preview-list {
860
+ overflow: hidden;
861
+ }
862
+
863
+ .activity-list {
864
+ max-height: calc(100vh - 320px);
865
+ }
866
+
867
+ .activity-item,
868
+ .route-item,
869
+ .change-item,
870
+ .contract-item {
871
+ border-bottom: 1px solid rgba(220, 207, 186, 0.72);
872
+ }
873
+
874
+ .activity-item:last-child,
875
+ .route-item:last-child,
876
+ .change-item:last-child,
877
+ .contract-item:last-child {
878
+ border-bottom: none;
879
+ }
880
+
881
+ .activity-item {
882
+ padding: 12px 14px;
883
+ font-size: 12px;
884
+ color: var(--ink);
885
+ }
886
+
887
+ .activity-time {
888
+ color: var(--muted);
889
+ }
890
+
891
+ .activity-tool {
892
+ color: var(--accent-strong);
893
+ }
894
+
895
+ .activity-detail {
896
+ color: var(--ink);
897
+ }
898
+
899
+ .route-item {
900
+ padding: 14px 16px;
901
+ }
902
+
903
+ .route-kind.page {
904
+ background: rgba(46, 102, 184, 0.12);
905
+ color: var(--info);
906
+ }
907
+
908
+ .route-kind.api {
909
+ background: rgba(23, 127, 86, 0.12);
910
+ color: var(--success);
911
+ }
912
+
913
+ .route-pattern {
914
+ color: var(--ink);
915
+ }
916
+
917
+ .badge {
918
+ background: rgba(30, 42, 58, 0.08);
919
+ color: var(--muted);
920
+ border: 1px solid rgba(220, 207, 186, 0.72);
921
+ }
922
+
923
+ .guard-status {
924
+ margin-bottom: 12px;
925
+ color: var(--muted);
926
+ font-size: 14px;
927
+ }
928
+
929
+ .guard-summary {
930
+ display: grid;
931
+ grid-template-columns: repeat(4, minmax(0, 1fr));
932
+ gap: 12px;
933
+ padding: 0;
934
+ background: transparent;
935
+ margin-bottom: 16px;
936
+ }
937
+
938
+ .guard-stat {
939
+ padding: 16px;
940
+ border-radius: 18px;
941
+ background: rgba(255, 255, 255, 0.72);
942
+ border: 1px solid rgba(220, 207, 186, 0.9);
943
+ }
944
+
945
+ .guard-stat-value {
946
+ font-size: 28px;
947
+ font-weight: 700;
948
+ letter-spacing: -0.04em;
949
+ color: var(--ink);
950
+ }
951
+
952
+ .guard-stat-label {
953
+ color: var(--muted);
954
+ font-size: 12px;
955
+ letter-spacing: 0.08em;
956
+ text-transform: uppercase;
957
+ margin-top: 8px;
958
+ }
959
+
960
+ .sev-error { color: var(--danger); }
961
+ .sev-warning { color: var(--warning); }
962
+ .sev-info { color: var(--info); }
963
+
964
+ .violation-item {
965
+ padding: 14px 16px;
966
+ border-bottom: 1px solid rgba(220, 207, 186, 0.72);
967
+ }
968
+
969
+ .violation-item:last-child {
970
+ border-bottom: none;
971
+ }
972
+
973
+ .violation-file {
974
+ color: var(--ink);
975
+ font-weight: 600;
976
+ margin-bottom: 4px;
977
+ }
978
+
979
+ .violation-msg {
980
+ color: var(--muted);
981
+ }
982
+
983
+ .preview-list,
984
+ .preview-diff {
985
+ margin-bottom: 16px;
986
+ }
987
+
988
+ .change-item {
989
+ padding: 14px 16px;
990
+ }
991
+
992
+ .change-item:hover,
993
+ .contract-item:hover {
994
+ background: rgba(184, 106, 18, 0.06);
995
+ }
996
+
997
+ .change-icon {
998
+ color: var(--accent-strong);
999
+ }
1000
+
1001
+ .change-path {
1002
+ color: var(--ink);
1003
+ }
1004
+
1005
+ .change-status {
1006
+ font-weight: 700;
1007
+ text-transform: uppercase;
1008
+ font-size: 11px;
1009
+ letter-spacing: 0.08em;
1010
+ }
1011
+
1012
+ .contracts-layout {
1013
+ grid-template-columns: minmax(280px, 0.9fr) minmax(420px, 1.6fr);
1014
+ gap: 16px;
1015
+ }
1016
+
1017
+ .contracts-list,
1018
+ .contracts-detail {
1019
+ padding: 8px;
1020
+ }
1021
+
1022
+ .contract-item {
1023
+ padding: 14px 14px;
1024
+ border-radius: 14px;
1025
+ }
1026
+
1027
+ .contract-item.selected {
1028
+ background: rgba(184, 106, 18, 0.12);
1029
+ border-color: rgba(184, 106, 18, 0.28);
1030
+ }
1031
+
1032
+ .method-badge {
1033
+ border-radius: 999px;
1034
+ padding: 4px 8px;
1035
+ font-size: 10px;
1036
+ font-weight: 800;
1037
+ letter-spacing: 0.08em;
1038
+ }
1039
+
1040
+ .contract-schema,
1041
+ .contract-playground {
1042
+ background: rgba(255, 255, 255, 0.78);
1043
+ border: 1px solid rgba(220, 207, 186, 0.9);
1044
+ border-radius: 16px;
1045
+ }
1046
+
1047
+ .contract-schema {
1048
+ padding: 16px;
1049
+ max-height: 420px;
1050
+ overflow: auto;
1051
+ color: var(--ink);
1052
+ }
1053
+
1054
+ .contract-playground {
1055
+ margin-top: 14px;
1056
+ padding: 16px;
1057
+ }
1058
+
1059
+ .playground-inputs label {
1060
+ color: var(--muted);
1061
+ font-size: 13px;
1062
+ }
1063
+
1064
+ textarea,
1065
+ .select-sm {
1066
+ background: rgba(255, 252, 246, 0.92);
1067
+ border: 1px solid var(--line);
1068
+ color: var(--ink);
1069
+ border-radius: 12px;
1070
+ }
1071
+
1072
+ textarea {
1073
+ width: 100%;
1074
+ margin-top: 6px;
1075
+ padding: 10px 12px;
1076
+ resize: vertical;
1077
+ font-family: "IBM Plex Mono", "Cascadia Code", monospace;
1078
+ }
1079
+
1080
+ .select-sm {
1081
+ padding: 8px 12px;
1082
+ }
1083
+
1084
+ .validate-result {
1085
+ margin-top: 12px;
1086
+ border-radius: 12px;
1087
+ padding: 12px 14px;
1088
+ }
1089
+
1090
+ .validate-result.success {
1091
+ background: rgba(23, 127, 86, 0.12);
1092
+ color: var(--success);
1093
+ }
1094
+
1095
+ .validate-result.error {
1096
+ background: rgba(188, 61, 61, 0.12);
1097
+ color: var(--danger);
1098
+ }
1099
+
1100
+ .diff-header,
1101
+ .diff-hunk-header {
1102
+ background: transparent;
1103
+ color: var(--ink);
1104
+ }
1105
+
1106
+ .diff-line-num {
1107
+ color: var(--muted);
1108
+ }
1109
+
1110
+ .diff-line.add {
1111
+ background: rgba(23, 127, 86, 0.08);
1112
+ }
1113
+
1114
+ .diff-line.remove {
1115
+ background: rgba(188, 61, 61, 0.08);
1116
+ }
1117
+
1118
+ .empty-state {
1119
+ padding: 48px 20px;
1120
+ color: var(--muted);
1121
+ }
1122
+
1123
+ .debug-bar {
1124
+ position: sticky;
1125
+ bottom: 0;
1126
+ margin-top: 16px;
1127
+ border-radius: 16px;
1128
+ background: rgba(30, 42, 58, 0.94);
1129
+ border: 1px solid rgba(30, 42, 58, 0.94);
1130
+ color: rgba(255, 255, 255, 0.76);
1131
+ box-shadow: 0 12px 28px rgba(30, 42, 58, 0.22);
1132
+ }
1133
+
1134
+ @media (max-width: 1040px) {
1135
+ body {
1136
+ padding: 18px;
1137
+ }
1138
+
1139
+ .hero {
1140
+ flex-direction: column;
1141
+ }
1142
+
1143
+ .hero-side {
1144
+ align-items: flex-start;
1145
+ }
1146
+
1147
+ .overview {
1148
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1149
+ }
1150
+
1151
+ .contracts-layout {
1152
+ grid-template-columns: 1fr;
1153
+ }
1154
+
1155
+ .guard-summary {
1156
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1157
+ }
1158
+ }
1159
+
1160
+ @media (max-width: 720px) {
1161
+ body {
1162
+ padding: 12px;
1163
+ }
1164
+
1165
+ .hero,
1166
+ .panel {
1167
+ padding: 18px;
1168
+ border-radius: 20px;
1169
+ }
1170
+
1171
+ .overview {
1172
+ grid-template-columns: 1fr;
1173
+ }
1174
+
1175
+ .tabs {
1176
+ gap: 8px;
1177
+ }
1178
+
1179
+ .tab {
1180
+ width: calc(50% - 4px);
1181
+ justify-content: center;
1182
+ }
1183
+
1184
+ .panel-header {
1185
+ flex-direction: column;
1186
+ }
1187
+ }
1188
+ `;
1189
+
1190
+ // ─── JavaScript ──────────────────────────────────
1191
+
1192
+ const JS = /* js */ `
1193
+ (function() {
1194
+ var dbg = document.getElementById('debug-bar');
1195
+ function log(msg, cls) {
1196
+ if (!dbg) return;
1197
+ var s = document.createElement('span');
1198
+ s.className = cls || '';
1199
+ s.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg + ' ';
1200
+ dbg.appendChild(s);
1201
+ dbg.scrollTop = dbg.scrollHeight;
1202
+ console.log('[Kitchen]', msg);
1203
+ }
1204
+
1205
+ function escapeHtml(str) {
1206
+ if (!str) return '';
1207
+ var d = document.createElement('div');
1208
+ d.appendChild(document.createTextNode(String(str)));
1209
+ return d.innerHTML;
1210
+ }
1211
+
1212
+ function setMetric(id, value) {
1213
+ var el = document.getElementById(id);
1214
+ if (!el) return;
1215
+ el.textContent = String(value);
1216
+ }
1217
+
1218
+ try { log('JS loaded', 'ok'); } catch(e) {}
1219
+
1220
+ // Tab switching
1221
+ var tabs = document.querySelectorAll('.tab');
1222
+ for (var i = 0; i < tabs.length; i++) {
1223
+ tabs[i].addEventListener('click', function() {
1224
+ var all = document.querySelectorAll('.tab');
1225
+ var panels = document.querySelectorAll('.panel');
1226
+ for (var j = 0; j < all.length; j++) all[j].classList.remove('active');
1227
+ for (var j = 0; j < panels.length; j++) panels[j].classList.remove('active');
1228
+ this.classList.add('active');
1229
+ var p = document.getElementById('panel-' + this.getAttribute('data-panel'));
1230
+ if (p) p.classList.add('active');
1231
+ });
1232
+ }
1233
+
1234
+ // ─── SSE Activity Stream ─────────────────────
1235
+ var statusDot = document.getElementById('sse-status');
1236
+ var statusLabel = document.getElementById('sse-label');
1237
+ var activityList = document.getElementById('activity-list');
1238
+ var activityCount = 0;
1239
+ var MAX_ITEMS = 200;
1240
+ var sseRetryCount = 0;
1241
+
1242
+ function connectSSE() {
1243
+ statusDot.className = 'status-dot connecting';
1244
+ statusLabel.textContent = 'Connecting...';
1245
+ log('SSE connecting...');
1246
+
1247
+ var es;
1248
+ try {
1249
+ es = new EventSource('/__kitchen/sse/activity');
1250
+ } catch(e) {
1251
+ log('SSE EventSource failed: ' + e.message, 'err');
1252
+ statusDot.className = 'status-dot disconnected';
1253
+ statusLabel.textContent = 'Failed';
1254
+ return;
1255
+ }
1256
+
1257
+ es.onopen = function() {
1258
+ statusDot.className = 'status-dot connected';
1259
+ statusLabel.textContent = 'Connected';
1260
+ sseRetryCount = 0;
1261
+ log('SSE connected', 'ok');
1262
+ };
1263
+
1264
+ es.onmessage = function(e) {
1265
+ try {
1266
+ var data = JSON.parse(e.data);
1267
+ if (data.type === 'connected') {
1268
+ log('SSE welcome: ' + data.clientId, 'ok');
1269
+ return;
1270
+ }
1271
+ if (data.type === 'heartbeat') return;
1272
+ appendActivity(data);
1273
+ } catch(err) {
1274
+ log('SSE parse error: ' + err.message, 'err');
1275
+ }
1276
+ };
1277
+
1278
+ es.onerror = function(evt) {
1279
+ log('SSE error (readyState=' + es.readyState + ')', 'err');
1280
+ statusDot.className = 'status-dot disconnected';
1281
+ statusLabel.textContent = 'Disconnected';
1282
+ es.close();
1283
+ sseRetryCount++;
1284
+ var delay = Math.min(3000 * sseRetryCount, 15000);
1285
+ log('SSE retry in ' + (delay/1000) + 's');
1286
+ setTimeout(connectSSE, delay);
1287
+ };
1288
+ }
1289
+
1290
+ function appendActivity(data) {
1291
+ if (activityCount === 0) {
1292
+ activityList.innerHTML = '';
1293
+ }
1294
+ activityCount++;
1295
+ setMetric('metric-activity', activityCount);
1296
+
1297
+ var item = document.createElement('div');
1298
+ item.className = 'activity-item';
1299
+
1300
+ var ts = data.ts || data.timestamp || new Date().toISOString();
1301
+ var time = new Date(ts).toLocaleTimeString();
1302
+ var tool = data.tool || data.type || 'event';
1303
+ var detail = data.description || data.message || data.resource || JSON.stringify(data).substring(0, 120);
1304
+
1305
+ item.innerHTML =
1306
+ '<span class="activity-time">' + escapeHtml(time) + '</span>' +
1307
+ '<span class="activity-tool">' + escapeHtml(tool) + '</span>' +
1308
+ '<span class="activity-detail">' + escapeHtml(detail) + '</span>';
1309
+
1310
+ activityList.insertBefore(item, activityList.firstChild);
1311
+
1312
+ while (activityList.children.length > MAX_ITEMS) {
1313
+ activityList.removeChild(activityList.lastChild);
1314
+ }
1315
+ }
1316
+
1317
+ document.getElementById('clear-activity').addEventListener('click', function() {
1318
+ activityList.innerHTML = '<div class="empty-state">Waiting for MCP activity...</div>';
1319
+ activityCount = 0;
1320
+ setMetric('metric-activity', 0);
1321
+ });
1322
+
1323
+ connectSSE();
1324
+
1325
+ // ─── Routes ──────────────────────────────────
1326
+ function loadRoutes() {
1327
+ log('Fetching routes...');
1328
+ var xhr = new XMLHttpRequest();
1329
+ xhr.open('GET', '/__kitchen/api/routes', true);
1330
+ xhr.onload = function() {
1331
+ if (xhr.status === 200) {
1332
+ try {
1333
+ var data = JSON.parse(xhr.responseText);
1334
+ log('Routes loaded: ' + data.summary.total + ' routes', 'ok');
1335
+ renderRoutes(data);
1336
+ } catch(e) {
1337
+ log('Routes parse error: ' + e.message, 'err');
1338
+ }
1339
+ } else {
1340
+ log('Routes HTTP ' + xhr.status, 'err');
1341
+ document.getElementById('routes-list').innerHTML =
1342
+ '<div class="empty-state">Failed to load routes (HTTP ' + xhr.status + ')</div>';
1343
+ }
1344
+ };
1345
+ xhr.onerror = function() {
1346
+ log('Routes network error', 'err');
1347
+ document.getElementById('routes-list').innerHTML =
1348
+ '<div class="empty-state">Network error loading routes.</div>';
1349
+ };
1350
+ xhr.send();
1351
+ }
1352
+
1353
+ function renderRoutes(data) {
1354
+ var summaryEl = document.getElementById('routes-summary');
1355
+ var listEl = document.getElementById('routes-list');
1356
+ var s = data.summary;
1357
+ setMetric('metric-routes', s.total);
1358
+
1359
+ summaryEl.innerHTML =
1360
+ '<span class="summary-item"><span class="summary-count">' + s.total + '</span> total</span>' +
1361
+ '<span class="summary-item"><span class="summary-count">' + s.pages + '</span> pages</span>' +
1362
+ '<span class="summary-item"><span class="summary-count">' + s.apis + '</span> APIs</span>' +
1363
+ '<span class="summary-item"><span class="summary-count">' + s.withIslands + '</span> islands</span>';
1364
+
1365
+ if (!data.routes.length) {
1366
+ listEl.innerHTML = '<div class="empty-state">No routes found.</div>';
1367
+ return;
1368
+ }
1369
+
1370
+ var html = '';
1371
+ for (var i = 0; i < data.routes.length; i++) {
1372
+ var r = data.routes[i];
1373
+ var badges = '';
1374
+ if (r.hasSlot) badges += '<span class="badge">slot</span>';
1375
+ if (r.hasContract) badges += '<span class="badge">contract</span>';
1376
+ if (r.hasClient) badges += '<span class="badge">island</span>';
1377
+ if (r.hasLayout) badges += '<span class="badge">layout</span>';
1378
+ if (r.hydration && r.hydration !== 'none') badges += '<span class="badge">' + escapeHtml(r.hydration) + '</span>';
1379
+
1380
+ html += '<div class="route-item">' +
1381
+ '<span class="route-kind ' + r.kind + '">' + r.kind + '</span>' +
1382
+ '<span class="route-pattern">' + escapeHtml(r.pattern) + '</span>' +
1383
+ '<span class="route-badges">' + badges + '</span>' +
1384
+ '</div>';
1385
+ }
1386
+ listEl.innerHTML = html;
1387
+ }
1388
+
1389
+ loadRoutes();
1390
+
1391
+ // ─── Guard ───────────────────────────────────
1392
+ var scanBtn = document.getElementById('scan-guard');
1393
+ var guardStatusEl = document.getElementById('guard-status');
1394
+ var guardListEl = document.getElementById('guard-list');
1395
+
1396
+ function loadGuardStatus() {
1397
+ log('Fetching guard status...');
1398
+ var xhr = new XMLHttpRequest();
1399
+ xhr.open('GET', '/__kitchen/api/guard', true);
1400
+ xhr.onload = function() {
1401
+ if (xhr.status === 200) {
1402
+ try {
1403
+ var data = JSON.parse(xhr.responseText);
1404
+ log('Guard: ' + (data.enabled ? 'enabled (' + data.preset + ')' : 'disabled'), 'ok');
1405
+ renderGuardData(data);
1406
+ } catch(e) {
1407
+ log('Guard parse error: ' + e.message, 'err');
1408
+ }
1409
+ }
1410
+ };
1411
+ xhr.onerror = function() { log('Guard network error', 'err'); };
1412
+ xhr.send();
1413
+ }
1414
+
1415
+ scanBtn.addEventListener('click', function() {
1416
+ scanBtn.disabled = true;
1417
+ scanBtn.textContent = 'Scanning...';
1418
+ log('Guard scan started...');
1419
+
1420
+ var xhr = new XMLHttpRequest();
1421
+ xhr.open('POST', '/__kitchen/api/guard/scan', true);
1422
+ xhr.onload = function() {
1423
+ scanBtn.disabled = false;
1424
+ scanBtn.textContent = 'Scan';
1425
+ if (xhr.status === 200) {
1426
+ try {
1427
+ var data = JSON.parse(xhr.responseText);
1428
+ log('Guard scan done: ' + (data.report ? data.report.totalViolations + ' violations' : 'no report'), 'ok');
1429
+ renderGuardData(data);
1430
+ } catch(e) {
1431
+ log('Guard scan parse error: ' + e.message, 'err');
1432
+ }
1433
+ } else {
1434
+ log('Guard scan HTTP ' + xhr.status, 'err');
1435
+ guardStatusEl.textContent = 'Scan failed.';
1436
+ }
1437
+ };
1438
+ xhr.onerror = function() {
1439
+ scanBtn.disabled = false;
1440
+ scanBtn.textContent = 'Scan';
1441
+ log('Guard scan network error', 'err');
1442
+ guardStatusEl.textContent = 'Scan failed.';
1443
+ };
1444
+ xhr.send();
1445
+ });
1446
+
1447
+ function renderGuardData(data) {
1448
+ if (!data.enabled) {
1449
+ guardStatusEl.textContent = 'Guard is not configured for this project.';
1450
+ guardListEl.innerHTML = '';
1451
+ setMetric('metric-guard', 'off');
1452
+ return;
1453
+ }
1454
+
1455
+ guardStatusEl.innerHTML = 'Preset: <strong>' + escapeHtml(data.preset) + '</strong>';
1456
+
1457
+ if (!data.report) {
1458
+ guardListEl.innerHTML = '<div class="empty-state">No scan results yet. Click "Scan" to check.</div>';
1459
+ setMetric('metric-guard', 'ready');
1460
+ return;
1461
+ }
1462
+
1463
+ var r = data.report;
1464
+ setMetric('metric-guard', r.totalViolations);
1465
+ var summaryHtml = '<div class="guard-summary">' +
1466
+ '<div class="guard-stat"><div class="guard-stat-value">' + r.totalViolations + '</div><div class="guard-stat-label">Total</div></div>' +
1467
+ '<div class="guard-stat"><div class="guard-stat-value sev-error">' + (r.bySeverity.error || 0) + '</div><div class="guard-stat-label">Errors</div></div>' +
1468
+ '<div class="guard-stat"><div class="guard-stat-value sev-warning">' + (r.bySeverity.warning || 0) + '</div><div class="guard-stat-label">Warnings</div></div>' +
1469
+ '<div class="guard-stat"><div class="guard-stat-value sev-info">' + (r.bySeverity.info || 0) + '</div><div class="guard-stat-label">Info</div></div>' +
1470
+ '</div>';
1471
+
1472
+ if (!r.violations.length) {
1473
+ guardListEl.innerHTML = summaryHtml + '<div class="empty-state">No violations found!</div>';
1474
+ return;
1475
+ }
1476
+
1477
+ var violHtml = '';
1478
+ var list = r.violations.length > 100 ? r.violations.slice(0, 100) : r.violations;
1479
+ for (var i = 0; i < list.length; i++) {
1480
+ var v = list[i];
1481
+ violHtml += '<div class="violation-item">' +
1482
+ '<div class="violation-file">' +
1483
+ '<span class="violation-sev ' + v.severity + '">' + v.severity + '</span>' +
1484
+ escapeHtml(v.filePath) + ':' + v.line +
1485
+ '</div>' +
1486
+ '<div class="violation-msg">' +
1487
+ escapeHtml(v.fromLayer) + ' &rarr; ' + escapeHtml(v.toLayer) + ': ' + escapeHtml(v.ruleDescription) +
1488
+ '</div>' +
1489
+ '</div>';
1490
+ }
1491
+ guardListEl.innerHTML = summaryHtml + violHtml;
1492
+ }
1493
+
1494
+ loadGuardStatus();
1495
+
1496
+ // ─── Preview ──────────────────────────────────
1497
+ var previewListEl = document.getElementById('preview-list');
1498
+ var previewDiffEl = document.getElementById('preview-diff');
1499
+ var refreshChangesBtn = document.getElementById('refresh-changes');
1500
+
1501
+ function loadFileChanges() {
1502
+ log('Fetching file changes...');
1503
+ var xhr = new XMLHttpRequest();
1504
+ xhr.open('GET', '/__kitchen/api/file/changes', true);
1505
+ xhr.onload = function() {
1506
+ if (xhr.status === 200) {
1507
+ try {
1508
+ var data = JSON.parse(xhr.responseText);
1509
+ log('Changes loaded: ' + data.changes.length, 'ok');
1510
+ renderFileChanges(data.changes);
1511
+ } catch(e) {
1512
+ log('Changes parse error: ' + e.message, 'err');
1513
+ }
1514
+ }
1515
+ };
1516
+ xhr.onerror = function() { log('Changes network error', 'err'); };
1517
+ xhr.send();
1518
+ }
1519
+
1520
+ function renderFileChanges(changes) {
1521
+ setMetric('metric-changes', changes.length);
1522
+ if (!changes.length) {
1523
+ previewListEl.innerHTML = '<div class="empty-state">No file changes detected.</div>';
1524
+ return;
1525
+ }
1526
+ var html = '';
1527
+ var icons = { added: '+', modified: '~', deleted: '-', untracked: '?', renamed: 'R' };
1528
+ for (var i = 0; i < changes.length; i++) {
1529
+ var c = changes[i];
1530
+ html += '<div class="change-item" data-path="' + escapeHtml(c.filePath) + '">' +
1531
+ '<span class="change-icon">' + (icons[c.status] || '?') + '</span>' +
1532
+ '<span class="change-path">' + escapeHtml(c.filePath) + '</span>' +
1533
+ '<span class="change-status ' + c.status + '">' + c.status + '</span>' +
1534
+ '</div>';
1535
+ }
1536
+ previewListEl.innerHTML = html;
1537
+
1538
+ // Attach click handlers
1539
+ var items = previewListEl.querySelectorAll('.change-item');
1540
+ for (var j = 0; j < items.length; j++) {
1541
+ items[j].addEventListener('click', function() {
1542
+ var p = this.getAttribute('data-path');
1543
+ loadFileDiff(p);
1544
+ });
1545
+ }
1546
+ }
1547
+
1548
+ function loadFileDiff(filePath) {
1549
+ log('Fetching diff for ' + filePath);
1550
+ var xhr = new XMLHttpRequest();
1551
+ xhr.open('GET', '/__kitchen/api/file/diff?path=' + encodeURIComponent(filePath), true);
1552
+ xhr.onload = function() {
1553
+ if (xhr.status === 200) {
1554
+ try {
1555
+ var diff = JSON.parse(xhr.responseText);
1556
+ renderDiff(diff);
1557
+ } catch(e) {
1558
+ log('Diff parse error: ' + e.message, 'err');
1559
+ }
1560
+ }
1561
+ };
1562
+ xhr.onerror = function() { log('Diff network error', 'err'); };
1563
+ xhr.send();
1564
+ }
1565
+
1566
+ function renderDiff(diff) {
1567
+ if (!diff.hunks || !diff.hunks.length) {
1568
+ previewDiffEl.innerHTML = '<div class="empty-state">No diff available.</div>';
1569
+ previewDiffEl.style.display = 'block';
1570
+ return;
1571
+ }
1572
+ var html = '<div class="diff-header">' +
1573
+ '<span class="diff-file">' + escapeHtml(diff.filePath) + '</span>' +
1574
+ '<span class="diff-stats"><span class="diff-add">+' + diff.additions + '</span><span class="diff-del">-' + diff.deletions + '</span></span>' +
1575
+ '<button class="btn-sm" onclick="document.getElementById(\'preview-diff\').style.display=\'none\'">Close</button>' +
1576
+ '</div>';
1577
+ for (var h = 0; h < diff.hunks.length; h++) {
1578
+ var hunk = diff.hunks[h];
1579
+ html += '<div class="diff-hunk-header">' + escapeHtml(hunk.header) + '</div>';
1580
+ for (var l = 0; l < hunk.lines.length; l++) {
1581
+ var line = hunk.lines[l];
1582
+ var cls = line.type === 'add' ? 'add' : line.type === 'remove' ? 'remove' : 'context';
1583
+ html += '<div class="diff-line ' + cls + '">' +
1584
+ '<span class="diff-line-num">' + (line.oldLine || '') + '</span>' +
1585
+ '<span class="diff-line-num">' + (line.newLine || '') + '</span>' +
1586
+ '<span class="diff-line-content">' + escapeHtml(line.content) + '</span>' +
1587
+ '</div>';
1588
+ }
1589
+ }
1590
+ previewDiffEl.innerHTML = html;
1591
+ previewDiffEl.style.display = 'block';
1592
+ }
1593
+
1594
+ refreshChangesBtn.addEventListener('click', loadFileChanges);
1595
+ loadFileChanges();
1596
+
1597
+ // ─── Contracts ─────────────────────────────────
1598
+ var contractsListEl = document.getElementById('contracts-list');
1599
+ var contractSchemaEl = document.getElementById('contract-schema');
1600
+ var validateBtn = document.getElementById('validate-btn');
1601
+ var validateResultEl = document.getElementById('validate-result');
1602
+ var exportJsonBtn = document.getElementById('export-openapi-json');
1603
+ var exportYamlBtn = document.getElementById('export-openapi-yaml');
1604
+ var selectedContractId = null;
1605
+
1606
+ function loadContracts() {
1607
+ log('Fetching contracts...');
1608
+ var xhr = new XMLHttpRequest();
1609
+ xhr.open('GET', '/__kitchen/api/contracts', true);
1610
+ xhr.onload = function() {
1611
+ if (xhr.status === 200) {
1612
+ try {
1613
+ var data = JSON.parse(xhr.responseText);
1614
+ log('Contracts loaded: ' + data.contracts.length, 'ok');
1615
+ renderContractsList(data.contracts);
1616
+ } catch(e) {
1617
+ log('Contracts parse error: ' + e.message, 'err');
1618
+ contractsListEl.innerHTML = '<div class="empty-state">Failed to parse contracts.</div>';
1619
+ }
1620
+ } else if (xhr.status === 404) {
1621
+ contractsListEl.innerHTML = '<div class="empty-state">Contracts API not available.</div>';
1622
+ }
1623
+ };
1624
+ xhr.onerror = function() {
1625
+ log('Contracts network error', 'err');
1626
+ contractsListEl.innerHTML = '<div class="empty-state">Network error.</div>';
1627
+ };
1628
+ xhr.send();
1629
+ }
1630
+
1631
+ function renderContractsList(contracts) {
1632
+ setMetric('metric-contracts', contracts.length);
1633
+ if (!contracts.length) {
1634
+ contractsListEl.innerHTML = '<div class="empty-state">No contracts found.</div>';
1635
+ return;
1636
+ }
1637
+ var html = '';
1638
+ for (var i = 0; i < contracts.length; i++) {
1639
+ var c = contracts[i];
1640
+ var methods = (c.methods || []).map(function(m) {
1641
+ return '<span class="method-badge ' + m.toLowerCase() + '">' + m + '</span>';
1642
+ }).join('');
1643
+ html += '<div class="contract-item" data-id="' + escapeHtml(c.id) + '">' +
1644
+ methods +
1645
+ '<span class="contract-pattern">' + escapeHtml(c.pattern) + '</span>' +
1646
+ '</div>';
1647
+ }
1648
+ contractsListEl.innerHTML = html;
1649
+
1650
+ var items = contractsListEl.querySelectorAll('.contract-item');
1651
+ for (var j = 0; j < items.length; j++) {
1652
+ items[j].addEventListener('click', function() {
1653
+ var id = this.getAttribute('data-id');
1654
+ selectedContractId = id;
1655
+ var all = contractsListEl.querySelectorAll('.contract-item');
1656
+ for (var k = 0; k < all.length; k++) all[k].classList.remove('selected');
1657
+ this.classList.add('selected');
1658
+ loadContractDetail(id);
1659
+ });
1660
+ }
1661
+ }
1662
+
1663
+ function loadContractDetail(id) {
1664
+ var xhr = new XMLHttpRequest();
1665
+ xhr.open('GET', '/__kitchen/api/contracts/' + encodeURIComponent(id), true);
1666
+ xhr.onload = function() {
1667
+ if (xhr.status === 200) {
1668
+ try {
1669
+ var data = JSON.parse(xhr.responseText);
1670
+ contractSchemaEl.textContent = JSON.stringify(data, null, 2);
1671
+ } catch(e) {
1672
+ contractSchemaEl.textContent = 'Parse error';
1673
+ }
1674
+ }
1675
+ };
1676
+ xhr.send();
1677
+ }
1678
+
1679
+ validateBtn.addEventListener('click', function() {
1680
+ if (!selectedContractId) {
1681
+ validateResultEl.className = 'validate-result error';
1682
+ validateResultEl.textContent = 'Select a contract first.';
1683
+ return;
1684
+ }
1685
+ var method = document.getElementById('validate-method').value;
1686
+ var input = {};
1687
+ try {
1688
+ var q = document.getElementById('validate-query').value.trim();
1689
+ var b = document.getElementById('validate-body').value.trim();
1690
+ var p = document.getElementById('validate-params').value.trim();
1691
+ if (q) input.query = JSON.parse(q);
1692
+ if (b) input.body = JSON.parse(b);
1693
+ if (p) input.params = JSON.parse(p);
1694
+ } catch(e) {
1695
+ validateResultEl.className = 'validate-result error';
1696
+ validateResultEl.textContent = 'Invalid JSON: ' + e.message;
1697
+ return;
1698
+ }
1699
+
1700
+ var xhr = new XMLHttpRequest();
1701
+ xhr.open('POST', '/__kitchen/api/contracts/validate', true);
1702
+ xhr.setRequestHeader('Content-Type', 'application/json');
1703
+ xhr.onload = function() {
1704
+ try {
1705
+ var result = JSON.parse(xhr.responseText);
1706
+ if (result.valid) {
1707
+ validateResultEl.className = 'validate-result success';
1708
+ validateResultEl.textContent = 'Validation passed!';
1709
+ } else {
1710
+ validateResultEl.className = 'validate-result error';
1711
+ validateResultEl.textContent = JSON.stringify(result.errors || result, null, 2);
1712
+ }
1713
+ } catch(e) {
1714
+ validateResultEl.className = 'validate-result error';
1715
+ validateResultEl.textContent = 'Response parse error';
1716
+ }
1717
+ };
1718
+ xhr.send(JSON.stringify({ contractId: selectedContractId, method: method, input: input }));
1719
+ });
1720
+
1721
+ exportJsonBtn.addEventListener('click', function() {
1722
+ window.open('/__kitchen/api/contracts/openapi', '_blank');
1723
+ });
1724
+
1725
+ exportYamlBtn.addEventListener('click', function() {
1726
+ window.open('/__kitchen/api/contracts/openapi.yaml', '_blank');
1727
+ });
1728
+
1729
+ loadContracts();
1730
+
1731
+ })();
1732
+ `;