@gravito/zenith 1.1.3 → 1.1.6
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/README.md +28 -10
- package/dist/bin.js +43235 -76691
- package/dist/client/index.html +13 -0
- package/dist/server/index.js +43235 -76691
- package/package.json +16 -7
- package/CHANGELOG.md +0 -62
- package/Dockerfile +0 -46
- package/Dockerfile.demo-worker +0 -29
- package/bin/flux-console.ts +0 -2
- package/doc/ECOSYSTEM_EXPANSION_RFC.md +0 -130
- package/docker-compose.yml +0 -40
- package/docs/ALERTING_GUIDE.md +0 -71
- package/docs/DEPLOYMENT.md +0 -157
- package/docs/DOCS_INTERNAL.md +0 -73
- package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
- package/docs/QUASAR_MASTER_PLAN.md +0 -140
- package/docs/QUICK_TEST_GUIDE.md +0 -72
- package/docs/ROADMAP.md +0 -85
- package/docs/integrations/LARAVEL.md +0 -207
- package/postcss.config.js +0 -6
- package/scripts/debug_redis_keys.ts +0 -24
- package/scripts/flood-logs.ts +0 -21
- package/scripts/seed.ts +0 -213
- package/scripts/verify-throttle.ts +0 -49
- package/scripts/worker.ts +0 -124
- package/specs/PULSE_SPEC.md +0 -86
- package/src/bin.ts +0 -6
- package/src/client/App.tsx +0 -72
- package/src/client/Layout.tsx +0 -669
- package/src/client/Sidebar.tsx +0 -112
- package/src/client/ThroughputChart.tsx +0 -158
- package/src/client/WorkerStatus.tsx +0 -202
- package/src/client/components/BrandIcons.tsx +0 -168
- package/src/client/components/ConfirmDialog.tsx +0 -134
- package/src/client/components/JobInspector.tsx +0 -487
- package/src/client/components/LogArchiveModal.tsx +0 -432
- package/src/client/components/NotificationBell.tsx +0 -212
- package/src/client/components/PageHeader.tsx +0 -47
- package/src/client/components/Toaster.tsx +0 -90
- package/src/client/components/UserProfileDropdown.tsx +0 -186
- package/src/client/contexts/AuthContext.tsx +0 -105
- package/src/client/contexts/NotificationContext.tsx +0 -128
- package/src/client/index.css +0 -172
- package/src/client/main.tsx +0 -15
- package/src/client/pages/LoginPage.tsx +0 -164
- package/src/client/pages/MetricsPage.tsx +0 -445
- package/src/client/pages/OverviewPage.tsx +0 -519
- package/src/client/pages/PulsePage.tsx +0 -409
- package/src/client/pages/QueuesPage.tsx +0 -378
- package/src/client/pages/SchedulesPage.tsx +0 -535
- package/src/client/pages/SettingsPage.tsx +0 -1001
- package/src/client/pages/WorkersPage.tsx +0 -380
- package/src/client/pages/index.ts +0 -8
- package/src/client/utils.ts +0 -15
- package/src/server/config/ServerConfigManager.ts +0 -90
- package/src/server/index.ts +0 -860
- package/src/server/middleware/auth.ts +0 -127
- package/src/server/services/AlertService.ts +0 -321
- package/src/server/services/CommandService.ts +0 -136
- package/src/server/services/LogStreamProcessor.ts +0 -93
- package/src/server/services/MaintenanceScheduler.ts +0 -78
- package/src/server/services/PulseService.ts +0 -148
- package/src/server/services/QueueMetricsCollector.ts +0 -138
- package/src/server/services/QueueService.ts +0 -924
- package/src/shared/types.ts +0 -223
- package/tailwind.config.js +0 -80
- package/tests/placeholder.test.ts +0 -7
- package/tsconfig.json +0 -29
- package/tsconfig.node.json +0 -10
- package/vite.config.ts +0 -27
|
@@ -1,519 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { AnimatePresence, animate, motion } from 'framer-motion'
|
|
3
|
-
import {
|
|
4
|
-
Activity,
|
|
5
|
-
AlertCircle,
|
|
6
|
-
ArrowRight,
|
|
7
|
-
ChevronRight,
|
|
8
|
-
Clock,
|
|
9
|
-
Cpu,
|
|
10
|
-
Hourglass,
|
|
11
|
-
ListTree,
|
|
12
|
-
Search,
|
|
13
|
-
Terminal,
|
|
14
|
-
} from 'lucide-react'
|
|
15
|
-
import React from 'react'
|
|
16
|
-
import { JobInspector } from '../components/JobInspector'
|
|
17
|
-
import { LogArchiveModal } from '../components/LogArchiveModal'
|
|
18
|
-
import { ThroughputChart } from '../ThroughputChart'
|
|
19
|
-
import { cn } from '../utils'
|
|
20
|
-
import { WorkerStatus } from '../WorkerStatus'
|
|
21
|
-
|
|
22
|
-
interface QueueStats {
|
|
23
|
-
name: string
|
|
24
|
-
waiting: number
|
|
25
|
-
delayed: number
|
|
26
|
-
active: number
|
|
27
|
-
failed: number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface SystemLog {
|
|
31
|
-
timestamp: string
|
|
32
|
-
level: 'error' | 'warn' | 'success' | 'info'
|
|
33
|
-
workerId: string
|
|
34
|
-
queue?: string
|
|
35
|
-
message: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface FluxStats {
|
|
39
|
-
queues: QueueStats[]
|
|
40
|
-
workers: any[]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const DEFAULT_STATS: FluxStats = {
|
|
44
|
-
queues: [],
|
|
45
|
-
workers: [],
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function LiveLogs({
|
|
49
|
-
logs,
|
|
50
|
-
onSearchArchive,
|
|
51
|
-
onWorkerHover,
|
|
52
|
-
}: {
|
|
53
|
-
logs: SystemLog[]
|
|
54
|
-
onSearchArchive: () => void
|
|
55
|
-
onWorkerHover?: (id: string | null) => void
|
|
56
|
-
}) {
|
|
57
|
-
const scrollRef = React.useRef<HTMLUListElement>(null)
|
|
58
|
-
|
|
59
|
-
React.useEffect(() => {
|
|
60
|
-
// Access logs to satisfy dependency check (and trigger on update)
|
|
61
|
-
if (scrollRef.current && logs) {
|
|
62
|
-
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
|
63
|
-
}
|
|
64
|
-
}, [logs])
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<div className="absolute inset-0 card-premium flex flex-col overflow-hidden group">
|
|
68
|
-
<div className="p-4 border-b bg-muted/5 flex justify-between items-center">
|
|
69
|
-
<div className="flex items-center gap-2">
|
|
70
|
-
<Terminal size={14} className="text-primary" />
|
|
71
|
-
<h2 className="text-xs font-black uppercase tracking-widest opacity-70">
|
|
72
|
-
Operational Logs
|
|
73
|
-
</h2>
|
|
74
|
-
</div>
|
|
75
|
-
<div className="flex items-center gap-3">
|
|
76
|
-
<button
|
|
77
|
-
type="button"
|
|
78
|
-
onClick={onSearchArchive}
|
|
79
|
-
aria-label="Search Logs Archive"
|
|
80
|
-
className="flex items-center gap-1.5 px-2 py-1 hover:bg-muted rounded-md text-[10px] font-black uppercase tracking-tighter text-muted-foreground transition-all"
|
|
81
|
-
>
|
|
82
|
-
<Search size={12} />
|
|
83
|
-
Search Archive
|
|
84
|
-
</button>
|
|
85
|
-
<div className="flex gap-1">
|
|
86
|
-
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></div>
|
|
87
|
-
<div className="w-1.5 h-1.5 rounded-full bg-green-500/40"></div>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
<ul
|
|
92
|
-
ref={scrollRef}
|
|
93
|
-
className="flex-1 min-h-0 overflow-y-auto p-4 font-mono text-[10px] space-y-2 scrollbar-thin scroll-smooth bg-black/20"
|
|
94
|
-
>
|
|
95
|
-
{logs.length === 0 ? (
|
|
96
|
-
<div className="h-full flex flex-col items-center justify-center text-muted-foreground/20 gap-2">
|
|
97
|
-
<Activity size={24} className="animate-pulse opacity-50" />
|
|
98
|
-
<p className="font-bold uppercase tracking-[0.3em] text-[8px]">Scanning spectrum...</p>
|
|
99
|
-
</div>
|
|
100
|
-
) : (
|
|
101
|
-
logs.map((log, i) => (
|
|
102
|
-
<li
|
|
103
|
-
key={i}
|
|
104
|
-
onMouseEnter={() => onWorkerHover?.(log.workerId)}
|
|
105
|
-
onMouseLeave={() => onWorkerHover?.(null)}
|
|
106
|
-
className="group flex gap-3 hover:bg-white/[0.03] -mx-2 px-3 py-1 rounded transition-all animate-in fade-in slide-in-from-left-1 duration-200 cursor-default border-l-2 border-transparent hover:border-primary/40"
|
|
107
|
-
>
|
|
108
|
-
<span className="text-muted-foreground/60 shrink-0 tabular-nums select-none opacity-60 group-hover:opacity-100 transition-opacity">
|
|
109
|
-
{new Date(log.timestamp).toLocaleTimeString([], {
|
|
110
|
-
hour12: false,
|
|
111
|
-
hour: '2-digit',
|
|
112
|
-
minute: '2-digit',
|
|
113
|
-
second: '2-digit',
|
|
114
|
-
})}
|
|
115
|
-
</span>
|
|
116
|
-
<div className="flex-1">
|
|
117
|
-
<div className="flex items-center gap-2 mb-0.5">
|
|
118
|
-
<span
|
|
119
|
-
className={cn(
|
|
120
|
-
'text-[8px] font-black uppercase tracking-widest px-1 rounded',
|
|
121
|
-
log.level === 'error'
|
|
122
|
-
? 'bg-red-500/10 text-red-500'
|
|
123
|
-
: log.level === 'warn'
|
|
124
|
-
? 'bg-amber-500/10 text-amber-500'
|
|
125
|
-
: log.level === 'success'
|
|
126
|
-
? 'bg-green-500/10 text-green-500'
|
|
127
|
-
: 'bg-primary/10 text-primary'
|
|
128
|
-
)}
|
|
129
|
-
>
|
|
130
|
-
{log.level}
|
|
131
|
-
</span>
|
|
132
|
-
<span className="text-[8px] font-bold text-muted-foreground/40 uppercase tracking-tighter opacity-0 group-hover:opacity-100 transition-all">
|
|
133
|
-
ID:{log.workerId}
|
|
134
|
-
</span>
|
|
135
|
-
</div>
|
|
136
|
-
<p className="text-foreground/70 group-hover:text-foreground leading-relaxed whitespace-pre-wrap break-all">
|
|
137
|
-
{log.message}
|
|
138
|
-
</p>
|
|
139
|
-
</div>
|
|
140
|
-
</li>
|
|
141
|
-
))
|
|
142
|
-
)}
|
|
143
|
-
</ul>
|
|
144
|
-
</div>
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function QueueHeatmap({ queues }: { queues: any[] }) {
|
|
149
|
-
return (
|
|
150
|
-
<div className="card-premium p-6 mb-8 overflow-hidden relative group">
|
|
151
|
-
<div className="absolute inset-0 opacity-10 group-hover:opacity-20 transition-opacity scanline pointer-events-none"></div>
|
|
152
|
-
<div className="flex items-center justify-between mb-4">
|
|
153
|
-
<div className="flex items-center gap-2">
|
|
154
|
-
<Activity size={14} className="text-primary animate-pulse" />
|
|
155
|
-
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/60">
|
|
156
|
-
Pipeline Load Distribution
|
|
157
|
-
</h3>
|
|
158
|
-
</div>
|
|
159
|
-
<div className="flex gap-1">
|
|
160
|
-
{[0.2, 0.4, 0.6, 0.8, 1].map((o) => (
|
|
161
|
-
<div key={o} className="w-2 h-2 rounded-sm bg-primary" style={{ opacity: o }} />
|
|
162
|
-
))}
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
<div className="grid grid-cols-10 sm:grid-cols-20 gap-1.5">
|
|
166
|
-
{queues.map((q, i) => {
|
|
167
|
-
const load = Math.min(1, q.waiting / 200)
|
|
168
|
-
return (
|
|
169
|
-
<div
|
|
170
|
-
key={i}
|
|
171
|
-
className="aspect-square rounded-sm transition-all duration-500 hover:scale-125 cursor-help group/tile relative"
|
|
172
|
-
style={{
|
|
173
|
-
backgroundColor: `hsl(var(--primary) / ${0.1 + load * 0.9})`,
|
|
174
|
-
boxShadow: load > 0.7 ? `0 0 10px hsl(var(--primary) / ${load})` : 'none',
|
|
175
|
-
}}
|
|
176
|
-
>
|
|
177
|
-
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 p-2 bg-popover text-popover-foreground rounded-lg text-[10px] whitespace-nowrap opacity-0 group-hover/tile:opacity-100 transition-opacity pointer-events-none border border-border z-50">
|
|
178
|
-
<span className="font-black">{q.name}</span>: {q.waiting} items
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
)
|
|
182
|
-
})}
|
|
183
|
-
{Array.from({ length: Math.max(0, 40 - queues.length) }).map((_, i) => (
|
|
184
|
-
<div
|
|
185
|
-
key={`empty-${i}`}
|
|
186
|
-
className="aspect-square rounded-sm bg-muted/20 border border-border/5 group-hover:border-border/10 transition-colors"
|
|
187
|
-
/>
|
|
188
|
-
))}
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function AnimatedNumber({ value }: { value: number }) {
|
|
195
|
-
const [displayValue, setDisplayValue] = React.useState(value)
|
|
196
|
-
|
|
197
|
-
React.useEffect(() => {
|
|
198
|
-
const controls = animate(displayValue, value, {
|
|
199
|
-
duration: 1.5,
|
|
200
|
-
ease: 'easeOut',
|
|
201
|
-
onUpdate: (latest: number) => setDisplayValue(Math.round(latest)),
|
|
202
|
-
})
|
|
203
|
-
return () => controls.stop()
|
|
204
|
-
}, [value, displayValue])
|
|
205
|
-
|
|
206
|
-
return <span>{displayValue.toLocaleString()}</span>
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
interface MetricCardProps {
|
|
210
|
-
title: string
|
|
211
|
-
value: number
|
|
212
|
-
icon: React.ReactNode
|
|
213
|
-
color: string
|
|
214
|
-
trend?: string
|
|
215
|
-
data?: number[]
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function MetricCard({ title, value, icon, color, trend, data }: MetricCardProps) {
|
|
219
|
-
const displayData = data && data.length > 0 ? data : [20, 30, 25, 40, 35, 50, 45, 60, 55, 70]
|
|
220
|
-
const max = Math.max(...displayData, 10)
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<div className="card-premium p-6 hover:shadow-2xl transform hover:-translate-y-1 group relative overflow-hidden flex flex-col justify-between">
|
|
224
|
-
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none scanline z-0"></div>
|
|
225
|
-
|
|
226
|
-
<div className="flex justify-between items-start mb-4 z-10 relative">
|
|
227
|
-
<div
|
|
228
|
-
className={cn(
|
|
229
|
-
'p-3 rounded-xl bg-muted/20 transition-all group-hover:scale-110 duration-500 border border-transparent group-hover:border-primary/20 shadow-inner',
|
|
230
|
-
color
|
|
231
|
-
)}
|
|
232
|
-
>
|
|
233
|
-
{icon}
|
|
234
|
-
</div>
|
|
235
|
-
{trend && (
|
|
236
|
-
<div className="flex flex-col items-end">
|
|
237
|
-
<span className={cn('text-[9px] font-black uppercase tracking-widest', color)}>
|
|
238
|
-
{trend}
|
|
239
|
-
</span>
|
|
240
|
-
<div className="w-10 h-1 bg-muted/30 rounded-full mt-1 overflow-hidden">
|
|
241
|
-
<motion.div
|
|
242
|
-
initial={{ width: 0 }}
|
|
243
|
-
animate={{ width: '100%' }}
|
|
244
|
-
transition={{ duration: 1.5, repeat: Infinity, repeatType: 'reverse' }}
|
|
245
|
-
className={cn('h-full', color.replace('text-', 'bg-'))}
|
|
246
|
-
/>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
)}
|
|
250
|
-
</div>
|
|
251
|
-
|
|
252
|
-
<div className="z-10 relative">
|
|
253
|
-
<p className="text-[10px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] mb-1 font-heading">
|
|
254
|
-
{title}
|
|
255
|
-
</p>
|
|
256
|
-
<div className="text-3xl font-black tracking-tight flex items-baseline gap-1 font-mono">
|
|
257
|
-
<AnimatedNumber value={value} />
|
|
258
|
-
{title === 'Waiting Jobs' && value > 100 && (
|
|
259
|
-
<span className="text-red-500 animate-pulse text-sm ml-1">!</span>
|
|
260
|
-
)}
|
|
261
|
-
</div>
|
|
262
|
-
</div>
|
|
263
|
-
|
|
264
|
-
<div className="mt-6 flex items-end gap-1 h-12 opacity-10 group-hover:opacity-30 transition-all duration-700 absolute bottom-0 left-0 right-0 pointer-events-none">
|
|
265
|
-
{displayData.map((v, i) => (
|
|
266
|
-
<div
|
|
267
|
-
key={i}
|
|
268
|
-
className={cn(
|
|
269
|
-
'flex-1 rounded-t-sm transition-all duration-1000',
|
|
270
|
-
color.replace('text-', 'bg-')
|
|
271
|
-
)}
|
|
272
|
-
style={{
|
|
273
|
-
height: `${(v / max) * 100}%`,
|
|
274
|
-
opacity: 0.2 + (i / displayData.length) * 0.8,
|
|
275
|
-
transitionDelay: `${i * 20}ms`,
|
|
276
|
-
}}
|
|
277
|
-
></div>
|
|
278
|
-
))}
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function QueueList({
|
|
285
|
-
queues,
|
|
286
|
-
setSelectedQueue,
|
|
287
|
-
}: {
|
|
288
|
-
queues: QueueStats[]
|
|
289
|
-
setSelectedQueue: (name: string | null) => void
|
|
290
|
-
}) {
|
|
291
|
-
return (
|
|
292
|
-
<div className="card-premium h-full flex flex-col overflow-hidden">
|
|
293
|
-
<div className="p-4 border-b bg-muted/5 flex justify-between items-center">
|
|
294
|
-
<div className="flex items-center gap-2">
|
|
295
|
-
<ListTree size={14} className="text-primary" />
|
|
296
|
-
<h2 className="text-xs font-black uppercase tracking-widest opacity-70">
|
|
297
|
-
Processing Pipelines
|
|
298
|
-
</h2>
|
|
299
|
-
</div>
|
|
300
|
-
<button
|
|
301
|
-
type="button"
|
|
302
|
-
className="text-[10px] font-black text-primary hover:underline flex items-center gap-2 uppercase tracking-widest transition-opacity"
|
|
303
|
-
>
|
|
304
|
-
Stats <ChevronRight size={12} />
|
|
305
|
-
</button>
|
|
306
|
-
</div>
|
|
307
|
-
<div className="flex-1 overflow-auto scrollbar-thin">
|
|
308
|
-
<table className="w-full text-left">
|
|
309
|
-
<thead className="bg-muted/10 text-muted-foreground uppercase text-[9px] font-black tracking-widest sticky top-0">
|
|
310
|
-
<tr>
|
|
311
|
-
<th className="px-4 py-3">Queue</th>
|
|
312
|
-
<th className="px-4 py-3">Waiting</th>
|
|
313
|
-
<th className="px-4 py-3 text-right">Ops</th>
|
|
314
|
-
</tr>
|
|
315
|
-
</thead>
|
|
316
|
-
<tbody className="divide-y divide-border/30 text-xs">
|
|
317
|
-
{queues.map((queue) => (
|
|
318
|
-
<tr key={queue.name} className="hover:bg-muted/5 transition-colors group">
|
|
319
|
-
<td className="px-4 py-4">
|
|
320
|
-
<div className="flex flex-col">
|
|
321
|
-
<span className="font-black text-foreground">{queue.name}</span>
|
|
322
|
-
{queue.failed > 0 && (
|
|
323
|
-
<span className="text-[9px] text-red-500 font-bold uppercase">
|
|
324
|
-
{queue.failed} FAILED
|
|
325
|
-
</span>
|
|
326
|
-
)}
|
|
327
|
-
</div>
|
|
328
|
-
</td>
|
|
329
|
-
<td className="px-4 py-4 font-mono font-black">{queue.waiting.toLocaleString()}</td>
|
|
330
|
-
<td className="px-4 py-4 text-right">
|
|
331
|
-
<div className="flex justify-end gap-2">
|
|
332
|
-
<button
|
|
333
|
-
type="button"
|
|
334
|
-
onClick={() => setSelectedQueue(queue.name)}
|
|
335
|
-
className="p-1.5 bg-muted hover:bg-primary/20 hover:text-primary rounded text-muted-foreground transition-all active:scale-90"
|
|
336
|
-
title="Inspect"
|
|
337
|
-
aria-label={`Inspect ${queue.name} queue`}
|
|
338
|
-
>
|
|
339
|
-
<ArrowRight size={14} />
|
|
340
|
-
</button>
|
|
341
|
-
</div>
|
|
342
|
-
</td>
|
|
343
|
-
</tr>
|
|
344
|
-
))}
|
|
345
|
-
</tbody>
|
|
346
|
-
</table>
|
|
347
|
-
</div>
|
|
348
|
-
</div>
|
|
349
|
-
)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* System Overview Dashboard Page.
|
|
354
|
-
*
|
|
355
|
-
* Provides a high-level, real-time view of all processing pipelines,
|
|
356
|
-
* including job counts, worker status, and live operational logs.
|
|
357
|
-
*
|
|
358
|
-
* @public
|
|
359
|
-
* @since 3.0.0
|
|
360
|
-
*/
|
|
361
|
-
export function OverviewPage() {
|
|
362
|
-
const [selectedQueue, setSelectedQueue] = React.useState<string | null>(null)
|
|
363
|
-
const [hoveredWorkerId, setHoveredWorkerId] = React.useState<string | null>(null)
|
|
364
|
-
|
|
365
|
-
const [logs, setLogs] = React.useState<SystemLog[]>([])
|
|
366
|
-
const [stats, setStats] = React.useState<FluxStats>(DEFAULT_STATS)
|
|
367
|
-
const [isLogArchiveOpen, setIsLogArchiveOpen] = React.useState(false)
|
|
368
|
-
|
|
369
|
-
React.useEffect(() => {
|
|
370
|
-
const handler = (e: any) => setSelectedQueue(e.detail)
|
|
371
|
-
window.addEventListener('select-queue', handler)
|
|
372
|
-
return () => window.removeEventListener('select-queue', handler)
|
|
373
|
-
}, [])
|
|
374
|
-
|
|
375
|
-
// Initial fetch
|
|
376
|
-
React.useEffect(() => {
|
|
377
|
-
fetch('/api/queues')
|
|
378
|
-
.then((res) => res.json())
|
|
379
|
-
.then((data) => {
|
|
380
|
-
if (data.queues) {
|
|
381
|
-
setStats((prev) => ({ ...prev, queues: data.queues }))
|
|
382
|
-
}
|
|
383
|
-
})
|
|
384
|
-
fetch('/api/workers')
|
|
385
|
-
.then((res) => res.json())
|
|
386
|
-
.then((data) => {
|
|
387
|
-
if (data.workers) {
|
|
388
|
-
setStats((prev) => ({ ...prev, workers: data.workers }))
|
|
389
|
-
}
|
|
390
|
-
})
|
|
391
|
-
}, [])
|
|
392
|
-
|
|
393
|
-
// Stats update listener
|
|
394
|
-
React.useEffect(() => {
|
|
395
|
-
const handler = (e: any) => {
|
|
396
|
-
const newStats = e.detail
|
|
397
|
-
if (newStats) {
|
|
398
|
-
setStats((prev) => ({
|
|
399
|
-
queues: newStats.queues || prev.queues,
|
|
400
|
-
workers: newStats.workers || prev.workers,
|
|
401
|
-
}))
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
window.addEventListener('flux-stats-update', handler)
|
|
405
|
-
return () => window.removeEventListener('flux-stats-update', handler)
|
|
406
|
-
}, [])
|
|
407
|
-
|
|
408
|
-
// Live log listener
|
|
409
|
-
React.useEffect(() => {
|
|
410
|
-
const handler = (e: CustomEvent) => {
|
|
411
|
-
const data = e.detail
|
|
412
|
-
setLogs((prev) => [...prev.slice(-99), data])
|
|
413
|
-
}
|
|
414
|
-
window.addEventListener('flux-log-update', handler as EventListener)
|
|
415
|
-
return () => window.removeEventListener('flux-log-update', handler as EventListener)
|
|
416
|
-
}, [])
|
|
417
|
-
|
|
418
|
-
// Clear logs listener
|
|
419
|
-
React.useEffect(() => {
|
|
420
|
-
const handler = () => setLogs([])
|
|
421
|
-
window.addEventListener('clear-logs', handler)
|
|
422
|
-
return () => window.removeEventListener('clear-logs', handler)
|
|
423
|
-
}, [])
|
|
424
|
-
|
|
425
|
-
const { data: historyData } = useQuery<{ history: Record<string, number[]> }>({
|
|
426
|
-
queryKey: ['metrics-history'],
|
|
427
|
-
queryFn: () => fetch('/api/metrics/history').then((res) => res.json()),
|
|
428
|
-
refetchInterval: 30000,
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
const history = historyData?.history || {}
|
|
432
|
-
const { queues, workers } = stats
|
|
433
|
-
|
|
434
|
-
const totalWaiting = queues.reduce((acc, q) => acc + q.waiting, 0)
|
|
435
|
-
const totalDelayed = queues.reduce((acc, q) => acc + q.delayed, 0)
|
|
436
|
-
const totalFailed = queues.reduce((acc, q) => acc + q.failed, 0)
|
|
437
|
-
const activeWorkers = workers.length
|
|
438
|
-
|
|
439
|
-
return (
|
|
440
|
-
<div className="space-y-12">
|
|
441
|
-
<LogArchiveModal isOpen={isLogArchiveOpen} onClose={() => setIsLogArchiveOpen(false)} />
|
|
442
|
-
<AnimatePresence>
|
|
443
|
-
{selectedQueue && (
|
|
444
|
-
<JobInspector queueName={selectedQueue} onClose={() => setSelectedQueue(null)} />
|
|
445
|
-
)}
|
|
446
|
-
</AnimatePresence>
|
|
447
|
-
|
|
448
|
-
<div className="flex justify-between items-end">
|
|
449
|
-
<div>
|
|
450
|
-
<h1 className="text-4xl font-black tracking-tighter">System Overview</h1>
|
|
451
|
-
<p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest">
|
|
452
|
-
Real-time status of your processing pipelines.
|
|
453
|
-
</p>
|
|
454
|
-
</div>
|
|
455
|
-
<div className="flex items-center gap-2 text-[10px] font-black text-green-500 bg-green-500/10 px-4 py-2 rounded-full border border-green-500/20 uppercase tracking-[0.2em] animate-pulse">
|
|
456
|
-
<span className="w-2 h-2 bg-green-500 rounded-full shadow-[0_0_8px_rgba(34,197,94,0.6)]"></span>
|
|
457
|
-
Live Syncing
|
|
458
|
-
</div>
|
|
459
|
-
</div>
|
|
460
|
-
|
|
461
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
462
|
-
<MetricCard
|
|
463
|
-
title="Waiting Jobs"
|
|
464
|
-
value={totalWaiting}
|
|
465
|
-
icon={<Hourglass size={20} />}
|
|
466
|
-
color="text-amber-500"
|
|
467
|
-
trend="+12% / hr"
|
|
468
|
-
data={history.waiting}
|
|
469
|
-
/>
|
|
470
|
-
<MetricCard
|
|
471
|
-
title="Delayed Jobs"
|
|
472
|
-
value={totalDelayed}
|
|
473
|
-
icon={<Clock size={20} />}
|
|
474
|
-
color="text-blue-500"
|
|
475
|
-
trend="Stable"
|
|
476
|
-
data={history.delayed}
|
|
477
|
-
/>
|
|
478
|
-
<MetricCard
|
|
479
|
-
title="Failed Jobs"
|
|
480
|
-
value={totalFailed}
|
|
481
|
-
icon={<AlertCircle size={20} />}
|
|
482
|
-
color="text-red-500"
|
|
483
|
-
trend={totalFailed > 0 ? 'CRITICAL' : 'CLEAN'}
|
|
484
|
-
data={history.failed}
|
|
485
|
-
/>
|
|
486
|
-
<MetricCard
|
|
487
|
-
title="Active Workers"
|
|
488
|
-
value={activeWorkers}
|
|
489
|
-
icon={<Cpu size={20} />}
|
|
490
|
-
color="text-indigo-500"
|
|
491
|
-
trend={activeWorkers > 0 ? 'ONLINE' : 'IDLE'}
|
|
492
|
-
data={history.workers}
|
|
493
|
-
/>
|
|
494
|
-
</div>
|
|
495
|
-
|
|
496
|
-
<ThroughputChart />
|
|
497
|
-
|
|
498
|
-
<QueueHeatmap queues={queues} />
|
|
499
|
-
|
|
500
|
-
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-8 lg:h-[600px]">
|
|
501
|
-
<div className="lg:col-span-1 h-[400px] lg:h-full">
|
|
502
|
-
<WorkerStatus highlightedWorkerId={hoveredWorkerId} workers={workers} />
|
|
503
|
-
</div>
|
|
504
|
-
<div className="lg:col-span-2 flex flex-col lg:grid lg:grid-rows-2 gap-6 lg:h-full">
|
|
505
|
-
<div className="relative h-[300px] lg:h-full min-h-0 overflow-hidden bg-card rounded-xl border border-border/50">
|
|
506
|
-
<LiveLogs
|
|
507
|
-
logs={logs}
|
|
508
|
-
onSearchArchive={() => setIsLogArchiveOpen(true)}
|
|
509
|
-
onWorkerHover={setHoveredWorkerId}
|
|
510
|
-
/>
|
|
511
|
-
</div>
|
|
512
|
-
<div className="h-[300px] lg:h-full min-h-0 overflow-hidden bg-card rounded-xl border border-border/50">
|
|
513
|
-
<QueueList queues={queues} setSelectedQueue={setSelectedQueue} />
|
|
514
|
-
</div>
|
|
515
|
-
</div>
|
|
516
|
-
</div>
|
|
517
|
-
</div>
|
|
518
|
-
)
|
|
519
|
-
}
|