@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,449 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import {
3
+ Bell,
4
+ Clock,
5
+ Database,
6
+ ExternalLink,
7
+ Info,
8
+ Monitor,
9
+ Moon,
10
+ Palette,
11
+ RefreshCcw,
12
+ Server,
13
+ Shield,
14
+ Sun,
15
+ Trash2,
16
+ } from 'lucide-react'
17
+ import React from 'react'
18
+ import { cn } from '../utils'
19
+
20
+ export function SettingsPage() {
21
+ const [theme, setTheme] = React.useState<'light' | 'dark' | 'system'>(() => {
22
+ if (typeof window !== 'undefined') {
23
+ const stored = localStorage.getItem('theme')
24
+ if (stored === 'light' || stored === 'dark') {
25
+ return stored
26
+ }
27
+ }
28
+ return 'system'
29
+ })
30
+
31
+ const { data: systemStatus } = useQuery<any>({
32
+ queryKey: ['system-status'],
33
+ queryFn: () => fetch('/api/system/status').then((res) => res.json()),
34
+ refetchInterval: 30000,
35
+ })
36
+
37
+ const { data: alertConfig } = useQuery<any>({
38
+ queryKey: ['alerts-config'],
39
+ queryFn: () => fetch('/api/alerts/config').then((res) => res.json()),
40
+ })
41
+
42
+ const handleThemeChange = (newTheme: 'light' | 'dark' | 'system') => {
43
+ setTheme(newTheme)
44
+ const root = window.document.documentElement
45
+
46
+ if (newTheme === 'system') {
47
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
48
+ if (prefersDark) {
49
+ root.classList.add('dark')
50
+ } else {
51
+ root.classList.remove('dark')
52
+ }
53
+ localStorage.removeItem('theme')
54
+ } else if (newTheme === 'dark') {
55
+ root.classList.add('dark')
56
+ localStorage.setItem('theme', 'dark')
57
+ } else {
58
+ root.classList.remove('dark')
59
+ localStorage.setItem('theme', 'light')
60
+ }
61
+ }
62
+
63
+ return (
64
+ <div className="space-y-8 max-w-4xl">
65
+ {/* Header */}
66
+ <div>
67
+ <h1 className="text-4xl font-black tracking-tighter">Settings</h1>
68
+ <p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest">
69
+ Configure your Flux Console preferences.
70
+ </p>
71
+ </div>
72
+
73
+ {/* Appearance Section */}
74
+ <section className="card-premium p-6">
75
+ <div className="flex items-center gap-3 mb-6">
76
+ <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
77
+ <Palette size={20} />
78
+ </div>
79
+ <div>
80
+ <h2 className="text-lg font-bold">Appearance</h2>
81
+ <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
82
+ Customize the look and feel
83
+ </p>
84
+ </div>
85
+ </div>
86
+
87
+ <div className="space-y-4">
88
+ <div>
89
+ <label htmlFor="theme-select" className="text-sm font-bold mb-3 block">
90
+ Theme
91
+ </label>
92
+ <div id="theme-select" className="flex gap-3">
93
+ {[
94
+ { value: 'light', icon: Sun, label: 'Light' },
95
+ { value: 'dark', icon: Moon, label: 'Dark' },
96
+ { value: 'system', icon: Monitor, label: 'System' },
97
+ ].map(({ value, icon: Icon, label }) => (
98
+ <button
99
+ type="button"
100
+ key={value}
101
+ onClick={() => handleThemeChange(value as 'light' | 'dark' | 'system')}
102
+ className={cn(
103
+ 'flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl border transition-all',
104
+ theme === value
105
+ ? 'bg-primary text-primary-foreground border-primary'
106
+ : 'bg-muted/40 border-border/50 hover:border-primary/30'
107
+ )}
108
+ >
109
+ <Icon size={18} />
110
+ <span className="font-bold text-sm">{label}</span>
111
+ </button>
112
+ ))}
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </section>
117
+
118
+ {/* Connection Info Section */}
119
+ <section className="card-premium p-6">
120
+ <div className="flex items-center gap-3 mb-6">
121
+ <div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-500">
122
+ <Database size={20} />
123
+ </div>
124
+ <div>
125
+ <h2 className="text-lg font-bold">Connection Status</h2>
126
+ <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
127
+ Redis and system connectivity
128
+ </p>
129
+ </div>
130
+ </div>
131
+
132
+ <div className="space-y-4">
133
+ <div className="flex items-center justify-between py-3 border-b border-border/30">
134
+ <div className="flex items-center gap-3">
135
+ <Server size={16} className="text-muted-foreground" />
136
+ <span className="font-medium">Redis Connection</span>
137
+ </div>
138
+ <div className="flex items-center gap-2">
139
+ <span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
140
+ <span className="text-sm font-bold text-green-500">Connected</span>
141
+ </div>
142
+ </div>
143
+
144
+ <div className="flex items-center justify-between py-3 border-b border-border/30">
145
+ <div className="flex items-center gap-3">
146
+ <Database size={16} className="text-muted-foreground" />
147
+ <span className="font-medium">Redis URL</span>
148
+ </div>
149
+ <code className="text-sm bg-muted px-2 py-1 rounded font-mono">
150
+ {process.env.REDIS_URL || 'redis://localhost:6379'}
151
+ </code>
152
+ </div>
153
+
154
+ <div className="flex items-center justify-between py-3 border-b border-border/30">
155
+ <div className="flex items-center gap-3">
156
+ <Clock size={16} className="text-muted-foreground" />
157
+ <span className="font-medium">Server Uptime</span>
158
+ </div>
159
+ <span className="text-sm font-mono font-bold">
160
+ {systemStatus?.uptime ? formatUptime(systemStatus.uptime) : 'Loading...'}
161
+ </span>
162
+ </div>
163
+
164
+ <div className="flex items-center justify-between py-3">
165
+ <div className="flex items-center gap-3">
166
+ <RefreshCcw size={16} className="text-muted-foreground" />
167
+ <span className="font-medium">Engine Version</span>
168
+ </div>
169
+ <span className="text-sm font-bold">{systemStatus?.engine || 'Loading...'}</span>
170
+ </div>
171
+ </div>
172
+ </section>
173
+
174
+ {/* System Info Section */}
175
+ <section className="card-premium p-6">
176
+ <div className="flex items-center gap-3 mb-6">
177
+ <div className="w-10 h-10 rounded-xl bg-green-500/10 flex items-center justify-center text-green-500">
178
+ <Info size={20} />
179
+ </div>
180
+ <div>
181
+ <h2 className="text-lg font-bold">System Information</h2>
182
+ <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
183
+ Runtime and memory details
184
+ </p>
185
+ </div>
186
+ </div>
187
+
188
+ <div className="grid grid-cols-2 gap-4">
189
+ <div className="bg-muted/20 rounded-xl p-4">
190
+ <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
191
+ Node.js Version
192
+ </p>
193
+ <p className="text-lg font-mono font-bold">{systemStatus?.node || '...'}</p>
194
+ </div>
195
+ <div className="bg-muted/20 rounded-xl p-4">
196
+ <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
197
+ Environment
198
+ </p>
199
+ <p className="text-lg font-mono font-bold">{systemStatus?.env || '...'}</p>
200
+ </div>
201
+ <div className="bg-muted/20 rounded-xl p-4">
202
+ <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
203
+ Memory (RSS)
204
+ </p>
205
+ <p className="text-lg font-mono font-bold">{systemStatus?.memory?.rss || '...'}</p>
206
+ </div>
207
+ <div className="bg-muted/20 rounded-xl p-4">
208
+ <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
209
+ Heap Used
210
+ </p>
211
+ <p className="text-lg font-mono font-bold">{systemStatus?.memory?.heapUsed || '...'}</p>
212
+ </div>
213
+ </div>
214
+ </section>
215
+
216
+ {/* Alerting Section */}
217
+ <section className="card-premium p-6">
218
+ <div className="flex items-center gap-3 mb-6">
219
+ <div className="w-10 h-10 rounded-xl bg-orange-500/10 flex items-center justify-center text-orange-500">
220
+ <Bell size={20} />
221
+ </div>
222
+ <div>
223
+ <h2 className="text-lg font-bold">Alerting & Notifications</h2>
224
+ <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
225
+ System health and failure monitoring
226
+ </p>
227
+ </div>
228
+ </div>
229
+
230
+ <div className="space-y-6">
231
+ <div className="flex items-center justify-between py-3 border-b border-border/30">
232
+ <div>
233
+ <h3 className="text-sm font-bold">Slack Webhook</h3>
234
+ <p className="text-xs text-muted-foreground">
235
+ Current status of notification integration.
236
+ </p>
237
+ </div>
238
+ <div className="flex items-center gap-2">
239
+ <span
240
+ className={cn(
241
+ 'w-2 h-2 rounded-full',
242
+ alertConfig?.webhookEnabled
243
+ ? 'bg-green-500 animate-pulse'
244
+ : 'bg-muted-foreground/30'
245
+ )}
246
+ ></span>
247
+ <span
248
+ className={cn(
249
+ 'text-sm font-bold',
250
+ alertConfig?.webhookEnabled ? 'text-green-500' : 'text-muted-foreground'
251
+ )}
252
+ >
253
+ {alertConfig?.webhookEnabled ? 'Enabled' : 'Not Configured'}
254
+ </span>
255
+ </div>
256
+ </div>
257
+
258
+ <div>
259
+ <h3 className="text-xs font-black uppercase tracking-widest text-muted-foreground/60 mb-3">
260
+ Active Rules
261
+ </h3>
262
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
263
+ {alertConfig?.rules?.map((rule: any) => (
264
+ <div
265
+ key={rule.id}
266
+ className="p-3 bg-muted/20 border border-border/10 rounded-xl flex items-center justify-between"
267
+ >
268
+ <div>
269
+ <p className="text-[11px] font-black uppercase tracking-tight">{rule.name}</p>
270
+ <p className="text-[10px] text-muted-foreground opacity-70">
271
+ {rule.type === 'backlog'
272
+ ? `Waiting > ${rule.threshold}`
273
+ : rule.type === 'failure'
274
+ ? `Failed > ${rule.threshold}`
275
+ : `Workers < ${rule.threshold}`}
276
+ </p>
277
+ </div>
278
+ <div className="px-2 py-0.5 bg-muted rounded text-[9px] font-bold text-muted-foreground">
279
+ {rule.cooldownMinutes}m cooldown
280
+ </div>
281
+ </div>
282
+ ))}
283
+ </div>
284
+ </div>
285
+
286
+ <div className="pt-4 flex flex-col sm:flex-row items-center justify-between gap-4 border-t border-border/30">
287
+ <p className="text-xs text-muted-foreground max-w-md">
288
+ Configure <code>SLACK_WEBHOOK_URL</code> in your environment variables to receive
289
+ notifications.
290
+ </p>
291
+ <button
292
+ type="button"
293
+ onClick={async () => {
294
+ const res = await fetch('/api/alerts/test', { method: 'POST' }).then((r) =>
295
+ r.json()
296
+ )
297
+ if (res.success) {
298
+ alert('Test alert dispatched to server processing loop.')
299
+ }
300
+ }}
301
+ disabled={!alertConfig?.webhookEnabled}
302
+ className="w-full sm:w-auto px-4 py-2 border border-primary/20 hover:bg-primary/5 text-primary rounded-lg text-xs font-black uppercase tracking-widest transition-all active:scale-95 disabled:opacity-50"
303
+ >
304
+ Test Notification
305
+ </button>
306
+ </div>
307
+ </div>
308
+ </section>
309
+
310
+ {/* Data Retention Section */}
311
+ <section className="card-premium p-6">
312
+ <div className="flex items-center gap-3 mb-6">
313
+ <div className="w-10 h-10 rounded-xl bg-red-500/10 flex items-center justify-center text-red-500">
314
+ <Trash2 size={20} />
315
+ </div>
316
+ <div>
317
+ <h2 className="text-lg font-bold">Data Retention</h2>
318
+ <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
319
+ Manage persistent archive storage
320
+ </p>
321
+ </div>
322
+ </div>
323
+
324
+ <div className="space-y-6">
325
+ <div>
326
+ <div className="flex justify-between items-center mb-4">
327
+ <div>
328
+ <h3 className="text-sm font-bold">SQL Job Archive Preservation</h3>
329
+ <p className="text-xs text-muted-foreground">
330
+ Keep archived jobs for a specific number of days before permanent removal.
331
+ </p>
332
+ </div>
333
+ <div className="flex items-center gap-3">
334
+ <span className="text-[10px] font-black uppercase text-muted-foreground/40 mr-2">
335
+ Retention Period
336
+ </span>
337
+ <select
338
+ className="bg-muted border border-border/50 rounded-lg px-3 py-1.5 text-sm font-bold outline-none focus:ring-1 focus:ring-primary/30 transition-all cursor-pointer"
339
+ defaultValue="30"
340
+ id="retention-days"
341
+ >
342
+ <option value="7">7 Days</option>
343
+ <option value="15">15 Days</option>
344
+ <option value="30">30 Days</option>
345
+ <option value="90">90 Days</option>
346
+ <option value="365">1 Year</option>
347
+ </select>
348
+ </div>
349
+ </div>
350
+
351
+ <div className="bg-red-500/5 border border-red-500/10 rounded-xl p-4 flex items-center justify-between">
352
+ <div className="flex items-center gap-3">
353
+ <Info size={16} className="text-red-500/60" />
354
+ <span className="text-xs font-medium text-red-900/60 dark:text-red-400/60">
355
+ Manual prune will remove all jobs older than the selected period.
356
+ </span>
357
+ </div>
358
+ <button
359
+ type="button"
360
+ onClick={async () => {
361
+ const days = (document.getElementById('retention-days') as HTMLSelectElement)
362
+ .value
363
+ if (confirm(`Are you sure you want to prune logs older than ${days} days?`)) {
364
+ const res = await fetch('/api/maintenance/cleanup-archive', {
365
+ method: 'POST',
366
+ headers: { 'Content-Type': 'application/json' },
367
+ body: JSON.stringify({ days: parseInt(days, 10) }),
368
+ }).then((r) => r.json())
369
+ alert(`Cleanup complete. Removed ${res.deleted || 0} archived jobs.`)
370
+ }
371
+ }}
372
+ className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg text-xs font-black uppercase tracking-widest transition-all active:scale-95 shadow-lg shadow-red-500/20"
373
+ >
374
+ Prune Archive Now
375
+ </button>
376
+ </div>
377
+ </div>
378
+
379
+ <div className="pt-4 border-t border-border/30">
380
+ <div className="flex justify-between items-center">
381
+ <div>
382
+ <h3 className="text-sm font-bold">Live Stats History (Redis)</h3>
383
+ <p className="text-xs text-muted-foreground">
384
+ Minute-by-minute metrics used for dashboard charts.
385
+ </p>
386
+ </div>
387
+ <div className="px-3 py-1 bg-muted rounded-full text-[10px] font-black text-muted-foreground uppercase tracking-widest">
388
+ Auto-Prunes (60m)
389
+ </div>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ </section>
394
+
395
+ {/* About Section */}
396
+ <section className="card-premium p-6">
397
+ <div className="flex items-center gap-3 mb-6">
398
+ <div className="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center text-amber-500">
399
+ <Shield size={20} />
400
+ </div>
401
+ <div>
402
+ <h2 className="text-lg font-bold">About Flux Console</h2>
403
+ <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
404
+ Version and documentation
405
+ </p>
406
+ </div>
407
+ </div>
408
+
409
+ <div className="space-y-4">
410
+ <div className="flex items-center justify-between py-3 border-b border-border/30">
411
+ <span className="font-medium">Version</span>
412
+ <span className="text-sm font-bold">0.1.0-alpha.1</span>
413
+ </div>
414
+ <div className="flex items-center justify-between py-3 border-b border-border/30">
415
+ <span className="font-medium">Package</span>
416
+ <code className="text-sm bg-muted px-2 py-1 rounded font-mono">
417
+ @gravito/flux-console
418
+ </code>
419
+ </div>
420
+ <div className="flex items-center justify-between py-3">
421
+ <span className="font-medium">Documentation</span>
422
+ <a
423
+ href="https://github.com/gravito-framework/gravito"
424
+ target="_blank"
425
+ rel="noopener noreferrer"
426
+ className="flex items-center gap-1 text-sm text-primary hover:underline font-bold"
427
+ >
428
+ View Docs <ExternalLink size={14} />
429
+ </a>
430
+ </div>
431
+ </div>
432
+ </section>
433
+ </div>
434
+ )
435
+ }
436
+
437
+ function formatUptime(seconds: number): string {
438
+ const days = Math.floor(seconds / 86400)
439
+ const hours = Math.floor((seconds % 86400) / 3600)
440
+ const minutes = Math.floor((seconds % 3600) / 60)
441
+
442
+ if (days > 0) {
443
+ return `${days}d ${hours}h ${minutes}m`
444
+ }
445
+ if (hours > 0) {
446
+ return `${hours}h ${minutes}m`
447
+ }
448
+ return `${minutes}m ${Math.floor(seconds % 60)}s`
449
+ }