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
+147
View File
@@ -0,0 +1,147 @@
"use client";
import { AlertTriangle, Building2, Clock, FileText, Plus, Radio, ServerCog } from "lucide-react";
import Link from "next/link";
import { useCurrentUser, useSystemStatus } from "@/hooks/use-auth";
import { useCompanies } from "@/hooks/use-companies";
import { useDashboardAnalytics } from "@/hooks/use-analytics";
import { StatCard } from "@/components/ui/stat-card";
import { DashboardAnalyticsSection } from "@/components/dashboard/analytics-section";
import { formatDateTime } from "@/lib/format";
import type { CompanyResponse } from "@/lib/types";
function mostRecentLastRun(companies: CompanyResponse[]): string | null {
const runs = companies
.map((c) => c.monitor_configuration?.last_run)
.filter((v): v is string => Boolean(v));
if (runs.length === 0) return null;
return runs.sort().at(-1) ?? null;
}
function nextUpcomingRun(companies: CompanyResponse[]): string | null {
const runs = companies
.filter((c) => c.monitor_configuration?.enabled)
.map((c) => c.monitor_configuration?.next_run)
.filter((v): v is string => Boolean(v));
if (runs.length === 0) return null;
return runs.sort().at(0) ?? null;
}
export default function DashboardPage() {
const { data: user } = useCurrentUser();
const { data: systemStatus } = useSystemStatus();
const { data: companies, isLoading } = useCompanies();
const { data: analytics } = useDashboardAnalytics();
const companyList = companies ?? [];
const activeCount = companyList.filter((c) => c.status === "active").length;
const reportCount = companyList.reduce((sum, c) => sum + c.report_count, 0);
const unreadAlertCount = companyList.reduce((sum, c) => sum + c.unresolved_alert_count, 0);
return (
<div>
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold text-slate-900">
Welcome{user ? `, ${user.display_name}` : ""}
</h1>
<p className="mt-1 text-sm text-slate-600">
Here&apos;s what&apos;s happening across your monitors.
</p>
</div>
<Link
href="/companies/new"
className="focus-ring inline-flex items-center gap-2 rounded-md bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700 active:scale-95"
>
<Plus className="h-4 w-4" aria-hidden /> Add company
</Link>
</div>
<div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard label="Monitored companies" value={companyList.length} icon={Building2} />
<StatCard label="Active monitors" value={activeCount} icon={Radio} />
<StatCard label="Reports generated" value={reportCount} icon={FileText} />
<StatCard label="Unread alerts" value={unreadAlertCount} icon={AlertTriangle} />
</div>
{analytics && <DashboardAnalyticsSection analytics={analytics} />}
<div className="mt-6 grid gap-4 lg:grid-cols-2">
<div className="rounded-lg border border-slate-200 bg-white p-5">
<div className="flex items-center gap-2 text-sm font-medium text-slate-700">
<Clock className="h-4 w-4" aria-hidden /> Monitoring schedule
</div>
<dl className="mt-3 space-y-2 text-sm">
<div className="flex justify-between">
<dt className="text-slate-500">Last successful run</dt>
<dd className="text-slate-900">{formatDateTime(mostRecentLastRun(companyList))}</dd>
</div>
<div className="flex justify-between">
<dt className="text-slate-500">Next scheduled run</dt>
<dd className="text-slate-900">{formatDateTime(nextUpcomingRun(companyList))}</dd>
</div>
</dl>
</div>
<div className="rounded-lg border border-slate-200 bg-white p-5">
<div className="flex items-center gap-2 text-sm font-medium text-slate-700">
<ServerCog className="h-4 w-4" aria-hidden /> System health
</div>
<dl className="mt-3 space-y-2 text-sm">
{systemStatus?.components.map((c) => (
<div key={c.name} className="flex justify-between capitalize">
<dt className="text-slate-500">{c.name}</dt>
<dd className={c.status === "ok" ? "text-green-700" : "text-red-700"}>
{c.status}
</dd>
</div>
))}
<div className="flex justify-between">
<dt className="text-slate-500">LLM provider</dt>
<dd className="text-slate-900">{systemStatus?.llm_provider ?? "—"}</dd>
</div>
</dl>
</div>
</div>
<div className="mt-6 rounded-lg border border-slate-200 bg-white p-5">
<div className="flex items-center justify-between">
<h2 className="text-sm font-medium text-slate-700">Companies</h2>
<Link
href="/companies"
className="text-sm font-medium text-brand-600 hover:text-brand-700"
>
View all
</Link>
</div>
{isLoading ? (
<p className="mt-4 text-sm text-slate-500">Loading</p>
) : companyList.length === 0 ? (
<div className="mt-4 rounded-md border border-dashed border-slate-300 p-6 text-center">
<p className="text-sm text-slate-600">You&apos;re not monitoring any companies yet.</p>
<Link
href="/companies/new"
className="focus-ring mt-3 inline-block rounded-md bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700"
>
Add your first company
</Link>
</div>
) : (
<ul className="mt-4 divide-y divide-slate-100">
{companyList.slice(0, 5).map((c) => (
<li key={c.id} className="flex items-center justify-between py-3 text-sm">
<Link
href={`/companies/${c.id}`}
className="font-medium text-slate-900 hover:text-brand-600"
>
{c.name}
</Link>
<span className="text-slate-500">{c.status}</span>
</li>
))}
</ul>
)}
</div>
</div>
);
}