@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.
Files changed (63) hide show
  1. package/ARCHITECTURE.md +88 -0
  2. package/BATCH_OPERATIONS_IMPLEMENTATION.md +159 -0
  3. package/DEMO.md +156 -0
  4. package/DEPLOYMENT.md +157 -0
  5. package/DOCS_INTERNAL.md +73 -0
  6. package/Dockerfile +46 -0
  7. package/Dockerfile.demo-worker +29 -0
  8. package/EVOLUTION_BLUEPRINT.md +112 -0
  9. package/JOBINSPECTOR_SCROLL_FIX.md +152 -0
  10. package/PULSE_IMPLEMENTATION_PLAN.md +111 -0
  11. package/QUICK_TEST_GUIDE.md +72 -0
  12. package/README.md +33 -0
  13. package/ROADMAP.md +85 -0
  14. package/TESTING_BATCH_OPERATIONS.md +252 -0
  15. package/bin/flux-console.ts +2 -0
  16. package/dist/bin.js +108196 -0
  17. package/dist/client/assets/index-DGYEwTDL.css +1 -0
  18. package/dist/client/assets/index-oyTdySX0.js +421 -0
  19. package/dist/client/index.html +13 -0
  20. package/dist/server/index.js +108191 -0
  21. package/docker-compose.yml +40 -0
  22. package/docs/integrations/LARAVEL.md +207 -0
  23. package/package.json +50 -0
  24. package/postcss.config.js +6 -0
  25. package/scripts/flood-logs.ts +21 -0
  26. package/scripts/seed.ts +213 -0
  27. package/scripts/verify-throttle.ts +45 -0
  28. package/scripts/worker.ts +123 -0
  29. package/src/bin.ts +6 -0
  30. package/src/client/App.tsx +70 -0
  31. package/src/client/Layout.tsx +644 -0
  32. package/src/client/Sidebar.tsx +102 -0
  33. package/src/client/ThroughputChart.tsx +135 -0
  34. package/src/client/WorkerStatus.tsx +170 -0
  35. package/src/client/components/ConfirmDialog.tsx +103 -0
  36. package/src/client/components/JobInspector.tsx +524 -0
  37. package/src/client/components/LogArchiveModal.tsx +383 -0
  38. package/src/client/components/NotificationBell.tsx +203 -0
  39. package/src/client/components/Toaster.tsx +80 -0
  40. package/src/client/components/UserProfileDropdown.tsx +177 -0
  41. package/src/client/contexts/AuthContext.tsx +93 -0
  42. package/src/client/contexts/NotificationContext.tsx +103 -0
  43. package/src/client/index.css +174 -0
  44. package/src/client/index.html +12 -0
  45. package/src/client/main.tsx +15 -0
  46. package/src/client/pages/LoginPage.tsx +153 -0
  47. package/src/client/pages/MetricsPage.tsx +408 -0
  48. package/src/client/pages/OverviewPage.tsx +511 -0
  49. package/src/client/pages/QueuesPage.tsx +372 -0
  50. package/src/client/pages/SchedulesPage.tsx +531 -0
  51. package/src/client/pages/SettingsPage.tsx +449 -0
  52. package/src/client/pages/WorkersPage.tsx +316 -0
  53. package/src/client/pages/index.ts +7 -0
  54. package/src/client/utils.ts +6 -0
  55. package/src/server/index.ts +556 -0
  56. package/src/server/middleware/auth.ts +127 -0
  57. package/src/server/services/AlertService.ts +160 -0
  58. package/src/server/services/QueueService.ts +828 -0
  59. package/tailwind.config.js +73 -0
  60. package/tests/placeholder.test.ts +7 -0
  61. package/tsconfig.json +38 -0
  62. package/tsconfig.node.json +12 -0
  63. package/vite.config.ts +27 -0
@@ -0,0 +1,372 @@
1
+ import { useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import { AnimatePresence, motion } from 'framer-motion'
3
+ import {
4
+ Activity,
5
+ AlertCircle,
6
+ ArrowRight,
7
+ CheckCircle2,
8
+ Clock,
9
+ Filter,
10
+ ListTree,
11
+ Pause,
12
+ Play,
13
+ RefreshCcw,
14
+ Search,
15
+ Trash2,
16
+ XCircle,
17
+ } from 'lucide-react'
18
+ import React from 'react'
19
+ import { JobInspector } from '../components/JobInspector'
20
+ import { cn } from '../utils'
21
+
22
+ interface QueueStats {
23
+ name: string
24
+ waiting: number
25
+ delayed: number
26
+ active: number
27
+ failed: number
28
+ paused?: boolean
29
+ }
30
+
31
+ export function QueuesPage() {
32
+ const [selectedQueue, setSelectedQueue] = React.useState<string | null>(null)
33
+ const [searchQuery, setSearchQuery] = React.useState('')
34
+ const [statusFilter, setStatusFilter] = React.useState<'all' | 'active' | 'idle' | 'critical'>(
35
+ 'all'
36
+ )
37
+ const queryClient = useQueryClient()
38
+
39
+ const { isPending, error, data } = useQuery<{ queues: QueueStats[] }>({
40
+ queryKey: ['queues'],
41
+ queryFn: () => fetch('/api/queues').then((res) => res.json()),
42
+ staleTime: Infinity, // No auto refetch
43
+ })
44
+
45
+ // Listen for real-time updates from Layout's global stream
46
+ React.useEffect(() => {
47
+ const handler = (e: CustomEvent) => {
48
+ if (e.detail?.queues) {
49
+ queryClient.setQueryData(['queues'], { queues: e.detail.queues })
50
+ }
51
+ }
52
+ window.addEventListener('flux-stats-update', handler as EventListener)
53
+ return () => window.removeEventListener('flux-stats-update', handler as EventListener)
54
+ }, [queryClient])
55
+
56
+ // Note: We intentionally do NOT scroll to top when JobInspector opens
57
+ // This allows users to quickly inspect multiple queues without losing their scroll position
58
+
59
+ const queues = data?.queues || []
60
+
61
+ const filteredQueues = queues.filter((q) => {
62
+ const matchesSearch = q.name.toLowerCase().includes(searchQuery.toLowerCase())
63
+ const status = q.failed > 0 ? 'critical' : q.active > 0 ? 'active' : 'idle'
64
+ const matchesStatus = statusFilter === 'all' || status === statusFilter
65
+ return matchesSearch && matchesStatus
66
+ })
67
+
68
+ const totalWaiting = queues.reduce((acc, q) => acc + q.waiting, 0)
69
+ const totalDelayed = queues.reduce((acc, q) => acc + q.delayed, 0)
70
+ const totalFailed = queues.reduce((acc, q) => acc + q.failed, 0)
71
+ const totalActive = queues.reduce((acc, q) => acc + q.active, 0)
72
+
73
+ if (isPending) {
74
+ return (
75
+ <div className="flex flex-col items-center justify-center p-20 space-y-6">
76
+ <RefreshCcw className="animate-spin text-primary" size={48} />
77
+ <p className="text-muted-foreground font-bold uppercase tracking-[0.3em] text-xs">
78
+ Loading queues...
79
+ </p>
80
+ </div>
81
+ )
82
+ }
83
+
84
+ if (error) {
85
+ return (
86
+ <div className="text-center p-20">
87
+ <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">
88
+ <AlertCircle size={56} className="mx-auto mb-6 opacity-80" />
89
+ <h3 className="text-2xl font-black mb-2 uppercase tracking-tight">
90
+ Failed to Load Queues
91
+ </h3>
92
+ <p className="text-sm font-medium opacity-70 mb-8">{error.message}</p>
93
+ </div>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ return (
99
+ <>
100
+ {/* JobInspector as full-screen modal overlay */}
101
+ <AnimatePresence>
102
+ {selectedQueue && (
103
+ <JobInspector queueName={selectedQueue} onClose={() => setSelectedQueue(null)} />
104
+ )}
105
+ </AnimatePresence>
106
+
107
+ {/* Main page content */}
108
+ <div className="space-y-8">
109
+ {/* Header */}
110
+ <div className="flex justify-between items-end">
111
+ <div>
112
+ <h1 className="text-4xl font-black tracking-tighter">Processing Queues</h1>
113
+ <p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest">
114
+ Manage and monitor all processing pipelines.
115
+ </p>
116
+ </div>
117
+ <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">
118
+ <span className="w-2 h-2 bg-green-500 rounded-full shadow-[0_0_8px_rgba(34,197,94,0.6)]"></span>
119
+ {queues.length} Queues
120
+ </div>
121
+ </div>
122
+
123
+ {/* Summary Cards */}
124
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
125
+ <div className="card-premium p-6">
126
+ <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
127
+ Total Waiting
128
+ </p>
129
+ <p className="text-3xl font-black">{totalWaiting.toLocaleString()}</p>
130
+ </div>
131
+ <div className="card-premium p-6">
132
+ <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
133
+ Total Delayed
134
+ </p>
135
+ <p className="text-3xl font-black text-amber-500">{totalDelayed.toLocaleString()}</p>
136
+ </div>
137
+ <div className="card-premium p-6">
138
+ <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
139
+ Total Failed
140
+ </p>
141
+ <p className="text-3xl font-black text-red-500">{totalFailed.toLocaleString()}</p>
142
+ </div>
143
+ <div className="card-premium p-6">
144
+ <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
145
+ Currently Active
146
+ </p>
147
+ <p className="text-3xl font-black text-green-500">{totalActive.toLocaleString()}</p>
148
+ </div>
149
+ </div>
150
+
151
+ {/* Filters */}
152
+ <div className="card-premium p-4 flex flex-wrap gap-4 items-center">
153
+ <div className="relative flex-1 min-w-[200px]">
154
+ <Search
155
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
156
+ size={18}
157
+ />
158
+ <input
159
+ type="text"
160
+ placeholder="Search queues..."
161
+ value={searchQuery}
162
+ onChange={(e) => setSearchQuery(e.target.value)}
163
+ className="w-full bg-muted/40 border border-border/50 rounded-xl py-2.5 pl-10 pr-4 text-sm font-medium placeholder:text-muted-foreground/40 focus:outline-none focus:ring-2 focus:ring-primary/20"
164
+ />
165
+ </div>
166
+ <div className="flex items-center gap-2">
167
+ <Filter size={16} className="text-muted-foreground" />
168
+ {(['all', 'active', 'idle', 'critical'] as const).map((status) => (
169
+ <button
170
+ type="button"
171
+ key={status}
172
+ onClick={() => setStatusFilter(status)}
173
+ className={cn(
174
+ 'px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all',
175
+ statusFilter === status
176
+ ? 'bg-primary text-primary-foreground'
177
+ : 'bg-muted text-muted-foreground hover:bg-muted/80'
178
+ )}
179
+ >
180
+ {status}
181
+ </button>
182
+ ))}
183
+ </div>
184
+ </div>
185
+
186
+ {/* Queue List */}
187
+ <div className="card-premium overflow-hidden">
188
+ <div className="overflow-x-auto">
189
+ <table className="w-full text-left">
190
+ <thead className="bg-muted/10 text-muted-foreground uppercase text-[10px] font-black tracking-[0.2em]">
191
+ <tr>
192
+ <th className="px-6 py-5">Queue Name</th>
193
+ <th className="px-6 py-5 text-center">Waiting</th>
194
+ <th className="px-6 py-5 text-center">Delayed</th>
195
+ <th className="px-6 py-5 text-center">Active</th>
196
+ <th className="px-6 py-5 text-center">Failed</th>
197
+ <th className="px-6 py-5 text-center">Status</th>
198
+ <th className="px-6 py-5 text-right">Actions</th>
199
+ </tr>
200
+ </thead>
201
+ <tbody className="divide-y divide-border/30 text-sm">
202
+ {filteredQueues.map((queue) => {
203
+ const status =
204
+ queue.failed > 0 ? 'critical' : queue.active > 0 ? 'active' : 'idle'
205
+ return (
206
+ <tr key={queue.name} className="hover:bg-muted/5 transition-colors group">
207
+ <td className="px-6 py-5">
208
+ <div className="flex items-center gap-3">
209
+ <div className="w-10 h-10 bg-primary/5 rounded-xl flex items-center justify-center text-primary group-hover:scale-110 transition-transform">
210
+ <ListTree size={20} />
211
+ </div>
212
+ <span className="font-black tracking-tight">{queue.name}</span>
213
+ </div>
214
+ </td>
215
+ <td className="px-6 py-5 text-center font-mono font-bold">
216
+ {queue.waiting.toLocaleString()}
217
+ </td>
218
+ <td className="px-6 py-5 text-center font-mono text-amber-500">
219
+ {queue.delayed}
220
+ </td>
221
+ <td className="px-6 py-5 text-center font-mono text-green-500">
222
+ {queue.active}
223
+ </td>
224
+ <td className="px-6 py-5 text-center">
225
+ <span
226
+ className={cn(
227
+ 'font-mono font-black',
228
+ queue.failed > 0 ? 'text-red-500' : 'text-muted-foreground/40'
229
+ )}
230
+ >
231
+ {queue.failed}
232
+ </span>
233
+ </td>
234
+ <td className="px-6 py-5 text-center">
235
+ <span
236
+ className={cn(
237
+ 'px-3 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest border',
238
+ queue.paused
239
+ ? 'bg-amber-500/20 text-amber-500 border-amber-500/30'
240
+ : status === 'critical'
241
+ ? 'bg-red-500 text-white border-red-600'
242
+ : status === 'active'
243
+ ? 'bg-green-500/10 text-green-500 border-green-500/20'
244
+ : 'bg-muted/40 text-muted-foreground border-transparent'
245
+ )}
246
+ >
247
+ {queue.paused ? 'paused' : status}
248
+ </span>
249
+ </td>
250
+ <td className="px-6 py-5">
251
+ <div className="flex justify-end gap-2 items-center">
252
+ {/* Pause/Resume button */}
253
+ <button
254
+ type="button"
255
+ onClick={async () => {
256
+ const action = queue.paused ? 'resume' : 'pause'
257
+ await fetch(`/api/queues/${queue.name}/${action}`, { method: 'POST' })
258
+ queryClient.invalidateQueries({ queryKey: ['queues'] })
259
+ }}
260
+ className={cn(
261
+ 'p-2 rounded-lg transition-all',
262
+ queue.paused
263
+ ? 'text-green-500 hover:bg-green-500/10'
264
+ : 'text-muted-foreground hover:bg-amber-500/10 hover:text-amber-500'
265
+ )}
266
+ title={queue.paused ? 'Resume Queue' : 'Pause Queue'}
267
+ >
268
+ {queue.paused ? <Play size={16} /> : <Pause size={16} />}
269
+ </button>
270
+ {queue.delayed > 0 && (
271
+ <button
272
+ type="button"
273
+ onClick={() =>
274
+ fetch(`/api/queues/${queue.name}/retry-all`, {
275
+ method: 'POST',
276
+ }).then(() =>
277
+ queryClient.invalidateQueries({ queryKey: ['queues'] })
278
+ )
279
+ }
280
+ className="p-2 text-amber-500 hover:bg-amber-500/10 rounded-lg transition-all"
281
+ title="Retry All Delayed"
282
+ >
283
+ <RefreshCcw size={16} />
284
+ </button>
285
+ )}
286
+ {queue.failed > 0 && (
287
+ <>
288
+ <button
289
+ type="button"
290
+ onClick={() =>
291
+ fetch(`/api/queues/${queue.name}/retry-all-failed`, {
292
+ method: 'POST',
293
+ }).then(() =>
294
+ queryClient.invalidateQueries({ queryKey: ['queues'] })
295
+ )
296
+ }
297
+ className="p-2 text-blue-500 hover:bg-blue-500/10 rounded-lg transition-all"
298
+ title="Retry All Failed"
299
+ >
300
+ <RefreshCcw size={16} />
301
+ </button>
302
+ <button
303
+ type="button"
304
+ onClick={() => {
305
+ if (
306
+ confirm(
307
+ `Are you sure you want to clear all failed jobs in queue "${queue.name}"?`
308
+ )
309
+ ) {
310
+ fetch(`/api/queues/${queue.name}/clear-failed`, {
311
+ method: 'POST',
312
+ }).then(() =>
313
+ queryClient.invalidateQueries({ queryKey: ['queues'] })
314
+ )
315
+ }
316
+ }}
317
+ className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-all"
318
+ title="Clear Failed Jobs"
319
+ >
320
+ <XCircle size={16} />
321
+ </button>
322
+ </>
323
+ )}
324
+ <button
325
+ type="button"
326
+ onClick={async () => {
327
+ if (
328
+ confirm(
329
+ `Are you sure you want to purge all jobs in queue "${queue.name}"?`
330
+ )
331
+ ) {
332
+ await fetch(`/api/queues/${queue.name}/purge`, { method: 'POST' })
333
+ queryClient.invalidateQueries({ queryKey: ['queues'] })
334
+ }
335
+ }}
336
+ className="p-2 text-muted-foreground hover:bg-red-500/10 hover:text-red-500 rounded-lg transition-all"
337
+ title="Purge Queue"
338
+ >
339
+ <Trash2 size={16} />
340
+ </button>
341
+ <button
342
+ type="button"
343
+ onClick={() => setSelectedQueue(queue.name)}
344
+ className="px-4 py-1.5 bg-muted text-foreground rounded-lg transition-all flex items-center gap-2 text-[10px] font-black uppercase tracking-widest border border-border/50 hover:border-primary/50 hover:bg-background"
345
+ >
346
+ Inspect <ArrowRight size={12} />
347
+ </button>
348
+ </div>
349
+ </td>
350
+ </tr>
351
+ )
352
+ })}
353
+ {filteredQueues.length === 0 && (
354
+ <tr>
355
+ <td colSpan={7} className="px-6 py-20 text-center text-muted-foreground">
356
+ <Activity size={40} className="mx-auto mb-4 opacity-10 animate-pulse" />
357
+ <p className="text-sm font-bold opacity-30 italic uppercase tracking-widest">
358
+ {searchQuery || statusFilter !== 'all'
359
+ ? 'No queues match your filters'
360
+ : 'No queues available'}
361
+ </p>
362
+ </td>
363
+ </tr>
364
+ )}
365
+ </tbody>
366
+ </table>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </>
371
+ )
372
+ }