@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +77 -22
  3. package/README.zh-TW.md +88 -0
  4. package/dist/bin.js +64681 -15842
  5. package/dist/client/assets/index-C80c1frR.css +1 -0
  6. package/dist/client/assets/index-CrWem9u3.js +434 -0
  7. package/dist/server/index.js +64681 -15842
  8. package/package.json +9 -7
  9. package/postcss.config.js +4 -4
  10. package/src/client/Layout.tsx +36 -39
  11. package/src/client/Sidebar.tsx +7 -7
  12. package/src/client/ThroughputChart.tsx +31 -17
  13. package/src/client/WorkerStatus.tsx +56 -80
  14. package/src/client/components/ConfirmDialog.tsx +22 -14
  15. package/src/client/components/JobInspector.tsx +95 -162
  16. package/src/client/index.css +29 -31
  17. package/src/client/pages/LoginPage.tsx +33 -31
  18. package/src/client/pages/MetricsPage.tsx +65 -37
  19. package/src/client/pages/OverviewPage.tsx +30 -28
  20. package/src/client/pages/PulsePage.tsx +111 -190
  21. package/src/client/pages/QueuesPage.tsx +82 -83
  22. package/src/client/pages/SchedulesPage.tsx +56 -61
  23. package/src/client/pages/SettingsPage.tsx +118 -137
  24. package/src/client/pages/WorkersPage.tsx +101 -115
  25. package/src/server/services/CommandService.ts +8 -9
  26. package/src/server/services/PulseService.ts +61 -4
  27. package/src/server/services/QueueService.ts +293 -0
  28. package/src/shared/types.ts +38 -13
  29. package/tailwind.config.js +75 -68
  30. package/tsconfig.json +28 -37
  31. package/tsconfig.node.json +9 -11
  32. package/dist/client/assets/index-BSMp8oq_.js +0 -436
  33. package/dist/client/assets/index-BwxlHx-_.css +0 -1
  34. package/dist/client/index.html +0 -13
  35. package/src/client/index.html +0 -12
  36. /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-6 relative overflow-hidden group">
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-2">
127
- <Server size={16} className="text-green-500" />
128
- <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest">
129
- Online Nodes
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-green-500">{onlineWorkers.length}</p>
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-6 relative overflow-hidden group">
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-2">
139
- <Zap size={16} className="text-muted-foreground" />
140
- <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest">
141
- Offline Nodes
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-muted-foreground">{offlineWorkers.length}</p>
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-6 relative overflow-hidden group">
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-2">
151
- <Gauge size={16} className="text-primary" />
152
- <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest">
153
- Avg Load
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">{avgCpu.toFixed(2)}</p>
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-6 relative overflow-hidden group">
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-2">
163
- <MemoryStick size={16} className="text-indigo-500" />
164
- <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest">
165
- Cluster RAM
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-indigo-500">{(totalRam / 1024).toFixed(2)}</p>
170
- {totalCapacity > 0 && (
171
- <span className="text-sm font-bold text-muted-foreground opacity-50">
172
- / {(totalCapacity / 1024).toFixed(0)} GB
173
- </span>
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-20 text-center text-muted-foreground/30">
184
- <Cpu size={48} className="mx-auto mb-4 opacity-20 animate-pulse" />
185
- <p className="text-sm font-bold uppercase tracking-widest">No worker nodes connected</p>
186
- <p className="text-xs opacity-60 mt-2">Start a worker to see it appear here</p>
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, y: 20 }}
193
- animate={{ opacity: 1, y: 0 }}
194
- transition={{ delay: index * 0.1 }}
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-6">
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-2xl flex items-center justify-center transition-all',
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-green-500/10 text-green-500'
214
- : 'bg-muted text-muted-foreground'
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
- <div
220
- className={cn(
221
- 'absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-card',
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-lg group-hover:text-primary transition-colors">
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-[10px] font-bold text-muted-foreground uppercase tracking-widest">
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-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border',
229
+ 'px-2 py-1 rounded-md text-[8px] font-black uppercase tracking-widest border transition-all',
240
230
  worker.status === 'online'
241
- ? 'bg-green-500/10 text-green-500 border-green-500/20'
242
- : 'bg-muted/40 text-muted-foreground border-transparent'
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-4">
241
+ <div className="space-y-5 font-mono">
252
242
  {/* CPU */}
253
243
  <div>
254
- <div className="flex justify-between text-[10px] font-black uppercase tracking-widest mb-2">
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-green-500'
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-2 w-full bg-muted rounded-full overflow-hidden">
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: 0.5 }}
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-green-500'
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-[10px] font-black uppercase tracking-widest mb-2">
292
- <span className="text-muted-foreground">Memory (RSS / Total)</span>
293
- <span className="text-indigo-500">
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-2 w-full bg-muted rounded-full overflow-hidden">
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: 0.5 }}
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-6 space-y-3">
300
+ <div className="mt-8 space-y-3">
317
301
  {/* Monitored Queues */}
318
302
  {worker.queues && worker.queues.length > 0 && (
319
- <div className="bg-muted/10 p-3 rounded-xl border border-border/50">
320
- <div className="flex items-center gap-2 mb-2">
321
- <div className="w-1.5 h-1.5 bg-orange-500 rounded-full" />
322
- <span className="text-[9px] font-black uppercase tracking-widest text-muted-foreground">
323
- Monitored Queues
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-1.5 text-xs font-bold text-foreground/80 bg-background/80 px-2 py-1 rounded-md shadow-sm border border-border/50"
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-70">{q.name}</span>
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 bg-muted text-[9px]',
320
+ 'px-1 rounded text-[8px] border',
337
321
  q.size.failed > 0
338
- ? 'text-red-500 bg-red-500/10'
339
- : 'text-amber-500 bg-amber-500/10'
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-3 bg-red-500/5 border border-red-500/10 rounded-xl">
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-[10px] font-black uppercase tracking-widest text-red-500/80">
359
- Laravel Workers
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-4 pt-4 border-t border-border/30 flex items-center justify-between">
371
- <div className="flex items-center gap-2 text-muted-foreground">
372
- <Clock size={14} />
373
- <span className="text-[10px] font-bold uppercase tracking-widest">Uptime</span>
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-sm font-bold">{formatUptime(worker.uptime)}</span>
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: QuasarCommand['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
- jobId?: string
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
- jobId,
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
- * It scans Redis for heartbeat keys emitted by Quasar agents and groups
8
- * them by service name for the Zenith dashboard.
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 using SCAN.
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 record a heartbeat (for this Zenith server itself).
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}`