@gravito/zenith 1.0.0-beta.1 → 1.0.1

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 (35) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/bin.js +436 -43
  3. package/dist/client/assets/index-C332gZ-J.css +1 -0
  4. package/dist/client/assets/{index-oXEse8ih.js → index-D4HibwTK.js} +88 -88
  5. package/dist/client/index.html +2 -2
  6. package/dist/server/index.js +436 -43
  7. package/docs/LARAVEL_ZENITH_ROADMAP.md +109 -0
  8. package/{QUASAR_MASTER_PLAN.md → docs/QUASAR_MASTER_PLAN.md} +6 -3
  9. package/package.json +1 -1
  10. package/scripts/debug_redis_keys.ts +24 -0
  11. package/src/client/App.tsx +1 -1
  12. package/src/client/Layout.tsx +11 -12
  13. package/src/client/WorkerStatus.tsx +97 -56
  14. package/src/client/components/BrandIcons.tsx +119 -44
  15. package/src/client/components/ConfirmDialog.tsx +0 -1
  16. package/src/client/components/JobInspector.tsx +18 -6
  17. package/src/client/components/PageHeader.tsx +32 -28
  18. package/src/client/pages/OverviewPage.tsx +0 -1
  19. package/src/client/pages/PulsePage.tsx +422 -340
  20. package/src/client/pages/SettingsPage.tsx +69 -15
  21. package/src/client/pages/WorkersPage.tsx +70 -2
  22. package/src/server/index.ts +171 -11
  23. package/src/server/services/QueueService.ts +6 -3
  24. package/src/shared/types.ts +2 -0
  25. package/ARCHITECTURE.md +0 -88
  26. package/BATCH_OPERATIONS_IMPLEMENTATION.md +0 -159
  27. package/EVOLUTION_BLUEPRINT.md +0 -112
  28. package/JOBINSPECTOR_SCROLL_FIX.md +0 -152
  29. package/TESTING_BATCH_OPERATIONS.md +0 -252
  30. package/dist/client/assets/index-BSTyMCFd.css +0 -1
  31. /package/{ALERTING_GUIDE.md → docs/ALERTING_GUIDE.md} +0 -0
  32. /package/{DEPLOYMENT.md → docs/DEPLOYMENT.md} +0 -0
  33. /package/{DOCS_INTERNAL.md → docs/DOCS_INTERNAL.md} +0 -0
  34. /package/{QUICK_TEST_GUIDE.md → docs/QUICK_TEST_GUIDE.md} +0 -0
  35. /package/{ROADMAP.md → docs/ROADMAP.md} +0 -0
@@ -291,7 +291,9 @@ export function SettingsPage() {
291
291
  defaultValue={alertConfig?.config?.channels?.slack?.webhookUrl || ''}
292
292
  onBlur={async (e) => {
293
293
  const val = e.target.value
294
- if (val === alertConfig?.config?.channels?.slack?.webhookUrl) return
294
+ if (val === alertConfig?.config?.channels?.slack?.webhookUrl) {
295
+ return
296
+ }
295
297
  await fetch('/api/alerts/config', {
296
298
  method: 'POST',
297
299
  headers: { 'Content-Type': 'application/json' },
@@ -362,7 +364,9 @@ export function SettingsPage() {
362
364
  defaultValue={alertConfig?.config?.channels?.discord?.webhookUrl || ''}
363
365
  onBlur={async (e) => {
364
366
  const val = e.target.value
365
- if (val === alertConfig?.config?.channels?.discord?.webhookUrl) return
367
+ if (val === alertConfig?.config?.channels?.discord?.webhookUrl) {
368
+ return
369
+ }
366
370
  await fetch('/api/alerts/config', {
367
371
  method: 'POST',
368
372
  headers: { 'Content-Type': 'application/json' },
@@ -430,10 +434,14 @@ export function SettingsPage() {
430
434
  {alertConfig?.config?.channels?.email?.enabled && (
431
435
  <div className="grid grid-cols-2 gap-3 mt-4 animate-in fade-in slide-in-from-top-2">
432
436
  <div className="col-span-2 space-y-1">
433
- <label className="text-[9px] font-black uppercase text-muted-foreground/60">
437
+ <label
438
+ htmlFor="email-to"
439
+ className="text-[9px] font-black uppercase text-muted-foreground/60"
440
+ >
434
441
  Destination Address
435
442
  </label>
436
443
  <input
444
+ id="email-to"
437
445
  placeholder="admin@example.com"
438
446
  defaultValue={alertConfig?.config?.channels?.email?.to || ''}
439
447
  onBlur={async (e) => {
@@ -457,10 +465,14 @@ export function SettingsPage() {
457
465
  />
458
466
  </div>
459
467
  <div className="space-y-1">
460
- <label className="text-[9px] font-black uppercase text-muted-foreground/60">
468
+ <label
469
+ htmlFor="smtp-host"
470
+ className="text-[9px] font-black uppercase text-muted-foreground/60"
471
+ >
461
472
  SMTP Host
462
473
  </label>
463
474
  <input
475
+ id="smtp-host"
464
476
  placeholder="smtp.gmail.com"
465
477
  defaultValue={alertConfig?.config?.channels?.email?.smtpHost || ''}
466
478
  onBlur={async (e) => {
@@ -483,10 +495,14 @@ export function SettingsPage() {
483
495
  />
484
496
  </div>
485
497
  <div className="space-y-1">
486
- <label className="text-[9px] font-black uppercase text-muted-foreground/60">
498
+ <label
499
+ htmlFor="smtp-port"
500
+ className="text-[9px] font-black uppercase text-muted-foreground/60"
501
+ >
487
502
  Port
488
503
  </label>
489
504
  <input
505
+ id="smtp-port"
490
506
  type="number"
491
507
  placeholder="465"
492
508
  defaultValue={alertConfig?.config?.channels?.email?.smtpPort || 465}
@@ -510,10 +526,14 @@ export function SettingsPage() {
510
526
  />
511
527
  </div>
512
528
  <div className="space-y-1">
513
- <label className="text-[9px] font-black uppercase text-muted-foreground/60">
529
+ <label
530
+ htmlFor="smtp-user"
531
+ className="text-[9px] font-black uppercase text-muted-foreground/60"
532
+ >
514
533
  Username
515
534
  </label>
516
535
  <input
536
+ id="smtp-user"
517
537
  placeholder="user@example.com"
518
538
  defaultValue={alertConfig?.config?.channels?.email?.smtpUser || ''}
519
539
  onBlur={async (e) => {
@@ -536,10 +556,14 @@ export function SettingsPage() {
536
556
  />
537
557
  </div>
538
558
  <div className="space-y-1">
539
- <label className="text-[9px] font-black uppercase text-muted-foreground/60">
559
+ <label
560
+ htmlFor="smtp-pass"
561
+ className="text-[9px] font-black uppercase text-muted-foreground/60"
562
+ >
540
563
  Password
541
564
  </label>
542
565
  <input
566
+ id="smtp-pass"
543
567
  type="password"
544
568
  placeholder="••••••••"
545
569
  defaultValue={alertConfig?.config?.channels?.email?.smtpPass || ''}
@@ -563,10 +587,14 @@ export function SettingsPage() {
563
587
  />
564
588
  </div>
565
589
  <div className="col-span-2 space-y-1">
566
- <label className="text-[9px] font-black uppercase text-muted-foreground/60">
590
+ <label
591
+ htmlFor="email-from"
592
+ className="text-[9px] font-black uppercase text-muted-foreground/60"
593
+ >
567
594
  From Address
568
595
  </label>
569
596
  <input
597
+ id="email-from"
570
598
  placeholder="Zenith Monitor <noreply@example.com>"
571
599
  defaultValue={alertConfig?.config?.channels?.email?.from || ''}
572
600
  onBlur={async (e) => {
@@ -598,6 +626,7 @@ export function SettingsPage() {
598
626
  Active Rules
599
627
  </h3>
600
628
  <button
629
+ type="button"
601
630
  onClick={() => setShowAddRule(!showAddRule)}
602
631
  className="text-[10px] font-black uppercase tracking-widest text-primary hover:underline border-none bg-transparent cursor-pointer"
603
632
  >
@@ -630,10 +659,14 @@ export function SettingsPage() {
630
659
  >
631
660
  <div className="grid grid-cols-2 gap-4">
632
661
  <div className="space-y-1">
633
- <label className="text-[10px] font-black uppercase text-muted-foreground">
662
+ <label
663
+ htmlFor="rule-name"
664
+ className="text-[10px] font-black uppercase text-muted-foreground"
665
+ >
634
666
  Rule Name
635
667
  </label>
636
668
  <input
669
+ id="rule-name"
637
670
  name="name"
638
671
  required
639
672
  placeholder="High CPU"
@@ -641,10 +674,14 @@ export function SettingsPage() {
641
674
  />
642
675
  </div>
643
676
  <div className="space-y-1">
644
- <label className="text-[10px] font-black uppercase text-muted-foreground">
677
+ <label
678
+ htmlFor="rule-type"
679
+ className="text-[10px] font-black uppercase text-muted-foreground"
680
+ >
645
681
  Type
646
682
  </label>
647
683
  <select
684
+ id="rule-type"
648
685
  name="type"
649
686
  className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none cursor-pointer"
650
687
  >
@@ -658,10 +695,14 @@ export function SettingsPage() {
658
695
  </div>
659
696
  <div className="grid grid-cols-3 gap-4">
660
697
  <div className="space-y-1">
661
- <label className="text-[10px] font-black uppercase text-muted-foreground">
698
+ <label
699
+ htmlFor="rule-threshold"
700
+ className="text-[10px] font-black uppercase text-muted-foreground"
701
+ >
662
702
  Threshold
663
703
  </label>
664
704
  <input
705
+ id="rule-threshold"
665
706
  name="threshold"
666
707
  type="number"
667
708
  required
@@ -670,10 +711,14 @@ export function SettingsPage() {
670
711
  />
671
712
  </div>
672
713
  <div className="space-y-1">
673
- <label className="text-[10px] font-black uppercase text-muted-foreground">
714
+ <label
715
+ htmlFor="rule-cooldown"
716
+ className="text-[10px] font-black uppercase text-muted-foreground"
717
+ >
674
718
  Cooldown (Min)
675
719
  </label>
676
720
  <input
721
+ id="rule-cooldown"
677
722
  name="cooldown"
678
723
  type="number"
679
724
  required
@@ -682,10 +727,14 @@ export function SettingsPage() {
682
727
  />
683
728
  </div>
684
729
  <div className="space-y-1">
685
- <label className="text-[10px] font-black uppercase text-muted-foreground">
730
+ <label
731
+ htmlFor="rule-queue"
732
+ className="text-[10px] font-black uppercase text-muted-foreground"
733
+ >
686
734
  Queue (Optional)
687
735
  </label>
688
736
  <input
737
+ id="rule-queue"
689
738
  name="queue"
690
739
  placeholder="default"
691
740
  className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none"
@@ -733,6 +782,7 @@ export function SettingsPage() {
733
782
  {rule.cooldownMinutes}m
734
783
  </div>
735
784
  <button
785
+ type="button"
736
786
  onClick={async () => {
737
787
  if (confirm('Delete this alert rule?')) {
738
788
  await fetch(`/api/alerts/rules/${rule.id}`, { method: 'DELETE' })
@@ -799,6 +849,7 @@ export function SettingsPage() {
799
849
  Auto-Cleanup
800
850
  </span>
801
851
  <button
852
+ type="button"
802
853
  onClick={async () => {
803
854
  const enabled = !alertConfig?.maintenance?.autoCleanup
804
855
  await fetch('/api/maintenance/config', {
@@ -813,7 +864,9 @@ export function SettingsPage() {
813
864
  }}
814
865
  className={cn(
815
866
  'w-10 h-5 rounded-full p-1 transition-all flex items-center',
816
- alertConfig?.maintenance?.autoCleanup ? 'bg-green-500 justify-end' : 'bg-muted justify-start'
867
+ alertConfig?.maintenance?.autoCleanup
868
+ ? 'bg-green-500 justify-end'
869
+ : 'bg-muted justify-start'
817
870
  )}
818
871
  >
819
872
  <div className="w-3 h-3 bg-white rounded-full shadow-sm" />
@@ -859,7 +912,8 @@ export function SettingsPage() {
859
912
  </span>
860
913
  {alertConfig?.maintenance?.lastRun && (
861
914
  <span className="text-[10px] text-muted-foreground/60">
862
- Last auto-cleanup run: {new Date(alertConfig.maintenance.lastRun).toLocaleString()}
915
+ Last auto-cleanup run:{' '}
916
+ {new Date(alertConfig.maintenance.lastRun).toLocaleString()}
863
917
  </span>
864
918
  )}
865
919
  </div>
@@ -18,7 +18,21 @@ interface Worker {
18
18
  total?: number
19
19
  }
20
20
  }
21
- queues?: string[]
21
+ queues?: {
22
+ name: string
23
+ size: {
24
+ waiting: number
25
+ active: number
26
+ failed: number
27
+ delayed: number
28
+ }
29
+ }[]
30
+ meta?: {
31
+ laravel?: {
32
+ workerCount: number
33
+ roots: string[]
34
+ }
35
+ }
22
36
  }
23
37
 
24
38
  export function WorkersPage() {
@@ -288,8 +302,62 @@ export function WorkersPage() {
288
302
  </div>
289
303
  )}
290
304
 
305
+ {/* Laravel & Queue Info (New) */}
306
+ <div className="mt-6 space-y-3">
307
+ {/* Monitored Queues */}
308
+ {worker.queues && worker.queues.length > 0 && (
309
+ <div className="bg-muted/10 p-3 rounded-xl border border-border/50">
310
+ <div className="flex items-center gap-2 mb-2">
311
+ <div className="w-1.5 h-1.5 bg-orange-500 rounded-full" />
312
+ <span className="text-[9px] font-black uppercase tracking-widest text-muted-foreground">
313
+ Monitored Queues
314
+ </span>
315
+ </div>
316
+ <div className="flex flex-wrap gap-2">
317
+ {worker.queues.map((q, i) => (
318
+ <div
319
+ key={i}
320
+ 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"
321
+ >
322
+ <span className="opacity-70">{q.name}</span>
323
+ {(q.size.waiting > 0 || q.size.failed > 0) && (
324
+ <span
325
+ className={cn(
326
+ 'px-1 rounded bg-muted text-[9px]',
327
+ q.size.failed > 0
328
+ ? 'text-red-500 bg-red-500/10'
329
+ : 'text-amber-500 bg-amber-500/10'
330
+ )}
331
+ >
332
+ {q.size.failed > 0
333
+ ? `${q.size.failed} failed`
334
+ : `${q.size.waiting} wait`}
335
+ </span>
336
+ )}
337
+ </div>
338
+ ))}
339
+ </div>
340
+ </div>
341
+ )}
342
+
343
+ {/* Laravel Workers Info */}
344
+ {worker.meta?.laravel && (
345
+ <div className="flex items-center justify-between p-3 bg-red-500/5 border border-red-500/10 rounded-xl">
346
+ <div className="flex items-center gap-2">
347
+ <span className="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse" />
348
+ <span className="text-[10px] font-black uppercase tracking-widest text-red-500/80">
349
+ Laravel Workers
350
+ </span>
351
+ </div>
352
+ <span className="font-mono text-sm font-black text-red-500">
353
+ {worker.meta.laravel.workerCount || 0}
354
+ </span>
355
+ </div>
356
+ )}
357
+ </div>
358
+
291
359
  {/* Uptime */}
292
- <div className="mt-6 pt-4 border-t border-border/30 flex items-center justify-between">
360
+ <div className="mt-4 pt-4 border-t border-border/30 flex items-center justify-between">
293
361
  <div className="flex items-center gap-2 text-muted-foreground">
294
362
  <Clock size={14} />
295
363
  <span className="text-[10px] font-bold uppercase tracking-widest">Uptime</span>
@@ -82,10 +82,88 @@ queueService
82
82
 
83
83
  console.log(`[FluxConsole] Connected to Redis at ${REDIS_URL}`)
84
84
  // Start background metrics recording (Reduced from 5s to 2s for better real-time feel)
85
- setInterval(async () => {
86
- const nodes = await pulseService.getNodes()
87
- queueService.recordStatusMetrics(nodes).catch(console.error)
88
- }, 2000)
85
+ const updateMetrics = async () => {
86
+ try {
87
+ const [pulseNodes, legacyWorkers] = await Promise.all([
88
+ pulseService.getNodes(),
89
+ queueService.listWorkers(),
90
+ ])
91
+
92
+ const pulseWorkers = Object.values(pulseNodes)
93
+ .flat()
94
+ .flatMap((node) => {
95
+ const mainNode = {
96
+ id: node.id,
97
+ service: node.service,
98
+ status: node.runtime.status || 'online',
99
+ pid: node.pid,
100
+ uptime: node.runtime.uptime,
101
+ metrics: {
102
+ cpu: node.cpu.process,
103
+ cores: node.cpu.cores,
104
+ ram: {
105
+ rss: node.memory.process.rss,
106
+ heapUsed: node.memory.process.heapUsed,
107
+ total: node.memory.system.total,
108
+ },
109
+ },
110
+ queues: node.queues,
111
+ meta: node.meta,
112
+ }
113
+
114
+ const subWorkers: any[] = []
115
+ if (node.meta?.laravel?.workers && Array.isArray(node.meta.laravel.workers)) {
116
+ node.meta.laravel.workers.forEach((w: any) => {
117
+ subWorkers.push({
118
+ id: `${node.id}-php-${w.pid}`,
119
+ service: `${node.service} / LARAVEL`,
120
+ status: w.status === 'running' || w.status === 'sleep' ? 'online' : 'idle',
121
+ pid: w.pid,
122
+ uptime: node.runtime.uptime,
123
+ metrics: {
124
+ cpu: w.cpu,
125
+ cores: 1,
126
+ ram: {
127
+ rss: w.memory,
128
+ heapUsed: w.memory,
129
+ total: node.memory.system.total,
130
+ },
131
+ },
132
+ meta: { isVirtual: true, cmdline: w.cmdline },
133
+ })
134
+ })
135
+ }
136
+ return [mainNode, ...subWorkers]
137
+ })
138
+
139
+ const formattedLegacy = legacyWorkers.map((w) => ({
140
+ id: w.id,
141
+ status: 'online',
142
+ pid: w.pid,
143
+ uptime: w.uptime,
144
+ metrics: {
145
+ cpu: (w.loadAvg[0] || 0) * 100,
146
+ cores: 0,
147
+ ram: {
148
+ rss: parseInt(w.memory.rss || '0', 10),
149
+ heapUsed: parseInt(w.memory.heapUsed || '0', 10),
150
+ total: 0,
151
+ },
152
+ },
153
+ queues: w.queues.map((q) => ({
154
+ name: q,
155
+ size: { waiting: 0, active: 0, failed: 0, delayed: 0 },
156
+ })),
157
+ meta: {},
158
+ }))
159
+
160
+ await queueService.recordStatusMetrics(pulseNodes, [...pulseWorkers, ...formattedLegacy])
161
+ } catch (err) {
162
+ console.error('[FluxConsole] Metrics Update Error:', err)
163
+ }
164
+ }
165
+
166
+ setInterval(updateMetrics, 2000)
89
167
 
90
168
  // Start Scheduler Tick (Reduced from 10s to 5s)
91
169
  setInterval(() => {
@@ -93,10 +171,7 @@ queueService
93
171
  }, 5000)
94
172
 
95
173
  // Record initial snapshot
96
- pulseService
97
- .getNodes()
98
- .then((nodes) => queueService.recordStatusMetrics(nodes))
99
- .catch(console.error)
174
+ updateMetrics()
100
175
  })
101
176
  .catch((err) => {
102
177
  console.error('[FluxConsole] Failed to connect to Redis', err)
@@ -324,9 +399,91 @@ api.get('/throughput', async (c) => {
324
399
 
325
400
  api.get('/workers', async (c) => {
326
401
  try {
327
- const workers = await queueService.listWorkers()
328
- return c.json({ workers })
402
+ const [legacyWorkers, pulseNodes] = await Promise.all([
403
+ queueService.listWorkers(),
404
+ pulseService.getNodes(),
405
+ ])
406
+
407
+ // Transform PulseNodes to match the frontend Worker interface
408
+ const pulseWorkers = Object.values(pulseNodes)
409
+ .flat()
410
+ .flatMap((node) => {
411
+ // 1. The Main Agent Node
412
+ const mainNode = {
413
+ id: node.id,
414
+ service: node.service,
415
+ status: node.runtime.status || 'online',
416
+ pid: node.pid,
417
+ uptime: node.runtime.uptime,
418
+ metrics: {
419
+ cpu: node.cpu.process,
420
+ cores: node.cpu.cores,
421
+ ram: {
422
+ rss: node.memory.process.rss,
423
+ heapUsed: node.memory.process.heapUsed,
424
+ total: node.memory.system.total,
425
+ },
426
+ },
427
+ queues: node.queues,
428
+ meta: node.meta,
429
+ }
430
+
431
+ // 2. Virtual Child Workers (e.g. Laravel)
432
+ const subWorkers: any[] = []
433
+ if (node.meta?.laravel?.workers && Array.isArray(node.meta.laravel.workers)) {
434
+ node.meta.laravel.workers.forEach((w: any) => {
435
+ subWorkers.push({
436
+ id: `${node.id}-php-${w.pid}`,
437
+ service: `${node.service} / LARAVEL`, // Distinct service name
438
+ status: w.status === 'running' || w.status === 'sleep' ? 'online' : 'idle',
439
+ pid: w.pid,
440
+ uptime: node.runtime.uptime, // Inherit uptime for now, or 0
441
+ metrics: {
442
+ cpu: w.cpu, // Per-process CPU
443
+ cores: 1, // Single threaded PHP
444
+ ram: {
445
+ rss: w.memory,
446
+ heapUsed: w.memory,
447
+ total: node.memory.system.total,
448
+ },
449
+ },
450
+ meta: {
451
+ // Tag it so UI can maybe style it differently?
452
+ isVirtual: true,
453
+ cmdline: w.cmdline,
454
+ },
455
+ })
456
+ })
457
+ }
458
+
459
+ return [mainNode, ...subWorkers]
460
+ })
461
+
462
+ // Transform Legacy Workers to match interface (best effort)
463
+ const formattedLegacy = legacyWorkers.map((w) => ({
464
+ id: w.id,
465
+ status: 'online',
466
+ pid: w.pid,
467
+ uptime: w.uptime,
468
+ metrics: {
469
+ cpu: (w.loadAvg[0] || 0) * 100, // Rough estimate
470
+ cores: 0,
471
+ ram: {
472
+ rss: parseInt(w.memory.rss || '0', 10),
473
+ heapUsed: parseInt(w.memory.heapUsed || '0', 10),
474
+ total: 0,
475
+ },
476
+ },
477
+ queues: w.queues.map((q) => ({
478
+ name: q,
479
+ size: { waiting: 0, active: 0, failed: 0, delayed: 0 },
480
+ })),
481
+ meta: {},
482
+ }))
483
+
484
+ return c.json({ workers: [...pulseWorkers, ...formattedLegacy] })
329
485
  } catch (_err) {
486
+ console.error(_err)
330
487
  return c.json({ error: 'Failed to fetch workers' }, 500)
331
488
  }
332
489
  })
@@ -403,7 +560,10 @@ api.post('/pulse/command', async (c) => {
403
560
 
404
561
  // Validate command type
405
562
  if (type !== 'RETRY_JOB' && type !== 'DELETE_JOB' && type !== 'LARAVEL_ACTION') {
406
- return c.json({ error: 'Invalid command type. Allowed: RETRY_JOB, DELETE_JOB, LARAVEL_ACTION' }, 400)
563
+ return c.json(
564
+ { error: 'Invalid command type. Allowed: RETRY_JOB, DELETE_JOB, LARAVEL_ACTION' },
565
+ 400
566
+ )
407
567
  }
408
568
 
409
569
  const commandId = await commandService.sendCommand(service, nodeId, type, {
@@ -344,7 +344,10 @@ export class QueueService {
344
344
  /**
345
345
  * Records a snapshot of current global statistics for sparklines.
346
346
  */
347
- async recordStatusMetrics(nodes: Record<string, any> = {}): Promise<void> {
347
+ async recordStatusMetrics(
348
+ nodes: Record<string, any> = {},
349
+ injectedWorkers?: any[]
350
+ ): Promise<void> {
348
351
  const stats = await this.listQueues()
349
352
  const totals = stats.reduce(
350
353
  (acc, q) => {
@@ -365,7 +368,7 @@ export class QueueService {
365
368
  pipe.set(`flux_console:metrics:failed:${now}`, totals.failed, 'EX', 3600)
366
369
 
367
370
  // Also record worker count
368
- const workers = await this.listWorkers()
371
+ const workers = injectedWorkers || (await this.listWorkers())
369
372
  pipe.set(`flux_console:metrics:workers:${now}`, workers.length, 'EX', 3600)
370
373
 
371
374
  await pipe.exec()
@@ -382,7 +385,7 @@ export class QueueService {
382
385
  .check({
383
386
  queues: stats,
384
387
  nodes: nodes as any,
385
- workers,
388
+ workers: workers as any,
386
389
  totals,
387
390
  })
388
391
  .catch((err) => console.error('[AlertService] Rule Evaluation Error:', err))
@@ -20,6 +20,8 @@ export interface PulseMemory {
20
20
  export interface PulseRuntime {
21
21
  uptime: number // seconds
22
22
  framework: string // e.g. "Node 20.1", "Laravel 10.0"
23
+ status?: string
24
+ errors?: string[]
23
25
  }
24
26
 
25
27
  export interface QueueSnapshot {
package/ARCHITECTURE.md DELETED
@@ -1,88 +0,0 @@
1
-
2
- # 🏗️ Gravito Flux Console Architecture
3
-
4
- > The official, standalone visualization and management console for Gravito Flux & Stream.
5
-
6
- ## 1. Project Manifesto
7
-
8
- - **Dogfooding First**: Uses `@gravito/photon` for HTTP serving and `@gravito/stream` for queue interaction.
9
- - **Zero-Config**: Should work out-of-the-box via `npx` with minimal arguments.
10
- - **Stateless**: The console itself holds no long-term state; Redis is the source of truth.
11
- - **Micro-Frontend Ready**: Built with React, matching the Gravito Admin ecosystem, but capable of running standalone.
12
-
13
- ## 2. System Architecture
14
-
15
- ```mermaid
16
- graph TD
17
- CLI[CLI Entry (bin)] --> Boot[Bootstrapper]
18
- Boot -->|Init| Server[Photon Server (Node/Bun)]
19
-
20
- subgraph "Backend Layer"
21
- Server -->|Serve| API[Management API]
22
- Server -->|Serve| Static[Frontend Assets]
23
-
24
- API -->|Command| QM[QueueManager (@gravito/stream)]
25
- QM -->|Protocol| Redis[(Redis)]
26
- end
27
-
28
- subgraph "Frontend Layer (React/Vite)"
29
- UI[Dashboard UI] -->|Fetch| API
30
- end
31
- ```
32
-
33
- ## 3. Technical Stack
34
-
35
- ### Backend
36
- - **Runtime**: Bun / Node.js (Compat)
37
- - **Framework**: **`@gravito/photon`** (Hono wrapper)
38
- - **Data Access**: **`@gravito/stream`** (Directly uses QueueDrivers)
39
- - **Persistence**: **`MySQLPersistence`** / **`SQLitePersistence`** for long-term auditing.
40
-
41
- ### Frontend
42
- - **Framework**: React 19
43
- - **Build Tool**: Vite
44
- - **Styling**: TailwindCSS (keeping consistent with `admin-shell`)
45
- - **State Management**: React Query (TanStack Query) for real-time polling.
46
-
47
- ## 4. Key Features (Phase 1 MVP)
48
-
49
- ### A. Dashboard
50
- - **System Overview**: Connection status, Driver type (Redis/Rabbit/Kafka).
51
- - **Throughput Metrics**: Jobs processed per second (calculated window).
52
-
53
- ### B. Queue Management
54
- - **List Queues**: Show all active queues with counts (Waiting, Active, Failed).
55
- - **Inspect Queue**: View jobs in a paginated list.
56
- - **Job Detail**: View JSON payload and stack trace.
57
-
58
- ### C. Actions
59
- - **Retry Job**: Move job from `failed` to `waiting`.
60
- - **Delete Job**: Remove job permanently.
61
-
62
- ### D. Persistence & Auditing
63
- - **Job Archive**: Completed and Failed jobs move to SQL storage.
64
- - **Operational Log Archiving**: Persistent storage for system events and worker activities with history search.
65
- - **Hybrid Search**: Query both Redis (Live) and SQL (Archive) simultaneously.
66
- - **Retention Management**: Configurable auto-cleanup for historical data.
67
-
68
- ### E. Alerting System
69
- - **Real-time Checks**: Monitoring for failure spikes and worker loss.
70
- - **Notifications**: Slack integration via Webhooks.
71
- - **Cool-down Logic**: Prevents duplicated alerts for the same event.
72
-
73
- ## 5. Deployment Strategy
74
-
75
- The package is published as a standard NPM package. It contains a `bin` entry point.
76
-
77
- ### Usage Scenarios
78
- 1. **Local Ad-hoc**: `npx @gravito/flux-console start --url redis://...`
79
- 2. **Project Integration**: Add to `package.json` scripts.
80
- 3. **Docker**: Official image wrapping the CLI.
81
-
82
- ## 6. Development Workflow
83
-
84
- Since this is a monolithic package (Backend + Frontend):
85
- - `npm run dev` should start:
86
- 1. Vite Dev Server (Frontend)
87
- 2. Photon Watch Mode (Backend)
88
- - Backend should proxy `/` requests to Vite during development.