@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.
- package/README.md +28 -10
- package/dist/bin.js +43235 -76691
- package/dist/client/index.html +13 -0
- package/dist/server/index.js +43235 -76691
- package/package.json +16 -7
- package/CHANGELOG.md +0 -62
- package/Dockerfile +0 -46
- package/Dockerfile.demo-worker +0 -29
- package/bin/flux-console.ts +0 -2
- package/doc/ECOSYSTEM_EXPANSION_RFC.md +0 -130
- package/docker-compose.yml +0 -40
- package/docs/ALERTING_GUIDE.md +0 -71
- package/docs/DEPLOYMENT.md +0 -157
- package/docs/DOCS_INTERNAL.md +0 -73
- package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
- package/docs/QUASAR_MASTER_PLAN.md +0 -140
- package/docs/QUICK_TEST_GUIDE.md +0 -72
- package/docs/ROADMAP.md +0 -85
- package/docs/integrations/LARAVEL.md +0 -207
- package/postcss.config.js +0 -6
- package/scripts/debug_redis_keys.ts +0 -24
- package/scripts/flood-logs.ts +0 -21
- package/scripts/seed.ts +0 -213
- package/scripts/verify-throttle.ts +0 -49
- package/scripts/worker.ts +0 -124
- package/specs/PULSE_SPEC.md +0 -86
- package/src/bin.ts +0 -6
- package/src/client/App.tsx +0 -72
- package/src/client/Layout.tsx +0 -669
- package/src/client/Sidebar.tsx +0 -112
- package/src/client/ThroughputChart.tsx +0 -158
- package/src/client/WorkerStatus.tsx +0 -202
- package/src/client/components/BrandIcons.tsx +0 -168
- package/src/client/components/ConfirmDialog.tsx +0 -134
- package/src/client/components/JobInspector.tsx +0 -487
- package/src/client/components/LogArchiveModal.tsx +0 -432
- package/src/client/components/NotificationBell.tsx +0 -212
- package/src/client/components/PageHeader.tsx +0 -47
- package/src/client/components/Toaster.tsx +0 -90
- package/src/client/components/UserProfileDropdown.tsx +0 -186
- package/src/client/contexts/AuthContext.tsx +0 -105
- package/src/client/contexts/NotificationContext.tsx +0 -128
- package/src/client/index.css +0 -172
- package/src/client/main.tsx +0 -15
- package/src/client/pages/LoginPage.tsx +0 -164
- package/src/client/pages/MetricsPage.tsx +0 -445
- package/src/client/pages/OverviewPage.tsx +0 -519
- package/src/client/pages/PulsePage.tsx +0 -409
- package/src/client/pages/QueuesPage.tsx +0 -378
- package/src/client/pages/SchedulesPage.tsx +0 -535
- package/src/client/pages/SettingsPage.tsx +0 -1001
- package/src/client/pages/WorkersPage.tsx +0 -380
- package/src/client/pages/index.ts +0 -8
- package/src/client/utils.ts +0 -15
- package/src/server/config/ServerConfigManager.ts +0 -90
- package/src/server/index.ts +0 -860
- package/src/server/middleware/auth.ts +0 -127
- package/src/server/services/AlertService.ts +0 -321
- package/src/server/services/CommandService.ts +0 -136
- package/src/server/services/LogStreamProcessor.ts +0 -93
- package/src/server/services/MaintenanceScheduler.ts +0 -78
- package/src/server/services/PulseService.ts +0 -148
- package/src/server/services/QueueMetricsCollector.ts +0 -138
- package/src/server/services/QueueService.ts +0 -924
- package/src/shared/types.ts +0 -223
- package/tailwind.config.js +0 -80
- package/tests/placeholder.test.ts +0 -7
- package/tsconfig.json +0 -29
- package/tsconfig.node.json +0 -10
- 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
|
-
}
|