@gravito/zenith 1.1.2 → 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 (76) hide show
  1. package/README.md +95 -22
  2. package/README.zh-TW.md +88 -0
  3. package/dist/bin.js +54699 -39316
  4. package/dist/client/assets/index-C80c1frR.css +1 -0
  5. package/dist/client/assets/index-CrWem9u3.js +434 -0
  6. package/dist/client/index.html +2 -2
  7. package/dist/server/index.js +54699 -39316
  8. package/package.json +20 -9
  9. package/CHANGELOG.md +0 -47
  10. package/Dockerfile +0 -46
  11. package/Dockerfile.demo-worker +0 -29
  12. package/ECOSYSTEM_EXPANSION_RFC.md +0 -130
  13. package/bin/flux-console.ts +0 -2
  14. package/dist/client/assets/index-BSMp8oq_.js +0 -436
  15. package/dist/client/assets/index-BwxlHx-_.css +0 -1
  16. package/docker-compose.yml +0 -40
  17. package/docs/ALERTING_GUIDE.md +0 -71
  18. package/docs/DEPLOYMENT.md +0 -157
  19. package/docs/DOCS_INTERNAL.md +0 -73
  20. package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
  21. package/docs/QUASAR_MASTER_PLAN.md +0 -140
  22. package/docs/QUICK_TEST_GUIDE.md +0 -72
  23. package/docs/ROADMAP.md +0 -85
  24. package/docs/integrations/LARAVEL.md +0 -207
  25. package/postcss.config.js +0 -6
  26. package/scripts/debug_redis_keys.ts +0 -24
  27. package/scripts/flood-logs.ts +0 -21
  28. package/scripts/seed.ts +0 -213
  29. package/scripts/verify-throttle.ts +0 -49
  30. package/scripts/worker.ts +0 -124
  31. package/specs/PULSE_SPEC.md +0 -86
  32. package/src/bin.ts +0 -6
  33. package/src/client/App.tsx +0 -72
  34. package/src/client/Layout.tsx +0 -672
  35. package/src/client/Sidebar.tsx +0 -112
  36. package/src/client/ThroughputChart.tsx +0 -144
  37. package/src/client/WorkerStatus.tsx +0 -226
  38. package/src/client/components/BrandIcons.tsx +0 -168
  39. package/src/client/components/ConfirmDialog.tsx +0 -126
  40. package/src/client/components/JobInspector.tsx +0 -554
  41. package/src/client/components/LogArchiveModal.tsx +0 -432
  42. package/src/client/components/NotificationBell.tsx +0 -212
  43. package/src/client/components/PageHeader.tsx +0 -47
  44. package/src/client/components/Toaster.tsx +0 -90
  45. package/src/client/components/UserProfileDropdown.tsx +0 -186
  46. package/src/client/contexts/AuthContext.tsx +0 -105
  47. package/src/client/contexts/NotificationContext.tsx +0 -128
  48. package/src/client/index.css +0 -174
  49. package/src/client/index.html +0 -12
  50. package/src/client/main.tsx +0 -15
  51. package/src/client/pages/LoginPage.tsx +0 -162
  52. package/src/client/pages/MetricsPage.tsx +0 -417
  53. package/src/client/pages/OverviewPage.tsx +0 -517
  54. package/src/client/pages/PulsePage.tsx +0 -488
  55. package/src/client/pages/QueuesPage.tsx +0 -379
  56. package/src/client/pages/SchedulesPage.tsx +0 -540
  57. package/src/client/pages/SettingsPage.tsx +0 -1020
  58. package/src/client/pages/WorkersPage.tsx +0 -394
  59. package/src/client/pages/index.ts +0 -8
  60. package/src/client/utils.ts +0 -15
  61. package/src/server/config/ServerConfigManager.ts +0 -90
  62. package/src/server/index.ts +0 -860
  63. package/src/server/middleware/auth.ts +0 -127
  64. package/src/server/services/AlertService.ts +0 -321
  65. package/src/server/services/CommandService.ts +0 -137
  66. package/src/server/services/LogStreamProcessor.ts +0 -93
  67. package/src/server/services/MaintenanceScheduler.ts +0 -78
  68. package/src/server/services/PulseService.ts +0 -91
  69. package/src/server/services/QueueMetricsCollector.ts +0 -138
  70. package/src/server/services/QueueService.ts +0 -631
  71. package/src/shared/types.ts +0 -198
  72. package/tailwind.config.js +0 -73
  73. package/tests/placeholder.test.ts +0 -7
  74. package/tsconfig.json +0 -38
  75. package/tsconfig.node.json +0 -12
  76. package/vite.config.ts +0 -27
@@ -1,1020 +0,0 @@
1
- import { useQuery, useQueryClient } 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
- /**
21
- * System Settings Page.
22
- *
23
- * Allows administrators to configure dashboard appearance, monitoring alerts,
24
- * and data retention policies. It also provides system-level information.
25
- *
26
- * @public
27
- * @since 3.0.0
28
- */
29
- export function SettingsPage() {
30
- const queryClient = useQueryClient()
31
- const [showAddRule, setShowAddRule] = React.useState(false)
32
- const [theme, setTheme] = React.useState<'light' | 'dark' | 'system'>(() => {
33
- if (typeof window !== 'undefined') {
34
- const stored = localStorage.getItem('theme')
35
- if (stored === 'light' || stored === 'dark') {
36
- return stored
37
- }
38
- }
39
- return 'system'
40
- })
41
-
42
- const { data: systemStatus } = useQuery<any>({
43
- queryKey: ['system-status'],
44
- queryFn: () => fetch('/api/system/status').then((res) => res.json()),
45
- refetchInterval: 30000,
46
- })
47
-
48
- const { data: alertConfig } = useQuery<any>({
49
- queryKey: ['alerts-config'],
50
- queryFn: () => fetch('/api/alerts/config').then((res) => res.json()),
51
- })
52
-
53
- const handleThemeChange = (newTheme: 'light' | 'dark' | 'system') => {
54
- setTheme(newTheme)
55
- const root = window.document.documentElement
56
-
57
- if (newTheme === 'system') {
58
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
59
- if (prefersDark) {
60
- root.classList.add('dark')
61
- } else {
62
- root.classList.remove('dark')
63
- }
64
- localStorage.removeItem('theme')
65
- } else if (newTheme === 'dark') {
66
- root.classList.add('dark')
67
- localStorage.setItem('theme', 'dark')
68
- } else {
69
- root.classList.remove('dark')
70
- localStorage.setItem('theme', 'light')
71
- }
72
- }
73
-
74
- return (
75
- <div className="space-y-8 max-w-4xl">
76
- {/* Header */}
77
- <div>
78
- <h1 className="text-4xl font-black tracking-tighter">Settings</h1>
79
- <p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest">
80
- Configure your Flux Console preferences.
81
- </p>
82
- </div>
83
-
84
- {/* Appearance Section */}
85
- <section className="card-premium p-6">
86
- <div className="flex items-center gap-3 mb-6">
87
- <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
88
- <Palette size={20} />
89
- </div>
90
- <div>
91
- <h2 className="text-lg font-bold">Appearance</h2>
92
- <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
93
- Customize the look and feel
94
- </p>
95
- </div>
96
- </div>
97
-
98
- <div className="space-y-4">
99
- <div>
100
- <label htmlFor="theme-select" className="text-sm font-bold mb-3 block">
101
- Theme
102
- </label>
103
- <div id="theme-select" className="flex gap-3">
104
- {[
105
- { value: 'light', icon: Sun, label: 'Light' },
106
- { value: 'dark', icon: Moon, label: 'Dark' },
107
- { value: 'system', icon: Monitor, label: 'System' },
108
- ].map(({ value, icon: Icon, label }) => (
109
- <button
110
- type="button"
111
- key={value}
112
- onClick={() => handleThemeChange(value as 'light' | 'dark' | 'system')}
113
- className={cn(
114
- 'flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl border transition-all',
115
- theme === value
116
- ? 'bg-primary text-primary-foreground border-primary'
117
- : 'bg-muted/40 border-border/50 hover:border-primary/30'
118
- )}
119
- >
120
- <Icon size={18} />
121
- <span className="font-bold text-sm">{label}</span>
122
- </button>
123
- ))}
124
- </div>
125
- </div>
126
- </div>
127
- </section>
128
-
129
- {/* Connection Info Section */}
130
- <section className="card-premium p-6">
131
- <div className="flex items-center gap-3 mb-6">
132
- <div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-500">
133
- <Database size={20} />
134
- </div>
135
- <div>
136
- <h2 className="text-lg font-bold">Connection Status</h2>
137
- <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
138
- Redis and system connectivity
139
- </p>
140
- </div>
141
- </div>
142
-
143
- <div className="space-y-4">
144
- <div className="flex items-center justify-between py-3 border-b border-border/30">
145
- <div className="flex items-center gap-3">
146
- <Server size={16} className="text-muted-foreground" />
147
- <span className="font-medium">Redis Connection</span>
148
- </div>
149
- <div className="flex items-center gap-2">
150
- <span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
151
- <span className="text-sm font-bold text-green-500">Connected</span>
152
- </div>
153
- </div>
154
-
155
- <div className="flex items-center justify-between py-3 border-b border-border/30">
156
- <div className="flex items-center gap-3">
157
- <Database size={16} className="text-muted-foreground" />
158
- <span className="font-medium">Redis URL</span>
159
- </div>
160
- <code className="text-sm bg-muted px-2 py-1 rounded font-mono">
161
- {systemStatus?.redisUrl || 'redis://localhost:6379'}
162
- </code>
163
- </div>
164
-
165
- <div className="flex items-center justify-between py-3 border-b border-border/30">
166
- <div className="flex items-center gap-3">
167
- <Clock size={16} className="text-muted-foreground" />
168
- <span className="font-medium">Service Uptime</span>
169
- </div>
170
- <span className="text-sm font-mono font-bold">
171
- {systemStatus?.uptime ? formatUptime(systemStatus.uptime) : 'Loading...'}
172
- </span>
173
- </div>
174
-
175
- <div className="flex items-center justify-between py-3">
176
- <div className="flex items-center gap-3">
177
- <RefreshCcw size={16} className="text-muted-foreground" />
178
- <span className="font-medium">Engine Version</span>
179
- </div>
180
- <span className="text-sm font-bold">{systemStatus?.engine || 'Loading...'}</span>
181
- </div>
182
- </div>
183
- </section>
184
-
185
- {/* System Info Section */}
186
- <section className="card-premium p-6">
187
- <div className="flex items-center gap-3 mb-6">
188
- <div className="w-10 h-10 rounded-xl bg-green-500/10 flex items-center justify-center text-green-500">
189
- <Info size={20} />
190
- </div>
191
- <div>
192
- <h2 className="text-lg font-bold">System Information</h2>
193
- <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
194
- Runtime and memory details
195
- </p>
196
- </div>
197
- </div>
198
-
199
- <div className="grid grid-cols-2 gap-4">
200
- <div className="bg-muted/20 rounded-xl p-4">
201
- <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
202
- Node.js Version
203
- </p>
204
- <p className="text-lg font-mono font-bold">{systemStatus?.node || '...'}</p>
205
- </div>
206
- <div className="bg-muted/20 rounded-xl p-4">
207
- <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
208
- Environment
209
- </p>
210
- <p className="text-lg font-mono font-bold">{systemStatus?.env || '...'}</p>
211
- </div>
212
- <div className="bg-muted/20 rounded-xl p-4">
213
- <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
214
- Memory (RSS)
215
- </p>
216
- <p className="text-lg font-mono font-bold">{systemStatus?.memory?.rss || '...'}</p>
217
- </div>
218
- <div className="bg-muted/20 rounded-xl p-4">
219
- <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
220
- Heap Used
221
- </p>
222
- <p className="text-lg font-mono font-bold">{systemStatus?.memory?.heapUsed || '...'}</p>
223
- </div>
224
- <div className="bg-muted/20 rounded-xl p-4">
225
- <p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
226
- Total System RAM
227
- </p>
228
- <p className="text-lg font-mono font-bold">{systemStatus?.memory?.total || '...'}</p>
229
- </div>
230
- </div>
231
- </section>
232
-
233
- {/* Alerting Section */}
234
- <section className="card-premium p-6">
235
- <div className="flex items-center gap-3 mb-6">
236
- <div className="w-10 h-10 rounded-xl bg-orange-500/10 flex items-center justify-center text-orange-500">
237
- <Bell size={20} />
238
- </div>
239
- <div>
240
- <h2 className="text-lg font-bold">Alerting & Notifications</h2>
241
- <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
242
- System health and failure monitoring
243
- </p>
244
- </div>
245
- </div>
246
-
247
- <div className="space-y-8">
248
- {/* Notification Channels */}
249
- <div className="space-y-4">
250
- <h3 className="text-xs font-black uppercase tracking-widest text-muted-foreground/60 mb-2">
251
- Notification Channels
252
- </h3>
253
-
254
- {/* Slack */}
255
- <div className="p-4 bg-muted/20 rounded-xl border border-border/10">
256
- <div className="flex items-center justify-between mb-4">
257
- <div className="flex items-center gap-3">
258
- <div className="w-8 h-8 rounded-lg bg-[#4A154B]/10 flex items-center justify-center text-[#4A154B] dark:text-[#E01E5A]">
259
- <Bell size={16} />
260
- </div>
261
- <div>
262
- <h4 className="text-sm font-bold">Slack</h4>
263
- <p className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter">
264
- Standard notification webhook
265
- </p>
266
- </div>
267
- </div>
268
- <button
269
- type="button"
270
- onClick={async () => {
271
- const enabled = !alertConfig?.config?.channels?.slack?.enabled
272
- const current = alertConfig?.config?.channels?.slack || {}
273
- await fetch('/api/alerts/config', {
274
- method: 'POST',
275
- headers: { 'Content-Type': 'application/json' },
276
- body: JSON.stringify({
277
- ...alertConfig.config,
278
- channels: {
279
- ...alertConfig.config.channels,
280
- slack: { ...current, enabled },
281
- },
282
- }),
283
- })
284
- queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
285
- }}
286
- className={cn(
287
- 'px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all',
288
- alertConfig?.config?.channels?.slack?.enabled
289
- ? 'bg-green-500 text-white shadow-lg shadow-green-500/20'
290
- : 'bg-muted-foreground/20 text-muted-foreground'
291
- )}
292
- >
293
- {alertConfig?.config?.channels?.slack?.enabled ? 'Enabled' : 'Disabled'}
294
- </button>
295
- </div>
296
- <div className="flex gap-3">
297
- <input
298
- type="password"
299
- placeholder="https://hooks.slack.com/services/..."
300
- defaultValue={alertConfig?.config?.channels?.slack?.webhookUrl || ''}
301
- onBlur={async (e) => {
302
- const val = e.target.value
303
- if (val === alertConfig?.config?.channels?.slack?.webhookUrl) {
304
- return
305
- }
306
- await fetch('/api/alerts/config', {
307
- method: 'POST',
308
- headers: { 'Content-Type': 'application/json' },
309
- body: JSON.stringify({
310
- ...alertConfig.config,
311
- channels: {
312
- ...alertConfig.config.channels,
313
- slack: {
314
- ...alertConfig?.config?.channels?.slack,
315
- webhookUrl: val,
316
- },
317
- },
318
- }),
319
- })
320
- queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
321
- }}
322
- className="flex-1 bg-background/50 border border-border/50 rounded-lg px-3 py-2 text-xs font-mono outline-none focus:ring-1 focus:ring-primary/30"
323
- />
324
- </div>
325
- </div>
326
-
327
- {/* Discord */}
328
- <div className="p-4 bg-muted/20 rounded-xl border border-border/10">
329
- <div className="flex items-center justify-between mb-4">
330
- <div className="flex items-center gap-3">
331
- <div className="w-8 h-8 rounded-lg bg-[#5865F2]/10 flex items-center justify-center text-[#5865F2]">
332
- <Monitor size={16} />
333
- </div>
334
- <div>
335
- <h4 className="text-sm font-bold">Discord</h4>
336
- <p className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter">
337
- Webhook integration for servers
338
- </p>
339
- </div>
340
- </div>
341
- <button
342
- type="button"
343
- onClick={async () => {
344
- const enabled = !alertConfig?.config?.channels?.discord?.enabled
345
- const current = alertConfig?.config?.channels?.discord || {}
346
- await fetch('/api/alerts/config', {
347
- method: 'POST',
348
- headers: { 'Content-Type': 'application/json' },
349
- body: JSON.stringify({
350
- ...alertConfig.config,
351
- channels: {
352
- ...alertConfig.config.channels,
353
- discord: { ...current, enabled },
354
- },
355
- }),
356
- })
357
- queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
358
- }}
359
- className={cn(
360
- 'px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all',
361
- alertConfig?.config?.channels?.discord?.enabled
362
- ? 'bg-green-500 text-white shadow-lg shadow-green-500/20'
363
- : 'bg-muted-foreground/20 text-muted-foreground'
364
- )}
365
- >
366
- {alertConfig?.config?.channels?.discord?.enabled ? 'Enabled' : 'Disabled'}
367
- </button>
368
- </div>
369
- <div className="flex gap-3">
370
- <input
371
- type="password"
372
- placeholder="https://discord.com/api/webhooks/..."
373
- defaultValue={alertConfig?.config?.channels?.discord?.webhookUrl || ''}
374
- onBlur={async (e) => {
375
- const val = e.target.value
376
- if (val === alertConfig?.config?.channels?.discord?.webhookUrl) {
377
- return
378
- }
379
- await fetch('/api/alerts/config', {
380
- method: 'POST',
381
- headers: { 'Content-Type': 'application/json' },
382
- body: JSON.stringify({
383
- ...alertConfig.config,
384
- channels: {
385
- ...alertConfig.config.channels,
386
- discord: {
387
- ...alertConfig?.config?.channels?.discord,
388
- webhookUrl: val,
389
- },
390
- },
391
- }),
392
- })
393
- queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
394
- }}
395
- className="flex-1 bg-background/50 border border-border/50 rounded-lg px-3 py-2 text-xs font-mono outline-none focus:ring-1 focus:ring-primary/30"
396
- />
397
- </div>
398
- </div>
399
-
400
- {/* Email (SMTP) */}
401
- <div className="p-4 bg-muted/20 rounded-xl border border-border/10">
402
- <div className="flex items-center justify-between mb-4">
403
- <div className="flex items-center gap-3">
404
- <div className="w-8 h-8 rounded-lg bg-red-500/10 flex items-center justify-center text-red-500">
405
- <Info size={16} />
406
- </div>
407
- <div>
408
- <h4 className="text-sm font-bold">Email (SMTP)</h4>
409
- <p className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter">
410
- Standard mail delivery
411
- </p>
412
- </div>
413
- </div>
414
- <button
415
- type="button"
416
- onClick={async () => {
417
- const enabled = !alertConfig?.config?.channels?.email?.enabled
418
- const current = alertConfig?.config?.channels?.email || {}
419
- await fetch('/api/alerts/config', {
420
- method: 'POST',
421
- headers: { 'Content-Type': 'application/json' },
422
- body: JSON.stringify({
423
- ...alertConfig.config,
424
- channels: {
425
- ...alertConfig.config.channels,
426
- email: { ...current, enabled },
427
- },
428
- }),
429
- })
430
- queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
431
- }}
432
- className={cn(
433
- 'px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all',
434
- alertConfig?.config?.channels?.email?.enabled
435
- ? 'bg-green-500 text-white shadow-lg shadow-green-500/20'
436
- : 'bg-muted-foreground/20 text-muted-foreground'
437
- )}
438
- >
439
- {alertConfig?.config?.channels?.email?.enabled ? 'Enabled' : 'Disabled'}
440
- </button>
441
- </div>
442
-
443
- {alertConfig?.config?.channels?.email?.enabled && (
444
- <div className="grid grid-cols-2 gap-3 mt-4 animate-in fade-in slide-in-from-top-2">
445
- <div className="col-span-2 space-y-1">
446
- <label
447
- htmlFor="email-to"
448
- className="text-[9px] font-black uppercase text-muted-foreground/60"
449
- >
450
- Destination Address
451
- </label>
452
- <input
453
- id="email-to"
454
- placeholder="admin@example.com"
455
- defaultValue={alertConfig?.config?.channels?.email?.to || ''}
456
- onBlur={async (e) => {
457
- const val = e.target.value
458
- await fetch('/api/alerts/config', {
459
- method: 'POST',
460
- headers: { 'Content-Type': 'application/json' },
461
- body: JSON.stringify({
462
- ...alertConfig.config,
463
- channels: {
464
- ...alertConfig.config.channels,
465
- email: {
466
- ...alertConfig?.config?.channels?.email,
467
- to: val,
468
- },
469
- },
470
- }),
471
- })
472
- }}
473
- className="w-full bg-background/50 border border-border/50 rounded-lg px-3 py-2 text-xs outline-none focus:ring-1 focus:ring-primary/30"
474
- />
475
- </div>
476
- <div className="space-y-1">
477
- <label
478
- htmlFor="smtp-host"
479
- className="text-[9px] font-black uppercase text-muted-foreground/60"
480
- >
481
- SMTP Host
482
- </label>
483
- <input
484
- id="smtp-host"
485
- placeholder="smtp.gmail.com"
486
- defaultValue={alertConfig?.config?.channels?.email?.smtpHost || ''}
487
- onBlur={async (e) => {
488
- await fetch('/api/alerts/config', {
489
- method: 'POST',
490
- headers: { 'Content-Type': 'application/json' },
491
- body: JSON.stringify({
492
- ...alertConfig.config,
493
- channels: {
494
- ...alertConfig.config.channels,
495
- email: {
496
- ...alertConfig?.config?.channels?.email,
497
- smtpHost: e.target.value,
498
- },
499
- },
500
- }),
501
- })
502
- }}
503
- className="w-full bg-background/50 border border-border/50 rounded-lg px-3 py-2 text-xs outline-none focus:ring-1 focus:ring-primary/30"
504
- />
505
- </div>
506
- <div className="space-y-1">
507
- <label
508
- htmlFor="smtp-port"
509
- className="text-[9px] font-black uppercase text-muted-foreground/60"
510
- >
511
- Port
512
- </label>
513
- <input
514
- id="smtp-port"
515
- type="number"
516
- placeholder="465"
517
- defaultValue={alertConfig?.config?.channels?.email?.smtpPort || 465}
518
- onBlur={async (e) => {
519
- await fetch('/api/alerts/config', {
520
- method: 'POST',
521
- headers: { 'Content-Type': 'application/json' },
522
- body: JSON.stringify({
523
- ...alertConfig.config,
524
- channels: {
525
- ...alertConfig.config.channels,
526
- email: {
527
- ...alertConfig?.config?.channels?.email,
528
- smtpPort: parseInt(e.target.value, 10),
529
- },
530
- },
531
- }),
532
- })
533
- }}
534
- className="w-full bg-background/50 border border-border/50 rounded-lg px-3 py-2 text-xs outline-none focus:ring-1 focus:ring-primary/30"
535
- />
536
- </div>
537
- <div className="space-y-1">
538
- <label
539
- htmlFor="smtp-user"
540
- className="text-[9px] font-black uppercase text-muted-foreground/60"
541
- >
542
- Username
543
- </label>
544
- <input
545
- id="smtp-user"
546
- placeholder="user@example.com"
547
- defaultValue={alertConfig?.config?.channels?.email?.smtpUser || ''}
548
- onBlur={async (e) => {
549
- await fetch('/api/alerts/config', {
550
- method: 'POST',
551
- headers: { 'Content-Type': 'application/json' },
552
- body: JSON.stringify({
553
- ...alertConfig.config,
554
- channels: {
555
- ...alertConfig.config.channels,
556
- email: {
557
- ...alertConfig?.config?.channels?.email,
558
- smtpUser: e.target.value,
559
- },
560
- },
561
- }),
562
- })
563
- }}
564
- className="w-full bg-background/50 border border-border/50 rounded-lg px-3 py-2 text-xs outline-none focus:ring-1 focus:ring-primary/30"
565
- />
566
- </div>
567
- <div className="space-y-1">
568
- <label
569
- htmlFor="smtp-pass"
570
- className="text-[9px] font-black uppercase text-muted-foreground/60"
571
- >
572
- Password
573
- </label>
574
- <input
575
- id="smtp-pass"
576
- type="password"
577
- placeholder="••••••••"
578
- defaultValue={alertConfig?.config?.channels?.email?.smtpPass || ''}
579
- onBlur={async (e) => {
580
- await fetch('/api/alerts/config', {
581
- method: 'POST',
582
- headers: { 'Content-Type': 'application/json' },
583
- body: JSON.stringify({
584
- ...alertConfig.config,
585
- channels: {
586
- ...alertConfig.config.channels,
587
- email: {
588
- ...alertConfig?.config?.channels?.email,
589
- smtpPass: e.target.value,
590
- },
591
- },
592
- }),
593
- })
594
- }}
595
- className="w-full bg-background/50 border border-border/50 rounded-lg px-3 py-2 text-xs outline-none focus:ring-1 focus:ring-primary/30"
596
- />
597
- </div>
598
- <div className="col-span-2 space-y-1">
599
- <label
600
- htmlFor="email-from"
601
- className="text-[9px] font-black uppercase text-muted-foreground/60"
602
- >
603
- From Address
604
- </label>
605
- <input
606
- id="email-from"
607
- placeholder="Zenith Monitor <noreply@example.com>"
608
- defaultValue={alertConfig?.config?.channels?.email?.from || ''}
609
- onBlur={async (e) => {
610
- await fetch('/api/alerts/config', {
611
- method: 'POST',
612
- headers: { 'Content-Type': 'application/json' },
613
- body: JSON.stringify({
614
- ...alertConfig.config,
615
- channels: {
616
- ...alertConfig.config.channels,
617
- email: {
618
- ...alertConfig?.config?.channels?.email,
619
- from: e.target.value,
620
- },
621
- },
622
- }),
623
- })
624
- }}
625
- className="w-full bg-background/50 border border-border/50 rounded-lg px-3 py-2 text-xs outline-none focus:ring-1 focus:ring-primary/30"
626
- />
627
- </div>
628
- </div>
629
- )}
630
- </div>
631
- </div>
632
-
633
- <div className="flex items-center justify-between mb-3 mt-8">
634
- <h3 className="text-xs font-black uppercase tracking-widest text-muted-foreground/60">
635
- Active Rules
636
- </h3>
637
- <button
638
- type="button"
639
- onClick={() => setShowAddRule(!showAddRule)}
640
- className="text-[10px] font-black uppercase tracking-widest text-primary hover:underline border-none bg-transparent cursor-pointer"
641
- >
642
- {showAddRule ? 'Cancel' : '+ Add Rule'}
643
- </button>
644
- </div>
645
-
646
- {showAddRule && (
647
- <form
648
- onSubmit={async (e) => {
649
- e.preventDefault()
650
- const formData = new FormData(e.currentTarget)
651
- const rule = {
652
- id: Math.random().toString(36).substring(7),
653
- name: formData.get('name'),
654
- type: formData.get('type'),
655
- threshold: parseInt(formData.get('threshold') as string, 10),
656
- cooldownMinutes: parseInt(formData.get('cooldown') as string, 10),
657
- queue: formData.get('queue') || undefined,
658
- }
659
- await fetch('/api/alerts/rules', {
660
- method: 'POST',
661
- headers: { 'Content-Type': 'application/json' },
662
- body: JSON.stringify(rule),
663
- })
664
- setShowAddRule(false)
665
- queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
666
- }}
667
- className="p-4 bg-muted/40 rounded-xl border border-primary/20 space-y-4 mb-6"
668
- >
669
- <div className="grid grid-cols-2 gap-4">
670
- <div className="space-y-1">
671
- <label
672
- htmlFor="rule-name"
673
- className="text-[10px] font-black uppercase text-muted-foreground"
674
- >
675
- Rule Name
676
- </label>
677
- <input
678
- id="rule-name"
679
- name="name"
680
- required
681
- placeholder="High CPU"
682
- className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-primary/30"
683
- />
684
- </div>
685
- <div className="space-y-1">
686
- <label
687
- htmlFor="rule-type"
688
- className="text-[10px] font-black uppercase text-muted-foreground"
689
- >
690
- Type
691
- </label>
692
- <select
693
- id="rule-type"
694
- name="type"
695
- className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none cursor-pointer"
696
- >
697
- <option value="backlog">Queue Backlog</option>
698
- <option value="failure">High Failure Count</option>
699
- <option value="worker_lost">Worker Loss</option>
700
- <option value="node_cpu">Node CPU (%)</option>
701
- <option value="node_ram">Node RAM (%)</option>
702
- </select>
703
- </div>
704
- </div>
705
- <div className="grid grid-cols-3 gap-4">
706
- <div className="space-y-1">
707
- <label
708
- htmlFor="rule-threshold"
709
- className="text-[10px] font-black uppercase text-muted-foreground"
710
- >
711
- Threshold
712
- </label>
713
- <input
714
- id="rule-threshold"
715
- name="threshold"
716
- type="number"
717
- required
718
- defaultValue="80"
719
- className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none"
720
- />
721
- </div>
722
- <div className="space-y-1">
723
- <label
724
- htmlFor="rule-cooldown"
725
- className="text-[10px] font-black uppercase text-muted-foreground"
726
- >
727
- Cooldown (Min)
728
- </label>
729
- <input
730
- id="rule-cooldown"
731
- name="cooldown"
732
- type="number"
733
- required
734
- defaultValue="30"
735
- className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none"
736
- />
737
- </div>
738
- <div className="space-y-1">
739
- <label
740
- htmlFor="rule-queue"
741
- className="text-[10px] font-black uppercase text-muted-foreground"
742
- >
743
- Queue (Optional)
744
- </label>
745
- <input
746
- id="rule-queue"
747
- name="queue"
748
- placeholder="default"
749
- className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none"
750
- />
751
- </div>
752
- </div>
753
- <button
754
- type="submit"
755
- className="w-full py-2 bg-primary text-primary-foreground rounded-lg text-xs font-black uppercase tracking-widest shadow-lg shadow-primary/20 cursor-pointer"
756
- >
757
- Save Alert Rule
758
- </button>
759
- </form>
760
- )}
761
-
762
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
763
- {alertConfig?.rules?.map((rule: any) => (
764
- <div
765
- key={rule.id}
766
- className="p-3 bg-muted/20 border border-border/10 rounded-xl flex items-center justify-between group"
767
- >
768
- <div className="flex-1">
769
- <p className="text-[11px] font-black uppercase tracking-tight flex items-center gap-2">
770
- {rule.name}
771
- {rule.queue && (
772
- <span className="text-[9px] px-1.5 py-0.5 rounded bg-primary/10 text-primary">
773
- {rule.queue}
774
- </span>
775
- )}
776
- </p>
777
- <p className="text-[10px] text-muted-foreground opacity-70">
778
- {rule.type === 'backlog'
779
- ? `Waiting > ${rule.threshold}`
780
- : rule.type === 'failure'
781
- ? `Failed > ${rule.threshold}`
782
- : rule.type === 'worker_lost'
783
- ? `Workers < ${rule.threshold}`
784
- : rule.type === 'node_cpu'
785
- ? `CPU > ${rule.threshold}%`
786
- : `RAM > ${rule.threshold}%`}
787
- </p>
788
- </div>
789
- <div className="flex items-center gap-2">
790
- <div className="px-2 py-0.5 bg-muted rounded text-[9px] font-bold text-muted-foreground">
791
- {rule.cooldownMinutes}m
792
- </div>
793
- <button
794
- type="button"
795
- onClick={async () => {
796
- if (confirm('Delete this alert rule?')) {
797
- await fetch(`/api/alerts/rules/${rule.id}`, { method: 'DELETE' })
798
- queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
799
- }
800
- }}
801
- className="p-1 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all"
802
- >
803
- <Trash2 size={12} />
804
- </button>
805
- </div>
806
- </div>
807
- ))}
808
- </div>
809
-
810
- <div className="pt-4 flex flex-col sm:flex-row items-center justify-between gap-4 border-t border-border/30">
811
- <p className="text-xs text-muted-foreground max-w-md">
812
- Configure notification channels above to receive real-time alerts.
813
- </p>
814
- <button
815
- type="button"
816
- onClick={async () => {
817
- const res = await fetch('/api/alerts/test', { method: 'POST' }).then((r) =>
818
- r.json()
819
- )
820
- if (res.success) {
821
- alert('Test alert dispatched to all enabled channels.')
822
- }
823
- }}
824
- 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 shadow-lg shadow-primary/10 cursor-pointer"
825
- >
826
- Test Dispatch Now
827
- </button>
828
- </div>
829
- </div>
830
- </section>
831
-
832
- {/* Data Retention Section */}
833
- <section className="card-premium p-6">
834
- <div className="flex items-center gap-3 mb-6">
835
- <div className="w-10 h-10 rounded-xl bg-red-500/10 flex items-center justify-center text-red-500">
836
- <Trash2 size={20} />
837
- </div>
838
- <div>
839
- <h2 className="text-lg font-bold">Data Retention</h2>
840
- <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
841
- Manage persistent archive storage
842
- </p>
843
- </div>
844
- </div>
845
-
846
- <div className="space-y-6">
847
- <div>
848
- <div className="flex items-center justify-between py-3 border-b border-border/30">
849
- <div>
850
- <h3 className="text-sm font-bold">SQL Job Archive Preservation</h3>
851
- <p className="text-xs text-muted-foreground">
852
- Keep archived jobs for a specific number of days before permanent removal.
853
- </p>
854
- </div>
855
- <div className="flex items-center gap-6">
856
- <div className="flex items-center gap-3">
857
- <span className="text-[10px] font-black uppercase text-muted-foreground/40">
858
- Auto-Cleanup
859
- </span>
860
- <button
861
- type="button"
862
- onClick={async () => {
863
- const enabled = !alertConfig?.maintenance?.autoCleanup
864
- await fetch('/api/maintenance/config', {
865
- method: 'POST',
866
- headers: { 'Content-Type': 'application/json' },
867
- body: JSON.stringify({
868
- ...alertConfig.maintenance,
869
- autoCleanup: enabled,
870
- }),
871
- })
872
- queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
873
- }}
874
- className={cn(
875
- 'w-10 h-5 rounded-full p-1 transition-all flex items-center',
876
- alertConfig?.maintenance?.autoCleanup
877
- ? 'bg-green-500 justify-end'
878
- : 'bg-muted justify-start'
879
- )}
880
- >
881
- <div className="w-3 h-3 bg-white rounded-full shadow-sm" />
882
- </button>
883
- </div>
884
-
885
- <div className="flex items-center gap-3">
886
- <span className="text-[10px] font-black uppercase text-muted-foreground/40">
887
- Retention Days
888
- </span>
889
- <select
890
- 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"
891
- value={alertConfig?.maintenance?.retentionDays || 30}
892
- onChange={async (e) => {
893
- const days = parseInt(e.target.value, 10)
894
- await fetch('/api/maintenance/config', {
895
- method: 'POST',
896
- headers: { 'Content-Type': 'application/json' },
897
- body: JSON.stringify({
898
- ...alertConfig.maintenance,
899
- retentionDays: days,
900
- }),
901
- })
902
- queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
903
- }}
904
- >
905
- <option value="7">7 Days</option>
906
- <option value="15">15 Days</option>
907
- <option value="30">30 Days</option>
908
- <option value="90">90 Days</option>
909
- <option value="365">1 Year</option>
910
- </select>
911
- </div>
912
- </div>
913
- </div>
914
-
915
- <div className="bg-red-500/5 border border-red-500/10 rounded-xl p-4 flex items-center justify-between">
916
- <div className="flex items-center gap-3">
917
- <Info size={16} className="text-red-500/60" />
918
- <div className="flex flex-col">
919
- <span className="text-xs font-medium text-red-900/60 dark:text-red-400/60">
920
- Manual prune will remove all jobs older than the selected period.
921
- </span>
922
- {alertConfig?.maintenance?.lastRun && (
923
- <span className="text-[10px] text-muted-foreground/60">
924
- Last auto-cleanup run:{' '}
925
- {new Date(alertConfig.maintenance.lastRun).toLocaleString()}
926
- </span>
927
- )}
928
- </div>
929
- </div>
930
- <button
931
- type="button"
932
- onClick={async () => {
933
- const days = alertConfig?.maintenance?.retentionDays || 30
934
- if (confirm(`Are you sure you want to prune logs older than ${days} days?`)) {
935
- const res = await fetch('/api/maintenance/cleanup-archive', {
936
- method: 'POST',
937
- headers: { 'Content-Type': 'application/json' },
938
- body: JSON.stringify({ days }),
939
- }).then((r) => r.json())
940
- alert(`Cleanup complete. Removed ${res.deleted || 0} archived jobs.`)
941
- }
942
- }}
943
- 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"
944
- >
945
- Prune Archive Now
946
- </button>
947
- </div>
948
- </div>
949
-
950
- <div className="pt-4 border-t border-border/30">
951
- <div className="flex justify-between items-center">
952
- <div>
953
- <h3 className="text-sm font-bold">Live Stats History (Redis)</h3>
954
- <p className="text-xs text-muted-foreground">
955
- Minute-by-minute metrics used for dashboard charts.
956
- </p>
957
- </div>
958
- <div className="px-3 py-1 bg-muted rounded-full text-[10px] font-black text-muted-foreground uppercase tracking-widest">
959
- Auto-Prunes (60m)
960
- </div>
961
- </div>
962
- </div>
963
- </div>
964
- </section>
965
-
966
- {/* About Section */}
967
- <section className="card-premium p-6">
968
- <div className="flex items-center gap-3 mb-6">
969
- <div className="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center text-amber-500">
970
- <Shield size={20} />
971
- </div>
972
- <div>
973
- <h2 className="text-lg font-bold">About Flux Console</h2>
974
- <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-bold">
975
- Version and documentation
976
- </p>
977
- </div>
978
- </div>
979
-
980
- <div className="space-y-4">
981
- <div className="flex items-center justify-between py-3 border-b border-border/30">
982
- <span className="font-medium">Version</span>
983
- <span className="text-sm font-bold">{systemStatus?.version || '0.1.0-alpha.1'}</span>
984
- </div>
985
- <div className="flex items-center justify-between py-3 border-b border-border/30">
986
- <span className="font-medium">Package</span>
987
- <code className="text-sm bg-muted px-2 py-1 rounded font-mono">
988
- {systemStatus?.package || '@gravito/flux-console'}
989
- </code>
990
- </div>
991
- <div className="flex items-center justify-between py-3">
992
- <span className="font-medium">Documentation</span>
993
- <a
994
- href="https://github.com/gravito-framework/gravito"
995
- target="_blank"
996
- rel="noopener noreferrer"
997
- className="flex items-center gap-1 text-sm text-primary hover:underline font-bold"
998
- >
999
- View Docs <ExternalLink size={14} />
1000
- </a>
1001
- </div>
1002
- </div>
1003
- </section>
1004
- </div>
1005
- )
1006
- }
1007
-
1008
- function formatUptime(seconds: number): string {
1009
- const days = Math.floor(seconds / 86400)
1010
- const hours = Math.floor((seconds % 86400) / 3600)
1011
- const minutes = Math.floor((seconds % 3600) / 60)
1012
-
1013
- if (days > 0) {
1014
- return `${days}d ${hours}h ${minutes}m`
1015
- }
1016
- if (hours > 0) {
1017
- return `${hours}h ${minutes}m`
1018
- }
1019
- return `${minutes}m ${Math.floor(seconds % 60)}s`
1020
- }