@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,409 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { motion } from 'framer-motion'
|
|
3
|
-
import { Activity, Cpu, Database, HelpCircle, Laptop, RotateCw, Server } from 'lucide-react'
|
|
4
|
-
import { useEffect, useState } from 'react'
|
|
5
|
-
import type { PulseNode } from '../../shared/types'
|
|
6
|
-
import { BunIcon, DenoIcon, GoIcon, NodeIcon, PhpIcon, PythonIcon } from '../components/BrandIcons'
|
|
7
|
-
import { PageHeader } from '../components/PageHeader'
|
|
8
|
-
import { cn } from '../utils'
|
|
9
|
-
|
|
10
|
-
// Helper to format bytes
|
|
11
|
-
const formatBytes = (bytes: number) => {
|
|
12
|
-
if (bytes === 0) {
|
|
13
|
-
return '0 B'
|
|
14
|
-
}
|
|
15
|
-
const k = 1024
|
|
16
|
-
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
17
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
18
|
-
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Helper to send remote commands to Quasar agents
|
|
22
|
-
const sendCommand = async (
|
|
23
|
-
service: string,
|
|
24
|
-
nodeId: string,
|
|
25
|
-
type: 'RETRY_JOB' | 'DELETE_JOB' | 'LARAVEL_ACTION',
|
|
26
|
-
queue: string,
|
|
27
|
-
action?: string
|
|
28
|
-
) => {
|
|
29
|
-
try {
|
|
30
|
-
const response = await fetch('/api/pulse/command', {
|
|
31
|
-
method: 'POST',
|
|
32
|
-
headers: { 'Content-Type': 'application/json' },
|
|
33
|
-
body: JSON.stringify({
|
|
34
|
-
service,
|
|
35
|
-
nodeId,
|
|
36
|
-
type,
|
|
37
|
-
queue,
|
|
38
|
-
action,
|
|
39
|
-
// For now, we send a wildcard jobKey to indicate "all failed jobs"
|
|
40
|
-
// The agent will interpret this appropriately
|
|
41
|
-
jobKey: '*',
|
|
42
|
-
driver: 'redis', // Default to redis, could be detected from queue config
|
|
43
|
-
}),
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const result = await response.json()
|
|
47
|
-
if (result.success) {
|
|
48
|
-
console.log(`[Pulse] ${type} command sent:`, result.message)
|
|
49
|
-
} else {
|
|
50
|
-
console.error(`[Pulse] Command failed:`, result.error)
|
|
51
|
-
}
|
|
52
|
-
} catch (err) {
|
|
53
|
-
console.error('[Pulse] Failed to send command:', err)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function NodeCard({ node }: { node: PulseNode }) {
|
|
58
|
-
const isHealthy = Date.now() - node.timestamp < 30000 // 30s threshold
|
|
59
|
-
const isWarning = !isHealthy && Date.now() - node.timestamp < 60000 // 60s warning
|
|
60
|
-
|
|
61
|
-
const renderIcon = () => {
|
|
62
|
-
switch (node.language) {
|
|
63
|
-
case 'node':
|
|
64
|
-
return <NodeIcon className="w-6 h-6" />
|
|
65
|
-
case 'bun':
|
|
66
|
-
return <BunIcon className="w-6 h-6 text-black" />
|
|
67
|
-
case 'deno':
|
|
68
|
-
return <DenoIcon className="w-6 h-6" />
|
|
69
|
-
case 'php':
|
|
70
|
-
return <PhpIcon className="w-6 h-6" />
|
|
71
|
-
case 'go':
|
|
72
|
-
return <GoIcon className="w-6 h-6" />
|
|
73
|
-
case 'python':
|
|
74
|
-
return <PythonIcon className="w-6 h-6" />
|
|
75
|
-
default:
|
|
76
|
-
return <HelpCircle className="w-6 h-6 text-white" />
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const laravel = node.meta?.laravel
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<motion.div
|
|
84
|
-
initial={{ opacity: 0, scale: 0.98 }}
|
|
85
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
86
|
-
className="card-premium p-5 relative overflow-hidden group border-l-4"
|
|
87
|
-
style={{
|
|
88
|
-
borderLeftColor: isHealthy ? '#10B981' : isWarning ? '#F59E0B' : '#EF4444',
|
|
89
|
-
}}
|
|
90
|
-
>
|
|
91
|
-
{/* Background Pulse Effect */}
|
|
92
|
-
<div
|
|
93
|
-
className={cn(
|
|
94
|
-
'absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl opacity-5 rounded-bl-full transition-all duration-700',
|
|
95
|
-
isHealthy
|
|
96
|
-
? 'from-emerald-500 to-transparent'
|
|
97
|
-
: isWarning
|
|
98
|
-
? 'from-yellow-500 to-transparent'
|
|
99
|
-
: 'from-red-500 to-transparent'
|
|
100
|
-
)}
|
|
101
|
-
/>
|
|
102
|
-
|
|
103
|
-
<div className="flex items-start justify-between mb-5 relative z-10">
|
|
104
|
-
<div className="flex items-center gap-4">
|
|
105
|
-
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-white dark:bg-zinc-800/50 border border-white/10 shadow-xl shrink-0">
|
|
106
|
-
{renderIcon()}
|
|
107
|
-
</div>
|
|
108
|
-
<div>
|
|
109
|
-
<h3 className="font-black text-foreground text-base flex items-center gap-2 font-heading tracking-tight">
|
|
110
|
-
{node.id}
|
|
111
|
-
<span className="text-[9px] px-2 py-0.5 rounded-md bg-primary/10 text-primary uppercase font-mono border border-primary/20">
|
|
112
|
-
{node.platform}
|
|
113
|
-
</span>
|
|
114
|
-
</h3>
|
|
115
|
-
<div className="text-[10px] text-muted-foreground font-bold flex items-center gap-1.5 mt-1 uppercase tracking-wider opacity-60">
|
|
116
|
-
<Laptop size={12} className="opacity-40" /> {node.hostname}{' '}
|
|
117
|
-
<span className="opacity-20">/</span> PID:{' '}
|
|
118
|
-
<span className="font-mono">{node.pid}</span>
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
<div
|
|
123
|
-
className={cn(
|
|
124
|
-
'w-3 h-3 rounded-full glow-pulse',
|
|
125
|
-
isHealthy
|
|
126
|
-
? 'bg-emerald-500 text-emerald-500'
|
|
127
|
-
: isWarning
|
|
128
|
-
? 'bg-yellow-500 text-yellow-500'
|
|
129
|
-
: 'bg-red-500 text-red-500'
|
|
130
|
-
)}
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
{/* Metrics Grid - Vertical Stack */}
|
|
135
|
-
<div className="space-y-4 font-mono">
|
|
136
|
-
{/* Laravel Specific Tools (if detected) */}
|
|
137
|
-
{laravel && laravel.workerCount > 0 && (
|
|
138
|
-
<div className="bg-amber-500/5 rounded-xl p-3 border border-amber-500/10">
|
|
139
|
-
<div className="flex items-center justify-between text-[10px] mb-3">
|
|
140
|
-
<div className="flex items-center gap-2 font-black text-amber-500 uppercase tracking-widest">
|
|
141
|
-
<PhpIcon className="w-4 h-4" />
|
|
142
|
-
Laravel Ecosystem ({laravel.workerCount})
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
<div className="flex flex-wrap gap-2">
|
|
146
|
-
<button
|
|
147
|
-
type="button"
|
|
148
|
-
onClick={() => {
|
|
149
|
-
if (
|
|
150
|
-
confirm('Are you sure you want to retry ALL failed Laravel jobs on this host?')
|
|
151
|
-
) {
|
|
152
|
-
sendCommand(node.service, node.id, 'LARAVEL_ACTION', 'default', 'retry-all')
|
|
153
|
-
}
|
|
154
|
-
}}
|
|
155
|
-
className="bg-amber-500/10 hover:bg-amber-500/20 text-amber-500 px-3 py-1.5 rounded-lg text-[9px] font-black uppercase flex items-center gap-2 transition-all border border-amber-500/20"
|
|
156
|
-
>
|
|
157
|
-
<RotateCw size={12} /> Retry All
|
|
158
|
-
</button>
|
|
159
|
-
<button
|
|
160
|
-
type="button"
|
|
161
|
-
onClick={() => {
|
|
162
|
-
if (
|
|
163
|
-
confirm(
|
|
164
|
-
'Artisan queue:restart will signal all workers to quit. Supervisor will restart them. Proceed?'
|
|
165
|
-
)
|
|
166
|
-
) {
|
|
167
|
-
sendCommand(node.service, node.id, 'LARAVEL_ACTION', 'default', 'restart')
|
|
168
|
-
}
|
|
169
|
-
}}
|
|
170
|
-
className="bg-white/5 hover:bg-white/10 text-foreground/80 px-3 py-1.5 rounded-lg text-[9px] font-black uppercase flex items-center gap-2 transition-all border border-white/5"
|
|
171
|
-
>
|
|
172
|
-
<RotateCw size={12} /> Restart
|
|
173
|
-
</button>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
)}
|
|
177
|
-
|
|
178
|
-
{/* Queues Section (if present) */}
|
|
179
|
-
{node.queues && node.queues.length > 0 && (
|
|
180
|
-
<div className="bg-zinc-900/50 rounded-xl p-3 border border-white/5">
|
|
181
|
-
<div className="flex items-center justify-between text-[9px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 mb-3">
|
|
182
|
-
<div className="flex items-center gap-2">
|
|
183
|
-
<Database size={12} />
|
|
184
|
-
Monitored Pipelines
|
|
185
|
-
</div>
|
|
186
|
-
<span className="bg-white/5 px-1.5 rounded">{node.queues.length} ACTIVE</span>
|
|
187
|
-
</div>
|
|
188
|
-
<div className="space-y-3">
|
|
189
|
-
{node.queues.map((q) => (
|
|
190
|
-
<div key={q.name} className="flex flex-col gap-2">
|
|
191
|
-
<div className="flex justify-between items-center text-[11px]">
|
|
192
|
-
<span className="font-bold text-foreground/80 tracking-tighter">{q.name}</span>
|
|
193
|
-
<div className="flex gap-3 items-center">
|
|
194
|
-
{q.size.failed > 0 && (
|
|
195
|
-
<div className="flex items-center gap-1">
|
|
196
|
-
<span className="text-red-500 font-black">{q.size.failed} FAIL</span>
|
|
197
|
-
<button
|
|
198
|
-
type="button"
|
|
199
|
-
onClick={() => sendCommand(node.service, node.id, 'RETRY_JOB', q.name)}
|
|
200
|
-
className="p-1 rounded bg-red-500/10 text-red-500 hover:bg-red-500 hover:text-white transition-all"
|
|
201
|
-
>
|
|
202
|
-
<RotateCw size={10} />
|
|
203
|
-
</button>
|
|
204
|
-
</div>
|
|
205
|
-
)}
|
|
206
|
-
<span className="text-muted-foreground/60">{q.size.waiting} WAIT</span>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
<div className="h-1.5 w-full bg-black/40 rounded-full overflow-hidden flex border border-white/5">
|
|
210
|
-
<div
|
|
211
|
-
className="bg-red-500 h-full transition-all"
|
|
212
|
-
style={{
|
|
213
|
-
width: `${(q.size.failed / Math.max(1, q.size.waiting + q.size.active + q.size.failed)) * 100}%`,
|
|
214
|
-
}}
|
|
215
|
-
/>
|
|
216
|
-
<div
|
|
217
|
-
className="bg-emerald-500 h-full transition-all shadow-[0_0_10px_#10B981]"
|
|
218
|
-
style={{
|
|
219
|
-
width: `${(q.size.active / Math.max(1, q.size.waiting + q.size.active + q.size.failed)) * 100}%`,
|
|
220
|
-
}}
|
|
221
|
-
/>
|
|
222
|
-
</div>
|
|
223
|
-
</div>
|
|
224
|
-
))}
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
)}
|
|
228
|
-
|
|
229
|
-
{/* System Load */}
|
|
230
|
-
<div className="grid grid-cols-2 gap-3">
|
|
231
|
-
<div className="bg-zinc-900/50 rounded-xl p-3 border border-white/5 flex flex-col justify-between">
|
|
232
|
-
<div className="flex justify-between items-center text-[9px] font-black uppercase text-muted-foreground/40 mb-2">
|
|
233
|
-
<span className="flex items-center gap-1.5">
|
|
234
|
-
<Cpu size={10} /> CPU
|
|
235
|
-
</span>
|
|
236
|
-
<span>{node.cpu.cores}C</span>
|
|
237
|
-
</div>
|
|
238
|
-
<div className="flex items-baseline gap-1">
|
|
239
|
-
<span className="text-2xl font-black text-primary tracking-tighter">
|
|
240
|
-
{node.cpu.process.toFixed(0)}%
|
|
241
|
-
</span>
|
|
242
|
-
<span className="text-[10px] font-bold opacity-40 uppercase">Load</span>
|
|
243
|
-
</div>
|
|
244
|
-
<div className="h-1 w-full bg-black/40 rounded-full mt-3 overflow-hidden">
|
|
245
|
-
<div
|
|
246
|
-
className={cn(
|
|
247
|
-
'h-full transition-all duration-1000',
|
|
248
|
-
node.cpu.process > 80
|
|
249
|
-
? 'bg-red-500 shadow-[0_0_10px_#EF4444]'
|
|
250
|
-
: 'bg-primary shadow-[0_0_10px_#00F0FF]'
|
|
251
|
-
)}
|
|
252
|
-
style={{ width: `${node.cpu.process}%` }}
|
|
253
|
-
/>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
|
|
257
|
-
<div className="bg-zinc-900/50 rounded-xl p-3 border border-white/5 flex flex-col justify-between">
|
|
258
|
-
<div className="flex justify-between items-center text-[9px] font-black uppercase text-muted-foreground/40 mb-2">
|
|
259
|
-
<span className="flex items-center gap-1.5">
|
|
260
|
-
<Database size={10} /> RAM
|
|
261
|
-
</span>
|
|
262
|
-
<span>RSS</span>
|
|
263
|
-
</div>
|
|
264
|
-
<div className="flex items-baseline gap-1">
|
|
265
|
-
<span className="text-2xl font-black text-white tracking-tighter">
|
|
266
|
-
{formatBytes(node.memory.process.rss).split(' ')[0]}
|
|
267
|
-
</span>
|
|
268
|
-
<span className="text-[10px] font-bold opacity-40 uppercase">
|
|
269
|
-
{formatBytes(node.memory.process.rss).split(' ')[1]}
|
|
270
|
-
</span>
|
|
271
|
-
</div>
|
|
272
|
-
<div className="h-1 w-full bg-black/40 rounded-full mt-3 overflow-hidden">
|
|
273
|
-
<div
|
|
274
|
-
className="bg-indigo-500 h-full transition-all duration-1000 shadow-[0_0_10px_#6366F1]"
|
|
275
|
-
style={{ width: `${(node.memory.process.rss / node.memory.system.total) * 100}%` }}
|
|
276
|
-
/>
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
|
|
282
|
-
<div className="mt-5 pt-4 border-t border-white/5 flex items-center justify-between text-[10px] font-black uppercase tracking-widest text-muted-foreground/40">
|
|
283
|
-
<span className="flex items-center gap-2">
|
|
284
|
-
<Server size={12} className="text-primary/40" />
|
|
285
|
-
{node.runtime.framework} <span className="opacity-20">•</span> v{node.version}
|
|
286
|
-
</span>
|
|
287
|
-
<span className="font-mono tabular-nums">UP: {Math.floor(node.runtime.uptime / 60)}M</span>
|
|
288
|
-
</div>
|
|
289
|
-
</motion.div>
|
|
290
|
-
)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Compact Service Group Component
|
|
294
|
-
function ServiceGroup({ service, nodes }: { service: string; nodes: PulseNode[] }) {
|
|
295
|
-
const isSingle = nodes.length === 1
|
|
296
|
-
|
|
297
|
-
return (
|
|
298
|
-
<div className="bg-card/50 border border-border/40 rounded-xl p-4 flex flex-col h-full">
|
|
299
|
-
<div className="flex items-center gap-2 mb-4 pb-3 border-b border-border/40">
|
|
300
|
-
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
301
|
-
<h2 className="text-sm font-bold uppercase tracking-widest text-muted-foreground flex-1">
|
|
302
|
-
{service}
|
|
303
|
-
</h2>
|
|
304
|
-
<span className="bg-muted text-foreground px-2 py-0.5 rounded-md text-xs font-mono">
|
|
305
|
-
{nodes.length}
|
|
306
|
-
</span>
|
|
307
|
-
</div>
|
|
308
|
-
|
|
309
|
-
<div className={cn('grid gap-3', isSingle ? 'grid-cols-1' : 'grid-cols-1 xl:grid-cols-2')}>
|
|
310
|
-
{nodes.map((node) => (
|
|
311
|
-
<NodeCard key={node.id} node={node} />
|
|
312
|
-
))}
|
|
313
|
-
</div>
|
|
314
|
-
</div>
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* System Pulse Dashboard Page.
|
|
320
|
-
*
|
|
321
|
-
* Provides real-time resource monitoring (CPU, RAM) and service discovery
|
|
322
|
-
* for all connected Quasar agents. It also allows remote management of
|
|
323
|
-
* connected worker nodes.
|
|
324
|
-
*
|
|
325
|
-
* @public
|
|
326
|
-
* @since 3.0.0
|
|
327
|
-
*/
|
|
328
|
-
export function PulsePage() {
|
|
329
|
-
const { data: initialData, isLoading } = useQuery<{ nodes: Record<string, PulseNode[]> }>({
|
|
330
|
-
queryKey: ['pulse-nodes'],
|
|
331
|
-
queryFn: async () => {
|
|
332
|
-
const res = await fetch('/api/pulse/nodes')
|
|
333
|
-
return res.json()
|
|
334
|
-
},
|
|
335
|
-
// Remove polling
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
const [nodes, setNodes] = useState<Record<string, PulseNode[]>>({})
|
|
339
|
-
|
|
340
|
-
// Hydrate initial data
|
|
341
|
-
useEffect(() => {
|
|
342
|
-
if (initialData?.nodes) {
|
|
343
|
-
setNodes(initialData.nodes)
|
|
344
|
-
}
|
|
345
|
-
}, [initialData])
|
|
346
|
-
|
|
347
|
-
// Listen for SSE updates
|
|
348
|
-
useEffect(() => {
|
|
349
|
-
const handler = (e: Event) => {
|
|
350
|
-
const customEvent = e as CustomEvent
|
|
351
|
-
if (customEvent.detail?.nodes) {
|
|
352
|
-
setNodes(customEvent.detail.nodes)
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
window.addEventListener('flux-pulse-update', handler)
|
|
356
|
-
return () => window.removeEventListener('flux-pulse-update', handler)
|
|
357
|
-
}, [])
|
|
358
|
-
|
|
359
|
-
// Loading Skeleton
|
|
360
|
-
if (isLoading && Object.keys(nodes).length === 0) {
|
|
361
|
-
return (
|
|
362
|
-
<div className="p-6 md:p-10 max-w-7xl mx-auto space-y-8 animate-pulse">
|
|
363
|
-
<div className="h-8 w-48 bg-muted rounded-lg" />
|
|
364
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
365
|
-
{[1, 2, 3].map((i) => (
|
|
366
|
-
<div key={i} className="h-48 bg-muted rounded-xl" />
|
|
367
|
-
))}
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const services = Object.entries(nodes).sort(([a], [b]) => a.localeCompare(b))
|
|
374
|
-
|
|
375
|
-
return (
|
|
376
|
-
<div className="min-h-screen bg-background text-foreground pb-20">
|
|
377
|
-
<div className="p-6 md:p-10 max-w-7xl mx-auto space-y-8">
|
|
378
|
-
<PageHeader
|
|
379
|
-
icon={Activity}
|
|
380
|
-
title="System Pulse"
|
|
381
|
-
description="Real-time infrastructure monitoring across your entire stack."
|
|
382
|
-
>
|
|
383
|
-
<div className="flex items-center gap-2 text-xs font-mono text-muted-foreground bg-muted px-3 py-1.5 rounded-lg border border-border/50">
|
|
384
|
-
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
|
385
|
-
LIVE CONNECTION
|
|
386
|
-
</div>
|
|
387
|
-
</PageHeader>
|
|
388
|
-
|
|
389
|
-
{services.length === 0 ? (
|
|
390
|
-
<div className="flex flex-col items-center justify-center py-20 text-center border-2 border-dashed border-border rounded-xl">
|
|
391
|
-
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
|
392
|
-
<Activity className="text-muted-foreground" size={32} />
|
|
393
|
-
</div>
|
|
394
|
-
<h3 className="text-lg font-bold">No Pulse Signals Detected</h3>
|
|
395
|
-
<p className="text-muted-foreground max-w-sm mt-2">
|
|
396
|
-
Start a worker with the pulse agent enabled or check your Redis connection.
|
|
397
|
-
</p>
|
|
398
|
-
</div>
|
|
399
|
-
) : (
|
|
400
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-start">
|
|
401
|
-
{services.map(([service, nodes]) => (
|
|
402
|
-
<ServiceGroup key={service} service={service} nodes={nodes} />
|
|
403
|
-
))}
|
|
404
|
-
</div>
|
|
405
|
-
)}
|
|
406
|
-
</div>
|
|
407
|
-
</div>
|
|
408
|
-
)
|
|
409
|
-
}
|