@gravito/zenith 1.0.0-beta.1 → 1.0.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/CHANGELOG.md +9 -0
- package/dist/bin.js +436 -43
- package/dist/client/assets/index-C332gZ-J.css +1 -0
- package/dist/client/assets/{index-oXEse8ih.js → index-D4HibwTK.js} +88 -88
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +436 -43
- package/docs/LARAVEL_ZENITH_ROADMAP.md +109 -0
- package/{QUASAR_MASTER_PLAN.md → docs/QUASAR_MASTER_PLAN.md} +6 -3
- package/package.json +1 -1
- package/scripts/debug_redis_keys.ts +24 -0
- package/src/client/App.tsx +1 -1
- package/src/client/Layout.tsx +11 -12
- package/src/client/WorkerStatus.tsx +97 -56
- package/src/client/components/BrandIcons.tsx +119 -44
- package/src/client/components/ConfirmDialog.tsx +0 -1
- package/src/client/components/JobInspector.tsx +18 -6
- package/src/client/components/PageHeader.tsx +32 -28
- package/src/client/pages/OverviewPage.tsx +0 -1
- package/src/client/pages/PulsePage.tsx +422 -340
- package/src/client/pages/SettingsPage.tsx +69 -15
- package/src/client/pages/WorkersPage.tsx +70 -2
- package/src/server/index.ts +171 -11
- package/src/server/services/QueueService.ts +6 -3
- package/src/shared/types.ts +2 -0
- package/ARCHITECTURE.md +0 -88
- package/BATCH_OPERATIONS_IMPLEMENTATION.md +0 -159
- package/EVOLUTION_BLUEPRINT.md +0 -112
- package/JOBINSPECTOR_SCROLL_FIX.md +0 -152
- package/TESTING_BATCH_OPERATIONS.md +0 -252
- package/dist/client/assets/index-BSTyMCFd.css +0 -1
- /package/{ALERTING_GUIDE.md → docs/ALERTING_GUIDE.md} +0 -0
- /package/{DEPLOYMENT.md → docs/DEPLOYMENT.md} +0 -0
- /package/{DOCS_INTERNAL.md → docs/DOCS_INTERNAL.md} +0 -0
- /package/{QUICK_TEST_GUIDE.md → docs/QUICK_TEST_GUIDE.md} +0 -0
- /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)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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?:
|
|
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-
|
|
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>
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
328
|
-
|
|
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(
|
|
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(
|
|
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))
|
package/src/shared/types.ts
CHANGED
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.
|