Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function RedirectToSession() {

let cancelled = false;
const routeAfterInitCheck = async () => {
if (authRequired && user?.role !== 'admin') {
if (authRequired && user !== null && user?.role !== 'admin') {
if (!cancelled) setReady('chat');
return;
}
Expand Down
65 changes: 60 additions & 5 deletions frontend/src/pages/AlertDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
slack: 'Slack',
sns: 'SNS',
telegram: 'Telegram',
email: 'Email',
pagerduty: 'PagerDuty',
};

const CHANNEL_ICONS: Record<string, string> = {
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 (
<div className="flex items-center gap-2.5">
<span className="text-sm">{icon}</span>
<span className="text-[13px] text-gray-700 dark:text-[#CBD5E1] font-medium w-20">{label}</span>
{n.status === 'delivered'
? <CheckCircle2 size={13} className="text-emerald-500" />
: n.status === 'failed'
? <XCircle size={13} className="text-red-500" />
: <Radio size={13} className="text-gray-400 dark:text-[#64748B]" />
}
<span className={cn(
'text-[12px] font-medium',
n.status === 'delivered' ? 'text-emerald-600 dark:text-emerald-400' :
n.status === 'failed' ? 'text-red-500 dark:text-red-400' :
'text-gray-400 dark:text-[#64748B]'
)}>
{n.status}
</span>
{n.error && (
<span className="text-[11px] text-red-400 truncate max-w-[200px]" title={n.error}>{n.error}</span>
)}
</div>
);
}

export default function AlertDetailPage() {
const { alertId } = useParams<{ alertId: string }>();
Expand Down Expand Up @@ -73,9 +117,9 @@ export default function AlertDetailPage() {
</div>
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-[#71717A]">
<span className="flex items-center gap-1"><Clock size={12} /> {new Date(alert.timestamp).toLocaleString()}</span>
{alert.sns_sent && (
<span className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400">
<Send size={12} /> SNS notified
{alert.trigger_source && (
<span className="flex items-center gap-1.5 text-[12px] font-medium px-2 py-0.5 rounded-full bg-gray-100 dark:bg-[#27272A] text-gray-600 dark:text-[#94A3B8]">
{alert.trigger_source === 'poller' ? '⏱ Poller' : '⚡ Event consumer'}
</span>
)}
</div>
Expand All @@ -102,6 +146,17 @@ export default function AlertDetailPage() {
</section>
)}

{alert.notifications && alert.notifications.length > 0 && (
<section className="mb-6">
<h2 className="text-xs font-semibold text-gray-500 dark:text-[#71717A] uppercase tracking-wider mb-3">Notified via</h2>
<div className="bg-gray-50 dark:bg-[#18181B] border border-gray-200 dark:border-[#27272A] rounded-xl px-4 py-3 space-y-2.5">
{alert.notifications.map((n, i) => (
<NotificationRow key={i} n={n} />
))}
</div>
</section>
)}

</div>
</div>
);
Expand Down
60 changes: 51 additions & 9 deletions frontend/src/pages/InitPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -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 ? <Loader2 size={18} className="animate-spin" /> : <>Continue <ArrowRight size={16} /></>}
</button>
<button onClick={finishWithoutInfra} disabled={loading}
<button onClick={() => setShowSkipDisclaimer2(p => !p)} disabled={loading}
className="w-full mt-3 text-sm text-gray-400 dark:text-[#52525B] hover:text-gray-600 dark:hover:text-[#A1A1AA] transition-colors py-2">
Skip setup — configure later in Settings
</button>
{showSkipDisclaimer2 && (
<div className="mt-2 p-4 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20">
<p className="text-sm font-semibold text-amber-700 dark:text-amber-300 mb-2">Skipping this step disables event monitoring</p>
<ul className="space-y-1 mb-3">
{['Automatic incident detection will not run', 'Monitoring page will have no alerts', 'Slack / SNS notifications will not fire'].map(item => (
<li key={item} className="flex items-center gap-2 text-sm text-amber-700 dark:text-amber-400">
<XCircle size={12} className="shrink-0" /> {item}
</li>
))}
</ul>
<p className="text-xs text-amber-600 dark:text-amber-400 mb-3">
You can enable this anytime from <span className="font-medium">Settings → AWS Configuration</span>.{' '}
<a href="https://github.com/AhmadHammad21/OpenDevOps/blob/main/docs/monitoring.md"
target="_blank" rel="noopener noreferrer"
className="underline hover:text-amber-800 dark:hover:text-amber-200">
Learn more in monitoring.md
</a>
</p>
<button onClick={finishWithoutInfra} disabled={loading}
className="w-full text-sm font-medium text-gray-700 dark:text-[#D4D4D8] bg-white dark:bg-[#27272A] border border-gray-200 dark:border-[#3F3F46] rounded-lg py-2.5 hover:bg-gray-50 dark:hover:bg-[#3F3F46] transition-colors">
Go to dashboard anyway
</button>
</div>
)}
</>
)}

Expand Down Expand Up @@ -366,8 +391,14 @@ export default function InitPage() {

{!requiredPassed && (
<div className="mb-4 p-3.5 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 text-sm text-amber-700 dark:text-amber-300">
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{' '}
<span className="font-medium">Settings → AWS Configuration</span>.{' '}
<a href="https://github.com/AhmadHammad21/OpenDevOps/blob/main/docs/monitoring.md"
target="_blank" rel="noopener noreferrer"
className="underline hover:text-amber-800 dark:hover:text-amber-200">
monitoring.md
</a>
</div>
)}

Expand All @@ -378,10 +409,16 @@ export default function InitPage() {
Continue anyway <ChevronRight size={16} />
</button>

<button onClick={checkPerms} disabled={loading}
className="w-full mt-3 flex items-center justify-center gap-2 text-sm font-medium text-gray-500 dark:text-[#A1A1AA] hover:text-gray-700 dark:hover:text-white py-2 transition-colors">
{loading ? <Loader2 size={14} className="animate-spin" /> : 'Re-check'}
</button>
<div className="flex items-center justify-between mt-3">
<button onClick={() => setStep(2)}
className="flex items-center gap-1.5 text-sm font-medium text-gray-500 dark:text-[#A1A1AA] hover:text-gray-700 dark:hover:text-white py-2 transition-colors">
<ArrowLeft size={14} /> Back
</button>
<button onClick={checkPerms} disabled={loading}
className="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-[#A1A1AA] hover:text-gray-700 dark:hover:text-white py-2 transition-colors">
{loading ? <Loader2 size={14} className="animate-spin" /> : 'Re-check'}
</button>
</div>
</>
)}
</>
Expand Down Expand Up @@ -423,6 +460,11 @@ export default function InitPage() {
}
</button>

<button onClick={() => setStep(3)}
className="flex items-center gap-1.5 mt-3 text-sm font-medium text-gray-500 dark:text-[#A1A1AA] hover:text-gray-700 dark:hover:text-white py-1 transition-colors">
<ArrowLeft size={14} /> Back
</button>

{/* Skip option */}
<button onClick={() => setShowSkipDisclaimer(p => !p)}
className="w-full mt-3 text-sm text-gray-400 dark:text-[#52525B] hover:text-gray-600 dark:hover:text-[#A1A1AA] transition-colors py-2">
Expand Down
99 changes: 84 additions & 15 deletions frontend/src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -26,11 +26,11 @@ const SVC: Record<string, { label: string; desc: string }> = {
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]';
Expand All @@ -42,6 +42,10 @@ export default function SettingsPage() {
const [shown, setShown] = useState<Record<string, boolean>>({});
const [data, setData] = useState<SettingsData | null>(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('');
Expand All @@ -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(() => {});
}, []);

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -235,28 +257,75 @@ export default function SettingsPage() {

{/* Integrations tab */}
{tab === 'integrations' && (
<>
<div className="bg-white dark:bg-[#18181C] border border-gray-200 dark:border-[#27272F] rounded-lg overflow-hidden shadow-[0_1px_2px_rgba(0,0,0,0.04)] mb-4">
{INTEGRATIONS.map((intg, i) => (
<div key={i} className={`flex items-center gap-3 px-[18px] py-[14px] ${i < INTEGRATIONS.length - 1 ? 'border-b border-gray-200 dark:border-[#27272F]' : ''}`}>
<div className="space-y-4">

{/* Slack — live integration */}
<div className="bg-white dark:bg-[#18181C] border border-gray-200 dark:border-[#27272F] rounded-lg overflow-hidden shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
<div className="px-4 py-[11px] border-b border-gray-200 dark:border-[#27272F] bg-gray-50 dark:bg-[#1E1E24] flex items-center justify-between">
<span className="text-[11px] font-semibold text-gray-400 dark:text-[#64748B] uppercase tracking-[0.07em]">Slack</span>
<span className={cn(
'text-[10px] font-semibold px-1.5 py-0.5 rounded',
slackConfigured
? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10'
: 'text-gray-500 dark:text-[#64748B] bg-gray-100 dark:bg-[#27272F]'
)}>
{slackConfigured ? 'Configured' : 'Not configured'}
</span>
</div>

<div className="px-[18px] py-[14px] flex items-start gap-3">
<div className="w-[34px] h-[34px] bg-gray-100 dark:bg-[#27272F] rounded-lg flex items-center justify-center shrink-0 mt-0.5">
<span className="text-sm">💬</span>
</div>
<div className="flex-1 min-w-0">
<div className="text-[14px] font-medium text-gray-900 dark:text-[#F1F5F9] mb-0.5">Slack Notifications</div>
<div className="text-[13px] text-gray-500 dark:text-[#94A3B8] mb-3">
Sends a rich Block Kit message to your Slack channel after each investigation completes.
</div>
{!slackConfigured && (
<div className="text-[12px] text-gray-500 dark:text-[#94A3B8] bg-gray-50 dark:bg-[#1E1E24] border border-gray-200 dark:border-[#27272F] rounded-md px-3 py-2 font-mono">
Add <span className="text-indigo-500 dark:text-[#818CF8] font-semibold">SLACK_WEBHOOK_URL</span>=https://hooks.slack.com/… to your <span className="text-gray-700 dark:text-[#CBD5E1]">.env</span> file and restart
</div>
)}
{slackConfigured && (
<button
onClick={testSlack}
disabled={slackTesting}
className="flex items-center gap-1.5 text-[12px] font-medium px-3 py-[5px] bg-indigo-500 hover:bg-indigo-600 disabled:opacity-50 text-white rounded-[5px] transition-colors"
>
{slackTesting
? <><Loader2 size={11} className="animate-spin" /> Sending…</>
: <><Send size={11} /> Send test message</>
}
</button>
)}
</div>
</div>
</div>

{/* Coming soon integrations */}
<div className="bg-white dark:bg-[#18181C] border border-gray-200 dark:border-[#27272F] rounded-lg overflow-hidden shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
{COMING_SOON_INTEGRATIONS.map((intg, i) => (
<div key={i} className={`flex items-center gap-3 px-[18px] py-[14px] ${i < COMING_SOON_INTEGRATIONS.length - 1 ? 'border-b border-gray-200 dark:border-[#27272F]' : ''}`}>
<div className="w-[34px] h-[34px] bg-gray-100 dark:bg-[#27272F] rounded-lg flex items-center justify-center shrink-0">
<span className="text-sm">{intg.icon}</span>
</div>
<div className="flex-1">
<div className="text-[14px] font-medium text-gray-900 dark:text-[#F1F5F9]">{intg.name}</div>
<div className="text-[13px] text-gray-500 dark:text-[#94A3B8]">{intg.desc}</div>
</div>
<button className="text-[12px] font-medium text-gray-600 dark:text-[#94A3B8] bg-white dark:bg-[#18181C] hover:bg-gray-50 dark:hover:bg-[#27272F] border border-gray-300 dark:border-[#3F3F47] rounded-[5px] px-2.5 py-[5px] shadow-[0_1px_2px_rgba(0,0,0,0.04)] transition-colors">
Connect
</button>
<span className="text-[11px] font-medium text-gray-400 dark:text-[#64748B] bg-gray-100 dark:bg-[#27272F] px-2 py-[3px] rounded-full">
Coming soon
</span>
</div>
))}
</div>

<a href="https://openrouter.ai/models" target="_blank" rel="noreferrer"
className="inline-flex items-center gap-1.5 text-[13px] text-indigo-500 dark:text-[#818CF8] hover:text-indigo-600 dark:hover:text-[#6366F1] transition-colors">
Browse available models on OpenRouter <ExternalLink size={13} />
</a>
</>
</div>
)}

{/* AWS Configuration tab */}
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
Loading
Loading