@forinda/kickjs-devtools 2.0.0 → 2.1.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/dist/index.d.mts +118 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +396 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +28 -11
- package/public/devtools/index.html +557 -41
- package/dist/adapter.d.ts +0 -109
- package/dist/adapter.d.ts.map +0 -1
- package/dist/devtools/index.html +0 -575
- package/dist/devtools/tailwind.global.js +0 -897
- package/dist/devtools/vue.global.min.js +0 -25
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -205
|
@@ -26,12 +26,18 @@
|
|
|
26
26
|
<header class="border-b border-slate-800 px-6 py-4">
|
|
27
27
|
<div class="flex items-center justify-between">
|
|
28
28
|
<div>
|
|
29
|
-
<h1 class="text-2xl font-bold text-kick-500"
|
|
29
|
+
<h1 class="text-2xl font-bold text-kick-500">⚡ KickJS DevTools</h1>
|
|
30
30
|
<p class="text-sm text-slate-500">Development introspection dashboard</p>
|
|
31
31
|
</div>
|
|
32
32
|
<div class="flex items-center gap-3 text-sm text-slate-500">
|
|
33
|
-
<
|
|
34
|
-
|
|
33
|
+
<template v-if="sseConnected">
|
|
34
|
+
<span class="inline-block w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
|
35
|
+
<span class="text-emerald-400 font-medium">Live</span>
|
|
36
|
+
</template>
|
|
37
|
+
<template v-else>
|
|
38
|
+
<span class="inline-block w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
|
|
39
|
+
<span class="text-amber-400 font-medium">Polling</span>
|
|
40
|
+
</template>
|
|
35
41
|
<span class="text-slate-600">·</span>
|
|
36
42
|
{{ lastUpdate }}
|
|
37
43
|
</div>
|
|
@@ -220,7 +226,7 @@
|
|
|
220
226
|
<td class="py-2 px-3 font-mono text-sm">{{ r.path }}</td>
|
|
221
227
|
<td class="py-2 px-3">{{ r.controller }}</td>
|
|
222
228
|
<td class="py-2 px-3 text-slate-400">{{ r.handler }}</td>
|
|
223
|
-
<td class="py-2 px-3 text-slate-500 text-xs">{{ r.middleware.length ? r.middleware.join(', ') : '
|
|
229
|
+
<td class="py-2 px-3 text-slate-500 text-xs">{{ r.middleware.length ? r.middleware.join(', ') : '—' }}</td>
|
|
224
230
|
</tr>
|
|
225
231
|
</tbody>
|
|
226
232
|
</table>
|
|
@@ -228,7 +234,7 @@
|
|
|
228
234
|
<!-- Pagination -->
|
|
229
235
|
<div v-if="filteredRoutes.length > routePageSize" class="flex items-center justify-between mt-4 pt-3 border-t border-slate-800">
|
|
230
236
|
<span class="text-sm text-slate-500">
|
|
231
|
-
Showing {{ (routePage - 1) * routePageSize + 1 }}
|
|
237
|
+
Showing {{ (routePage - 1) * routePageSize + 1 }}–{{ Math.min(routePage * routePageSize, filteredRoutes.length) }}
|
|
232
238
|
of {{ filteredRoutes.length }}
|
|
233
239
|
</span>
|
|
234
240
|
<div class="flex gap-1">
|
|
@@ -248,11 +254,37 @@
|
|
|
248
254
|
<!-- ─── Container Tab ────────────────────────────────────────── -->
|
|
249
255
|
<div v-show="activeTab === 'container'">
|
|
250
256
|
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
|
251
|
-
<!-- Search -->
|
|
252
|
-
<div class="
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
<!-- Search + Kind Filter + Scope Filter -->
|
|
258
|
+
<div class="flex flex-col gap-3 mb-4">
|
|
259
|
+
<div class="relative">
|
|
260
|
+
<svg class="absolute left-3 top-2.5 w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
261
|
+
<input v-model="containerSearch" type="text" placeholder="Search tokens..."
|
|
262
|
+
class="w-full bg-slate-800 border border-slate-700 rounded-lg pl-10 pr-4 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-kick-500">
|
|
263
|
+
</div>
|
|
264
|
+
<div class="flex flex-col sm:flex-row gap-3">
|
|
265
|
+
<div class="flex gap-1">
|
|
266
|
+
<span class="text-xs text-slate-500 self-center mr-1">Kind:</span>
|
|
267
|
+
<button v-for="k in ['ALL','controller','service','repository','other']" :key="k"
|
|
268
|
+
@click="containerKindFilter = k"
|
|
269
|
+
:class="[
|
|
270
|
+
'px-3 py-1.5 text-xs font-semibold rounded-lg transition-colors',
|
|
271
|
+
containerKindFilter === k
|
|
272
|
+
? kindFilterActiveClass(k)
|
|
273
|
+
: 'bg-slate-800 text-slate-400 border border-slate-700 hover:text-slate-200'
|
|
274
|
+
]">{{ k === 'ALL' ? 'ALL' : k }}</button>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="flex gap-1">
|
|
277
|
+
<span class="text-xs text-slate-500 self-center mr-1">Scope:</span>
|
|
278
|
+
<button v-for="s in ['ALL','singleton','transient','request']" :key="s"
|
|
279
|
+
@click="containerScopeFilter = s"
|
|
280
|
+
:class="[
|
|
281
|
+
'px-3 py-1.5 text-xs font-semibold rounded-lg transition-colors',
|
|
282
|
+
containerScopeFilter === s
|
|
283
|
+
? 'bg-kick-500/20 text-kick-500 border border-kick-500/30'
|
|
284
|
+
: 'bg-slate-800 text-slate-400 border border-slate-700 hover:text-slate-200'
|
|
285
|
+
]">{{ s === 'ALL' ? 'ALL' : s }}</button>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
256
288
|
</div>
|
|
257
289
|
|
|
258
290
|
<!-- Container Table -->
|
|
@@ -260,30 +292,96 @@
|
|
|
260
292
|
<table class="w-full text-sm">
|
|
261
293
|
<thead>
|
|
262
294
|
<tr class="border-b-2 border-slate-700">
|
|
295
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold w-6"></th>
|
|
263
296
|
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Token</th>
|
|
297
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Kind</th>
|
|
264
298
|
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Scope</th>
|
|
265
|
-
<th class="text-left py-2 px-3 text-slate-400 font-semibold">
|
|
299
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Status</th>
|
|
300
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Resolves</th>
|
|
301
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Deps</th>
|
|
266
302
|
</tr>
|
|
267
303
|
</thead>
|
|
268
304
|
<tbody>
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<
|
|
277
|
-
class="px-2 py-0.5 rounded text-xs font-semibold">{{ r.
|
|
278
|
-
|
|
279
|
-
|
|
305
|
+
<template v-for="r in paginatedContainer" :key="r.token">
|
|
306
|
+
<tr @click="toggleContainerRow(r.token)"
|
|
307
|
+
class="border-b border-slate-800 hover:bg-slate-800/50 cursor-pointer select-none">
|
|
308
|
+
<td class="py-2 px-3">
|
|
309
|
+
<svg :class="{'rotate-90': expandedContainerRows[r.token]}" class="w-3 h-3 text-slate-500 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
310
|
+
</td>
|
|
311
|
+
<td class="py-2 px-3 font-mono text-sm">{{ r.token }}</td>
|
|
312
|
+
<td class="py-2 px-3">
|
|
313
|
+
<span :class="kindBadgeClass(r.kind)" class="px-2 py-0.5 rounded text-xs font-semibold">{{ r.kind || 'unknown' }}</span>
|
|
314
|
+
</td>
|
|
315
|
+
<td class="py-2 px-3">
|
|
316
|
+
<span class="bg-slate-700/50 text-slate-300 px-2 py-0.5 rounded text-xs font-semibold">{{ r.scope }}</span>
|
|
317
|
+
</td>
|
|
318
|
+
<td class="py-2 px-3">
|
|
319
|
+
<span :class="statusBadgeClass(r)" class="px-2 py-0.5 rounded text-xs font-semibold">{{ statusLabel(r) }}</span>
|
|
320
|
+
</td>
|
|
321
|
+
<td class="py-2 px-3 tabular-nums">{{ r.resolveCount ?? 0 }}</td>
|
|
322
|
+
<td class="py-2 px-3 text-slate-500 text-xs">{{ r.dependencies?.length || 0 }}</td>
|
|
323
|
+
</tr>
|
|
324
|
+
<!-- Expanded detail row -->
|
|
325
|
+
<tr v-if="expandedContainerRows[r.token]">
|
|
326
|
+
<td colspan="7" class="bg-slate-800/30 px-6 py-4 border-b border-slate-800">
|
|
327
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
328
|
+
<div>
|
|
329
|
+
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">Dependencies</h4>
|
|
330
|
+
<div v-if="r.dependencies && r.dependencies.length" class="flex flex-wrap gap-1">
|
|
331
|
+
<button v-for="dep in r.dependencies" :key="dep"
|
|
332
|
+
@click.stop="openDetailModal(findRegistration(dep))"
|
|
333
|
+
class="bg-slate-700/50 text-kick-500 hover:bg-slate-700 px-2 py-0.5 rounded text-xs font-mono transition-colors">
|
|
334
|
+
{{ dep }}
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
<span v-else class="text-slate-600 text-sm">None</span>
|
|
338
|
+
</div>
|
|
339
|
+
<div>
|
|
340
|
+
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">Resolve Stats</h4>
|
|
341
|
+
<div class="space-y-1 text-sm">
|
|
342
|
+
<div class="flex justify-between">
|
|
343
|
+
<span class="text-slate-400">Resolve Count</span>
|
|
344
|
+
<span class="tabular-nums">{{ r.resolveCount ?? 0 }}</span>
|
|
345
|
+
</div>
|
|
346
|
+
<div v-if="r.firstResolved" class="flex justify-between">
|
|
347
|
+
<span class="text-slate-400">First Resolved</span>
|
|
348
|
+
<span class="text-xs">{{ new Date(r.firstResolved).toLocaleTimeString() }}</span>
|
|
349
|
+
</div>
|
|
350
|
+
<div v-if="r.lastResolved" class="flex justify-between">
|
|
351
|
+
<span class="text-slate-400">Last Resolved</span>
|
|
352
|
+
<span class="text-xs">{{ new Date(r.lastResolved).toLocaleTimeString() }}</span>
|
|
353
|
+
</div>
|
|
354
|
+
<div v-if="r.resolveDurationMs != null" class="flex justify-between">
|
|
355
|
+
<span class="text-slate-400">Duration</span>
|
|
356
|
+
<span class="tabular-nums">{{ r.resolveDurationMs.toFixed(2) }}ms</span>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
<div>
|
|
361
|
+
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">PostConstruct</h4>
|
|
362
|
+
<span :class="{
|
|
363
|
+
'bg-emerald-900/50 text-emerald-300': r.postConstructStatus === 'done',
|
|
364
|
+
'bg-red-900/50 text-red-300': r.postConstructStatus === 'failed',
|
|
365
|
+
'bg-slate-700/50 text-slate-400': !r.postConstructStatus || r.postConstructStatus === 'none',
|
|
366
|
+
}" class="px-2 py-0.5 rounded text-xs font-semibold">{{ r.postConstructStatus || 'none' }}</span>
|
|
367
|
+
</div>
|
|
368
|
+
<div class="flex items-end">
|
|
369
|
+
<button @click.stop="openDetailModal(r)"
|
|
370
|
+
class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-kick-500/20 text-kick-500 border border-kick-500/30 hover:bg-kick-500/30 transition-colors">
|
|
371
|
+
View Full Details
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</td>
|
|
376
|
+
</tr>
|
|
377
|
+
</template>
|
|
280
378
|
</tbody>
|
|
281
379
|
</table>
|
|
282
380
|
|
|
283
381
|
<!-- Pagination -->
|
|
284
382
|
<div v-if="filteredContainer.length > containerPageSize" class="flex items-center justify-between mt-4 pt-3 border-t border-slate-800">
|
|
285
383
|
<span class="text-sm text-slate-500">
|
|
286
|
-
Showing {{ (containerPage - 1) * containerPageSize + 1 }}
|
|
384
|
+
Showing {{ (containerPage - 1) * containerPageSize + 1 }}–{{ Math.min(containerPage * containerPageSize, filteredContainer.length) }}
|
|
287
385
|
of {{ filteredContainer.length }}
|
|
288
386
|
</span>
|
|
289
387
|
<div class="flex gap-1">
|
|
@@ -296,7 +394,126 @@
|
|
|
296
394
|
</div>
|
|
297
395
|
</div>
|
|
298
396
|
</div>
|
|
299
|
-
<p v-else class="text-slate-600 italic">{{ containerSearch ? 'No matching tokens' : 'No DI registrations' }}</p>
|
|
397
|
+
<p v-else class="text-slate-600 italic">{{ containerSearch || containerKindFilter !== 'ALL' || containerScopeFilter !== 'ALL' ? 'No matching tokens' : 'No DI registrations' }}</p>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<!-- ─── Graph Tab ────────────────────────────────────────────── -->
|
|
402
|
+
<div v-show="activeTab === 'graph'">
|
|
403
|
+
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
|
404
|
+
<div class="flex items-center justify-between mb-4">
|
|
405
|
+
<h2 class="text-sm font-semibold text-slate-400">Dependency Graph</h2>
|
|
406
|
+
<button @click="fetchGraph" class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-slate-800 text-slate-400 border border-slate-700 hover:text-slate-200 transition-colors">
|
|
407
|
+
Refresh
|
|
408
|
+
</button>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<template v-if="graphData">
|
|
412
|
+
<div v-if="graphData.nodes && graphData.nodes.length" class="space-y-6">
|
|
413
|
+
<!-- Controllers -->
|
|
414
|
+
<div v-if="graphNodesByKind.controllers.length">
|
|
415
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-violet-400 mb-2">Controllers</h3>
|
|
416
|
+
<div class="space-y-1">
|
|
417
|
+
<div v-for="node in graphNodesByKind.controllers" :key="node.id">
|
|
418
|
+
<button @click="openDetailModalFromGraph(node)"
|
|
419
|
+
class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800/50 transition-colors group">
|
|
420
|
+
<div class="flex items-center gap-2">
|
|
421
|
+
<span class="bg-violet-900/50 text-violet-300 px-2 py-0.5 rounded text-xs font-semibold">ctrl</span>
|
|
422
|
+
<span class="font-mono text-sm text-slate-200">{{ node.id }}</span>
|
|
423
|
+
<span v-if="node.scope" class="bg-slate-700/50 text-slate-400 px-1.5 py-0.5 rounded text-xs">{{ node.scope }}</span>
|
|
424
|
+
<span v-if="node.resolveCount" class="text-slate-600 text-xs ml-auto">{{ node.resolveCount }} resolves</span>
|
|
425
|
+
</div>
|
|
426
|
+
<!-- Dependencies (edges from this node) -->
|
|
427
|
+
<div v-if="graphEdgesFrom(node.id).length" class="ml-8 mt-1 space-y-0.5">
|
|
428
|
+
<div v-for="edge in graphEdgesFrom(node.id)" :key="edge.to" class="flex items-center gap-2 text-xs text-slate-500">
|
|
429
|
+
<svg class="w-3 h-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5-5 5M6 12h12"/></svg>
|
|
430
|
+
<span :class="graphNodeBadgeClass(edge.to)" class="px-1.5 py-0.5 rounded text-xs font-semibold">{{ graphNodeKindLabel(edge.to) }}</span>
|
|
431
|
+
<span class="font-mono">{{ edge.to }}</span>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</button>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<!-- Services -->
|
|
440
|
+
<div v-if="graphNodesByKind.services.length">
|
|
441
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-blue-400 mb-2">Services</h3>
|
|
442
|
+
<div class="space-y-1">
|
|
443
|
+
<div v-for="node in graphNodesByKind.services" :key="node.id">
|
|
444
|
+
<button @click="openDetailModalFromGraph(node)"
|
|
445
|
+
class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800/50 transition-colors group">
|
|
446
|
+
<div class="flex items-center gap-2">
|
|
447
|
+
<span class="bg-blue-900/50 text-blue-300 px-2 py-0.5 rounded text-xs font-semibold">svc</span>
|
|
448
|
+
<span class="font-mono text-sm text-slate-200">{{ node.id }}</span>
|
|
449
|
+
<span v-if="node.scope" class="bg-slate-700/50 text-slate-400 px-1.5 py-0.5 rounded text-xs">{{ node.scope }}</span>
|
|
450
|
+
<span v-if="node.resolveCount" class="text-slate-600 text-xs ml-auto">{{ node.resolveCount }} resolves</span>
|
|
451
|
+
</div>
|
|
452
|
+
<div v-if="graphEdgesFrom(node.id).length" class="ml-8 mt-1 space-y-0.5">
|
|
453
|
+
<div v-for="edge in graphEdgesFrom(node.id)" :key="edge.to" class="flex items-center gap-2 text-xs text-slate-500">
|
|
454
|
+
<svg class="w-3 h-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5-5 5M6 12h12"/></svg>
|
|
455
|
+
<span :class="graphNodeBadgeClass(edge.to)" class="px-1.5 py-0.5 rounded text-xs font-semibold">{{ graphNodeKindLabel(edge.to) }}</span>
|
|
456
|
+
<span class="font-mono">{{ edge.to }}</span>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<!-- Repositories -->
|
|
465
|
+
<div v-if="graphNodesByKind.repositories.length">
|
|
466
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-teal-400 mb-2">Repositories</h3>
|
|
467
|
+
<div class="space-y-1">
|
|
468
|
+
<div v-for="node in graphNodesByKind.repositories" :key="node.id">
|
|
469
|
+
<button @click="openDetailModalFromGraph(node)"
|
|
470
|
+
class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800/50 transition-colors group">
|
|
471
|
+
<div class="flex items-center gap-2">
|
|
472
|
+
<span class="bg-teal-900/50 text-teal-300 px-2 py-0.5 rounded text-xs font-semibold">repo</span>
|
|
473
|
+
<span class="font-mono text-sm text-slate-200">{{ node.id }}</span>
|
|
474
|
+
<span v-if="node.scope" class="bg-slate-700/50 text-slate-400 px-1.5 py-0.5 rounded text-xs">{{ node.scope }}</span>
|
|
475
|
+
<span v-if="node.resolveCount" class="text-slate-600 text-xs ml-auto">{{ node.resolveCount }} resolves</span>
|
|
476
|
+
</div>
|
|
477
|
+
<div v-if="graphEdgesFrom(node.id).length" class="ml-8 mt-1 space-y-0.5">
|
|
478
|
+
<div v-for="edge in graphEdgesFrom(node.id)" :key="edge.to" class="flex items-center gap-2 text-xs text-slate-500">
|
|
479
|
+
<svg class="w-3 h-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5-5 5M6 12h12"/></svg>
|
|
480
|
+
<span :class="graphNodeBadgeClass(edge.to)" class="px-1.5 py-0.5 rounded text-xs font-semibold">{{ graphNodeKindLabel(edge.to) }}</span>
|
|
481
|
+
<span class="font-mono">{{ edge.to }}</span>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<!-- Other -->
|
|
490
|
+
<div v-if="graphNodesByKind.other.length">
|
|
491
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-2">Other</h3>
|
|
492
|
+
<div class="space-y-1">
|
|
493
|
+
<div v-for="node in graphNodesByKind.other" :key="node.id">
|
|
494
|
+
<button @click="openDetailModalFromGraph(node)"
|
|
495
|
+
class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800/50 transition-colors group">
|
|
496
|
+
<div class="flex items-center gap-2">
|
|
497
|
+
<span class="bg-slate-700/50 text-slate-300 px-2 py-0.5 rounded text-xs font-semibold">other</span>
|
|
498
|
+
<span class="font-mono text-sm text-slate-200">{{ node.id }}</span>
|
|
499
|
+
<span v-if="node.scope" class="bg-slate-700/50 text-slate-400 px-1.5 py-0.5 rounded text-xs">{{ node.scope }}</span>
|
|
500
|
+
<span v-if="node.resolveCount" class="text-slate-600 text-xs ml-auto">{{ node.resolveCount }} resolves</span>
|
|
501
|
+
</div>
|
|
502
|
+
<div v-if="graphEdgesFrom(node.id).length" class="ml-8 mt-1 space-y-0.5">
|
|
503
|
+
<div v-for="edge in graphEdgesFrom(node.id)" :key="edge.to" class="flex items-center gap-2 text-xs text-slate-500">
|
|
504
|
+
<svg class="w-3 h-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5-5 5M6 12h12"/></svg>
|
|
505
|
+
<span :class="graphNodeBadgeClass(edge.to)" class="px-1.5 py-0.5 rounded text-xs font-semibold">{{ graphNodeKindLabel(edge.to) }}</span>
|
|
506
|
+
<span class="font-mono">{{ edge.to }}</span>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
<p v-else class="text-slate-600 italic">No nodes in graph</p>
|
|
515
|
+
</template>
|
|
516
|
+
<p v-else class="text-slate-600 italic">Loading graph...</p>
|
|
300
517
|
</div>
|
|
301
518
|
</div>
|
|
302
519
|
|
|
@@ -347,11 +564,90 @@
|
|
|
347
564
|
|
|
348
565
|
</main>
|
|
349
566
|
|
|
350
|
-
<!--
|
|
567
|
+
<!-- ─── Detail Modal ───────────────────────────────────────────── -->
|
|
568
|
+
<div v-if="detailModal" class="fixed inset-0 bg-slate-950/80 flex items-center justify-center z-50" @click.self="detailModal = null">
|
|
569
|
+
<div class="bg-slate-900 border border-slate-700 rounded-2xl p-6 max-w-lg w-full mx-4 shadow-2xl max-h-[90vh] overflow-y-auto">
|
|
570
|
+
<!-- Header -->
|
|
571
|
+
<div class="flex items-start justify-between mb-4">
|
|
572
|
+
<div>
|
|
573
|
+
<h2 class="text-lg font-bold font-mono text-slate-200 mb-1">{{ detailModal.token || detailModal.id }}</h2>
|
|
574
|
+
<div class="flex gap-2">
|
|
575
|
+
<span :class="kindBadgeClass(detailModal.kind)" class="px-2 py-0.5 rounded text-xs font-semibold">{{ detailModal.kind || 'unknown' }}</span>
|
|
576
|
+
<span class="bg-slate-700/50 text-slate-300 px-2 py-0.5 rounded text-xs font-semibold">{{ detailModal.scope || 'singleton' }}</span>
|
|
577
|
+
<span :class="statusBadgeClass(detailModal)" class="px-2 py-0.5 rounded text-xs font-semibold">{{ statusLabel(detailModal) }}</span>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
<button @click="detailModal = null" class="text-slate-500 hover:text-slate-300 transition-colors p-1">
|
|
581
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
582
|
+
</button>
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
<!-- Dependencies -->
|
|
586
|
+
<div class="mb-4">
|
|
587
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">Dependencies</h3>
|
|
588
|
+
<div v-if="detailModalDependencies.length" class="flex flex-wrap gap-1">
|
|
589
|
+
<button v-for="dep in detailModalDependencies" :key="dep"
|
|
590
|
+
@click="openDetailModal(findRegistration(dep))"
|
|
591
|
+
class="bg-slate-800 text-kick-500 hover:bg-slate-700 px-2.5 py-1 rounded text-xs font-mono transition-colors border border-slate-700">
|
|
592
|
+
{{ dep }}
|
|
593
|
+
</button>
|
|
594
|
+
</div>
|
|
595
|
+
<span v-else class="text-slate-600 text-sm">None</span>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<!-- Dependents -->
|
|
599
|
+
<div class="mb-4">
|
|
600
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">Dependents</h3>
|
|
601
|
+
<div v-if="detailModalDependents.length" class="flex flex-wrap gap-1">
|
|
602
|
+
<button v-for="dep in detailModalDependents" :key="dep"
|
|
603
|
+
@click="openDetailModal(findRegistration(dep))"
|
|
604
|
+
class="bg-slate-800 text-amber-400 hover:bg-slate-700 px-2.5 py-1 rounded text-xs font-mono transition-colors border border-slate-700">
|
|
605
|
+
{{ dep }}
|
|
606
|
+
</button>
|
|
607
|
+
</div>
|
|
608
|
+
<span v-else class="text-slate-600 text-sm">None</span>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
<!-- Resolve Stats -->
|
|
612
|
+
<div class="mb-4">
|
|
613
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">Resolve Stats</h3>
|
|
614
|
+
<div class="bg-slate-800/50 rounded-lg border border-slate-700 p-3 space-y-2 text-sm">
|
|
615
|
+
<div class="flex justify-between">
|
|
616
|
+
<span class="text-slate-400">Resolve Count</span>
|
|
617
|
+
<span class="font-semibold tabular-nums">{{ detailModal.resolveCount ?? 0 }}</span>
|
|
618
|
+
</div>
|
|
619
|
+
<div v-if="detailModal.firstResolved" class="flex justify-between">
|
|
620
|
+
<span class="text-slate-400">First Resolved</span>
|
|
621
|
+
<span>{{ new Date(detailModal.firstResolved).toLocaleString() }}</span>
|
|
622
|
+
</div>
|
|
623
|
+
<div v-if="detailModal.lastResolved" class="flex justify-between">
|
|
624
|
+
<span class="text-slate-400">Last Resolved</span>
|
|
625
|
+
<span>{{ new Date(detailModal.lastResolved).toLocaleString() }}</span>
|
|
626
|
+
</div>
|
|
627
|
+
<div v-if="detailModal.resolveDurationMs != null" class="flex justify-between">
|
|
628
|
+
<span class="text-slate-400">Resolution Duration</span>
|
|
629
|
+
<span class="tabular-nums">{{ detailModal.resolveDurationMs.toFixed(2) }}ms</span>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<!-- PostConstruct -->
|
|
635
|
+
<div>
|
|
636
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">PostConstruct</h3>
|
|
637
|
+
<span :class="{
|
|
638
|
+
'bg-emerald-900/50 text-emerald-300': detailModal.postConstructStatus === 'done',
|
|
639
|
+
'bg-red-900/50 text-red-300': detailModal.postConstructStatus === 'failed',
|
|
640
|
+
'bg-slate-700/50 text-slate-400': !detailModal.postConstructStatus || detailModal.postConstructStatus === 'none',
|
|
641
|
+
}" class="px-2 py-0.5 rounded text-xs font-semibold">{{ detailModal.postConstructStatus || 'none' }}</span>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<!-- Auth Gate -->
|
|
351
647
|
<div v-if="showAuthGate" class="fixed inset-0 bg-slate-950/90 flex items-center justify-center z-50">
|
|
352
648
|
<div class="bg-slate-900 border border-slate-700 rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl">
|
|
353
649
|
<div class="text-center mb-6">
|
|
354
|
-
<h2 class="text-xl font-bold text-kick-500 mb-2"
|
|
650
|
+
<h2 class="text-xl font-bold text-kick-500 mb-2">⚡ KickJS DevTools</h2>
|
|
355
651
|
<p class="text-sm text-slate-400">Enter your DevTools token to continue</p>
|
|
356
652
|
<p class="text-xs text-slate-600 mt-1">Token is shown in your server console on startup</p>
|
|
357
653
|
</div>
|
|
@@ -378,15 +674,12 @@ const BASE = document.body.dataset.base || '/_debug'
|
|
|
378
674
|
function getToken() {
|
|
379
675
|
const fromUrl = new URLSearchParams(window.location.search).get('token')
|
|
380
676
|
if (fromUrl) {
|
|
381
|
-
// Save to cookie for future visits
|
|
382
677
|
document.cookie = `kickjs_devtools_token=${fromUrl}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`
|
|
383
|
-
// Clean URL (remove token from query string)
|
|
384
678
|
const url = new URL(window.location)
|
|
385
679
|
url.searchParams.delete('token')
|
|
386
680
|
window.history.replaceState({}, '', url.toString())
|
|
387
681
|
return fromUrl
|
|
388
682
|
}
|
|
389
|
-
// Try cookie
|
|
390
683
|
const match = document.cookie.match(/kickjs_devtools_token=([^;]+)/)
|
|
391
684
|
return match ? match[1] : ''
|
|
392
685
|
}
|
|
@@ -396,11 +689,11 @@ let TOKEN = getToken()
|
|
|
396
689
|
createApp({
|
|
397
690
|
setup() {
|
|
398
691
|
const activeTab = ref('overview')
|
|
399
|
-
const pollInterval = ref(30000)
|
|
400
692
|
const lastUpdate = ref('loading...')
|
|
401
693
|
const showAuthGate = ref(false)
|
|
402
694
|
const tokenInput = ref('')
|
|
403
695
|
const authError = ref('')
|
|
696
|
+
const sseConnected = ref(false)
|
|
404
697
|
|
|
405
698
|
// Data
|
|
406
699
|
const health = ref(null)
|
|
@@ -409,6 +702,10 @@ createApp({
|
|
|
409
702
|
const routeData = ref(null)
|
|
410
703
|
const containerData = ref(null)
|
|
411
704
|
const queueData = ref(null)
|
|
705
|
+
const graphData = ref(null)
|
|
706
|
+
|
|
707
|
+
// Detail modal
|
|
708
|
+
const detailModal = ref(null)
|
|
412
709
|
|
|
413
710
|
// Collapsible state
|
|
414
711
|
const collapsed = reactive({
|
|
@@ -449,16 +746,28 @@ createApp({
|
|
|
449
746
|
return filteredRoutes.value.slice(start, start + routePageSize.value)
|
|
450
747
|
})
|
|
451
748
|
|
|
452
|
-
// Reset page when search/filter changes
|
|
453
749
|
watch([routeSearch, routeMethodFilter], () => { routePage.value = 1 })
|
|
454
750
|
|
|
455
|
-
// Container search + pagination
|
|
751
|
+
// Container search + filter + pagination
|
|
456
752
|
const containerSearch = ref('')
|
|
753
|
+
const containerKindFilter = ref('ALL')
|
|
754
|
+
const containerScopeFilter = ref('ALL')
|
|
457
755
|
const containerPage = ref(1)
|
|
458
756
|
const containerPageSize = ref(20)
|
|
757
|
+
const expandedContainerRows = reactive({})
|
|
459
758
|
|
|
460
759
|
const filteredContainer = computed(() => {
|
|
461
760
|
let regs = containerData.value?.registrations || []
|
|
761
|
+
if (containerKindFilter.value !== 'ALL') {
|
|
762
|
+
if (containerKindFilter.value === 'other') {
|
|
763
|
+
regs = regs.filter(r => !['controller', 'service', 'repository'].includes(r.kind))
|
|
764
|
+
} else {
|
|
765
|
+
regs = regs.filter(r => r.kind === containerKindFilter.value)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (containerScopeFilter.value !== 'ALL') {
|
|
769
|
+
regs = regs.filter(r => r.scope === containerScopeFilter.value)
|
|
770
|
+
}
|
|
462
771
|
if (containerSearch.value) {
|
|
463
772
|
const q = containerSearch.value.toLowerCase()
|
|
464
773
|
regs = regs.filter(r => r.token.toLowerCase().includes(q))
|
|
@@ -472,13 +781,134 @@ createApp({
|
|
|
472
781
|
return filteredContainer.value.slice(start, start + containerPageSize.value)
|
|
473
782
|
})
|
|
474
783
|
|
|
475
|
-
watch(containerSearch, () => { containerPage.value = 1 })
|
|
784
|
+
watch([containerSearch, containerKindFilter, containerScopeFilter], () => { containerPage.value = 1 })
|
|
785
|
+
|
|
786
|
+
function toggleContainerRow(token) {
|
|
787
|
+
expandedContainerRows[token] = !expandedContainerRows[token]
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Helper to find a registration by token
|
|
791
|
+
function findRegistration(token) {
|
|
792
|
+
const regs = containerData.value?.registrations || []
|
|
793
|
+
return regs.find(r => r.token === token) || { token: token, kind: 'unknown', scope: 'unknown' }
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Detail modal helpers
|
|
797
|
+
function openDetailModal(reg) {
|
|
798
|
+
if (reg) detailModal.value = reg
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function openDetailModalFromGraph(node) {
|
|
802
|
+
const reg = findRegistration(node.id)
|
|
803
|
+
// Merge graph node data into the registration
|
|
804
|
+
detailModal.value = {
|
|
805
|
+
...reg,
|
|
806
|
+
token: reg.token || node.id,
|
|
807
|
+
kind: reg.kind || node.kind,
|
|
808
|
+
scope: reg.scope || node.scope,
|
|
809
|
+
resolveCount: reg.resolveCount ?? node.resolveCount,
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const detailModalDependencies = computed(() => {
|
|
814
|
+
if (!detailModal.value) return []
|
|
815
|
+
// From container registration
|
|
816
|
+
if (detailModal.value.dependencies && detailModal.value.dependencies.length) {
|
|
817
|
+
return detailModal.value.dependencies
|
|
818
|
+
}
|
|
819
|
+
// From graph edges
|
|
820
|
+
if (graphData.value && graphData.value.edges) {
|
|
821
|
+
const token = detailModal.value.token || detailModal.value.id
|
|
822
|
+
return graphData.value.edges.filter(e => e.from === token).map(e => e.to)
|
|
823
|
+
}
|
|
824
|
+
return []
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
const detailModalDependents = computed(() => {
|
|
828
|
+
if (!detailModal.value) return []
|
|
829
|
+
const token = detailModal.value.token || detailModal.value.id
|
|
830
|
+
// From container: find all registrations that list this token as a dependency
|
|
831
|
+
const regs = containerData.value?.registrations || []
|
|
832
|
+
const fromContainer = regs
|
|
833
|
+
.filter(r => r.dependencies && r.dependencies.includes(token))
|
|
834
|
+
.map(r => r.token)
|
|
835
|
+
if (fromContainer.length) return fromContainer
|
|
836
|
+
// From graph edges (reverse)
|
|
837
|
+
if (graphData.value && graphData.value.edges) {
|
|
838
|
+
return graphData.value.edges.filter(e => e.to === token).map(e => e.from)
|
|
839
|
+
}
|
|
840
|
+
return []
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
// Kind/status badge helpers
|
|
844
|
+
function kindBadgeClass(kind) {
|
|
845
|
+
return {
|
|
846
|
+
'bg-violet-900/50 text-violet-300': kind === 'controller',
|
|
847
|
+
'bg-blue-900/50 text-blue-300': kind === 'service',
|
|
848
|
+
'bg-teal-900/50 text-teal-300': kind === 'repository',
|
|
849
|
+
'bg-slate-700/50 text-slate-300': !['controller', 'service', 'repository'].includes(kind),
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function statusBadgeClass(r) {
|
|
854
|
+
return {
|
|
855
|
+
'bg-emerald-900/50 text-emerald-300': r.instantiated,
|
|
856
|
+
'bg-amber-900/50 text-amber-300': !r.instantiated && r.postConstructStatus !== 'failed',
|
|
857
|
+
'bg-red-900/50 text-red-300': r.postConstructStatus === 'failed',
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function statusLabel(r) {
|
|
862
|
+
return r.instantiated ? 'active' : (r.postConstructStatus === 'failed' ? 'failed' : 'registered')
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function kindFilterActiveClass(k) {
|
|
866
|
+
if (k === 'ALL') return 'bg-kick-500/20 text-kick-500 border border-kick-500/30'
|
|
867
|
+
if (k === 'controller') return 'bg-violet-900/50 text-violet-300 border border-violet-700/50'
|
|
868
|
+
if (k === 'service') return 'bg-blue-900/50 text-blue-300 border border-blue-700/50'
|
|
869
|
+
if (k === 'repository') return 'bg-teal-900/50 text-teal-300 border border-teal-700/50'
|
|
870
|
+
return 'bg-slate-700/50 text-slate-300 border border-slate-600/50'
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Graph helpers
|
|
874
|
+
const graphNodesByKind = computed(() => {
|
|
875
|
+
const nodes = graphData.value?.nodes || []
|
|
876
|
+
return {
|
|
877
|
+
controllers: nodes.filter(n => n.kind === 'controller'),
|
|
878
|
+
services: nodes.filter(n => n.kind === 'service'),
|
|
879
|
+
repositories: nodes.filter(n => n.kind === 'repository'),
|
|
880
|
+
other: nodes.filter(n => !['controller', 'service', 'repository'].includes(n.kind)),
|
|
881
|
+
}
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
function graphEdgesFrom(nodeId) {
|
|
885
|
+
return (graphData.value?.edges || []).filter(e => e.from === nodeId)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function graphNodeKindLabel(nodeId) {
|
|
889
|
+
const node = (graphData.value?.nodes || []).find(n => n.id === nodeId)
|
|
890
|
+
if (!node) return '?'
|
|
891
|
+
if (node.kind === 'controller') return 'ctrl'
|
|
892
|
+
if (node.kind === 'service') return 'svc'
|
|
893
|
+
if (node.kind === 'repository') return 'repo'
|
|
894
|
+
return 'other'
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function graphNodeBadgeClass(nodeId) {
|
|
898
|
+
const node = (graphData.value?.nodes || []).find(n => n.id === nodeId)
|
|
899
|
+
if (!node) return 'bg-slate-700/50 text-slate-300'
|
|
900
|
+
if (node.kind === 'controller') return 'bg-violet-900/50 text-violet-300'
|
|
901
|
+
if (node.kind === 'service') return 'bg-blue-900/50 text-blue-300'
|
|
902
|
+
if (node.kind === 'repository') return 'bg-teal-900/50 text-teal-300'
|
|
903
|
+
return 'bg-slate-700/50 text-slate-300'
|
|
904
|
+
}
|
|
476
905
|
|
|
477
906
|
// Tab badges
|
|
478
907
|
const tabs = computed(() => [
|
|
479
908
|
{ id: 'overview', label: 'Overview' },
|
|
480
909
|
{ id: 'routes', label: 'Routes', count: routeData.value?.routes?.length },
|
|
481
910
|
{ id: 'container', label: 'Container', count: containerData.value?.count },
|
|
911
|
+
{ id: 'graph', label: 'Graph', count: graphData.value?.nodes?.length },
|
|
482
912
|
{ id: 'queues', label: 'Queues', count: queueData.value?.queues?.length },
|
|
483
913
|
])
|
|
484
914
|
|
|
@@ -500,7 +930,6 @@ createApp({
|
|
|
500
930
|
const testToken = tokenInput.value.trim()
|
|
501
931
|
if (!testToken) { authError.value = 'Token is required'; return }
|
|
502
932
|
|
|
503
|
-
// Test the token
|
|
504
933
|
try {
|
|
505
934
|
const r = await fetch(BASE + '/health', { headers: { 'x-devtools-token': testToken } })
|
|
506
935
|
if (r.status === 403) {
|
|
@@ -512,11 +941,11 @@ createApp({
|
|
|
512
941
|
return
|
|
513
942
|
}
|
|
514
943
|
|
|
515
|
-
// Save to cookie and reload
|
|
516
944
|
TOKEN = testToken
|
|
517
945
|
document.cookie = `kickjs_devtools_token=${testToken}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`
|
|
518
946
|
showAuthGate.value = false
|
|
519
947
|
refresh()
|
|
948
|
+
connectSSE()
|
|
520
949
|
}
|
|
521
950
|
|
|
522
951
|
async function refresh() {
|
|
@@ -537,6 +966,11 @@ createApp({
|
|
|
537
966
|
lastUpdate.value = new Date().toLocaleTimeString()
|
|
538
967
|
}
|
|
539
968
|
|
|
969
|
+
async function fetchGraph() {
|
|
970
|
+
const g = await fetchJSON('/graph')
|
|
971
|
+
if (g) graphData.value = g
|
|
972
|
+
}
|
|
973
|
+
|
|
540
974
|
function formatDuration(seconds) {
|
|
541
975
|
if (seconds < 60) return seconds + 's'
|
|
542
976
|
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's'
|
|
@@ -552,19 +986,101 @@ createApp({
|
|
|
552
986
|
}[method] || 'text-slate-400'
|
|
553
987
|
}
|
|
554
988
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
989
|
+
// SSE connection
|
|
990
|
+
let eventSource = null
|
|
991
|
+
let pollTimer = null
|
|
992
|
+
|
|
993
|
+
function connectSSE() {
|
|
994
|
+
if (eventSource) {
|
|
995
|
+
eventSource.close()
|
|
996
|
+
eventSource = null
|
|
997
|
+
}
|
|
998
|
+
if (pollTimer) {
|
|
999
|
+
clearInterval(pollTimer)
|
|
1000
|
+
pollTimer = null
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const sseUrl = `${BASE}/stream${TOKEN ? '?token=' + TOKEN : ''}`
|
|
1004
|
+
eventSource = new EventSource(sseUrl)
|
|
1005
|
+
|
|
1006
|
+
eventSource.onopen = () => {
|
|
1007
|
+
sseConnected.value = true
|
|
1008
|
+
if (pollTimer) {
|
|
1009
|
+
clearInterval(pollTimer)
|
|
1010
|
+
pollTimer = null
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
eventSource.onmessage = (event) => {
|
|
1015
|
+
try {
|
|
1016
|
+
const data = JSON.parse(event.data)
|
|
1017
|
+
if (data.type === 'metrics') {
|
|
1018
|
+
if (metrics.value) {
|
|
1019
|
+
metrics.value.requests = data.requestCount
|
|
1020
|
+
metrics.value.serverErrors = data.errorCount
|
|
1021
|
+
metrics.value.clientErrors = data.clientErrorCount
|
|
1022
|
+
metrics.value.errorRate = data.errorRate
|
|
1023
|
+
}
|
|
1024
|
+
if (health.value) {
|
|
1025
|
+
health.value.uptime = data.uptimeSeconds
|
|
1026
|
+
health.value.errorRate = data.errorRate
|
|
1027
|
+
}
|
|
1028
|
+
lastUpdate.value = new Date().toLocaleTimeString()
|
|
1029
|
+
}
|
|
1030
|
+
if (data.type === 'container') {
|
|
1031
|
+
fetchJSON('/container').then(c => { if (c) containerData.value = c })
|
|
1032
|
+
fetchJSON('/routes').then(r => { if (r) routeData.value = r })
|
|
1033
|
+
fetchGraph()
|
|
1034
|
+
}
|
|
1035
|
+
} catch {}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
eventSource.onerror = () => {
|
|
1039
|
+
sseConnected.value = false
|
|
1040
|
+
if (eventSource) {
|
|
1041
|
+
eventSource.close()
|
|
1042
|
+
eventSource = null
|
|
1043
|
+
}
|
|
1044
|
+
// Fallback to polling
|
|
1045
|
+
if (!pollTimer) {
|
|
1046
|
+
pollTimer = setInterval(refresh, 5000)
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
onMounted(() => {
|
|
1052
|
+
refresh()
|
|
1053
|
+
fetchGraph()
|
|
1054
|
+
connectSSE()
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
onUnmounted(() => {
|
|
1058
|
+
if (eventSource) {
|
|
1059
|
+
eventSource.close()
|
|
1060
|
+
eventSource = null
|
|
1061
|
+
}
|
|
1062
|
+
if (pollTimer) {
|
|
1063
|
+
clearInterval(pollTimer)
|
|
1064
|
+
pollTimer = null
|
|
1065
|
+
}
|
|
1066
|
+
})
|
|
558
1067
|
|
|
559
1068
|
return {
|
|
560
|
-
activeTab, tabs,
|
|
561
|
-
health, metrics, ws, routeData, containerData, queueData,
|
|
1069
|
+
activeTab, tabs, lastUpdate, sseConnected,
|
|
1070
|
+
health, metrics, ws, routeData, containerData, queueData, graphData,
|
|
562
1071
|
collapsed, toggle,
|
|
563
1072
|
showAuthGate, tokenInput, authError, submitToken,
|
|
564
1073
|
routeSearch, routeMethodFilter, routePage, routePageSize,
|
|
565
1074
|
filteredRoutes, paginatedRoutes, routeTotalPages,
|
|
566
|
-
containerSearch,
|
|
1075
|
+
containerSearch, containerKindFilter, containerScopeFilter,
|
|
1076
|
+
containerPage, containerPageSize,
|
|
567
1077
|
filteredContainer, paginatedContainer, containerTotalPages,
|
|
1078
|
+
expandedContainerRows, toggleContainerRow,
|
|
1079
|
+
detailModal, detailModalDependencies, detailModalDependents,
|
|
1080
|
+
openDetailModal, openDetailModalFromGraph, findRegistration,
|
|
1081
|
+
graphData, graphNodesByKind, graphEdgesFrom, graphNodeKindLabel, graphNodeBadgeClass,
|
|
1082
|
+
fetchGraph,
|
|
1083
|
+
kindBadgeClass, statusBadgeClass, statusLabel, kindFilterActiveClass,
|
|
568
1084
|
formatDuration, methodColor,
|
|
569
1085
|
}
|
|
570
1086
|
}
|