@gravito/zenith 0.1.0-beta.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ALERTING_GUIDE.md +71 -0
- package/QUASAR_MASTER_PLAN.md +137 -0
- package/dist/bin.js +38061 -26911
- package/dist/client/assets/index-BSTyMCFd.css +1 -0
- package/dist/client/assets/index-oXEse8ih.js +436 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +38061 -26911
- package/package.json +52 -48
- package/specs/PULSE_SPEC.md +86 -0
- package/src/client/App.tsx +2 -0
- package/src/client/Layout.tsx +30 -11
- package/src/client/Sidebar.tsx +2 -1
- package/src/client/WorkerStatus.tsx +25 -21
- package/src/client/components/BrandIcons.tsx +63 -0
- package/src/client/components/PageHeader.tsx +34 -0
- package/src/client/pages/OverviewPage.tsx +18 -20
- package/src/client/pages/PulsePage.tsx +396 -0
- package/src/client/pages/QueuesPage.tsx +1 -3
- package/src/client/pages/SettingsPage.tsx +586 -78
- package/src/client/pages/WorkersPage.tsx +1 -1
- package/src/client/pages/index.ts +1 -0
- package/src/server/index.ts +148 -8
- package/src/server/services/AlertService.ts +189 -41
- package/src/server/services/CommandService.ts +137 -0
- package/src/server/services/PulseService.ts +80 -0
- package/src/server/services/QueueService.ts +58 -4
- package/src/shared/types.ts +97 -0
- package/tsconfig.json +2 -2
- package/PULSE_IMPLEMENTATION_PLAN.md +0 -111
- package/dist/client/assets/index-DGYEwTDL.css +0 -1
- package/dist/client/assets/index-oyTdySX0.js +0 -421
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
1
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
2
|
import {
|
|
3
3
|
Bell,
|
|
4
4
|
Clock,
|
|
@@ -18,6 +18,8 @@ import React from 'react'
|
|
|
18
18
|
import { cn } from '../utils'
|
|
19
19
|
|
|
20
20
|
export function SettingsPage() {
|
|
21
|
+
const queryClient = useQueryClient()
|
|
22
|
+
const [showAddRule, setShowAddRule] = React.useState(false)
|
|
21
23
|
const [theme, setTheme] = React.useState<'light' | 'dark' | 'system'>(() => {
|
|
22
24
|
if (typeof window !== 'undefined') {
|
|
23
25
|
const stored = localStorage.getItem('theme')
|
|
@@ -147,14 +149,14 @@ export function SettingsPage() {
|
|
|
147
149
|
<span className="font-medium">Redis URL</span>
|
|
148
150
|
</div>
|
|
149
151
|
<code className="text-sm bg-muted px-2 py-1 rounded font-mono">
|
|
150
|
-
{
|
|
152
|
+
{systemStatus?.redisUrl || 'redis://localhost:6379'}
|
|
151
153
|
</code>
|
|
152
154
|
</div>
|
|
153
155
|
|
|
154
156
|
<div className="flex items-center justify-between py-3 border-b border-border/30">
|
|
155
157
|
<div className="flex items-center gap-3">
|
|
156
158
|
<Clock size={16} className="text-muted-foreground" />
|
|
157
|
-
<span className="font-medium">
|
|
159
|
+
<span className="font-medium">Service Uptime</span>
|
|
158
160
|
</div>
|
|
159
161
|
<span className="text-sm font-mono font-bold">
|
|
160
162
|
{systemStatus?.uptime ? formatUptime(systemStatus.uptime) : 'Loading...'}
|
|
@@ -210,6 +212,12 @@ export function SettingsPage() {
|
|
|
210
212
|
</p>
|
|
211
213
|
<p className="text-lg font-mono font-bold">{systemStatus?.memory?.heapUsed || '...'}</p>
|
|
212
214
|
</div>
|
|
215
|
+
<div className="bg-muted/20 rounded-xl p-4">
|
|
216
|
+
<p className="text-[10px] font-black text-muted-foreground/50 uppercase tracking-widest mb-1">
|
|
217
|
+
Total System RAM
|
|
218
|
+
</p>
|
|
219
|
+
<p className="text-lg font-mono font-bold">{systemStatus?.memory?.total || '...'}</p>
|
|
220
|
+
</div>
|
|
213
221
|
</div>
|
|
214
222
|
</section>
|
|
215
223
|
|
|
@@ -227,66 +235,522 @@ export function SettingsPage() {
|
|
|
227
235
|
</div>
|
|
228
236
|
</div>
|
|
229
237
|
|
|
230
|
-
<div className="space-y-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
<div className="
|
|
239
|
-
<
|
|
240
|
-
className=
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
238
|
+
<div className="space-y-8">
|
|
239
|
+
{/* Notification Channels */}
|
|
240
|
+
<div className="space-y-4">
|
|
241
|
+
<h3 className="text-xs font-black uppercase tracking-widest text-muted-foreground/60 mb-2">
|
|
242
|
+
Notification Channels
|
|
243
|
+
</h3>
|
|
244
|
+
|
|
245
|
+
{/* Slack */}
|
|
246
|
+
<div className="p-4 bg-muted/20 rounded-xl border border-border/10">
|
|
247
|
+
<div className="flex items-center justify-between mb-4">
|
|
248
|
+
<div className="flex items-center gap-3">
|
|
249
|
+
<div className="w-8 h-8 rounded-lg bg-[#4A154B]/10 flex items-center justify-center text-[#4A154B] dark:text-[#E01E5A]">
|
|
250
|
+
<Bell size={16} />
|
|
251
|
+
</div>
|
|
252
|
+
<div>
|
|
253
|
+
<h4 className="text-sm font-bold">Slack</h4>
|
|
254
|
+
<p className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter">
|
|
255
|
+
Standard notification webhook
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
onClick={async () => {
|
|
262
|
+
const enabled = !alertConfig?.config?.channels?.slack?.enabled
|
|
263
|
+
const current = alertConfig?.config?.channels?.slack || {}
|
|
264
|
+
await fetch('/api/alerts/config', {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers: { 'Content-Type': 'application/json' },
|
|
267
|
+
body: JSON.stringify({
|
|
268
|
+
...alertConfig.config,
|
|
269
|
+
channels: {
|
|
270
|
+
...alertConfig.config.channels,
|
|
271
|
+
slack: { ...current, enabled },
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
})
|
|
275
|
+
queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
|
|
276
|
+
}}
|
|
277
|
+
className={cn(
|
|
278
|
+
'px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all',
|
|
279
|
+
alertConfig?.config?.channels?.slack?.enabled
|
|
280
|
+
? 'bg-green-500 text-white shadow-lg shadow-green-500/20'
|
|
281
|
+
: 'bg-muted-foreground/20 text-muted-foreground'
|
|
282
|
+
)}
|
|
283
|
+
>
|
|
284
|
+
{alertConfig?.config?.channels?.slack?.enabled ? 'Enabled' : 'Disabled'}
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
<div className="flex gap-3">
|
|
288
|
+
<input
|
|
289
|
+
type="password"
|
|
290
|
+
placeholder="https://hooks.slack.com/services/..."
|
|
291
|
+
defaultValue={alertConfig?.config?.channels?.slack?.webhookUrl || ''}
|
|
292
|
+
onBlur={async (e) => {
|
|
293
|
+
const val = e.target.value
|
|
294
|
+
if (val === alertConfig?.config?.channels?.slack?.webhookUrl) return
|
|
295
|
+
await fetch('/api/alerts/config', {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers: { 'Content-Type': 'application/json' },
|
|
298
|
+
body: JSON.stringify({
|
|
299
|
+
...alertConfig.config,
|
|
300
|
+
channels: {
|
|
301
|
+
...alertConfig.config.channels,
|
|
302
|
+
slack: {
|
|
303
|
+
...alertConfig?.config?.channels?.slack,
|
|
304
|
+
webhookUrl: val,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
})
|
|
309
|
+
queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
|
|
310
|
+
}}
|
|
311
|
+
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"
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
255
314
|
</div>
|
|
256
|
-
</div>
|
|
257
315
|
|
|
258
|
-
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
316
|
+
{/* Discord */}
|
|
317
|
+
<div className="p-4 bg-muted/20 rounded-xl border border-border/10">
|
|
318
|
+
<div className="flex items-center justify-between mb-4">
|
|
319
|
+
<div className="flex items-center gap-3">
|
|
320
|
+
<div className="w-8 h-8 rounded-lg bg-[#5865F2]/10 flex items-center justify-center text-[#5865F2]">
|
|
321
|
+
<Monitor size={16} />
|
|
322
|
+
</div>
|
|
323
|
+
<div>
|
|
324
|
+
<h4 className="text-sm font-bold">Discord</h4>
|
|
325
|
+
<p className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter">
|
|
326
|
+
Webhook integration for servers
|
|
327
|
+
</p>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
<button
|
|
331
|
+
type="button"
|
|
332
|
+
onClick={async () => {
|
|
333
|
+
const enabled = !alertConfig?.config?.channels?.discord?.enabled
|
|
334
|
+
const current = alertConfig?.config?.channels?.discord || {}
|
|
335
|
+
await fetch('/api/alerts/config', {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
...alertConfig.config,
|
|
340
|
+
channels: {
|
|
341
|
+
...alertConfig.config.channels,
|
|
342
|
+
discord: { ...current, enabled },
|
|
343
|
+
},
|
|
344
|
+
}),
|
|
345
|
+
})
|
|
346
|
+
queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
|
|
347
|
+
}}
|
|
348
|
+
className={cn(
|
|
349
|
+
'px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all',
|
|
350
|
+
alertConfig?.config?.channels?.discord?.enabled
|
|
351
|
+
? 'bg-green-500 text-white shadow-lg shadow-green-500/20'
|
|
352
|
+
: 'bg-muted-foreground/20 text-muted-foreground'
|
|
353
|
+
)}
|
|
267
354
|
>
|
|
355
|
+
{alertConfig?.config?.channels?.discord?.enabled ? 'Enabled' : 'Disabled'}
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
358
|
+
<div className="flex gap-3">
|
|
359
|
+
<input
|
|
360
|
+
type="password"
|
|
361
|
+
placeholder="https://discord.com/api/webhooks/..."
|
|
362
|
+
defaultValue={alertConfig?.config?.channels?.discord?.webhookUrl || ''}
|
|
363
|
+
onBlur={async (e) => {
|
|
364
|
+
const val = e.target.value
|
|
365
|
+
if (val === alertConfig?.config?.channels?.discord?.webhookUrl) return
|
|
366
|
+
await fetch('/api/alerts/config', {
|
|
367
|
+
method: 'POST',
|
|
368
|
+
headers: { 'Content-Type': 'application/json' },
|
|
369
|
+
body: JSON.stringify({
|
|
370
|
+
...alertConfig.config,
|
|
371
|
+
channels: {
|
|
372
|
+
...alertConfig.config.channels,
|
|
373
|
+
discord: {
|
|
374
|
+
...alertConfig?.config?.channels?.discord,
|
|
375
|
+
webhookUrl: val,
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
}),
|
|
379
|
+
})
|
|
380
|
+
queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
|
|
381
|
+
}}
|
|
382
|
+
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"
|
|
383
|
+
/>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
{/* Email (SMTP) */}
|
|
388
|
+
<div className="p-4 bg-muted/20 rounded-xl border border-border/10">
|
|
389
|
+
<div className="flex items-center justify-between mb-4">
|
|
390
|
+
<div className="flex items-center gap-3">
|
|
391
|
+
<div className="w-8 h-8 rounded-lg bg-red-500/10 flex items-center justify-center text-red-500">
|
|
392
|
+
<Info size={16} />
|
|
393
|
+
</div>
|
|
268
394
|
<div>
|
|
269
|
-
<
|
|
270
|
-
<p className="text-[10px] text-muted-foreground
|
|
271
|
-
|
|
272
|
-
? `Waiting > ${rule.threshold}`
|
|
273
|
-
: rule.type === 'failure'
|
|
274
|
-
? `Failed > ${rule.threshold}`
|
|
275
|
-
: `Workers < ${rule.threshold}`}
|
|
395
|
+
<h4 className="text-sm font-bold">Email (SMTP)</h4>
|
|
396
|
+
<p className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter">
|
|
397
|
+
Standard mail delivery
|
|
276
398
|
</p>
|
|
277
399
|
</div>
|
|
278
|
-
|
|
279
|
-
|
|
400
|
+
</div>
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
onClick={async () => {
|
|
404
|
+
const enabled = !alertConfig?.config?.channels?.email?.enabled
|
|
405
|
+
const current = alertConfig?.config?.channels?.email || {}
|
|
406
|
+
await fetch('/api/alerts/config', {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
409
|
+
body: JSON.stringify({
|
|
410
|
+
...alertConfig.config,
|
|
411
|
+
channels: {
|
|
412
|
+
...alertConfig.config.channels,
|
|
413
|
+
email: { ...current, enabled },
|
|
414
|
+
},
|
|
415
|
+
}),
|
|
416
|
+
})
|
|
417
|
+
queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
|
|
418
|
+
}}
|
|
419
|
+
className={cn(
|
|
420
|
+
'px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all',
|
|
421
|
+
alertConfig?.config?.channels?.email?.enabled
|
|
422
|
+
? 'bg-green-500 text-white shadow-lg shadow-green-500/20'
|
|
423
|
+
: 'bg-muted-foreground/20 text-muted-foreground'
|
|
424
|
+
)}
|
|
425
|
+
>
|
|
426
|
+
{alertConfig?.config?.channels?.email?.enabled ? 'Enabled' : 'Disabled'}
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{alertConfig?.config?.channels?.email?.enabled && (
|
|
431
|
+
<div className="grid grid-cols-2 gap-3 mt-4 animate-in fade-in slide-in-from-top-2">
|
|
432
|
+
<div className="col-span-2 space-y-1">
|
|
433
|
+
<label className="text-[9px] font-black uppercase text-muted-foreground/60">
|
|
434
|
+
Destination Address
|
|
435
|
+
</label>
|
|
436
|
+
<input
|
|
437
|
+
placeholder="admin@example.com"
|
|
438
|
+
defaultValue={alertConfig?.config?.channels?.email?.to || ''}
|
|
439
|
+
onBlur={async (e) => {
|
|
440
|
+
const val = e.target.value
|
|
441
|
+
await fetch('/api/alerts/config', {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
headers: { 'Content-Type': 'application/json' },
|
|
444
|
+
body: JSON.stringify({
|
|
445
|
+
...alertConfig.config,
|
|
446
|
+
channels: {
|
|
447
|
+
...alertConfig.config.channels,
|
|
448
|
+
email: {
|
|
449
|
+
...alertConfig?.config?.channels?.email,
|
|
450
|
+
to: val,
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
}),
|
|
454
|
+
})
|
|
455
|
+
}}
|
|
456
|
+
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"
|
|
457
|
+
/>
|
|
458
|
+
</div>
|
|
459
|
+
<div className="space-y-1">
|
|
460
|
+
<label className="text-[9px] font-black uppercase text-muted-foreground/60">
|
|
461
|
+
SMTP Host
|
|
462
|
+
</label>
|
|
463
|
+
<input
|
|
464
|
+
placeholder="smtp.gmail.com"
|
|
465
|
+
defaultValue={alertConfig?.config?.channels?.email?.smtpHost || ''}
|
|
466
|
+
onBlur={async (e) => {
|
|
467
|
+
await fetch('/api/alerts/config', {
|
|
468
|
+
method: 'POST',
|
|
469
|
+
headers: { 'Content-Type': 'application/json' },
|
|
470
|
+
body: JSON.stringify({
|
|
471
|
+
...alertConfig.config,
|
|
472
|
+
channels: {
|
|
473
|
+
...alertConfig.config.channels,
|
|
474
|
+
email: {
|
|
475
|
+
...alertConfig?.config?.channels?.email,
|
|
476
|
+
smtpHost: e.target.value,
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
}),
|
|
480
|
+
})
|
|
481
|
+
}}
|
|
482
|
+
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"
|
|
483
|
+
/>
|
|
484
|
+
</div>
|
|
485
|
+
<div className="space-y-1">
|
|
486
|
+
<label className="text-[9px] font-black uppercase text-muted-foreground/60">
|
|
487
|
+
Port
|
|
488
|
+
</label>
|
|
489
|
+
<input
|
|
490
|
+
type="number"
|
|
491
|
+
placeholder="465"
|
|
492
|
+
defaultValue={alertConfig?.config?.channels?.email?.smtpPort || 465}
|
|
493
|
+
onBlur={async (e) => {
|
|
494
|
+
await fetch('/api/alerts/config', {
|
|
495
|
+
method: 'POST',
|
|
496
|
+
headers: { 'Content-Type': 'application/json' },
|
|
497
|
+
body: JSON.stringify({
|
|
498
|
+
...alertConfig.config,
|
|
499
|
+
channels: {
|
|
500
|
+
...alertConfig.config.channels,
|
|
501
|
+
email: {
|
|
502
|
+
...alertConfig?.config?.channels?.email,
|
|
503
|
+
smtpPort: parseInt(e.target.value, 10),
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
}),
|
|
507
|
+
})
|
|
508
|
+
}}
|
|
509
|
+
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"
|
|
510
|
+
/>
|
|
511
|
+
</div>
|
|
512
|
+
<div className="space-y-1">
|
|
513
|
+
<label className="text-[9px] font-black uppercase text-muted-foreground/60">
|
|
514
|
+
Username
|
|
515
|
+
</label>
|
|
516
|
+
<input
|
|
517
|
+
placeholder="user@example.com"
|
|
518
|
+
defaultValue={alertConfig?.config?.channels?.email?.smtpUser || ''}
|
|
519
|
+
onBlur={async (e) => {
|
|
520
|
+
await fetch('/api/alerts/config', {
|
|
521
|
+
method: 'POST',
|
|
522
|
+
headers: { 'Content-Type': 'application/json' },
|
|
523
|
+
body: JSON.stringify({
|
|
524
|
+
...alertConfig.config,
|
|
525
|
+
channels: {
|
|
526
|
+
...alertConfig.config.channels,
|
|
527
|
+
email: {
|
|
528
|
+
...alertConfig?.config?.channels?.email,
|
|
529
|
+
smtpUser: e.target.value,
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
}),
|
|
533
|
+
})
|
|
534
|
+
}}
|
|
535
|
+
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"
|
|
536
|
+
/>
|
|
537
|
+
</div>
|
|
538
|
+
<div className="space-y-1">
|
|
539
|
+
<label className="text-[9px] font-black uppercase text-muted-foreground/60">
|
|
540
|
+
Password
|
|
541
|
+
</label>
|
|
542
|
+
<input
|
|
543
|
+
type="password"
|
|
544
|
+
placeholder="••••••••"
|
|
545
|
+
defaultValue={alertConfig?.config?.channels?.email?.smtpPass || ''}
|
|
546
|
+
onBlur={async (e) => {
|
|
547
|
+
await fetch('/api/alerts/config', {
|
|
548
|
+
method: 'POST',
|
|
549
|
+
headers: { 'Content-Type': 'application/json' },
|
|
550
|
+
body: JSON.stringify({
|
|
551
|
+
...alertConfig.config,
|
|
552
|
+
channels: {
|
|
553
|
+
...alertConfig.config.channels,
|
|
554
|
+
email: {
|
|
555
|
+
...alertConfig?.config?.channels?.email,
|
|
556
|
+
smtpPass: e.target.value,
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
}),
|
|
560
|
+
})
|
|
561
|
+
}}
|
|
562
|
+
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"
|
|
563
|
+
/>
|
|
564
|
+
</div>
|
|
565
|
+
<div className="col-span-2 space-y-1">
|
|
566
|
+
<label className="text-[9px] font-black uppercase text-muted-foreground/60">
|
|
567
|
+
From Address
|
|
568
|
+
</label>
|
|
569
|
+
<input
|
|
570
|
+
placeholder="Zenith Monitor <noreply@example.com>"
|
|
571
|
+
defaultValue={alertConfig?.config?.channels?.email?.from || ''}
|
|
572
|
+
onBlur={async (e) => {
|
|
573
|
+
await fetch('/api/alerts/config', {
|
|
574
|
+
method: 'POST',
|
|
575
|
+
headers: { 'Content-Type': 'application/json' },
|
|
576
|
+
body: JSON.stringify({
|
|
577
|
+
...alertConfig.config,
|
|
578
|
+
channels: {
|
|
579
|
+
...alertConfig.config.channels,
|
|
580
|
+
email: {
|
|
581
|
+
...alertConfig?.config?.channels?.email,
|
|
582
|
+
from: e.target.value,
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
})
|
|
587
|
+
}}
|
|
588
|
+
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"
|
|
589
|
+
/>
|
|
280
590
|
</div>
|
|
281
591
|
</div>
|
|
282
|
-
)
|
|
592
|
+
)}
|
|
283
593
|
</div>
|
|
284
594
|
</div>
|
|
285
595
|
|
|
596
|
+
<div className="flex items-center justify-between mb-3 mt-8">
|
|
597
|
+
<h3 className="text-xs font-black uppercase tracking-widest text-muted-foreground/60">
|
|
598
|
+
Active Rules
|
|
599
|
+
</h3>
|
|
600
|
+
<button
|
|
601
|
+
onClick={() => setShowAddRule(!showAddRule)}
|
|
602
|
+
className="text-[10px] font-black uppercase tracking-widest text-primary hover:underline border-none bg-transparent cursor-pointer"
|
|
603
|
+
>
|
|
604
|
+
{showAddRule ? 'Cancel' : '+ Add Rule'}
|
|
605
|
+
</button>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
{showAddRule && (
|
|
609
|
+
<form
|
|
610
|
+
onSubmit={async (e) => {
|
|
611
|
+
e.preventDefault()
|
|
612
|
+
const formData = new FormData(e.currentTarget)
|
|
613
|
+
const rule = {
|
|
614
|
+
id: Math.random().toString(36).substring(7),
|
|
615
|
+
name: formData.get('name'),
|
|
616
|
+
type: formData.get('type'),
|
|
617
|
+
threshold: parseInt(formData.get('threshold') as string, 10),
|
|
618
|
+
cooldownMinutes: parseInt(formData.get('cooldown') as string, 10),
|
|
619
|
+
queue: formData.get('queue') || undefined,
|
|
620
|
+
}
|
|
621
|
+
await fetch('/api/alerts/rules', {
|
|
622
|
+
method: 'POST',
|
|
623
|
+
headers: { 'Content-Type': 'application/json' },
|
|
624
|
+
body: JSON.stringify(rule),
|
|
625
|
+
})
|
|
626
|
+
setShowAddRule(false)
|
|
627
|
+
queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
|
|
628
|
+
}}
|
|
629
|
+
className="p-4 bg-muted/40 rounded-xl border border-primary/20 space-y-4 mb-6"
|
|
630
|
+
>
|
|
631
|
+
<div className="grid grid-cols-2 gap-4">
|
|
632
|
+
<div className="space-y-1">
|
|
633
|
+
<label className="text-[10px] font-black uppercase text-muted-foreground">
|
|
634
|
+
Rule Name
|
|
635
|
+
</label>
|
|
636
|
+
<input
|
|
637
|
+
name="name"
|
|
638
|
+
required
|
|
639
|
+
placeholder="High CPU"
|
|
640
|
+
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"
|
|
641
|
+
/>
|
|
642
|
+
</div>
|
|
643
|
+
<div className="space-y-1">
|
|
644
|
+
<label className="text-[10px] font-black uppercase text-muted-foreground">
|
|
645
|
+
Type
|
|
646
|
+
</label>
|
|
647
|
+
<select
|
|
648
|
+
name="type"
|
|
649
|
+
className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none cursor-pointer"
|
|
650
|
+
>
|
|
651
|
+
<option value="backlog">Queue Backlog</option>
|
|
652
|
+
<option value="failure">High Failure Count</option>
|
|
653
|
+
<option value="worker_lost">Worker Loss</option>
|
|
654
|
+
<option value="node_cpu">Node CPU (%)</option>
|
|
655
|
+
<option value="node_ram">Node RAM (%)</option>
|
|
656
|
+
</select>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
<div className="grid grid-cols-3 gap-4">
|
|
660
|
+
<div className="space-y-1">
|
|
661
|
+
<label className="text-[10px] font-black uppercase text-muted-foreground">
|
|
662
|
+
Threshold
|
|
663
|
+
</label>
|
|
664
|
+
<input
|
|
665
|
+
name="threshold"
|
|
666
|
+
type="number"
|
|
667
|
+
required
|
|
668
|
+
defaultValue="80"
|
|
669
|
+
className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none"
|
|
670
|
+
/>
|
|
671
|
+
</div>
|
|
672
|
+
<div className="space-y-1">
|
|
673
|
+
<label className="text-[10px] font-black uppercase text-muted-foreground">
|
|
674
|
+
Cooldown (Min)
|
|
675
|
+
</label>
|
|
676
|
+
<input
|
|
677
|
+
name="cooldown"
|
|
678
|
+
type="number"
|
|
679
|
+
required
|
|
680
|
+
defaultValue="30"
|
|
681
|
+
className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none"
|
|
682
|
+
/>
|
|
683
|
+
</div>
|
|
684
|
+
<div className="space-y-1">
|
|
685
|
+
<label className="text-[10px] font-black uppercase text-muted-foreground">
|
|
686
|
+
Queue (Optional)
|
|
687
|
+
</label>
|
|
688
|
+
<input
|
|
689
|
+
name="queue"
|
|
690
|
+
placeholder="default"
|
|
691
|
+
className="w-full bg-background border border-border/50 rounded-lg px-3 py-2 text-sm outline-none"
|
|
692
|
+
/>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
<button
|
|
696
|
+
type="submit"
|
|
697
|
+
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"
|
|
698
|
+
>
|
|
699
|
+
Save Alert Rule
|
|
700
|
+
</button>
|
|
701
|
+
</form>
|
|
702
|
+
)}
|
|
703
|
+
|
|
704
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
705
|
+
{alertConfig?.rules?.map((rule: any) => (
|
|
706
|
+
<div
|
|
707
|
+
key={rule.id}
|
|
708
|
+
className="p-3 bg-muted/20 border border-border/10 rounded-xl flex items-center justify-between group"
|
|
709
|
+
>
|
|
710
|
+
<div className="flex-1">
|
|
711
|
+
<p className="text-[11px] font-black uppercase tracking-tight flex items-center gap-2">
|
|
712
|
+
{rule.name}
|
|
713
|
+
{rule.queue && (
|
|
714
|
+
<span className="text-[9px] px-1.5 py-0.5 rounded bg-primary/10 text-primary">
|
|
715
|
+
{rule.queue}
|
|
716
|
+
</span>
|
|
717
|
+
)}
|
|
718
|
+
</p>
|
|
719
|
+
<p className="text-[10px] text-muted-foreground opacity-70">
|
|
720
|
+
{rule.type === 'backlog'
|
|
721
|
+
? `Waiting > ${rule.threshold}`
|
|
722
|
+
: rule.type === 'failure'
|
|
723
|
+
? `Failed > ${rule.threshold}`
|
|
724
|
+
: rule.type === 'worker_lost'
|
|
725
|
+
? `Workers < ${rule.threshold}`
|
|
726
|
+
: rule.type === 'node_cpu'
|
|
727
|
+
? `CPU > ${rule.threshold}%`
|
|
728
|
+
: `RAM > ${rule.threshold}%`}
|
|
729
|
+
</p>
|
|
730
|
+
</div>
|
|
731
|
+
<div className="flex items-center gap-2">
|
|
732
|
+
<div className="px-2 py-0.5 bg-muted rounded text-[9px] font-bold text-muted-foreground">
|
|
733
|
+
{rule.cooldownMinutes}m
|
|
734
|
+
</div>
|
|
735
|
+
<button
|
|
736
|
+
onClick={async () => {
|
|
737
|
+
if (confirm('Delete this alert rule?')) {
|
|
738
|
+
await fetch(`/api/alerts/rules/${rule.id}`, { method: 'DELETE' })
|
|
739
|
+
queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
|
|
740
|
+
}
|
|
741
|
+
}}
|
|
742
|
+
className="p-1 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all"
|
|
743
|
+
>
|
|
744
|
+
<Trash2 size={12} />
|
|
745
|
+
</button>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
))}
|
|
749
|
+
</div>
|
|
750
|
+
|
|
286
751
|
<div className="pt-4 flex flex-col sm:flex-row items-center justify-between gap-4 border-t border-border/30">
|
|
287
752
|
<p className="text-xs text-muted-foreground max-w-md">
|
|
288
|
-
Configure
|
|
289
|
-
notifications.
|
|
753
|
+
Configure notification channels above to receive real-time alerts.
|
|
290
754
|
</p>
|
|
291
755
|
<button
|
|
292
756
|
type="button"
|
|
@@ -295,13 +759,12 @@ export function SettingsPage() {
|
|
|
295
759
|
r.json()
|
|
296
760
|
)
|
|
297
761
|
if (res.success) {
|
|
298
|
-
alert('Test alert dispatched to
|
|
762
|
+
alert('Test alert dispatched to all enabled channels.')
|
|
299
763
|
}
|
|
300
764
|
}}
|
|
301
|
-
|
|
302
|
-
className="w-full sm:w-auto px-4 py-2 border border-primary/20 hover:bg-primary/5 text-primary rounded-lg text-xs font-black uppercase tracking-widest transition-all active:scale-95 disabled:opacity-50"
|
|
765
|
+
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"
|
|
303
766
|
>
|
|
304
|
-
Test
|
|
767
|
+
Test Dispatch Now
|
|
305
768
|
</button>
|
|
306
769
|
</div>
|
|
307
770
|
</div>
|
|
@@ -323,48 +786,93 @@ export function SettingsPage() {
|
|
|
323
786
|
|
|
324
787
|
<div className="space-y-6">
|
|
325
788
|
<div>
|
|
326
|
-
<div className="flex justify-between
|
|
789
|
+
<div className="flex items-center justify-between py-3 border-b border-border/30">
|
|
327
790
|
<div>
|
|
328
791
|
<h3 className="text-sm font-bold">SQL Job Archive Preservation</h3>
|
|
329
792
|
<p className="text-xs text-muted-foreground">
|
|
330
793
|
Keep archived jobs for a specific number of days before permanent removal.
|
|
331
794
|
</p>
|
|
332
795
|
</div>
|
|
333
|
-
<div className="flex items-center gap-
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
796
|
+
<div className="flex items-center gap-6">
|
|
797
|
+
<div className="flex items-center gap-3">
|
|
798
|
+
<span className="text-[10px] font-black uppercase text-muted-foreground/40">
|
|
799
|
+
Auto-Cleanup
|
|
800
|
+
</span>
|
|
801
|
+
<button
|
|
802
|
+
onClick={async () => {
|
|
803
|
+
const enabled = !alertConfig?.maintenance?.autoCleanup
|
|
804
|
+
await fetch('/api/maintenance/config', {
|
|
805
|
+
method: 'POST',
|
|
806
|
+
headers: { 'Content-Type': 'application/json' },
|
|
807
|
+
body: JSON.stringify({
|
|
808
|
+
...alertConfig.maintenance,
|
|
809
|
+
autoCleanup: enabled,
|
|
810
|
+
}),
|
|
811
|
+
})
|
|
812
|
+
queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
|
|
813
|
+
}}
|
|
814
|
+
className={cn(
|
|
815
|
+
'w-10 h-5 rounded-full p-1 transition-all flex items-center',
|
|
816
|
+
alertConfig?.maintenance?.autoCleanup ? 'bg-green-500 justify-end' : 'bg-muted justify-start'
|
|
817
|
+
)}
|
|
818
|
+
>
|
|
819
|
+
<div className="w-3 h-3 bg-white rounded-full shadow-sm" />
|
|
820
|
+
</button>
|
|
821
|
+
</div>
|
|
822
|
+
|
|
823
|
+
<div className="flex items-center gap-3">
|
|
824
|
+
<span className="text-[10px] font-black uppercase text-muted-foreground/40">
|
|
825
|
+
Retention Days
|
|
826
|
+
</span>
|
|
827
|
+
<select
|
|
828
|
+
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"
|
|
829
|
+
value={alertConfig?.maintenance?.retentionDays || 30}
|
|
830
|
+
onChange={async (e) => {
|
|
831
|
+
const days = parseInt(e.target.value, 10)
|
|
832
|
+
await fetch('/api/maintenance/config', {
|
|
833
|
+
method: 'POST',
|
|
834
|
+
headers: { 'Content-Type': 'application/json' },
|
|
835
|
+
body: JSON.stringify({
|
|
836
|
+
...alertConfig.maintenance,
|
|
837
|
+
retentionDays: days,
|
|
838
|
+
}),
|
|
839
|
+
})
|
|
840
|
+
queryClient.invalidateQueries({ queryKey: ['alerts-config'] })
|
|
841
|
+
}}
|
|
842
|
+
>
|
|
843
|
+
<option value="7">7 Days</option>
|
|
844
|
+
<option value="15">15 Days</option>
|
|
845
|
+
<option value="30">30 Days</option>
|
|
846
|
+
<option value="90">90 Days</option>
|
|
847
|
+
<option value="365">1 Year</option>
|
|
848
|
+
</select>
|
|
849
|
+
</div>
|
|
348
850
|
</div>
|
|
349
851
|
</div>
|
|
350
852
|
|
|
351
853
|
<div className="bg-red-500/5 border border-red-500/10 rounded-xl p-4 flex items-center justify-between">
|
|
352
854
|
<div className="flex items-center gap-3">
|
|
353
855
|
<Info size={16} className="text-red-500/60" />
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
|
|
856
|
+
<div className="flex flex-col">
|
|
857
|
+
<span className="text-xs font-medium text-red-900/60 dark:text-red-400/60">
|
|
858
|
+
Manual prune will remove all jobs older than the selected period.
|
|
859
|
+
</span>
|
|
860
|
+
{alertConfig?.maintenance?.lastRun && (
|
|
861
|
+
<span className="text-[10px] text-muted-foreground/60">
|
|
862
|
+
Last auto-cleanup run: {new Date(alertConfig.maintenance.lastRun).toLocaleString()}
|
|
863
|
+
</span>
|
|
864
|
+
)}
|
|
865
|
+
</div>
|
|
357
866
|
</div>
|
|
358
867
|
<button
|
|
359
868
|
type="button"
|
|
360
869
|
onClick={async () => {
|
|
361
|
-
const days =
|
|
362
|
-
.value
|
|
870
|
+
const days = alertConfig?.maintenance?.retentionDays || 30
|
|
363
871
|
if (confirm(`Are you sure you want to prune logs older than ${days} days?`)) {
|
|
364
872
|
const res = await fetch('/api/maintenance/cleanup-archive', {
|
|
365
873
|
method: 'POST',
|
|
366
874
|
headers: { 'Content-Type': 'application/json' },
|
|
367
|
-
body: JSON.stringify({ days
|
|
875
|
+
body: JSON.stringify({ days }),
|
|
368
876
|
}).then((r) => r.json())
|
|
369
877
|
alert(`Cleanup complete. Removed ${res.deleted || 0} archived jobs.`)
|
|
370
878
|
}
|
|
@@ -409,12 +917,12 @@ export function SettingsPage() {
|
|
|
409
917
|
<div className="space-y-4">
|
|
410
918
|
<div className="flex items-center justify-between py-3 border-b border-border/30">
|
|
411
919
|
<span className="font-medium">Version</span>
|
|
412
|
-
<span className="text-sm font-bold">0.1.0-alpha.1</span>
|
|
920
|
+
<span className="text-sm font-bold">{systemStatus?.version || '0.1.0-alpha.1'}</span>
|
|
413
921
|
</div>
|
|
414
922
|
<div className="flex items-center justify-between py-3 border-b border-border/30">
|
|
415
923
|
<span className="font-medium">Package</span>
|
|
416
924
|
<code className="text-sm bg-muted px-2 py-1 rounded font-mono">
|
|
417
|
-
@gravito/flux-console
|
|
925
|
+
{systemStatus?.package || '@gravito/flux-console'}
|
|
418
926
|
</code>
|
|
419
927
|
</div>
|
|
420
928
|
<div className="flex items-center justify-between py-3">
|