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