Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, Mail, MessageSquare, Terminal } from "lucide-react";
|
||||
import { SeverityBadge } from "@/components/ui/badge";
|
||||
import { useAlert, useMarkAlertRead, useResolveAlert } from "@/hooks/use-alerts";
|
||||
import { useCompany } from "@/hooks/use-companies";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import type { NotificationDeliveryStatus } from "@/lib/types";
|
||||
|
||||
const DELIVERY_STATUS_CLASSES: Record<NotificationDeliveryStatus, string> = {
|
||||
sent: "bg-green-100 text-green-800",
|
||||
failed: "bg-red-100 text-red-800",
|
||||
pending: "bg-slate-200 text-slate-700",
|
||||
};
|
||||
|
||||
const PROVIDER_ICON = {
|
||||
smtp: Mail,
|
||||
twilio_sms: MessageSquare,
|
||||
console: Terminal,
|
||||
} as const;
|
||||
|
||||
export default function AlertDetailPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const { data: alert, isLoading } = useAlert(params.id);
|
||||
const { data: company } = useCompany(alert?.company_id);
|
||||
const markRead = useMarkAlertRead();
|
||||
const resolve = useResolveAlert();
|
||||
|
||||
if (isLoading || !alert) {
|
||||
return <p className="text-sm text-slate-500">Loading…</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
href="/alerts"
|
||||
className="inline-flex items-center gap-1 text-sm font-medium text-slate-600 hover:text-slate-900"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" aria-hidden /> Back to alerts
|
||||
</Link>
|
||||
|
||||
<div className="mt-4 rounded-lg border border-slate-200 bg-white p-6">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<SeverityBadge severity={alert.severity} />
|
||||
{!alert.read && (
|
||||
<span className="rounded-full bg-brand-100 px-2 py-0.5 text-xs font-medium text-brand-700">
|
||||
Unread
|
||||
</span>
|
||||
)}
|
||||
{alert.resolved && (
|
||||
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-xs font-medium text-slate-700">
|
||||
Resolved
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-slate-500">
|
||||
{Math.round(alert.confidence * 100)}% confidence
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="mt-3 text-xl font-semibold text-slate-900">{alert.title}</h1>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{company ? (
|
||||
<Link href={`/companies/${company.id}`} className="hover:text-brand-600">
|
||||
{company.name}
|
||||
</Link>
|
||||
) : (
|
||||
"—"
|
||||
)}{" "}
|
||||
· {formatDateTime(alert.created_at)}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 grid gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-slate-700">What changed</h2>
|
||||
<p className="mt-1 text-sm text-slate-600">{alert.summary}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-slate-700">Why it matters</h2>
|
||||
<p className="mt-1 text-sm text-slate-600">{alert.why_it_matters}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex gap-2">
|
||||
{!alert.read && (
|
||||
<button
|
||||
onClick={() => markRead.mutate(alert.id)}
|
||||
className="focus-ring rounded-md border border-slate-300 px-3 py-1.5 text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
Mark read
|
||||
</button>
|
||||
)}
|
||||
{!alert.resolved && (
|
||||
<button
|
||||
onClick={() => resolve.mutate(alert.id)}
|
||||
className="focus-ring rounded-md bg-brand-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-brand-700"
|
||||
>
|
||||
Resolve
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-lg border border-slate-200 bg-white p-6">
|
||||
<h2 className="text-sm font-semibold text-slate-700">Notification deliveries</h2>
|
||||
{alert.deliveries.length === 0 ? (
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
No destinations were notified for this alert.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="mt-3 divide-y divide-slate-100">
|
||||
{alert.deliveries.map((delivery) => {
|
||||
const Icon =
|
||||
PROVIDER_ICON[delivery.provider as keyof typeof PROVIDER_ICON] ?? Terminal;
|
||||
return (
|
||||
<li key={delivery.id} className="flex items-center justify-between py-3 text-sm">
|
||||
<div className="flex items-center gap-2 text-slate-700">
|
||||
<Icon className="h-4 w-4 text-slate-400" aria-hidden />
|
||||
<span className="capitalize">{delivery.provider.replace("_", " ")}</span>
|
||||
{delivery.error_message && (
|
||||
<span className="text-xs text-red-600">{delivery.error_message}</span>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium capitalize ${DELIVERY_STATUS_CLASSES[delivery.status]}`}
|
||||
>
|
||||
{delivery.status}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user