@forinda/kickjs-devtools 1.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/LICENSE +21 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +323 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
- package/public/devtools/index.html +575 -0
- package/public/devtools/tailwind.global.js +897 -0
- package/public/devtools/vue.global.min.js +25 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>KickJS DevTools</title>
|
|
7
|
+
<script src="tailwind.global.js"></script>
|
|
8
|
+
<script src="vue.global.min.js"></script>
|
|
9
|
+
<script>
|
|
10
|
+
tailwind.config = {
|
|
11
|
+
darkMode: 'class',
|
|
12
|
+
theme: {
|
|
13
|
+
extend: {
|
|
14
|
+
colors: {
|
|
15
|
+
kick: { 50: '#f0f9ff', 500: '#38bdf8', 600: '#0284c7', 900: '#0c4a6e' }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
</head>
|
|
22
|
+
<body class="bg-slate-950 text-slate-200 min-h-screen">
|
|
23
|
+
|
|
24
|
+
<div id="app">
|
|
25
|
+
<!-- Header -->
|
|
26
|
+
<header class="border-b border-slate-800 px-6 py-4">
|
|
27
|
+
<div class="flex items-center justify-between">
|
|
28
|
+
<div>
|
|
29
|
+
<h1 class="text-2xl font-bold text-kick-500">⚡ KickJS DevTools</h1>
|
|
30
|
+
<p class="text-sm text-slate-500">Development introspection dashboard</p>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="flex items-center gap-3 text-sm text-slate-500">
|
|
33
|
+
<span class="inline-block w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
|
34
|
+
Auto-refresh {{ pollInterval / 1000 }}s
|
|
35
|
+
<span class="text-slate-600">·</span>
|
|
36
|
+
{{ lastUpdate }}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Tabs -->
|
|
41
|
+
<nav class="flex gap-1 mt-4">
|
|
42
|
+
<button v-for="t in tabs" :key="t.id"
|
|
43
|
+
@click="activeTab = t.id"
|
|
44
|
+
:class="[
|
|
45
|
+
'px-4 py-2 text-sm font-medium rounded-t-lg transition-colors',
|
|
46
|
+
activeTab === t.id
|
|
47
|
+
? 'bg-slate-800 text-kick-500 border border-slate-700 border-b-slate-800'
|
|
48
|
+
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900'
|
|
49
|
+
]">
|
|
50
|
+
{{ t.label }}
|
|
51
|
+
<span v-if="t.count !== undefined"
|
|
52
|
+
class="ml-1.5 text-xs bg-slate-700 text-slate-300 px-1.5 py-0.5 rounded-full">{{ t.count }}</span>
|
|
53
|
+
</button>
|
|
54
|
+
</nav>
|
|
55
|
+
</header>
|
|
56
|
+
|
|
57
|
+
<main class="p-6">
|
|
58
|
+
|
|
59
|
+
<!-- ─── Overview Tab ─────────────────────────────────────────── -->
|
|
60
|
+
<div v-show="activeTab === 'overview'">
|
|
61
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
62
|
+
|
|
63
|
+
<!-- Health -->
|
|
64
|
+
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
|
65
|
+
<button @click="toggle('health')" class="w-full flex items-center justify-between mb-3">
|
|
66
|
+
<h2 class="text-xs font-semibold uppercase tracking-wider text-slate-500">Health</h2>
|
|
67
|
+
<svg :class="{'rotate-180': !collapsed.health}" class="w-4 h-4 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="M19 9l-7 7-7-7"/></svg>
|
|
68
|
+
</button>
|
|
69
|
+
<div v-show="!collapsed.health">
|
|
70
|
+
<template v-if="health">
|
|
71
|
+
<div class="flex items-center justify-between mb-2">
|
|
72
|
+
<span class="text-slate-400">Status</span>
|
|
73
|
+
<span :class="health.status === 'healthy' ? 'bg-emerald-900 text-emerald-300' : 'bg-red-900 text-red-300'"
|
|
74
|
+
class="px-2 py-0.5 rounded text-xs font-semibold">{{ health.status }}</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="flex items-center justify-between mb-2">
|
|
77
|
+
<span class="text-slate-400">Uptime</span>
|
|
78
|
+
<span class="font-semibold tabular-nums">{{ formatDuration(health.uptime) }}</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="flex items-center justify-between mb-2">
|
|
81
|
+
<span class="text-slate-400">Error Rate</span>
|
|
82
|
+
<span class="font-semibold tabular-nums">{{ (health.errorRate * 100).toFixed(2) }}%</span>
|
|
83
|
+
</div>
|
|
84
|
+
<!-- Adapter statuses (collapsible) -->
|
|
85
|
+
<template v-if="health.adapters && Object.keys(health.adapters).length">
|
|
86
|
+
<div class="border-t border-slate-800 mt-2 pt-2">
|
|
87
|
+
<button @click="toggle('adapters')" class="text-xs text-slate-500 hover:text-slate-300 flex items-center gap-1">
|
|
88
|
+
<svg :class="{'rotate-180': !collapsed.adapters}" class="w-3 h-3 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
89
|
+
Adapters ({{ Object.keys(health.adapters).length }})
|
|
90
|
+
</button>
|
|
91
|
+
<div v-show="!collapsed.adapters" class="mt-1">
|
|
92
|
+
<div v-for="(status, name) in health.adapters" :key="name" class="flex items-center justify-between py-1">
|
|
93
|
+
<span class="text-slate-400 text-sm">{{ name }}</span>
|
|
94
|
+
<span :class="status === 'running' ? 'bg-emerald-900/50 text-emerald-300' : 'bg-amber-900/50 text-amber-300'"
|
|
95
|
+
class="px-2 py-0.5 rounded text-xs font-semibold">{{ status }}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
</template>
|
|
101
|
+
<p v-else class="text-slate-600 italic">Loading...</p>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Metrics -->
|
|
106
|
+
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
|
107
|
+
<button @click="toggle('metrics')" class="w-full flex items-center justify-between mb-3">
|
|
108
|
+
<h2 class="text-xs font-semibold uppercase tracking-wider text-slate-500">Metrics</h2>
|
|
109
|
+
<svg :class="{'rotate-180': !collapsed.metrics}" class="w-4 h-4 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="M19 9l-7 7-7-7"/></svg>
|
|
110
|
+
</button>
|
|
111
|
+
<div v-show="!collapsed.metrics">
|
|
112
|
+
<template v-if="metrics">
|
|
113
|
+
<div class="flex items-center justify-between mb-2">
|
|
114
|
+
<span class="text-slate-400">Total Requests</span>
|
|
115
|
+
<span class="font-semibold tabular-nums">{{ metrics.requests.toLocaleString() }}</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="flex items-center justify-between mb-2">
|
|
118
|
+
<span class="text-slate-400">5xx Errors</span>
|
|
119
|
+
<span class="font-semibold tabular-nums text-red-400">{{ metrics.serverErrors }}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="flex items-center justify-between mb-2">
|
|
122
|
+
<span class="text-slate-400">4xx Errors</span>
|
|
123
|
+
<span class="font-semibold tabular-nums text-amber-400">{{ metrics.clientErrors }}</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="flex items-center justify-between">
|
|
126
|
+
<span class="text-slate-400">Started</span>
|
|
127
|
+
<span class="font-semibold text-sm">{{ new Date(metrics.startedAt).toLocaleTimeString() }}</span>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
<p v-else class="text-slate-600 italic">Loading...</p>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<!-- WebSocket -->
|
|
135
|
+
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
|
136
|
+
<button @click="toggle('ws')" class="w-full flex items-center justify-between mb-3">
|
|
137
|
+
<h2 class="text-xs font-semibold uppercase tracking-wider text-slate-500">WebSocket</h2>
|
|
138
|
+
<svg :class="{'rotate-180': !collapsed.ws}" class="w-4 h-4 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="M19 9l-7 7-7-7"/></svg>
|
|
139
|
+
</button>
|
|
140
|
+
<div v-show="!collapsed.ws">
|
|
141
|
+
<template v-if="ws && ws.enabled">
|
|
142
|
+
<div class="flex items-center justify-between mb-2">
|
|
143
|
+
<span class="text-slate-400">Active</span>
|
|
144
|
+
<span class="font-semibold tabular-nums text-emerald-400">{{ ws.activeConnections }}</span>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="flex items-center justify-between mb-2">
|
|
147
|
+
<span class="text-slate-400">Total</span>
|
|
148
|
+
<span class="font-semibold tabular-nums">{{ ws.totalConnections }}</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="flex items-center justify-between mb-2">
|
|
151
|
+
<span class="text-slate-400">Messages In</span>
|
|
152
|
+
<span class="font-semibold tabular-nums">{{ ws.messagesReceived }}</span>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="flex items-center justify-between">
|
|
155
|
+
<span class="text-slate-400">Messages Out</span>
|
|
156
|
+
<span class="font-semibold tabular-nums">{{ ws.messagesSent }}</span>
|
|
157
|
+
</div>
|
|
158
|
+
<!-- Namespaces (collapsible) -->
|
|
159
|
+
<template v-if="ws.namespaces && Object.keys(ws.namespaces).length">
|
|
160
|
+
<div class="border-t border-slate-800 mt-2 pt-2">
|
|
161
|
+
<button @click="toggle('wsNs')" class="text-xs text-slate-500 hover:text-slate-300 flex items-center gap-1">
|
|
162
|
+
<svg :class="{'rotate-180': !collapsed.wsNs}" class="w-3 h-3 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
163
|
+
Namespaces ({{ Object.keys(ws.namespaces).length }})
|
|
164
|
+
</button>
|
|
165
|
+
<div v-show="!collapsed.wsNs" class="mt-1">
|
|
166
|
+
<div v-for="(ns, name) in ws.namespaces" :key="name" class="flex items-center justify-between py-1">
|
|
167
|
+
<span class="text-slate-400 text-sm font-mono">{{ name }}</span>
|
|
168
|
+
<span class="text-sm">{{ ns.connections }} conn / {{ ns.handlers }} handlers</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</template>
|
|
173
|
+
</template>
|
|
174
|
+
<p v-else class="text-slate-600 italic">{{ ws ? 'No WsAdapter' : 'Loading...' }}</p>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- ─── Routes Tab ───────────────────────────────────────────── -->
|
|
181
|
+
<div v-show="activeTab === 'routes'">
|
|
182
|
+
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
|
183
|
+
<!-- Search + Method Filter -->
|
|
184
|
+
<div class="flex flex-col sm:flex-row gap-3 mb-4">
|
|
185
|
+
<div class="relative flex-1">
|
|
186
|
+
<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>
|
|
187
|
+
<input v-model="routeSearch" type="text" placeholder="Search routes..."
|
|
188
|
+
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">
|
|
189
|
+
</div>
|
|
190
|
+
<div class="flex gap-1">
|
|
191
|
+
<button v-for="m in ['ALL','GET','POST','PUT','DELETE','PATCH']" :key="m"
|
|
192
|
+
@click="routeMethodFilter = m"
|
|
193
|
+
:class="[
|
|
194
|
+
'px-3 py-1.5 text-xs font-semibold rounded-lg transition-colors',
|
|
195
|
+
routeMethodFilter === m
|
|
196
|
+
? 'bg-kick-500/20 text-kick-500 border border-kick-500/30'
|
|
197
|
+
: 'bg-slate-800 text-slate-400 border border-slate-700 hover:text-slate-200'
|
|
198
|
+
]">{{ m }}</button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<!-- Routes Table -->
|
|
203
|
+
<div class="overflow-x-auto" v-if="paginatedRoutes.length">
|
|
204
|
+
<table class="w-full text-sm">
|
|
205
|
+
<thead>
|
|
206
|
+
<tr class="border-b-2 border-slate-700">
|
|
207
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Method</th>
|
|
208
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Path</th>
|
|
209
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Controller</th>
|
|
210
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Handler</th>
|
|
211
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Middleware</th>
|
|
212
|
+
</tr>
|
|
213
|
+
</thead>
|
|
214
|
+
<tbody>
|
|
215
|
+
<tr v-for="r in paginatedRoutes" :key="r.path + r.method"
|
|
216
|
+
class="border-b border-slate-800 hover:bg-slate-800/50">
|
|
217
|
+
<td class="py-2 px-3">
|
|
218
|
+
<span :class="methodColor(r.method)" class="text-xs font-bold">{{ r.method }}</span>
|
|
219
|
+
</td>
|
|
220
|
+
<td class="py-2 px-3 font-mono text-sm">{{ r.path }}</td>
|
|
221
|
+
<td class="py-2 px-3">{{ r.controller }}</td>
|
|
222
|
+
<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(', ') : '—' }}</td>
|
|
224
|
+
</tr>
|
|
225
|
+
</tbody>
|
|
226
|
+
</table>
|
|
227
|
+
|
|
228
|
+
<!-- Pagination -->
|
|
229
|
+
<div v-if="filteredRoutes.length > routePageSize" class="flex items-center justify-between mt-4 pt-3 border-t border-slate-800">
|
|
230
|
+
<span class="text-sm text-slate-500">
|
|
231
|
+
Showing {{ (routePage - 1) * routePageSize + 1 }}–{{ Math.min(routePage * routePageSize, filteredRoutes.length) }}
|
|
232
|
+
of {{ filteredRoutes.length }}
|
|
233
|
+
</span>
|
|
234
|
+
<div class="flex gap-1">
|
|
235
|
+
<button @click="routePage = Math.max(1, routePage - 1)" :disabled="routePage === 1"
|
|
236
|
+
class="px-3 py-1 text-sm rounded bg-slate-800 border border-slate-700 disabled:opacity-30 hover:bg-slate-700">Prev</button>
|
|
237
|
+
<button v-for="p in routeTotalPages" :key="p" @click="routePage = p"
|
|
238
|
+
:class="['px-3 py-1 text-sm rounded border', p === routePage ? 'bg-kick-500/20 border-kick-500/30 text-kick-500' : 'bg-slate-800 border-slate-700 hover:bg-slate-700']">{{ p }}</button>
|
|
239
|
+
<button @click="routePage = Math.min(routeTotalPages, routePage + 1)" :disabled="routePage === routeTotalPages"
|
|
240
|
+
class="px-3 py-1 text-sm rounded bg-slate-800 border border-slate-700 disabled:opacity-30 hover:bg-slate-700">Next</button>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
<p v-else class="text-slate-600 italic">{{ routeSearch || routeMethodFilter !== 'ALL' ? 'No matching routes' : 'No routes registered' }}</p>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<!-- ─── Container Tab ────────────────────────────────────────── -->
|
|
249
|
+
<div v-show="activeTab === 'container'">
|
|
250
|
+
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
|
251
|
+
<!-- Search -->
|
|
252
|
+
<div class="relative mb-4">
|
|
253
|
+
<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>
|
|
254
|
+
<input v-model="containerSearch" type="text" placeholder="Search tokens..."
|
|
255
|
+
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">
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<!-- Container Table -->
|
|
259
|
+
<div class="overflow-x-auto" v-if="paginatedContainer.length">
|
|
260
|
+
<table class="w-full text-sm">
|
|
261
|
+
<thead>
|
|
262
|
+
<tr class="border-b-2 border-slate-700">
|
|
263
|
+
<th class="text-left py-2 px-3 text-slate-400 font-semibold">Token</th>
|
|
264
|
+
<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">Instantiated</th>
|
|
266
|
+
</tr>
|
|
267
|
+
</thead>
|
|
268
|
+
<tbody>
|
|
269
|
+
<tr v-for="r in paginatedContainer" :key="r.token"
|
|
270
|
+
class="border-b border-slate-800 hover:bg-slate-800/50">
|
|
271
|
+
<td class="py-2 px-3 font-mono text-sm">{{ r.token }}</td>
|
|
272
|
+
<td class="py-2 px-3">
|
|
273
|
+
<span class="bg-blue-900/50 text-blue-300 px-2 py-0.5 rounded text-xs font-semibold">{{ r.scope }}</span>
|
|
274
|
+
</td>
|
|
275
|
+
<td class="py-2 px-3">
|
|
276
|
+
<span :class="r.instantiated ? 'bg-emerald-900/50 text-emerald-300' : 'bg-amber-900/50 text-amber-300'"
|
|
277
|
+
class="px-2 py-0.5 rounded text-xs font-semibold">{{ r.instantiated ? 'yes' : 'no' }}</span>
|
|
278
|
+
</td>
|
|
279
|
+
</tr>
|
|
280
|
+
</tbody>
|
|
281
|
+
</table>
|
|
282
|
+
|
|
283
|
+
<!-- Pagination -->
|
|
284
|
+
<div v-if="filteredContainer.length > containerPageSize" class="flex items-center justify-between mt-4 pt-3 border-t border-slate-800">
|
|
285
|
+
<span class="text-sm text-slate-500">
|
|
286
|
+
Showing {{ (containerPage - 1) * containerPageSize + 1 }}–{{ Math.min(containerPage * containerPageSize, filteredContainer.length) }}
|
|
287
|
+
of {{ filteredContainer.length }}
|
|
288
|
+
</span>
|
|
289
|
+
<div class="flex gap-1">
|
|
290
|
+
<button @click="containerPage = Math.max(1, containerPage - 1)" :disabled="containerPage === 1"
|
|
291
|
+
class="px-3 py-1 text-sm rounded bg-slate-800 border border-slate-700 disabled:opacity-30 hover:bg-slate-700">Prev</button>
|
|
292
|
+
<button v-for="p in containerTotalPages" :key="p" @click="containerPage = p"
|
|
293
|
+
:class="['px-3 py-1 text-sm rounded border', p === containerPage ? 'bg-kick-500/20 border-kick-500/30 text-kick-500' : 'bg-slate-800 border-slate-700 hover:bg-slate-700']">{{ p }}</button>
|
|
294
|
+
<button @click="containerPage = Math.min(containerTotalPages, containerPage + 1)" :disabled="containerPage === containerTotalPages"
|
|
295
|
+
class="px-3 py-1 text-sm rounded bg-slate-800 border border-slate-700 disabled:opacity-30 hover:bg-slate-700">Next</button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
<p v-else class="text-slate-600 italic">{{ containerSearch ? 'No matching tokens' : 'No DI registrations' }}</p>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<!-- ─── Queues Tab ─────────────────────────────────────────── -->
|
|
304
|
+
<div v-show="activeTab === 'queues'">
|
|
305
|
+
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
|
306
|
+
<template v-if="queueData && queueData.enabled">
|
|
307
|
+
<div v-if="queueData.queues.length" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
308
|
+
<div v-for="q in queueData.queues" :key="q.name"
|
|
309
|
+
class="bg-slate-800/50 rounded-lg border border-slate-700 p-4">
|
|
310
|
+
<h3 class="font-mono text-sm font-semibold text-kick-500 mb-3">{{ q.name }}</h3>
|
|
311
|
+
<div v-if="q.error" class="text-red-400 text-sm">{{ q.error }}</div>
|
|
312
|
+
<template v-else>
|
|
313
|
+
<div class="grid grid-cols-2 gap-2 text-sm">
|
|
314
|
+
<div class="flex justify-between">
|
|
315
|
+
<span class="text-slate-400">Waiting</span>
|
|
316
|
+
<span class="font-semibold tabular-nums text-amber-400">{{ q.waiting }}</span>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="flex justify-between">
|
|
319
|
+
<span class="text-slate-400">Active</span>
|
|
320
|
+
<span class="font-semibold tabular-nums text-blue-400">{{ q.active }}</span>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="flex justify-between">
|
|
323
|
+
<span class="text-slate-400">Completed</span>
|
|
324
|
+
<span class="font-semibold tabular-nums text-emerald-400">{{ q.completed }}</span>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="flex justify-between">
|
|
327
|
+
<span class="text-slate-400">Failed</span>
|
|
328
|
+
<span class="font-semibold tabular-nums text-red-400">{{ q.failed }}</span>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="flex justify-between">
|
|
331
|
+
<span class="text-slate-400">Delayed</span>
|
|
332
|
+
<span class="font-semibold tabular-nums text-violet-400">{{ q.delayed }}</span>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="flex justify-between">
|
|
335
|
+
<span class="text-slate-400">Paused</span>
|
|
336
|
+
<span class="font-semibold tabular-nums text-slate-400">{{ q.paused }}</span>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</template>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
<p v-else class="text-slate-600 italic">No queues registered</p>
|
|
343
|
+
</template>
|
|
344
|
+
<p v-else class="text-slate-600 italic">{{ queueData ? 'QueueAdapter not found' : 'Loading...' }}</p>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
</main>
|
|
349
|
+
|
|
350
|
+
<!-- Auth Gate — shown when token is required but not found -->
|
|
351
|
+
<div v-if="showAuthGate" class="fixed inset-0 bg-slate-950/90 flex items-center justify-center z-50">
|
|
352
|
+
<div class="bg-slate-900 border border-slate-700 rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl">
|
|
353
|
+
<div class="text-center mb-6">
|
|
354
|
+
<h2 class="text-xl font-bold text-kick-500 mb-2">⚡ KickJS DevTools</h2>
|
|
355
|
+
<p class="text-sm text-slate-400">Enter your DevTools token to continue</p>
|
|
356
|
+
<p class="text-xs text-slate-600 mt-1">Token is shown in your server console on startup</p>
|
|
357
|
+
</div>
|
|
358
|
+
<form @submit.prevent="submitToken">
|
|
359
|
+
<input v-model="tokenInput" type="text" placeholder="Paste your token here..."
|
|
360
|
+
autofocus
|
|
361
|
+
class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-kick-500 font-mono mb-3">
|
|
362
|
+
<p v-if="authError" class="text-red-400 text-sm mb-3">{{ authError }}</p>
|
|
363
|
+
<button type="submit"
|
|
364
|
+
class="w-full bg-kick-500 hover:bg-kick-600 text-white font-semibold py-2.5 rounded-lg transition-colors">
|
|
365
|
+
Authenticate
|
|
366
|
+
</button>
|
|
367
|
+
</form>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<script>
|
|
373
|
+
const { createApp, ref, computed, onMounted, onUnmounted, reactive, watch } = Vue
|
|
374
|
+
|
|
375
|
+
const BASE = document.body.dataset.base || '/_debug'
|
|
376
|
+
|
|
377
|
+
// Token resolution: URL query > cookie > empty (will prompt)
|
|
378
|
+
function getToken() {
|
|
379
|
+
const fromUrl = new URLSearchParams(window.location.search).get('token')
|
|
380
|
+
if (fromUrl) {
|
|
381
|
+
// Save to cookie for future visits
|
|
382
|
+
document.cookie = `kickjs_devtools_token=${fromUrl}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`
|
|
383
|
+
// Clean URL (remove token from query string)
|
|
384
|
+
const url = new URL(window.location)
|
|
385
|
+
url.searchParams.delete('token')
|
|
386
|
+
window.history.replaceState({}, '', url.toString())
|
|
387
|
+
return fromUrl
|
|
388
|
+
}
|
|
389
|
+
// Try cookie
|
|
390
|
+
const match = document.cookie.match(/kickjs_devtools_token=([^;]+)/)
|
|
391
|
+
return match ? match[1] : ''
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let TOKEN = getToken()
|
|
395
|
+
|
|
396
|
+
createApp({
|
|
397
|
+
setup() {
|
|
398
|
+
const activeTab = ref('overview')
|
|
399
|
+
const pollInterval = ref(30000)
|
|
400
|
+
const lastUpdate = ref('loading...')
|
|
401
|
+
const showAuthGate = ref(false)
|
|
402
|
+
const tokenInput = ref('')
|
|
403
|
+
const authError = ref('')
|
|
404
|
+
|
|
405
|
+
// Data
|
|
406
|
+
const health = ref(null)
|
|
407
|
+
const metrics = ref(null)
|
|
408
|
+
const ws = ref(null)
|
|
409
|
+
const routeData = ref(null)
|
|
410
|
+
const containerData = ref(null)
|
|
411
|
+
const queueData = ref(null)
|
|
412
|
+
|
|
413
|
+
// Collapsible state
|
|
414
|
+
const collapsed = reactive({
|
|
415
|
+
health: false,
|
|
416
|
+
metrics: false,
|
|
417
|
+
ws: false,
|
|
418
|
+
adapters: true,
|
|
419
|
+
wsNs: true,
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
function toggle(key) { collapsed[key] = !collapsed[key] }
|
|
423
|
+
|
|
424
|
+
// Route search + filter + pagination
|
|
425
|
+
const routeSearch = ref('')
|
|
426
|
+
const routeMethodFilter = ref('ALL')
|
|
427
|
+
const routePage = ref(1)
|
|
428
|
+
const routePageSize = ref(15)
|
|
429
|
+
|
|
430
|
+
const filteredRoutes = computed(() => {
|
|
431
|
+
let routes = routeData.value?.routes || []
|
|
432
|
+
if (routeMethodFilter.value !== 'ALL') {
|
|
433
|
+
routes = routes.filter(r => r.method === routeMethodFilter.value)
|
|
434
|
+
}
|
|
435
|
+
if (routeSearch.value) {
|
|
436
|
+
const q = routeSearch.value.toLowerCase()
|
|
437
|
+
routes = routes.filter(r =>
|
|
438
|
+
r.path.toLowerCase().includes(q) ||
|
|
439
|
+
r.controller.toLowerCase().includes(q) ||
|
|
440
|
+
r.handler.toLowerCase().includes(q)
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
return routes
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const routeTotalPages = computed(() => Math.ceil(filteredRoutes.value.length / routePageSize.value) || 1)
|
|
447
|
+
const paginatedRoutes = computed(() => {
|
|
448
|
+
const start = (routePage.value - 1) * routePageSize.value
|
|
449
|
+
return filteredRoutes.value.slice(start, start + routePageSize.value)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// Reset page when search/filter changes
|
|
453
|
+
watch([routeSearch, routeMethodFilter], () => { routePage.value = 1 })
|
|
454
|
+
|
|
455
|
+
// Container search + pagination
|
|
456
|
+
const containerSearch = ref('')
|
|
457
|
+
const containerPage = ref(1)
|
|
458
|
+
const containerPageSize = ref(20)
|
|
459
|
+
|
|
460
|
+
const filteredContainer = computed(() => {
|
|
461
|
+
let regs = containerData.value?.registrations || []
|
|
462
|
+
if (containerSearch.value) {
|
|
463
|
+
const q = containerSearch.value.toLowerCase()
|
|
464
|
+
regs = regs.filter(r => r.token.toLowerCase().includes(q))
|
|
465
|
+
}
|
|
466
|
+
return regs
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
const containerTotalPages = computed(() => Math.ceil(filteredContainer.value.length / containerPageSize.value) || 1)
|
|
470
|
+
const paginatedContainer = computed(() => {
|
|
471
|
+
const start = (containerPage.value - 1) * containerPageSize.value
|
|
472
|
+
return filteredContainer.value.slice(start, start + containerPageSize.value)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
watch(containerSearch, () => { containerPage.value = 1 })
|
|
476
|
+
|
|
477
|
+
// Tab badges
|
|
478
|
+
const tabs = computed(() => [
|
|
479
|
+
{ id: 'overview', label: 'Overview' },
|
|
480
|
+
{ id: 'routes', label: 'Routes', count: routeData.value?.routes?.length },
|
|
481
|
+
{ id: 'container', label: 'Container', count: containerData.value?.count },
|
|
482
|
+
{ id: 'queues', label: 'Queues', count: queueData.value?.queues?.length },
|
|
483
|
+
])
|
|
484
|
+
|
|
485
|
+
// Data fetching
|
|
486
|
+
async function fetchJSON(path) {
|
|
487
|
+
try {
|
|
488
|
+
const headers = TOKEN ? { 'x-devtools-token': TOKEN } : {}
|
|
489
|
+
const r = await fetch(BASE + path, { headers })
|
|
490
|
+
if (r.status === 403) {
|
|
491
|
+
showAuthGate.value = true
|
|
492
|
+
return null
|
|
493
|
+
}
|
|
494
|
+
return r.ok ? r.json() : null
|
|
495
|
+
} catch { return null }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function submitToken() {
|
|
499
|
+
authError.value = ''
|
|
500
|
+
const testToken = tokenInput.value.trim()
|
|
501
|
+
if (!testToken) { authError.value = 'Token is required'; return }
|
|
502
|
+
|
|
503
|
+
// Test the token
|
|
504
|
+
try {
|
|
505
|
+
const r = await fetch(BASE + '/health', { headers: { 'x-devtools-token': testToken } })
|
|
506
|
+
if (r.status === 403) {
|
|
507
|
+
authError.value = 'Invalid token'
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
authError.value = 'Connection failed'
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Save to cookie and reload
|
|
516
|
+
TOKEN = testToken
|
|
517
|
+
document.cookie = `kickjs_devtools_token=${testToken}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`
|
|
518
|
+
showAuthGate.value = false
|
|
519
|
+
refresh()
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function refresh() {
|
|
523
|
+
const [h, m, r, c, w, q] = await Promise.all([
|
|
524
|
+
fetchJSON('/health'),
|
|
525
|
+
fetchJSON('/metrics'),
|
|
526
|
+
fetchJSON('/routes'),
|
|
527
|
+
fetchJSON('/container'),
|
|
528
|
+
fetchJSON('/ws'),
|
|
529
|
+
fetchJSON('/queues'),
|
|
530
|
+
])
|
|
531
|
+
if (h) health.value = h
|
|
532
|
+
if (m) metrics.value = m
|
|
533
|
+
if (r) routeData.value = r
|
|
534
|
+
if (c) containerData.value = c
|
|
535
|
+
ws.value = w || { enabled: false }
|
|
536
|
+
queueData.value = q || { enabled: false }
|
|
537
|
+
lastUpdate.value = new Date().toLocaleTimeString()
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function formatDuration(seconds) {
|
|
541
|
+
if (seconds < 60) return seconds + 's'
|
|
542
|
+
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's'
|
|
543
|
+
const h = Math.floor(seconds / 3600)
|
|
544
|
+
const m = Math.floor((seconds % 3600) / 60)
|
|
545
|
+
return h + 'h ' + m + 'm'
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function methodColor(method) {
|
|
549
|
+
return {
|
|
550
|
+
GET: 'text-emerald-400', POST: 'text-blue-400', PUT: 'text-amber-400',
|
|
551
|
+
DELETE: 'text-red-400', PATCH: 'text-violet-400',
|
|
552
|
+
}[method] || 'text-slate-400'
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let timer
|
|
556
|
+
onMounted(() => { refresh(); timer = setInterval(refresh, pollInterval.value) })
|
|
557
|
+
onUnmounted(() => clearInterval(timer))
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
activeTab, tabs, pollInterval, lastUpdate,
|
|
561
|
+
health, metrics, ws, routeData, containerData, queueData,
|
|
562
|
+
collapsed, toggle,
|
|
563
|
+
showAuthGate, tokenInput, authError, submitToken,
|
|
564
|
+
routeSearch, routeMethodFilter, routePage, routePageSize,
|
|
565
|
+
filteredRoutes, paginatedRoutes, routeTotalPages,
|
|
566
|
+
containerSearch, containerPage, containerPageSize,
|
|
567
|
+
filteredContainer, paginatedContainer, containerTotalPages,
|
|
568
|
+
formatDuration, methodColor,
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}).mount('#app')
|
|
572
|
+
</script>
|
|
573
|
+
|
|
574
|
+
</body>
|
|
575
|
+
</html>
|