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:
2026-08-05 10:48:20 -04:00
commit 1a4c80958f
365 changed files with 43541 additions and 0 deletions
+138
View File
@@ -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>
);
}
+152
View File
@@ -0,0 +1,152 @@
"use client";
import Link from "next/link";
import { useMemo, useState } from "react";
import { AlertTriangle, Check, CheckCheck } from "lucide-react";
import { SeverityBadge } from "@/components/ui/badge";
import { Select } from "@/components/ui/select";
import { useAlerts, useMarkAlertRead, useResolveAlert } from "@/hooks/use-alerts";
import { useCompanies } from "@/hooks/use-companies";
import { formatRelative } from "@/lib/format";
import { SEVERITY_LABELS, SEVERITY_LEVELS, type SeverityLevel } from "@/lib/types";
export default function AlertsPage() {
const [companyId, setCompanyId] = useState<string>("");
const [severity, setSeverity] = useState<SeverityLevel | "">("");
const [unreadOnly, setUnreadOnly] = useState(false);
const [showResolved, setShowResolved] = useState(false);
const { data: companies } = useCompanies();
const { data: alerts, isLoading } = useAlerts({
company_id: companyId || undefined,
severity: severity || undefined,
read: unreadOnly ? false : undefined,
resolved: showResolved ? undefined : false,
});
const markRead = useMarkAlertRead();
const resolve = useResolveAlert();
const companyNameById = useMemo(
() => new Map((companies ?? []).map((c) => [c.id, c.name])),
[companies],
);
const alertList = alerts ?? [];
return (
<div>
<div>
<h1 className="text-2xl font-semibold text-slate-900">Alerts</h1>
<p className="mt-1 text-sm text-slate-600">
Meaningful changes detected across your monitored companies.
</p>
</div>
<div className="mt-6 flex flex-wrap items-center gap-3 rounded-lg border border-slate-200 bg-white p-4">
<Select
value={companyId}
onValueChange={setCompanyId}
placeholder="All companies"
ariaLabel="Filter by company"
options={[
{ value: "", label: "All companies" },
...(companies ?? []).map((c) => ({ value: c.id, label: c.name })),
]}
triggerClassName="px-3 py-1.5 text-sm"
/>
<Select
value={severity}
onValueChange={(v) => setSeverity(v as SeverityLevel | "")}
placeholder="All severities"
ariaLabel="Filter by severity"
options={[
{ value: "", label: "All severities" },
...SEVERITY_LEVELS.map((s) => ({ value: s, label: SEVERITY_LABELS[s] })),
]}
triggerClassName="px-3 py-1.5 text-sm"
/>
<label className="flex items-center gap-2 text-sm text-slate-600">
<input
type="checkbox"
checked={unreadOnly}
onChange={(e) => setUnreadOnly(e.target.checked)}
className="focus-ring rounded border-slate-300"
/>
Unread only
</label>
<label className="flex items-center gap-2 text-sm text-slate-600">
<input
type="checkbox"
checked={showResolved}
onChange={(e) => setShowResolved(e.target.checked)}
className="focus-ring rounded border-slate-300"
/>
Show resolved
</label>
</div>
{isLoading ? (
<p className="mt-8 text-sm text-slate-500">Loading</p>
) : alertList.length === 0 ? (
<div className="mt-8 rounded-lg border border-dashed border-slate-300 bg-white p-10 text-center">
<AlertTriangle className="mx-auto h-8 w-8 text-slate-400" aria-hidden />
<p className="mt-3 text-sm text-slate-600">No alerts match these filters.</p>
</div>
) : (
<ul className="mt-6 divide-y divide-slate-100 rounded-lg border border-slate-200 bg-white">
{alertList.map((alert) => (
<li key={alert.id} className="flex items-start justify-between gap-4 px-4 py-4">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<SeverityBadge severity={alert.severity} />
{!alert.read && (
<span className="h-1.5 w-1.5 rounded-full bg-brand-600" aria-label="Unread" />
)}
{alert.resolved && (
<span className="text-xs font-medium text-slate-400">Resolved</span>
)}
</div>
<Link
href={`/alerts/${alert.id}`}
className="mt-1 block truncate font-medium text-slate-900 hover:text-brand-600"
>
{alert.title}
</Link>
<p className="mt-0.5 text-sm text-slate-600">
{companyNameById.get(alert.company_id) ?? "Unknown company"} ·{" "}
{formatRelative(alert.created_at)} · {Math.round(alert.confidence * 100)}%
confidence
</p>
</div>
<div className="flex shrink-0 items-center gap-1">
{!alert.read && (
<button
title="Mark read"
onClick={() => markRead.mutate(alert.id)}
className="focus-ring rounded-md p-2 text-slate-500 hover:bg-slate-100"
>
<Check className="h-4 w-4" aria-hidden />
<span className="sr-only">Mark read</span>
</button>
)}
{!alert.resolved && (
<button
title="Resolve"
onClick={() => resolve.mutate(alert.id)}
className="focus-ring rounded-md p-2 text-slate-500 hover:bg-slate-100"
>
<CheckCheck className="h-4 w-4" aria-hidden />
<span className="sr-only">Resolve</span>
</button>
)}
</div>
</li>
))}
</ul>
)}
</div>
);
}