@gravito/zenith 0.1.0-beta.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.
- package/ARCHITECTURE.md +88 -0
- package/BATCH_OPERATIONS_IMPLEMENTATION.md +159 -0
- package/DEMO.md +156 -0
- package/DEPLOYMENT.md +157 -0
- package/DOCS_INTERNAL.md +73 -0
- package/Dockerfile +46 -0
- package/Dockerfile.demo-worker +29 -0
- package/EVOLUTION_BLUEPRINT.md +112 -0
- package/JOBINSPECTOR_SCROLL_FIX.md +152 -0
- package/PULSE_IMPLEMENTATION_PLAN.md +111 -0
- package/QUICK_TEST_GUIDE.md +72 -0
- package/README.md +33 -0
- package/ROADMAP.md +85 -0
- package/TESTING_BATCH_OPERATIONS.md +252 -0
- package/bin/flux-console.ts +2 -0
- package/dist/bin.js +108196 -0
- package/dist/client/assets/index-DGYEwTDL.css +1 -0
- package/dist/client/assets/index-oyTdySX0.js +421 -0
- package/dist/client/index.html +13 -0
- package/dist/server/index.js +108191 -0
- package/docker-compose.yml +40 -0
- package/docs/integrations/LARAVEL.md +207 -0
- package/package.json +50 -0
- package/postcss.config.js +6 -0
- package/scripts/flood-logs.ts +21 -0
- package/scripts/seed.ts +213 -0
- package/scripts/verify-throttle.ts +45 -0
- package/scripts/worker.ts +123 -0
- package/src/bin.ts +6 -0
- package/src/client/App.tsx +70 -0
- package/src/client/Layout.tsx +644 -0
- package/src/client/Sidebar.tsx +102 -0
- package/src/client/ThroughputChart.tsx +135 -0
- package/src/client/WorkerStatus.tsx +170 -0
- package/src/client/components/ConfirmDialog.tsx +103 -0
- package/src/client/components/JobInspector.tsx +524 -0
- package/src/client/components/LogArchiveModal.tsx +383 -0
- package/src/client/components/NotificationBell.tsx +203 -0
- package/src/client/components/Toaster.tsx +80 -0
- package/src/client/components/UserProfileDropdown.tsx +177 -0
- package/src/client/contexts/AuthContext.tsx +93 -0
- package/src/client/contexts/NotificationContext.tsx +103 -0
- package/src/client/index.css +174 -0
- package/src/client/index.html +12 -0
- package/src/client/main.tsx +15 -0
- package/src/client/pages/LoginPage.tsx +153 -0
- package/src/client/pages/MetricsPage.tsx +408 -0
- package/src/client/pages/OverviewPage.tsx +511 -0
- package/src/client/pages/QueuesPage.tsx +372 -0
- package/src/client/pages/SchedulesPage.tsx +531 -0
- package/src/client/pages/SettingsPage.tsx +449 -0
- package/src/client/pages/WorkersPage.tsx +316 -0
- package/src/client/pages/index.ts +7 -0
- package/src/client/utils.ts +6 -0
- package/src/server/index.ts +556 -0
- package/src/server/middleware/auth.ts +127 -0
- package/src/server/services/AlertService.ts +160 -0
- package/src/server/services/QueueService.ts +828 -0
- package/tailwind.config.js +73 -0
- package/tests/placeholder.test.ts +7 -0
- package/tsconfig.json +38 -0
- package/tsconfig.node.json +12 -0
- package/vite.config.ts +27 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { motion } from 'framer-motion'
|
|
3
|
+
import { AlertCircle, Clock, Cpu, Gauge, MemoryStick, RefreshCcw, Server, Zap } from 'lucide-react'
|
|
4
|
+
import React, { useEffect } from 'react'
|
|
5
|
+
import { cn } from '../utils'
|
|
6
|
+
|
|
7
|
+
interface Worker {
|
|
8
|
+
id: string
|
|
9
|
+
status: string
|
|
10
|
+
pid: number
|
|
11
|
+
uptime: number
|
|
12
|
+
metrics?: {
|
|
13
|
+
cpu: number
|
|
14
|
+
cores?: number
|
|
15
|
+
ram: {
|
|
16
|
+
rss: number
|
|
17
|
+
heapUsed: number
|
|
18
|
+
total?: number
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
queues?: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function WorkersPage() {
|
|
25
|
+
const queryClient = useQueryClient()
|
|
26
|
+
const { isPending, error, data } = useQuery<{ workers: Worker[] }>({
|
|
27
|
+
queryKey: ['workers'],
|
|
28
|
+
queryFn: async () => {
|
|
29
|
+
const res = await fetch('/api/workers')
|
|
30
|
+
return res.json()
|
|
31
|
+
},
|
|
32
|
+
refetchInterval: 5000,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Listen to real-time stats updates from SSE
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const handler = (e: any) => {
|
|
38
|
+
if (e.detail?.workers) {
|
|
39
|
+
// Optimistically update the query cache with fresh worker data from SSE
|
|
40
|
+
queryClient.setQueryData(['workers'], { workers: e.detail.workers })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
window.addEventListener('flux-stats-update', handler)
|
|
44
|
+
return () => window.removeEventListener('flux-stats-update', handler)
|
|
45
|
+
}, [queryClient])
|
|
46
|
+
|
|
47
|
+
const workers = data?.workers || []
|
|
48
|
+
const onlineWorkers = workers.filter((w) => w.status === 'online')
|
|
49
|
+
const offlineWorkers = workers.filter((w) => w.status !== 'online')
|
|
50
|
+
|
|
51
|
+
const totalCpu = workers.reduce((acc, w) => acc + (w.metrics?.cpu || 0), 0)
|
|
52
|
+
const avgCpu = workers.length > 0 ? totalCpu / workers.length : 0
|
|
53
|
+
const totalRam = workers.reduce((acc, w) => acc + (w.metrics?.ram?.rss || 0), 0)
|
|
54
|
+
const totalCapacity = workers.reduce((acc, w) => acc + (w.metrics?.ram?.total || 0), 0)
|
|
55
|
+
|
|
56
|
+
if (isPending) {
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex flex-col items-center justify-center p-20 space-y-6">
|
|
59
|
+
<RefreshCcw className="animate-spin text-primary" size={48} />
|
|
60
|
+
<p className="text-muted-foreground font-bold uppercase tracking-[0.3em] text-xs">
|
|
61
|
+
Loading workers...
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (error) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="text-center p-20">
|
|
70
|
+
<div className="bg-red-500/10 text-red-500 p-10 rounded-3xl border border-red-500/20 max-w-md mx-auto shadow-2xl">
|
|
71
|
+
<AlertCircle size={56} className="mx-auto mb-6 opacity-80" />
|
|
72
|
+
<h3 className="text-2xl font-black mb-2 uppercase tracking-tight">
|
|
73
|
+
Failed to Load Workers
|
|
74
|
+
</h3>
|
|
75
|
+
<p className="text-sm font-medium opacity-70">{error.message}</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="space-y-8">
|
|
83
|
+
{/* Header */}
|
|
84
|
+
<div className="flex justify-between items-end">
|
|
85
|
+
<div>
|
|
86
|
+
<h1 className="text-4xl font-black tracking-tighter">Worker Nodes</h1>
|
|
87
|
+
<p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest">
|
|
88
|
+
Monitor and manage cluster processing nodes.
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
<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]">
|
|
92
|
+
<span className="w-2 h-2 bg-green-500 rounded-full shadow-[0_0_8px_rgba(34,197,94,0.6)] animate-pulse"></span>
|
|
93
|
+
{onlineWorkers.length} Online
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Summary Cards */}
|
|
98
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
99
|
+
<div className="card-premium p-6 relative overflow-hidden group">
|
|
100
|
+
<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" />
|
|
101
|
+
<div className="relative">
|
|
102
|
+
<div className="flex items-center gap-2 mb-2">
|
|
103
|
+
<Server size={16} className="text-green-500" />
|
|
104
|
+
<p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest">
|
|
105
|
+
Online Nodes
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
<p className="text-3xl font-black text-green-500">{onlineWorkers.length}</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="card-premium p-6 relative overflow-hidden group">
|
|
112
|
+
<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" />
|
|
113
|
+
<div className="relative">
|
|
114
|
+
<div className="flex items-center gap-2 mb-2">
|
|
115
|
+
<Zap size={16} className="text-muted-foreground" />
|
|
116
|
+
<p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest">
|
|
117
|
+
Offline Nodes
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
<p className="text-3xl font-black text-muted-foreground">{offlineWorkers.length}</p>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="card-premium p-6 relative overflow-hidden group">
|
|
124
|
+
<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" />
|
|
125
|
+
<div className="relative">
|
|
126
|
+
<div className="flex items-center gap-2 mb-2">
|
|
127
|
+
<Gauge size={16} className="text-primary" />
|
|
128
|
+
<p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest">
|
|
129
|
+
Avg Load
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
<p className="text-3xl font-black">{avgCpu.toFixed(2)}</p>
|
|
133
|
+
</div>
|
|
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-indigo-500/10 rounded-full -translate-y-1/2 translate-x-1/2 group-hover:scale-150 transition-transform duration-500" />
|
|
137
|
+
<div className="relative">
|
|
138
|
+
<div className="flex items-center gap-2 mb-2">
|
|
139
|
+
<MemoryStick size={16} className="text-indigo-500" />
|
|
140
|
+
<p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest">
|
|
141
|
+
Cluster RAM
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="flex items-baseline gap-1">
|
|
145
|
+
<p className="text-3xl font-black text-indigo-500">{(totalRam / 1024).toFixed(2)}</p>
|
|
146
|
+
{totalCapacity > 0 && (
|
|
147
|
+
<span className="text-sm font-bold text-muted-foreground opacity-50">
|
|
148
|
+
/ {(totalCapacity / 1024).toFixed(0)} GB
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Workers Grid */}
|
|
157
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
158
|
+
{workers.length === 0 && (
|
|
159
|
+
<div className="col-span-full py-20 text-center text-muted-foreground/30">
|
|
160
|
+
<Cpu size={48} className="mx-auto mb-4 opacity-20 animate-pulse" />
|
|
161
|
+
<p className="text-sm font-bold uppercase tracking-widest">No worker nodes connected</p>
|
|
162
|
+
<p className="text-xs opacity-60 mt-2">Start a worker to see it appear here</p>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
{workers.map((worker, index) => (
|
|
166
|
+
<motion.div
|
|
167
|
+
key={worker.id}
|
|
168
|
+
initial={{ opacity: 0, y: 20 }}
|
|
169
|
+
animate={{ opacity: 1, y: 0 }}
|
|
170
|
+
transition={{ delay: index * 0.1 }}
|
|
171
|
+
className="card-premium p-6 relative overflow-hidden group"
|
|
172
|
+
>
|
|
173
|
+
{/* Status indicator bar */}
|
|
174
|
+
<div
|
|
175
|
+
className={cn(
|
|
176
|
+
'absolute left-0 top-0 bottom-0 w-1.5 transition-all',
|
|
177
|
+
worker.status === 'online' ? 'bg-green-500' : 'bg-muted-foreground/30'
|
|
178
|
+
)}
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
{/* Header */}
|
|
182
|
+
<div className="flex items-start justify-between mb-6">
|
|
183
|
+
<div className="flex items-center gap-4">
|
|
184
|
+
<div className="relative">
|
|
185
|
+
<div
|
|
186
|
+
className={cn(
|
|
187
|
+
'w-12 h-12 rounded-2xl flex items-center justify-center transition-all',
|
|
188
|
+
worker.status === 'online'
|
|
189
|
+
? 'bg-green-500/10 text-green-500'
|
|
190
|
+
: 'bg-muted text-muted-foreground'
|
|
191
|
+
)}
|
|
192
|
+
>
|
|
193
|
+
<Cpu size={24} />
|
|
194
|
+
</div>
|
|
195
|
+
<div
|
|
196
|
+
className={cn(
|
|
197
|
+
'absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-card',
|
|
198
|
+
worker.status === 'online'
|
|
199
|
+
? 'bg-green-500 animate-pulse'
|
|
200
|
+
: 'bg-muted-foreground'
|
|
201
|
+
)}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
<div>
|
|
205
|
+
<h3 className="font-black tracking-tight text-lg group-hover:text-primary transition-colors">
|
|
206
|
+
{worker.id}
|
|
207
|
+
</h3>
|
|
208
|
+
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-widest">
|
|
209
|
+
PID: {worker.pid}
|
|
210
|
+
</p>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
<span
|
|
214
|
+
className={cn(
|
|
215
|
+
'px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border',
|
|
216
|
+
worker.status === 'online'
|
|
217
|
+
? 'bg-green-500/10 text-green-500 border-green-500/20'
|
|
218
|
+
: 'bg-muted/40 text-muted-foreground border-transparent'
|
|
219
|
+
)}
|
|
220
|
+
>
|
|
221
|
+
{worker.status}
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Metrics */}
|
|
226
|
+
{worker.metrics && (
|
|
227
|
+
<div className="space-y-4">
|
|
228
|
+
{/* CPU */}
|
|
229
|
+
<div>
|
|
230
|
+
<div className="flex justify-between text-[10px] font-black uppercase tracking-widest mb-2">
|
|
231
|
+
<span className="text-muted-foreground">
|
|
232
|
+
Load (Cap: {worker.metrics.cores || '-'})
|
|
233
|
+
</span>
|
|
234
|
+
<span
|
|
235
|
+
className={cn(
|
|
236
|
+
worker.metrics.cpu > (worker.metrics.cores || 4)
|
|
237
|
+
? 'text-red-500'
|
|
238
|
+
: worker.metrics.cpu > (worker.metrics.cores || 4) * 0.7
|
|
239
|
+
? 'text-amber-500'
|
|
240
|
+
: 'text-green-500'
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
{worker.metrics.cpu.toFixed(2)}
|
|
244
|
+
</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="h-2 w-full bg-muted rounded-full overflow-hidden">
|
|
247
|
+
<motion.div
|
|
248
|
+
initial={{ width: 0 }}
|
|
249
|
+
animate={{
|
|
250
|
+
width: `${Math.min(100, (worker.metrics.cpu / (worker.metrics.cores || 1)) * 100)}%`,
|
|
251
|
+
}}
|
|
252
|
+
transition={{ duration: 0.5 }}
|
|
253
|
+
className={cn(
|
|
254
|
+
'h-full transition-colors',
|
|
255
|
+
worker.metrics.cpu > (worker.metrics.cores || 4)
|
|
256
|
+
? 'bg-red-500'
|
|
257
|
+
: worker.metrics.cpu > (worker.metrics.cores || 4) * 0.7
|
|
258
|
+
? 'bg-amber-500'
|
|
259
|
+
: 'bg-green-500'
|
|
260
|
+
)}
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* RAM */}
|
|
266
|
+
<div>
|
|
267
|
+
<div className="flex justify-between text-[10px] font-black uppercase tracking-widest mb-2">
|
|
268
|
+
<span className="text-muted-foreground">Memory (RSS / Total)</span>
|
|
269
|
+
<span className="text-indigo-500">
|
|
270
|
+
{(worker.metrics.ram.rss / 1024).toFixed(2)} GB /{' '}
|
|
271
|
+
{worker.metrics.ram.total
|
|
272
|
+
? (worker.metrics.ram.total / 1024).toFixed(0)
|
|
273
|
+
: '-'}{' '}
|
|
274
|
+
GB
|
|
275
|
+
</span>
|
|
276
|
+
</div>
|
|
277
|
+
<div className="h-2 w-full bg-muted rounded-full overflow-hidden">
|
|
278
|
+
<motion.div
|
|
279
|
+
initial={{ width: 0 }}
|
|
280
|
+
animate={{
|
|
281
|
+
width: `${Math.min(100, (worker.metrics.ram.rss / (worker.metrics.ram.total || 2048)) * 100)}%`,
|
|
282
|
+
}}
|
|
283
|
+
transition={{ duration: 0.5 }}
|
|
284
|
+
className="h-full bg-indigo-500"
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{/* Uptime */}
|
|
292
|
+
<div className="mt-6 pt-4 border-t border-border/30 flex items-center justify-between">
|
|
293
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
294
|
+
<Clock size={14} />
|
|
295
|
+
<span className="text-[10px] font-bold uppercase tracking-widest">Uptime</span>
|
|
296
|
+
</div>
|
|
297
|
+
<span className="font-mono text-sm font-bold">{formatUptime(worker.uptime)}</span>
|
|
298
|
+
</div>
|
|
299
|
+
</motion.div>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatUptime(seconds: number): string {
|
|
307
|
+
if (seconds < 60) {
|
|
308
|
+
return `${Math.floor(seconds)}s`
|
|
309
|
+
}
|
|
310
|
+
if (seconds < 3600) {
|
|
311
|
+
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`
|
|
312
|
+
}
|
|
313
|
+
const hours = Math.floor(seconds / 3600)
|
|
314
|
+
const minutes = Math.floor((seconds % 3600) / 60)
|
|
315
|
+
return `${hours}h ${minutes}m`
|
|
316
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { LoginPage } from './LoginPage'
|
|
2
|
+
export { MetricsPage } from './MetricsPage'
|
|
3
|
+
export { OverviewPage } from './OverviewPage'
|
|
4
|
+
export { QueuesPage } from './QueuesPage'
|
|
5
|
+
export { SchedulesPage } from './SchedulesPage'
|
|
6
|
+
export { SettingsPage } from './SettingsPage'
|
|
7
|
+
export { WorkersPage } from './WorkersPage'
|