@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.
Files changed (70) hide show
  1. package/README.md +28 -10
  2. package/dist/bin.js +43235 -76691
  3. package/dist/client/index.html +13 -0
  4. package/dist/server/index.js +43235 -76691
  5. package/package.json +16 -7
  6. package/CHANGELOG.md +0 -62
  7. package/Dockerfile +0 -46
  8. package/Dockerfile.demo-worker +0 -29
  9. package/bin/flux-console.ts +0 -2
  10. package/doc/ECOSYSTEM_EXPANSION_RFC.md +0 -130
  11. package/docker-compose.yml +0 -40
  12. package/docs/ALERTING_GUIDE.md +0 -71
  13. package/docs/DEPLOYMENT.md +0 -157
  14. package/docs/DOCS_INTERNAL.md +0 -73
  15. package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
  16. package/docs/QUASAR_MASTER_PLAN.md +0 -140
  17. package/docs/QUICK_TEST_GUIDE.md +0 -72
  18. package/docs/ROADMAP.md +0 -85
  19. package/docs/integrations/LARAVEL.md +0 -207
  20. package/postcss.config.js +0 -6
  21. package/scripts/debug_redis_keys.ts +0 -24
  22. package/scripts/flood-logs.ts +0 -21
  23. package/scripts/seed.ts +0 -213
  24. package/scripts/verify-throttle.ts +0 -49
  25. package/scripts/worker.ts +0 -124
  26. package/specs/PULSE_SPEC.md +0 -86
  27. package/src/bin.ts +0 -6
  28. package/src/client/App.tsx +0 -72
  29. package/src/client/Layout.tsx +0 -669
  30. package/src/client/Sidebar.tsx +0 -112
  31. package/src/client/ThroughputChart.tsx +0 -158
  32. package/src/client/WorkerStatus.tsx +0 -202
  33. package/src/client/components/BrandIcons.tsx +0 -168
  34. package/src/client/components/ConfirmDialog.tsx +0 -134
  35. package/src/client/components/JobInspector.tsx +0 -487
  36. package/src/client/components/LogArchiveModal.tsx +0 -432
  37. package/src/client/components/NotificationBell.tsx +0 -212
  38. package/src/client/components/PageHeader.tsx +0 -47
  39. package/src/client/components/Toaster.tsx +0 -90
  40. package/src/client/components/UserProfileDropdown.tsx +0 -186
  41. package/src/client/contexts/AuthContext.tsx +0 -105
  42. package/src/client/contexts/NotificationContext.tsx +0 -128
  43. package/src/client/index.css +0 -172
  44. package/src/client/main.tsx +0 -15
  45. package/src/client/pages/LoginPage.tsx +0 -164
  46. package/src/client/pages/MetricsPage.tsx +0 -445
  47. package/src/client/pages/OverviewPage.tsx +0 -519
  48. package/src/client/pages/PulsePage.tsx +0 -409
  49. package/src/client/pages/QueuesPage.tsx +0 -378
  50. package/src/client/pages/SchedulesPage.tsx +0 -535
  51. package/src/client/pages/SettingsPage.tsx +0 -1001
  52. package/src/client/pages/WorkersPage.tsx +0 -380
  53. package/src/client/pages/index.ts +0 -8
  54. package/src/client/utils.ts +0 -15
  55. package/src/server/config/ServerConfigManager.ts +0 -90
  56. package/src/server/index.ts +0 -860
  57. package/src/server/middleware/auth.ts +0 -127
  58. package/src/server/services/AlertService.ts +0 -321
  59. package/src/server/services/CommandService.ts +0 -136
  60. package/src/server/services/LogStreamProcessor.ts +0 -93
  61. package/src/server/services/MaintenanceScheduler.ts +0 -78
  62. package/src/server/services/PulseService.ts +0 -148
  63. package/src/server/services/QueueMetricsCollector.ts +0 -138
  64. package/src/server/services/QueueService.ts +0 -924
  65. package/src/shared/types.ts +0 -223
  66. package/tailwind.config.js +0 -80
  67. package/tests/placeholder.test.ts +0 -7
  68. package/tsconfig.json +0 -29
  69. package/tsconfig.node.json +0 -10
  70. 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
- }