@gravito/zenith 1.1.2 → 1.1.3
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/CHANGELOG.md +15 -0
- package/README.md +77 -22
- package/README.zh-TW.md +88 -0
- package/dist/bin.js +64681 -15842
- package/dist/client/assets/index-C80c1frR.css +1 -0
- package/dist/client/assets/index-CrWem9u3.js +434 -0
- package/dist/server/index.js +64681 -15842
- package/package.json +9 -7
- package/postcss.config.js +4 -4
- package/src/client/Layout.tsx +36 -39
- package/src/client/Sidebar.tsx +7 -7
- package/src/client/ThroughputChart.tsx +31 -17
- package/src/client/WorkerStatus.tsx +56 -80
- package/src/client/components/ConfirmDialog.tsx +22 -14
- package/src/client/components/JobInspector.tsx +95 -162
- package/src/client/index.css +29 -31
- package/src/client/pages/LoginPage.tsx +33 -31
- package/src/client/pages/MetricsPage.tsx +65 -37
- package/src/client/pages/OverviewPage.tsx +30 -28
- package/src/client/pages/PulsePage.tsx +111 -190
- package/src/client/pages/QueuesPage.tsx +82 -83
- package/src/client/pages/SchedulesPage.tsx +56 -61
- package/src/client/pages/SettingsPage.tsx +118 -137
- package/src/client/pages/WorkersPage.tsx +101 -115
- package/src/server/services/CommandService.ts +8 -9
- package/src/server/services/PulseService.ts +61 -4
- package/src/server/services/QueueService.ts +293 -0
- package/src/shared/types.ts +38 -13
- package/tailwind.config.js +75 -68
- package/tsconfig.json +28 -37
- package/tsconfig.node.json +9 -11
- package/dist/client/assets/index-BSMp8oq_.js +0 -436
- package/dist/client/assets/index-BwxlHx-_.css +0 -1
- package/dist/client/index.html +0 -13
- package/src/client/index.html +0 -12
- /package/{ECOSYSTEM_EXPANSION_RFC.md → doc/ECOSYSTEM_EXPANSION_RFC.md} +0 -0
|
@@ -75,7 +75,6 @@ export function WorkersPage() {
|
|
|
75
75
|
const totalCpu = workers.reduce((acc, w) => acc + (w.metrics?.cpu || 0), 0)
|
|
76
76
|
const avgCpu = workers.length > 0 ? totalCpu / workers.length : 0
|
|
77
77
|
const totalRam = workers.reduce((acc, w) => acc + (w.metrics?.ram?.rss || 0), 0)
|
|
78
|
-
const totalCapacity = workers.reduce((acc, w) => acc + (w.metrics?.ram?.total || 0), 0)
|
|
79
78
|
|
|
80
79
|
if (isPending) {
|
|
81
80
|
return (
|
|
@@ -120,58 +119,60 @@ export function WorkersPage() {
|
|
|
120
119
|
|
|
121
120
|
{/* Summary Cards */}
|
|
122
121
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
123
|
-
<div className="card-premium p-
|
|
124
|
-
<div className="absolute top-0 right-0 w-20 h-20 bg-green-500/10 rounded-full -translate-y-1/2 translate-x-1/2 group-hover:scale-150 transition-transform duration-500" />
|
|
122
|
+
<div className="card-premium p-5 relative overflow-hidden group border-l-4 border-emerald-500">
|
|
125
123
|
<div className="relative">
|
|
126
|
-
<div className="flex items-center gap-2 mb-
|
|
127
|
-
<Server size={
|
|
128
|
-
<p className="text-[
|
|
129
|
-
|
|
124
|
+
<div className="flex items-center gap-2 mb-3">
|
|
125
|
+
<Server size={14} className="text-emerald-500" />
|
|
126
|
+
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] font-heading">
|
|
127
|
+
Operational Nodes
|
|
130
128
|
</p>
|
|
131
129
|
</div>
|
|
132
|
-
<p className="text-3xl font-black text-
|
|
130
|
+
<p className="text-3xl font-black text-white font-mono tracking-tighter">
|
|
131
|
+
{onlineWorkers.length}
|
|
132
|
+
</p>
|
|
133
133
|
</div>
|
|
134
134
|
</div>
|
|
135
|
-
<div className="card-premium p-
|
|
136
|
-
<div className="absolute top-0 right-0 w-20 h-20 bg-muted/20 rounded-full -translate-y-1/2 translate-x-1/2 group-hover:scale-150 transition-transform duration-500" />
|
|
135
|
+
<div className="card-premium p-5 relative overflow-hidden group border-l-4 border-white/10">
|
|
137
136
|
<div className="relative">
|
|
138
|
-
<div className="flex items-center gap-2 mb-
|
|
139
|
-
<Zap size={
|
|
140
|
-
<p className="text-[
|
|
141
|
-
|
|
137
|
+
<div className="flex items-center gap-2 mb-3">
|
|
138
|
+
<Zap size={14} className="text-white/20" />
|
|
139
|
+
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] font-heading">
|
|
140
|
+
Standby Nodes
|
|
142
141
|
</p>
|
|
143
142
|
</div>
|
|
144
|
-
<p className="text-3xl font-black text-
|
|
143
|
+
<p className="text-3xl font-black text-white/40 font-mono tracking-tighter">
|
|
144
|
+
{offlineWorkers.length}
|
|
145
|
+
</p>
|
|
145
146
|
</div>
|
|
146
147
|
</div>
|
|
147
|
-
<div className="card-premium p-
|
|
148
|
-
<div className="absolute top-0 right-0 w-20 h-20 bg-primary/10 rounded-full -translate-y-1/2 translate-x-1/2 group-hover:scale-150 transition-transform duration-500" />
|
|
148
|
+
<div className="card-premium p-5 relative overflow-hidden group border-l-4 border-primary/40">
|
|
149
149
|
<div className="relative">
|
|
150
|
-
<div className="flex items-center gap-2 mb-
|
|
151
|
-
<Gauge size={
|
|
152
|
-
<p className="text-[
|
|
153
|
-
|
|
150
|
+
<div className="flex items-center gap-2 mb-3">
|
|
151
|
+
<Gauge size={14} className="text-primary" />
|
|
152
|
+
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] font-heading">
|
|
153
|
+
Compute Load
|
|
154
154
|
</p>
|
|
155
155
|
</div>
|
|
156
|
-
<p className="text-3xl font-black">
|
|
156
|
+
<p className="text-3xl font-black text-white font-mono tracking-tighter">
|
|
157
|
+
{avgCpu.toFixed(2)}
|
|
158
|
+
</p>
|
|
157
159
|
</div>
|
|
158
160
|
</div>
|
|
159
|
-
<div className="card-premium p-
|
|
160
|
-
<div className="absolute top-0 right-0 w-20 h-20 bg-indigo-500/10 rounded-full -translate-y-1/2 translate-x-1/2 group-hover:scale-150 transition-transform duration-500" />
|
|
161
|
+
<div className="card-premium p-5 relative overflow-hidden group border-l-4 border-indigo-500/40">
|
|
161
162
|
<div className="relative">
|
|
162
|
-
<div className="flex items-center gap-2 mb-
|
|
163
|
-
<MemoryStick size={
|
|
164
|
-
<p className="text-[
|
|
165
|
-
Cluster
|
|
163
|
+
<div className="flex items-center gap-2 mb-3">
|
|
164
|
+
<MemoryStick size={14} className="text-indigo-400" />
|
|
165
|
+
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] font-heading">
|
|
166
|
+
Cluster Memory
|
|
166
167
|
</p>
|
|
167
168
|
</div>
|
|
168
169
|
<div className="flex items-baseline gap-1">
|
|
169
|
-
<p className="text-3xl font-black text-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
<p className="text-3xl font-black text-white font-mono tracking-tighter">
|
|
171
|
+
{(totalRam / 1024).toFixed(1)}
|
|
172
|
+
</p>
|
|
173
|
+
<span className="text-[10px] font-black text-white/20 uppercase tracking-tighter">
|
|
174
|
+
GB
|
|
175
|
+
</span>
|
|
175
176
|
</div>
|
|
176
177
|
</div>
|
|
177
178
|
</div>
|
|
@@ -180,66 +181,55 @@ export function WorkersPage() {
|
|
|
180
181
|
{/* Workers Grid */}
|
|
181
182
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
182
183
|
{workers.length === 0 && (
|
|
183
|
-
<div className="col-span-full py-
|
|
184
|
-
<Cpu size={48} className="mx-auto mb-4 opacity-
|
|
185
|
-
<p className="text-
|
|
186
|
-
|
|
184
|
+
<div className="col-span-full py-32 text-center text-muted-foreground/20">
|
|
185
|
+
<Cpu size={48} className="mx-auto mb-4 opacity-30 animate-pulse" />
|
|
186
|
+
<p className="text-[10px] font-black uppercase tracking-[0.3em]">
|
|
187
|
+
Awaiting signal from constellation...
|
|
188
|
+
</p>
|
|
187
189
|
</div>
|
|
188
190
|
)}
|
|
189
191
|
{workers.map((worker, index) => (
|
|
190
192
|
<motion.div
|
|
191
193
|
key={worker.id}
|
|
192
|
-
initial={{ opacity: 0,
|
|
193
|
-
animate={{ opacity: 1,
|
|
194
|
-
transition={{ delay: index * 0.
|
|
195
|
-
className="card-premium p-6 relative overflow-hidden group"
|
|
194
|
+
initial={{ opacity: 0, scale: 0.98 }}
|
|
195
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
196
|
+
transition={{ delay: index * 0.05 }}
|
|
197
|
+
className="card-premium p-6 relative overflow-hidden group border-l-4"
|
|
198
|
+
style={{ borderLeftColor: worker.status === 'online' ? '#10B981' : '#27272A' }}
|
|
196
199
|
>
|
|
197
|
-
{/* Status indicator bar */}
|
|
198
|
-
<div
|
|
199
|
-
className={cn(
|
|
200
|
-
'absolute left-0 top-0 bottom-0 w-1.5 transition-all',
|
|
201
|
-
worker.status === 'online' ? 'bg-green-500' : 'bg-muted-foreground/30'
|
|
202
|
-
)}
|
|
203
|
-
/>
|
|
204
|
-
|
|
205
200
|
{/* Header */}
|
|
206
|
-
<div className="flex items-start justify-between mb-
|
|
201
|
+
<div className="flex items-start justify-between mb-8">
|
|
207
202
|
<div className="flex items-center gap-4">
|
|
208
203
|
<div className="relative">
|
|
209
204
|
<div
|
|
210
205
|
className={cn(
|
|
211
|
-
'w-12 h-12 rounded-
|
|
206
|
+
'w-12 h-12 rounded-xl flex items-center justify-center transition-all border border-white/5',
|
|
212
207
|
worker.status === 'online'
|
|
213
|
-
? 'bg-
|
|
214
|
-
: 'bg-
|
|
208
|
+
? 'bg-emerald-500/10 text-emerald-500'
|
|
209
|
+
: 'bg-zinc-800 text-muted-foreground/40'
|
|
215
210
|
)}
|
|
216
211
|
>
|
|
217
212
|
<Cpu size={24} />
|
|
218
213
|
</div>
|
|
219
|
-
|
|
220
|
-
className=
|
|
221
|
-
|
|
222
|
-
worker.status === 'online'
|
|
223
|
-
? 'bg-green-500 animate-pulse'
|
|
224
|
-
: 'bg-muted-foreground'
|
|
225
|
-
)}
|
|
226
|
-
/>
|
|
214
|
+
{worker.status === 'online' && (
|
|
215
|
+
<div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-4 border-zinc-950 bg-emerald-500 animate-pulse shadow-[0_0_10px_#10B981]" />
|
|
216
|
+
)}
|
|
227
217
|
</div>
|
|
228
218
|
<div>
|
|
229
|
-
<h3 className="font-black tracking-tight text-
|
|
219
|
+
<h3 className="font-black tracking-tight text-base group-hover:text-primary transition-colors font-heading uppercase italic">
|
|
230
220
|
{worker.id}
|
|
231
221
|
</h3>
|
|
232
|
-
<p className="text-[
|
|
222
|
+
<p className="text-[9px] font-bold text-muted-foreground/40 uppercase tracking-[0.2em] font-mono mt-1">
|
|
233
223
|
PID: {worker.pid}
|
|
234
224
|
</p>
|
|
235
225
|
</div>
|
|
236
226
|
</div>
|
|
237
227
|
<span
|
|
238
228
|
className={cn(
|
|
239
|
-
'px-
|
|
229
|
+
'px-2 py-1 rounded-md text-[8px] font-black uppercase tracking-widest border transition-all',
|
|
240
230
|
worker.status === 'online'
|
|
241
|
-
? 'bg-
|
|
242
|
-
: 'bg-
|
|
231
|
+
? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.1)]'
|
|
232
|
+
: 'bg-zinc-800/50 text-muted-foreground/40 border-transparent'
|
|
243
233
|
)}
|
|
244
234
|
>
|
|
245
235
|
{worker.status}
|
|
@@ -248,39 +238,37 @@ export function WorkersPage() {
|
|
|
248
238
|
|
|
249
239
|
{/* Metrics */}
|
|
250
240
|
{worker.metrics && (
|
|
251
|
-
<div className="space-y-
|
|
241
|
+
<div className="space-y-5 font-mono">
|
|
252
242
|
{/* CPU */}
|
|
253
243
|
<div>
|
|
254
|
-
<div className="flex justify-between text-[
|
|
255
|
-
<span className="text-muted-foreground">
|
|
256
|
-
Load (Cap: {worker.metrics.cores || '-'})
|
|
257
|
-
</span>
|
|
244
|
+
<div className="flex justify-between text-[9px] font-black uppercase tracking-widest mb-2.5">
|
|
245
|
+
<span className="text-muted-foreground/60">CPU Compute Power</span>
|
|
258
246
|
<span
|
|
259
247
|
className={cn(
|
|
260
248
|
worker.metrics.cpu > (worker.metrics.cores || 4)
|
|
261
249
|
? 'text-red-500'
|
|
262
250
|
: worker.metrics.cpu > (worker.metrics.cores || 4) * 0.7
|
|
263
|
-
? 'text-amber-500'
|
|
264
|
-
: 'text-
|
|
251
|
+
? 'text-amber-500 font-black'
|
|
252
|
+
: 'text-primary font-black'
|
|
265
253
|
)}
|
|
266
254
|
>
|
|
267
|
-
{worker.metrics.cpu.toFixed(2)}
|
|
255
|
+
{worker.metrics.cpu.toFixed(2)}%
|
|
268
256
|
</span>
|
|
269
257
|
</div>
|
|
270
|
-
<div className="h-
|
|
258
|
+
<div className="h-1.5 w-full bg-black/40 rounded-full overflow-hidden border border-white/5">
|
|
271
259
|
<motion.div
|
|
272
260
|
initial={{ width: 0 }}
|
|
273
261
|
animate={{
|
|
274
262
|
width: `${Math.min(100, (worker.metrics.cpu / (worker.metrics.cores || 1)) * 100)}%`,
|
|
275
263
|
}}
|
|
276
|
-
transition={{ duration:
|
|
264
|
+
transition={{ duration: 1 }}
|
|
277
265
|
className={cn(
|
|
278
|
-
'h-full transition-colors',
|
|
266
|
+
'h-full transition-colors relative',
|
|
279
267
|
worker.metrics.cpu > (worker.metrics.cores || 4)
|
|
280
268
|
? 'bg-red-500'
|
|
281
269
|
: worker.metrics.cpu > (worker.metrics.cores || 4) * 0.7
|
|
282
270
|
? 'bg-amber-500'
|
|
283
|
-
: 'bg-
|
|
271
|
+
: 'bg-primary shadow-[0_0_10px_#00F0FF]'
|
|
284
272
|
)}
|
|
285
273
|
/>
|
|
286
274
|
</div>
|
|
@@ -288,24 +276,20 @@ export function WorkersPage() {
|
|
|
288
276
|
|
|
289
277
|
{/* RAM */}
|
|
290
278
|
<div>
|
|
291
|
-
<div className="flex justify-between text-[
|
|
292
|
-
<span className="text-muted-foreground">Memory
|
|
293
|
-
<span className="text-
|
|
294
|
-
{(worker.metrics.ram.rss / 1024).toFixed(2)} GB
|
|
295
|
-
{worker.metrics.ram.total
|
|
296
|
-
? (worker.metrics.ram.total / 1024).toFixed(0)
|
|
297
|
-
: '-'}{' '}
|
|
298
|
-
GB
|
|
279
|
+
<div className="flex justify-between text-[9px] font-black uppercase tracking-widest mb-2.5">
|
|
280
|
+
<span className="text-muted-foreground/60">Memory Integrity</span>
|
|
281
|
+
<span className="text-white/80 font-black">
|
|
282
|
+
{(worker.metrics.ram.rss / 1024).toFixed(2)} GB
|
|
299
283
|
</span>
|
|
300
284
|
</div>
|
|
301
|
-
<div className="h-
|
|
285
|
+
<div className="h-1.5 w-full bg-black/40 rounded-full overflow-hidden border border-white/5">
|
|
302
286
|
<motion.div
|
|
303
287
|
initial={{ width: 0 }}
|
|
304
288
|
animate={{
|
|
305
289
|
width: `${Math.min(100, (worker.metrics.ram.rss / (worker.metrics.ram.total || 2048)) * 100)}%`,
|
|
306
290
|
}}
|
|
307
|
-
transition={{ duration:
|
|
308
|
-
className="h-full bg-indigo-500"
|
|
291
|
+
transition={{ duration: 1 }}
|
|
292
|
+
className="h-full bg-indigo-500 shadow-[0_0_10px_#6366F1]"
|
|
309
293
|
/>
|
|
310
294
|
</div>
|
|
311
295
|
</div>
|
|
@@ -313,35 +297,33 @@ export function WorkersPage() {
|
|
|
313
297
|
)}
|
|
314
298
|
|
|
315
299
|
{/* Laravel & Queue Info (New) */}
|
|
316
|
-
<div className="mt-
|
|
300
|
+
<div className="mt-8 space-y-3">
|
|
317
301
|
{/* Monitored Queues */}
|
|
318
302
|
{worker.queues && worker.queues.length > 0 && (
|
|
319
|
-
<div className="bg-
|
|
320
|
-
<div className="flex items-center gap-2 mb-
|
|
321
|
-
<div className="w-1.5 h-1.5 bg-
|
|
322
|
-
<span className="text-[9px] font-black uppercase tracking-
|
|
323
|
-
|
|
303
|
+
<div className="bg-white/[0.02] p-4 rounded-xl border border-white/5">
|
|
304
|
+
<div className="flex items-center gap-2 mb-3">
|
|
305
|
+
<div className="w-1.5 h-1.5 bg-primary rounded-full shadow-[0_0_8px_#00F0FF]" />
|
|
306
|
+
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 font-heading">
|
|
307
|
+
Pipeline Access
|
|
324
308
|
</span>
|
|
325
309
|
</div>
|
|
326
310
|
<div className="flex flex-wrap gap-2">
|
|
327
311
|
{worker.queues.map((q, i) => (
|
|
328
312
|
<div
|
|
329
313
|
key={i}
|
|
330
|
-
className="flex items-center gap-
|
|
314
|
+
className="flex items-center gap-2 text-[10px] font-black text-foreground/60 bg-black/40 px-2 py-1 rounded border border-white/5 font-mono"
|
|
331
315
|
>
|
|
332
|
-
<span className="opacity-
|
|
316
|
+
<span className="opacity-40">{q.name}</span>
|
|
333
317
|
{(q.size.waiting > 0 || q.size.failed > 0) && (
|
|
334
318
|
<span
|
|
335
319
|
className={cn(
|
|
336
|
-
'px-1 rounded
|
|
320
|
+
'px-1 rounded text-[8px] border',
|
|
337
321
|
q.size.failed > 0
|
|
338
|
-
? 'text-red-500 bg-red-500/10'
|
|
339
|
-
: 'text-
|
|
322
|
+
? 'text-red-500 bg-red-500/10 border-red-500/20'
|
|
323
|
+
: 'text-primary bg-primary/10 border-primary/20'
|
|
340
324
|
)}
|
|
341
325
|
>
|
|
342
|
-
{q.size.failed > 0
|
|
343
|
-
? `${q.size.failed} failed`
|
|
344
|
-
: `${q.size.waiting} wait`}
|
|
326
|
+
{q.size.failed > 0 ? `! FAIL` : `${q.size.waiting}W`}
|
|
345
327
|
</span>
|
|
346
328
|
)}
|
|
347
329
|
</div>
|
|
@@ -352,27 +334,31 @@ export function WorkersPage() {
|
|
|
352
334
|
|
|
353
335
|
{/* Laravel Workers Info */}
|
|
354
336
|
{worker.meta?.laravel && (
|
|
355
|
-
<div className="flex items-center justify-between p-
|
|
337
|
+
<div className="flex items-center justify-between p-4 bg-red-500/5 border border-red-500/10 rounded-xl">
|
|
356
338
|
<div className="flex items-center gap-2">
|
|
357
|
-
<span className="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse" />
|
|
358
|
-
<span className="text-[
|
|
359
|
-
Laravel
|
|
339
|
+
<span className="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse shadow-[0_0_8px_#EF4444]" />
|
|
340
|
+
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-red-500/80 font-heading">
|
|
341
|
+
Laravel Threading
|
|
360
342
|
</span>
|
|
361
343
|
</div>
|
|
362
|
-
<span className="font-mono text-sm font-black text-red-500">
|
|
363
|
-
{worker.meta.laravel.workerCount || 0}
|
|
344
|
+
<span className="font-mono text-sm font-black text-red-500 tabular-nums tracking-tighter">
|
|
345
|
+
{worker.meta.laravel.workerCount || 0} PHP
|
|
364
346
|
</span>
|
|
365
347
|
</div>
|
|
366
348
|
)}
|
|
367
349
|
</div>
|
|
368
350
|
|
|
369
351
|
{/* Uptime */}
|
|
370
|
-
<div className="mt-
|
|
371
|
-
<div className="flex items-center gap-2 text-muted-foreground">
|
|
372
|
-
<Clock size={
|
|
373
|
-
<span className="text-[
|
|
352
|
+
<div className="mt-6 pt-5 border-t border-white/5 flex items-center justify-between">
|
|
353
|
+
<div className="flex items-center gap-2 text-muted-foreground/40 font-heading">
|
|
354
|
+
<Clock size={12} />
|
|
355
|
+
<span className="text-[9px] font-black uppercase tracking-[0.2em]">
|
|
356
|
+
Quantum Uptime
|
|
357
|
+
</span>
|
|
374
358
|
</div>
|
|
375
|
-
<span className="font-mono text-
|
|
359
|
+
<span className="font-mono text-[11px] font-black text-white/60 tabular-nums">
|
|
360
|
+
{formatUptime(worker.uptime)}
|
|
361
|
+
</span>
|
|
376
362
|
</div>
|
|
377
363
|
</motion.div>
|
|
378
364
|
))}
|
|
@@ -35,7 +35,7 @@ export class CommandService {
|
|
|
35
35
|
service: string,
|
|
36
36
|
nodeId: string,
|
|
37
37
|
type: CommandType,
|
|
38
|
-
payload:
|
|
38
|
+
payload: any
|
|
39
39
|
): Promise<string> {
|
|
40
40
|
const commandId = crypto.randomUUID()
|
|
41
41
|
|
|
@@ -46,7 +46,7 @@ export class CommandService {
|
|
|
46
46
|
payload,
|
|
47
47
|
timestamp: Date.now(),
|
|
48
48
|
issuer: 'zenith',
|
|
49
|
-
}
|
|
49
|
+
} as any
|
|
50
50
|
|
|
51
51
|
const channel = `gravito:quasar:cmd:${service}:${nodeId}`
|
|
52
52
|
|
|
@@ -64,7 +64,7 @@ export class CommandService {
|
|
|
64
64
|
nodeId: string,
|
|
65
65
|
queue: string,
|
|
66
66
|
jobKey: string,
|
|
67
|
-
driver: 'redis' | 'laravel' = 'redis'
|
|
67
|
+
driver: 'redis' | 'laravel' | 'bullmq' | 'bull' | 'bee-queue' = 'redis'
|
|
68
68
|
): Promise<string> {
|
|
69
69
|
return this.sendCommand(service, nodeId, 'RETRY_JOB', {
|
|
70
70
|
queue,
|
|
@@ -81,7 +81,7 @@ export class CommandService {
|
|
|
81
81
|
nodeId: string,
|
|
82
82
|
queue: string,
|
|
83
83
|
jobKey: string,
|
|
84
|
-
driver: 'redis' | 'laravel' = 'redis'
|
|
84
|
+
driver: 'redis' | 'laravel' | 'bullmq' | 'bull' | 'bee-queue' = 'redis'
|
|
85
85
|
): Promise<string> {
|
|
86
86
|
return this.sendCommand(service, nodeId, 'DELETE_JOB', {
|
|
87
87
|
queue,
|
|
@@ -97,7 +97,7 @@ export class CommandService {
|
|
|
97
97
|
service: string,
|
|
98
98
|
queue: string,
|
|
99
99
|
jobKey: string,
|
|
100
|
-
driver: 'redis' | 'laravel' = 'redis'
|
|
100
|
+
driver: 'redis' | 'laravel' | 'bullmq' | 'bull' | 'bee-queue' = 'redis'
|
|
101
101
|
): Promise<string> {
|
|
102
102
|
return this.retryJob(service, '*', queue, jobKey, driver)
|
|
103
103
|
}
|
|
@@ -109,7 +109,7 @@ export class CommandService {
|
|
|
109
109
|
service: string,
|
|
110
110
|
queue: string,
|
|
111
111
|
jobKey: string,
|
|
112
|
-
driver: 'redis' | 'laravel' = 'redis'
|
|
112
|
+
driver: 'redis' | 'laravel' | 'bullmq' | 'bull' | 'bee-queue' = 'redis'
|
|
113
113
|
): Promise<string> {
|
|
114
114
|
return this.deleteJob(service, '*', queue, jobKey, driver)
|
|
115
115
|
}
|
|
@@ -121,13 +121,12 @@ export class CommandService {
|
|
|
121
121
|
service: string,
|
|
122
122
|
nodeId: string,
|
|
123
123
|
action: 'retry-all' | 'restart' | 'retry',
|
|
124
|
-
|
|
124
|
+
_jobId?: string
|
|
125
125
|
): Promise<string> {
|
|
126
126
|
return this.sendCommand(service, nodeId, 'LARAVEL_ACTION', {
|
|
127
127
|
queue: 'default',
|
|
128
|
-
jobKey: '*',
|
|
129
128
|
action,
|
|
130
|
-
|
|
129
|
+
driver: 'laravel',
|
|
131
130
|
})
|
|
132
131
|
}
|
|
133
132
|
|
|
@@ -4,28 +4,68 @@ import type { PulseNode } from '../../shared/types'
|
|
|
4
4
|
/**
|
|
5
5
|
* PulseService manages the discovery and health monitoring of system nodes.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* This service acts as the heartbeat of the distributed system, scanning Redis
|
|
8
|
+
* for ephemeral keys emitted by active Quasar agents. It aggregates these
|
|
9
|
+
* signals to provide a real-time view of the cluster topology, grouping nodes
|
|
10
|
+
* by their service roles.
|
|
11
|
+
*
|
|
12
|
+
* It is essential for:
|
|
13
|
+
* - Visualizing cluster health and scale.
|
|
14
|
+
* - Detecting silent node failures via heartbeat expiry.
|
|
15
|
+
* - Providing metadata for alerting systems.
|
|
9
16
|
*
|
|
10
17
|
* @public
|
|
11
18
|
* @since 3.0.0
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const pulse = new PulseService('redis://localhost:6379');
|
|
23
|
+
* await pulse.connect();
|
|
24
|
+
* const nodes = await pulse.getNodes();
|
|
25
|
+
* ```
|
|
12
26
|
*/
|
|
13
27
|
export class PulseService {
|
|
14
28
|
private redis: Redis
|
|
15
29
|
private prefix = 'gravito:quasar:node:'
|
|
16
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Creates a new instance of PulseService.
|
|
33
|
+
*
|
|
34
|
+
* @param redisUrl - Connection string for the Redis instance used for coordination.
|
|
35
|
+
*/
|
|
17
36
|
constructor(redisUrl: string) {
|
|
18
37
|
this.redis = new Redis(redisUrl, {
|
|
19
38
|
lazyConnect: true,
|
|
20
39
|
})
|
|
21
40
|
}
|
|
22
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Establishes the connection to Redis.
|
|
44
|
+
*
|
|
45
|
+
* Must be called before any other operations.
|
|
46
|
+
*
|
|
47
|
+
* @returns Promise that resolves when connected.
|
|
48
|
+
* @throws {Error} If connection fails.
|
|
49
|
+
*/
|
|
23
50
|
async connect() {
|
|
24
51
|
await this.redis.connect()
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
/**
|
|
28
|
-
* Discovers active Pulse nodes
|
|
55
|
+
* Discovers active Pulse nodes across the cluster.
|
|
56
|
+
*
|
|
57
|
+
* Uses Redis SCAN to find all keys matching the heartbeat pattern.
|
|
58
|
+
* Nodes are grouped by service name to facilitate dashboard rendering.
|
|
59
|
+
* Stale nodes (older than 60s) are filtered out to ensure data freshness.
|
|
60
|
+
*
|
|
61
|
+
* @returns A map of service names to their active node instances.
|
|
62
|
+
* @throws {Error} If Redis operations fail.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* const map = await pulse.getNodes();
|
|
67
|
+
* console.log(map['worker-service']); // Array of worker nodes
|
|
68
|
+
* ```
|
|
29
69
|
*/
|
|
30
70
|
async getNodes(): Promise<Record<string, PulseNode[]>> {
|
|
31
71
|
const nodes: PulseNode[] = []
|
|
@@ -81,7 +121,24 @@ export class PulseService {
|
|
|
81
121
|
}
|
|
82
122
|
|
|
83
123
|
/**
|
|
84
|
-
* Manually
|
|
124
|
+
* Manually records a heartbeat for the current process.
|
|
125
|
+
*
|
|
126
|
+
* This is typically used when Zenith itself needs to appear in the node list.
|
|
127
|
+
* The heartbeat is stored with a short TTL (30s) to ensure auto-removal
|
|
128
|
+
* upon crash or shutdown.
|
|
129
|
+
*
|
|
130
|
+
* @param node - The node metadata to broadcast.
|
|
131
|
+
* @returns Promise resolving when the heartbeat is saved.
|
|
132
|
+
* @throws {Error} If Redis write fails.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* await pulse.recordHeartbeat({
|
|
137
|
+
* id: 'zenith-1',
|
|
138
|
+
* service: 'dashboard',
|
|
139
|
+
* // ...other props
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
85
142
|
*/
|
|
86
143
|
async recordHeartbeat(node: PulseNode): Promise<void> {
|
|
87
144
|
const key = `${this.prefix}${node.service}:${node.id}`
|