diff --git a/backend/cookies.txt b/backend/cookies.txt new file mode 100644 index 00000000..c31d9899 --- /dev/null +++ b/backend/cookies.txt @@ -0,0 +1,4 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + diff --git a/backend/package.json b/backend/package.json index 2001fa52..e2795079 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "tsc", "start": "node dist/index.js", - "dev": "nodemon --exec ts-node src/index.ts", + "dev": "nodemon --watch src --ext ts,json --exec ts-node src/index.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], @@ -42,4 +42,4 @@ "ws": "^8.19.0", "yaml": "^2.8.2" } -} +} \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index 27e67a87..1d5cefdf 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -663,6 +663,15 @@ app.get('/api/system/stats', async (req: Request, res: Response) => { // --- Notification & Alerting Routes --- +app.get('/api/stacks', async (req: Request, res: Response) => { + try { + const stacks = await fileSystemService.getStacks(); + res.json(stacks); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch stacks' }); + } +}); + app.get('/api/agents', async (req: Request, res: Response) => { try { const agents = DatabaseService.getInstance().getAgents(); diff --git a/frontend/src/components/NotificationSettingsModal.tsx b/frontend/src/components/NotificationSettingsModal.tsx index 2964c7f7..a8513e71 100644 --- a/frontend/src/components/NotificationSettingsModal.tsx +++ b/frontend/src/components/NotificationSettingsModal.tsx @@ -14,6 +14,18 @@ import { Label } from '@/components/ui/label'; import { Slider } from '@/components/ui/slider'; import { toast } from 'sonner'; import { apiFetch } from '@/lib/api'; +import { Trash2 } from 'lucide-react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; + +interface StackAlert { + id?: number; + stack_name: string; + metric: string; + operator: string; + threshold: number; + duration_mins: number; + cooldown_mins: number; +} interface Agent { type: 'discord' | 'slack' | 'webhook'; @@ -43,10 +55,31 @@ export function NotificationSettingsModal({ isOpen, onClose }: NotificationSetti const [isLoading, setIsLoading] = useState(false); + // Central Alerts State + const [alerts, setAlerts] = useState([]); + const [stacks, setStacks] = useState([]); + const [newAlertStack, setNewAlertStack] = useState(''); + const [newAlertMetric, setNewAlertMetric] = useState('cpu_percent'); + const [newAlertOperator, setNewAlertOperator] = useState('>'); + const [newAlertThreshold, setNewAlertThreshold] = useState(''); + const [newAlertDuration, setNewAlertDuration] = useState('5'); + const [newAlertCooldown, setNewAlertCooldown] = useState('60'); + + const metricLabels: Record = { + cpu_percent: 'CPU Usage (%)', + memory_percent: 'Memory Usage (%)', + memory_mb: 'Memory Usage (MB)', + net_rx: 'Network In (MB)', + net_tx: 'Network Out (MB)', + restart_count: 'Restart Count' + }; + useEffect(() => { if (isOpen) { fetchAgents(); fetchSettings(); + fetchAlerts(); + fetchStacks(); } }, [isOpen]); @@ -78,6 +111,33 @@ export function NotificationSettingsModal({ isOpen, onClose }: NotificationSetti } }; + const fetchAlerts = async () => { + try { + const res = await apiFetch('/alerts'); + if (res.ok) { + const data = await res.json(); + setAlerts(data); + } + } catch (e) { + console.error('Failed to fetch alerts', e); + } + }; + + const fetchStacks = async () => { + try { + const res = await apiFetch('/stacks'); + if (res.ok) { + const data = await res.json(); + setStacks(data); + if (data.length > 0 && !newAlertStack) { + setNewAlertStack(data[0]); + } + } + } catch (e) { + console.error('Failed to fetch stacks', e); + } + }; + const handleAgentChange = (type: string, field: keyof Agent, value: any) => { setAgents(prev => ({ ...prev, @@ -149,6 +209,62 @@ export function NotificationSettingsModal({ isOpen, onClose }: NotificationSetti } }; + const addAlert = async () => { + if (!newAlertStack) { + toast.error('Please select a stack.'); + return; + } + if (!newAlertThreshold) { + toast.error('Please enter a threshold.'); + return; + } + + setIsLoading(true); + const newAlert = { + stack_name: newAlertStack, + metric: newAlertMetric, + operator: newAlertOperator, + threshold: parseFloat(newAlertThreshold), + duration_mins: parseInt(newAlertDuration, 10), + cooldown_mins: parseInt(newAlertCooldown, 10) + }; + + try { + const res = await apiFetch('/alerts', { + method: 'POST', + body: JSON.stringify(newAlert) + }); + if (res.ok) { + toast.success('Alert rule added.'); + setNewAlertThreshold(''); + fetchAlerts(); + } else { + toast.error('Failed to add alert rule.'); + } + } catch (e) { + toast.error('Network error.'); + } finally { + setIsLoading(false); + } + }; + + const deleteAlert = async (id: number) => { + setIsLoading(true); + try { + const res = await apiFetch(`/alerts/${id}`, { method: 'DELETE' }); + if (res.ok) { + toast.success('Alert rule deleted.'); + fetchAlerts(); + } else { + toast.error('Failed to delete alert rule.'); + } + } catch (e) { + toast.error('Network error.'); + } finally { + setIsLoading(false); + } + }; + const renderAgentTab = (type: 'discord' | 'slack' | 'webhook', title: string) => (
@@ -186,11 +302,12 @@ export function NotificationSettingsModal({ isOpen, onClose }: NotificationSetti - + Global Discord Slack Webhook + Alert Rules @@ -258,6 +375,128 @@ export function NotificationSettingsModal({ isOpen, onClose }: NotificationSetti {renderAgentTab('discord', 'Discord')} {renderAgentTab('slack', 'Slack')} {renderAgentTab('webhook', 'Custom Webhook')} + + +
+
+

Existing Rules

+ {alerts.length === 0 ? ( +
+ No active alert rules across all stacks. +
+ ) : ( + alerts.map(alert => ( +
+
+
+ + [{alert.stack_name}] {metricLabels[alert.metric] || alert.metric} {alert.operator} {alert.threshold} + +
+ Trigger after {alert.duration_mins}m • Cooldown: {alert.cooldown_mins}m +
+
+ +
+
+ )) + )} +
+ +
+ +
+

Add New Rule

+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + setNewAlertThreshold(e.target.value)} + placeholder="e.g. 90" + /> +
+
+ +
+
+ + setNewAlertDuration(e.target.value)} + /> +
+
+ + setNewAlertCooldown(e.target.value)} + /> +
+
+ + +
+
+