From d9bfafb1af95b7270bd4b8e81a685d8f42c6ffc4 Mon Sep 17 00:00:00 2001 From: AhmadHammad21 Date: Mon, 18 May 2026 19:39:39 +0300 Subject: [PATCH 1/3] feat: omnichannel notification tracking and integrations UI - Add migration 011: trigger_source column on alerts, alert_notifications table - Record trigger_source ('poller'/'event_consumer') on every alert - Track per-channel delivery (slack, sns) in alert_notifications with delivered/failed status - Make slack _post() return bool; propagate through post_investigation/post_failed_investigation - notify_slack() returns bool for accurate delivery tracking - Add /api/integrations router with POST /slack/test endpoint - Integrations tab: Slack card with configured status + test button; GitHub/PagerDuty/Datadog/Telegram as coming soon - AlertDetailPage: trigger source badge + notified-via section with per-channel status - Fix Vite proxy missing /api/integrations entry --- frontend/src/pages/AlertDetailPage.tsx | 65 +++++++++++- frontend/src/pages/SettingsPage.tsx | 99 ++++++++++++++++--- frontend/src/types.ts | 9 ++ frontend/vite.config.ts | 5 +- .../011_trigger_source_notifications.sql | 14 +++ src/agent/db/base.py | 10 ++ src/agent/db/postgres.py | 46 +++++++-- src/agent/event_consumer.py | 18 +++- src/agent/monitor_store.py | 19 +++- src/agent/poller.py | 11 ++- src/agent/turns.py | 13 ++- src/api/app.py | 2 + src/api/routers/integrations.py | 44 +++++++++ src/integrations/slack_webhook.py | 25 +++-- 14 files changed, 329 insertions(+), 51 deletions(-) create mode 100644 migrations/011_trigger_source_notifications.sql create mode 100644 src/api/routers/integrations.py diff --git a/frontend/src/pages/AlertDetailPage.tsx b/frontend/src/pages/AlertDetailPage.tsx index 37df994..9e5f83d 100644 --- a/frontend/src/pages/AlertDetailPage.tsx +++ b/frontend/src/pages/AlertDetailPage.tsx @@ -1,9 +1,53 @@ import { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { ArrowLeft, Clock, Send, MessageSquare, Loader2 } from 'lucide-react'; +import { ArrowLeft, Clock, MessageSquare, Loader2, CheckCircle2, XCircle, Radio } from 'lucide-react'; import { cn } from '../lib/utils'; import { fetchAlert } from '../lib/api'; -import type { Alert } from '../types'; +import type { Alert, AlertNotification } from '../types'; + +const CHANNEL_LABELS: Record = { + slack: 'Slack', + sns: 'SNS', + telegram: 'Telegram', + email: 'Email', + pagerduty: 'PagerDuty', +}; + +const CHANNEL_ICONS: Record = { + slack: 'πŸ’¬', + sns: 'πŸ“‘', + telegram: '✈️', + email: 'πŸ“§', + pagerduty: 'πŸ“Ÿ', +}; + +function NotificationRow({ n }: { n: AlertNotification }) { + const label = CHANNEL_LABELS[n.channel] ?? n.channel; + const icon = CHANNEL_ICONS[n.channel] ?? 'πŸ””'; + return ( +
+ {icon} + {label} + {n.status === 'delivered' + ? + : n.status === 'failed' + ? + : + } + + {n.status} + + {n.error && ( + {n.error} + )} +
+ ); +} export default function AlertDetailPage() { const { alertId } = useParams<{ alertId: string }>(); @@ -73,9 +117,9 @@ export default function AlertDetailPage() {
{new Date(alert.timestamp).toLocaleString()} - {alert.sns_sent && ( - - SNS notified + {alert.trigger_source && ( + + {alert.trigger_source === 'poller' ? '⏱ Poller' : '⚑ Event consumer'} )}
@@ -102,6 +146,17 @@ export default function AlertDetailPage() { )} + {alert.notifications && alert.notifications.length > 0 && ( +
+

Notified via

+
+ {alert.notifications.map((n, i) => ( + + ))} +
+
+ )} + ); diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index c9ec16f..0730cdd 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { ExternalLink, Eye, EyeOff, Key, CheckCircle, XCircle, AlertTriangle, Loader2, Shield, Radio, Trash2, Zap } from 'lucide-react'; +import { ExternalLink, Eye, EyeOff, Key, CheckCircle, XCircle, AlertTriangle, Loader2, Shield, Radio, Trash2, Zap, Send } from 'lucide-react'; import { toast } from 'sonner'; import { apiFetch } from '../lib/api'; import { useAuth } from '../context/AuthContext'; @@ -26,11 +26,11 @@ const SVC: Record = { events: { label: 'EventBridge', desc: 'Event rules' }, }; -const INTEGRATIONS = [ - { name: 'GitHub', desc: 'Connect your repositories', icon: '🐱' }, - { name: 'Slack', desc: 'Get incident notifications', icon: 'πŸ’¬' }, - { name: 'PagerDuty', desc: 'Alert on failures', icon: 'πŸ“Ÿ' }, - { name: 'Datadog', desc: 'Send metrics and traces', icon: 'πŸ“Š' }, +const COMING_SOON_INTEGRATIONS = [ + { name: 'GitHub', desc: 'Connect your repositories', icon: '🐱' }, + { name: 'PagerDuty', desc: 'Alert on failures', icon: 'πŸ“Ÿ' }, + { name: 'Datadog', desc: 'Send metrics and traces', icon: 'πŸ“Š' }, + { name: 'Telegram', desc: 'Get alerts via Telegram bot', icon: '✈️' }, ]; const inputCls = 'w-full font-mono text-[12px] text-gray-700 dark:text-[#CBD5E1] bg-white dark:bg-[#0F0F12] border border-gray-200 dark:border-[#27272F] rounded-md px-3 py-1.5 outline-none focus:border-indigo-500 dark:focus:border-[#818CF8] focus:ring-1 focus:ring-indigo-500/20 dark:focus:ring-[#818CF8]/20 transition-all placeholder:text-gray-300 dark:placeholder:text-[#3F3F47]'; @@ -42,6 +42,10 @@ export default function SettingsPage() { const [shown, setShown] = useState>({}); const [data, setData] = useState(null); + // Integrations tab state + const [slackConfigured, setSlackConfigured] = useState(false); + const [slackTesting, setSlackTesting] = useState(false); + // AWS tab state const [snsArn, setSnsArn] = useState(''); const [sqsUrl, setSqsUrl] = useState(''); @@ -65,7 +69,12 @@ export default function SettingsPage() { useEffect(() => { apiFetch('/api/settings') .then(r => r.json()) - .then(d => setData(d as SettingsData)) + .then(d => { + const sd = d as SettingsData; + setData(sd); + const slackEnv = sd.env.find(e => e.key === 'SLACK_WEBHOOK_URL'); + setSlackConfigured(!!slackEnv && slackEnv.value !== '(not set)'); + }) .catch(() => {}); }, []); @@ -154,6 +163,19 @@ export default function SettingsPage() { } }; + const testSlack = async () => { + setSlackTesting(true); + try { + const r = await apiFetch('/api/integrations/slack/test', { method: 'POST' }); + if (!r.ok) throw new Error('Request failed'); + toast.success('Test message sent to Slack'); + } catch { + toast.error('Failed to send test message'); + } finally { + setSlackTesting(false); + } + }; + const passed = Object.values(perms).filter(r => r.passed).length; const total = Object.keys(perms).length; @@ -235,10 +257,56 @@ export default function SettingsPage() { {/* Integrations tab */} {tab === 'integrations' && ( - <> -
- {INTEGRATIONS.map((intg, i) => ( -
+
+ + {/* Slack β€” live integration */} +
+
+ Slack + + {slackConfigured ? 'Configured' : 'Not configured'} + +
+ +
+
+ πŸ’¬ +
+
+
Slack Notifications
+
+ Sends a rich Block Kit message to your Slack channel after each investigation completes. +
+ {!slackConfigured && ( +
+ Add SLACK_WEBHOOK_URL=https://hooks.slack.com/… to your .env file and restart +
+ )} + {slackConfigured && ( + + )} +
+
+
+ + {/* Coming soon integrations */} +
+ {COMING_SOON_INTEGRATIONS.map((intg, i) => ( +
{intg.icon}
@@ -246,17 +314,18 @@ export default function SettingsPage() {
{intg.name}
{intg.desc}
- + + Coming soon +
))}
+ Browse available models on OpenRouter - +
)} {/* AWS Configuration tab */} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 202ca3e..c7793cd 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -98,6 +98,13 @@ export interface MessageRecord { created_at: string; } +export interface AlertNotification { + channel: string; + status: 'delivered' | 'failed' | 'attempted'; + error: string | null; + sent_at: string; +} + export interface Alert { id: string; service: string; @@ -108,6 +115,8 @@ export interface Alert { sns_sent: boolean; timestamp: string; session_id?: string; + trigger_source?: 'poller' | 'event_consumer'; + notifications?: AlertNotification[]; } export interface ServiceStatus { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9e1cb1b..fc45b90 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -20,8 +20,9 @@ export default defineConfig({ '/api/settings': 'http://localhost:8000', '/api/history': 'http://localhost:8000', '/debug': 'http://localhost:8000', - '/api/monitoring': 'http://localhost:8000', - '/api/init': 'http://localhost:8000', + '/api/monitoring': 'http://localhost:8000', + '/api/init': 'http://localhost:8000', + '/api/integrations': 'http://localhost:8000', }, }, build: { diff --git a/migrations/011_trigger_source_notifications.sql b/migrations/011_trigger_source_notifications.sql new file mode 100644 index 0000000..63089d0 --- /dev/null +++ b/migrations/011_trigger_source_notifications.sql @@ -0,0 +1,14 @@ +-- Track which detection path triggered each alert +ALTER TABLE alerts ADD COLUMN IF NOT EXISTS trigger_source TEXT; -- 'poller' | 'event_consumer' + +-- Per-alert, per-channel delivery log +CREATE TABLE IF NOT EXISTS alert_notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + alert_id UUID NOT NULL REFERENCES alerts(id) ON DELETE CASCADE, + channel TEXT NOT NULL, -- 'slack' | 'sns' | 'telegram' | ... + status TEXT NOT NULL DEFAULT 'attempted', -- 'delivered' | 'failed' | 'attempted' + error TEXT, + sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_alert_notifications_alert_id ON alert_notifications (alert_id); diff --git a/src/agent/db/base.py b/src/agent/db/base.py index 40620ef..be79daf 100644 --- a/src/agent/db/base.py +++ b/src/agent/db/base.py @@ -140,9 +140,19 @@ async def add_alert( dedup_key: str | None = None, status: str = "completed", session_id: str | None = None, + trigger_source: str | None = None, ) -> str: return "" + async def add_notification( + self, + alert_id: str, + channel: str, + status: str = "attempted", + error: str | None = None, + ) -> None: + pass + async def is_recent_alert(self, dedup_key: str, within_minutes: int = 3) -> bool: return False diff --git a/src/agent/db/postgres.py b/src/agent/db/postgres.py index bc0c8ca..31b0ac9 100644 --- a/src/agent/db/postgres.py +++ b/src/agent/db/postgres.py @@ -577,16 +577,32 @@ async def add_alert( dedup_key: str | None = None, status: str = "completed", session_id: str | None = None, + trigger_source: str | None = None, ) -> str: row = await self._fetchrow( "INSERT INTO alerts" - " (service, error, resolution, confidence, sns_sent, dedup_key, status, session_id)" - " VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING id", + " (service, error, resolution, confidence, sns_sent, dedup_key, status," + " session_id, trigger_source)" + " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id", service, error, resolution, confidence, sns_sent, dedup_key, status, uuid.UUID(session_id) if session_id else None, + trigger_source, ) return str(row["id"]) if row else "" + async def add_notification( + self, + alert_id: str, + channel: str, + status: str = "attempted", + error: str | None = None, + ) -> None: + await self._exec( + "INSERT INTO alert_notifications (alert_id, channel, status, error)" + " VALUES (%s, %s, %s, %s)", + uuid.UUID(alert_id), channel, status, error, + ) + async def is_recent_alert(self, dedup_key: str, within_minutes: int = 3) -> bool: row = await self._fetchrow( "SELECT 1 FROM alerts WHERE dedup_key = %s" @@ -597,8 +613,9 @@ async def is_recent_alert(self, dedup_key: str, within_minutes: int = 3) -> bool async def get_alerts(self, limit: int = 50) -> list[dict]: rows = await self._fetchall( - "SELECT id, service, error, resolution, confidence, sns_sent, status, created_at, session_id " - "FROM alerts ORDER BY created_at DESC LIMIT %s", + "SELECT id, service, error, resolution, confidence, sns_sent, status," + " created_at, session_id, trigger_source" + " FROM alerts ORDER BY created_at DESC LIMIT %s", min(limit, 200), ) return [ @@ -612,18 +629,25 @@ async def get_alerts(self, limit: int = 50) -> list[dict]: "status": r["status"], "timestamp": r["created_at"].isoformat() if r["created_at"] else None, "session_id": str(r["session_id"]) if r["session_id"] else None, + "trigger_source": r["trigger_source"], } for r in rows ] async def get_alert(self, alert_id: str) -> dict | None: row = await self._fetchrow( - "SELECT id, service, error, resolution, confidence, sns_sent, status, created_at, session_id " - "FROM alerts WHERE id = %s", + "SELECT id, service, error, resolution, confidence, sns_sent, status," + " created_at, session_id, trigger_source" + " FROM alerts WHERE id = %s", uuid.UUID(alert_id), ) if not row: return None + notif_rows = await self._fetchall( + "SELECT channel, status, error, sent_at FROM alert_notifications" + " WHERE alert_id = %s ORDER BY sent_at ASC", + uuid.UUID(alert_id), + ) return { "id": str(row["id"]), "service": row["service"], @@ -634,6 +658,16 @@ async def get_alert(self, alert_id: str) -> dict | None: "status": row["status"], "timestamp": row["created_at"].isoformat() if row["created_at"] else None, "session_id": str(row["session_id"]) if row["session_id"] else None, + "trigger_source": row["trigger_source"], + "notifications": [ + { + "channel": n["channel"], + "status": n["status"], + "error": n["error"], + "sent_at": n["sent_at"].isoformat() if n["sent_at"] else None, + } + for n in notif_rows + ], } async def get_app_config(self, key: str) -> dict | None: diff --git a/src/agent/event_consumer.py b/src/agent/event_consumer.py index a00c243..e419b21 100644 --- a/src/agent/event_consumer.py +++ b/src/agent/event_consumer.py @@ -16,7 +16,7 @@ get_runtime_sns_topic_arn, get_runtime_sqs_queue_url, ) -from agent.monitor_store import add_alert, is_recent_alert, update_service +from agent.monitor_store import add_alert, add_notification, is_recent_alert, update_service from config import settings # In-memory set of dedup keys currently being investigated. @@ -286,7 +286,9 @@ async def _deliver(result: dict[str, Any], event: dict, dedup_key: str, session_ sns_arn = get_runtime_sns_topic_arn() sns_sent = False + sns_attempted = False if sns_arn: + sns_attempted = True try: from tools.sns import publish_sns_alert @@ -314,25 +316,25 @@ async def _deliver(result: dict[str, Any], event: dict, dedup_key: str, session_ except Exception as e: logger.error("SNS delivery failed: {}", e) - # Also notify Slack (keeps parity with the poller) + slack_sent = False if settings.slack_webhook_url: try: from integrations.slack_webhook import post_failed_investigation, post_investigation if status == "failed": - await post_failed_investigation( + slack_sent = await post_failed_investigation( settings.slack_webhook_url, service, root_cause, session_id, aws_error=aws_error, is_test=is_test, ) else: - await post_investigation( + slack_sent = await post_investigation( settings.slack_webhook_url, result, session_id, is_test=is_test ) except Exception as e: logger.error("Slack delivery failed from event consumer: {}", e) update_service(service, status, root_cause) - await add_alert( + alert_id = await add_alert( service=service, error=root_cause, resolution=resolution, @@ -341,7 +343,13 @@ async def _deliver(result: dict[str, Any], event: dict, dedup_key: str, session_ dedup_key=dedup_key, status=status, session_id=session_id, + trigger_source="event_consumer", ) + if alert_id: + if sns_attempted: + await add_notification(alert_id, "sns", "delivered" if sns_sent else "failed") + if settings.slack_webhook_url: + await add_notification(alert_id, "slack", "delivered" if slack_sent else "failed") logger.info("Alert delivered: service={} confidence={} sns={}", service, confidence, sns_sent) diff --git a/src/agent/monitor_store.py b/src/agent/monitor_store.py index 09d934a..9ddc2c6 100644 --- a/src/agent/monitor_store.py +++ b/src/agent/monitor_store.py @@ -19,19 +19,36 @@ async def add_alert( dedup_key: str | None = None, status: str = "completed", session_id: str | None = None, + trigger_source: str | None = None, ) -> str: """Persist an alert to the DB backend.""" from agent.db import db try: return await db.add_alert( - service, error, resolution, confidence, sns_sent, dedup_key, status, session_id + service, error, resolution, confidence, sns_sent, + dedup_key, status, session_id, trigger_source, ) except Exception as e: logger.error("Failed to persist alert: {}", e) return "" +async def add_notification( + alert_id: str, + channel: str, + status: str = "attempted", + error: str | None = None, +) -> None: + """Record a delivery attempt for a channel against an alert.""" + from agent.db import db + + try: + await db.add_notification(alert_id, channel, status, error) + except Exception as e: + logger.error("Failed to persist notification: {}", e) + + async def is_recent_alert(dedup_key: str, within_minutes: int = 3) -> bool: """Return True if an alert with this dedup_key was saved within the last N minutes.""" from agent.db import db diff --git a/src/agent/poller.py b/src/agent/poller.py index 9ca0362..3ae69a1 100644 --- a/src/agent/poller.py +++ b/src/agent/poller.py @@ -164,10 +164,10 @@ def _f(obj: Any, key: str, default: Any = None) -> Any: async def _persist_and_notify( result: dict[str, Any], tool_calls_log: list[dict[str, Any]], session_id: str, dedup_key: str ) -> None: - from agent.monitor_store import add_alert, update_service + from agent.monitor_store import add_alert, add_notification, update_service from agent.turns import notify_slack - await notify_slack(session_id, tool_calls_log) + slack_sent = await notify_slack(session_id, tool_calls_log) status = result.pop("_status", "completed") services_affected = result.get("services_affected", []) @@ -177,7 +177,7 @@ async def _persist_and_notify( confidence = result.get("confidence", "MEDIUM") resolution = "\n".join(mitigation) if isinstance(mitigation, list) else str(mitigation) update_service(service, status, root_cause) - await add_alert( + alert_id = await add_alert( service=service, error=root_cause, resolution=resolution, @@ -186,7 +186,12 @@ async def _persist_and_notify( dedup_key=dedup_key, status=status, session_id=session_id, + trigger_source="poller", ) + if alert_id and settings.slack_webhook_url: + await add_notification( + alert_id, "slack", "delivered" if slack_sent else "failed" + ) async def _check_alarms() -> None: diff --git a/src/agent/turns.py b/src/agent/turns.py index f440d7f..641c315 100644 --- a/src/agent/turns.py +++ b/src/agent/turns.py @@ -87,13 +87,16 @@ async def save_turn( logger.error("[{}] DB save failed: {}", session_id[:8], e) -async def notify_slack(session_id: str, tool_calls_log: list[dict[str, Any]]) -> None: - """Post to Slack if submit_investigation was called and a webhook is configured.""" +async def notify_slack(session_id: str, tool_calls_log: list[dict[str, Any]]) -> bool: + """Post to Slack if submit_investigation was called and a webhook is configured. + + Returns True if the message was sent successfully, False otherwise. + """ if not settings.slack_webhook_url: - return + return False for tc in tool_calls_log: if tc.get("tool") == "submit_investigation": from integrations.slack_webhook import post_investigation - await post_investigation(settings.slack_webhook_url, tc["args"], session_id) - return + return await post_investigation(settings.slack_webhook_url, tc["args"], session_id) + return False diff --git a/src/api/app.py b/src/api/app.py index b39ebc5..3cee542 100644 --- a/src/api/app.py +++ b/src/api/app.py @@ -20,6 +20,7 @@ chat, dashboard, history, + integrations, monitoring, sessions, settings, @@ -143,6 +144,7 @@ async def lifespan(_app: FastAPI): app.include_router(auth.router) app.include_router(users.router) app.include_router(settings.router) +app.include_router(integrations.router) app.include_router(init_router.router) app.include_router(monitoring.router) diff --git a/src/api/routers/integrations.py b/src/api/routers/integrations.py new file mode 100644 index 0000000..059ce41 --- /dev/null +++ b/src/api/routers/integrations.py @@ -0,0 +1,44 @@ +"""Integrations router β€” manage third-party integration actions (e.g. Slack test).""" + +from __future__ import annotations + +import uuid +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException + +from api.auth import get_current_user +from config import settings + +router = APIRouter(prefix="/api/integrations", tags=["integrations"]) + + +@router.post("/slack/test") +async def test_slack( + _user: Annotated[dict | None, Depends(get_current_user)], +) -> dict: + """Send a test Slack message to verify the webhook is working.""" + if not settings.slack_webhook_url: + raise HTTPException(status_code=400, detail="SLACK_WEBHOOK_URL is not configured") + + from integrations.slack_webhook import post_investigation + + test_result = { + "root_cause_category": "SYSTEM_CHANGE", + "root_cause_summary": ( + "This is a test message from OpenDevOps Agent " + "to verify your Slack integration is working correctly." + ), + "confidence": "HIGH", + "evidence": ["Test triggered from Settings β†’ Integrations"], + "mitigation_steps": ["No action needed β€” this is a test"], + "services_affected": ["OpenDevOps Agent"], + } + session_id = str(uuid.uuid4()) + await post_investigation( + webhook_url=settings.slack_webhook_url, + result=test_result, + session_id=session_id, + is_test=True, + ) + return {"ok": True} diff --git a/src/integrations/slack_webhook.py b/src/integrations/slack_webhook.py index 511120b..819a335 100644 --- a/src/integrations/slack_webhook.py +++ b/src/integrations/slack_webhook.py @@ -98,15 +98,18 @@ def _bullets(items: list[str]) -> str: } -async def _post(webhook_url: str, payload: dict) -> None: +async def _post(webhook_url: str, payload: dict) -> bool: try: import httpx async with httpx.AsyncClient(timeout=10) as client: resp = await client.post(webhook_url, json=payload) if resp.status_code != 200: logger.warning("Slack webhook returned {}: {}", resp.status_code, resp.text) + return False + return True except Exception as e: logger.error("Slack notification failed: {}", e) + return False async def post_investigation( @@ -115,11 +118,13 @@ async def post_investigation( session_id: str, app_url: str | None = None, is_test: bool = False, -) -> None: - """Post a completed investigation result to Slack.""" +) -> bool: + """Post a completed investigation result to Slack. Returns True on success.""" payload = _build_payload(result, session_id, app_url, is_test=is_test) - await _post(webhook_url, payload) - logger.info("Slack notification sent for session {}", session_id[:8]) + ok = await _post(webhook_url, payload) + if ok: + logger.info("Slack notification sent for session {}", session_id[:8]) + return ok async def post_failed_investigation( @@ -129,8 +134,8 @@ async def post_failed_investigation( session_id: str, aws_error: str = "", is_test: bool = False, -) -> None: - """Post a failed investigation alert to Slack.""" +) -> bool: + """Post a failed investigation alert to Slack. Returns True on success.""" header_text = ( "πŸ§ͺ [TEST] OpenDevOps Agent β€” Investigation Failed" if is_test @@ -151,5 +156,7 @@ async def post_failed_investigation( }, ] payload = {"attachments": [{"color": "#e74c3c", "blocks": blocks}]} - await _post(webhook_url, payload) - logger.info("Slack failure notification sent for session {}", session_id[:8]) + ok = await _post(webhook_url, payload) + if ok: + logger.info("Slack failure notification sent for session {}", session_id[:8]) + return ok From 1c7c29af6ac296c6eb06230b36d69715946e8b09 Mon Sep 17 00:00:00 2001 From: AhmadHammad21 Date: Mon, 18 May 2026 20:31:44 +0300 Subject: [PATCH 2/3] feat: poller improvements and init wizard UX - Poller: fix failed investigations sending no Slack notification by calling post_failed_investigation directly when status is 'failed' (matches event consumer) - Poller: use poll interval as Lambda error rate window so only errors since last tick trigger new investigations (prevents stale alerts re-firing) - Poller/event consumer: add debug logs for alarm counts, Lambda counts, investigation start (with truncated session id) and completion with confidence - Init wizard: add skip disclaimers to steps 2 and 4 (amber warning + monitoring.md link + "Go to dashboard anyway" confirmation) so users understand what they're skipping - Init wizard: add back buttons to steps 3 and 4 so users can revise earlier answers --- frontend/src/pages/InitPage.tsx | 60 ++++++++++++++++++++++++++++----- src/agent/event_consumer.py | 1 + src/agent/poller.py | 35 ++++++++++++++----- 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/frontend/src/pages/InitPage.tsx b/frontend/src/pages/InitPage.tsx index e8aec13..b596780 100644 --- a/frontend/src/pages/InitPage.tsx +++ b/frontend/src/pages/InitPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ArrowRight, Loader2, Terminal, Eye, EyeOff, CheckCircle, XCircle, AlertTriangle, ChevronRight, Check } from 'lucide-react'; +import { ArrowRight, ArrowLeft, Loader2, Terminal, Eye, EyeOff, CheckCircle, XCircle, AlertTriangle, ChevronRight, Check } from 'lucide-react'; import { cn } from '../lib/utils'; import { useAuth } from '../context/AuthContext'; import { apiFetch } from '../lib/api'; @@ -65,7 +65,8 @@ export default function InitPage() { // Step 4 const [infraDone, setInfraDone] = useState(false); const [queueUrl, setQueueUrl] = useState(''); - const [showSkipDisclaimer, setShowSkipDisclaimer] = useState(false); + const [showSkipDisclaimer, setShowSkipDisclaimer] = useState(false); + const [showSkipDisclaimer2, setShowSkipDisclaimer2] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); @@ -298,10 +299,34 @@ export default function InitPage() { className="w-full mt-10 flex items-center justify-center gap-2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 text-base font-semibold py-4 rounded-xl hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors disabled:opacity-50"> {loading ? : <>Continue } - + {showSkipDisclaimer2 && ( +
+

Skipping this step disables event monitoring

+
    + {['Automatic incident detection will not run', 'Monitoring page will have no alerts', 'Slack / SNS notifications will not fire'].map(item => ( +
  • + {item} +
  • + ))} +
+

+ You can enable this anytime from Settings β†’ AWS Configuration.{' '} + + Learn more in monitoring.md + +

+ +
+ )} )} @@ -366,8 +391,14 @@ export default function InitPage() { {!requiredPassed && (
- Some required permissions are missing. Event monitoring may not work correctly. - You can still continue β€” fix the IAM permissions and re-check in Settings. + Some required permissions are missing. Event monitoring will not work until these are fixed. + You can still continue and fix IAM permissions later β€” re-check in{' '} + Settings β†’ AWS Configuration.{' '} + + monitoring.md +
)} @@ -378,10 +409,16 @@ export default function InitPage() { Continue anyway - +
+ + +
)} @@ -423,6 +460,11 @@ export default function InitPage() { } + + {/* Skip option */} -
+