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,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's what'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'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user