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
@@ -0,0 +1,90 @@
"use client";
import { useState } from "react";
import { Loader2, Minus, Plus } from "lucide-react";
import {
useCreateNotificationDestination,
useUnlinkNotificationDestinationCompany,
} from "@/hooks/use-notification-destinations";
import type { NotificationDestinationResponse, NotificationType } from "@/lib/types";
/**
* A simplified single-destination view over the full notification
* destinations list, scoped to one company - shows the first destination
* of `type` linked to this company (if any) with an add/remove control.
* Shares the same React Query cache as the Settings page's full
* multi-destination UI, so changes here show up there instantly.
*/
export function NotificationChannelBox({
type,
label,
placeholder,
companyId,
destination,
disabled,
disabledReason,
}: {
type: NotificationType;
label: string;
placeholder: string;
companyId: string;
destination: NotificationDestinationResponse | undefined;
disabled?: boolean;
disabledReason?: string;
}) {
const [value, setValue] = useState("");
const create = useCreateNotificationDestination();
const unlink = useUnlinkNotificationDestinationCompany();
return (
<div>
<label className="block text-sm font-medium text-slate-700">{label}</label>
<div className="mt-1 flex items-center gap-2">
<input
value={destination ? destination.destination_value : value}
onChange={(e) => setValue(e.target.value)}
readOnly={Boolean(destination)}
disabled={disabled}
placeholder={placeholder}
className="focus-ring block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm transition-colors duration-150 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400"
/>
{destination ? (
<button
type="button"
onClick={() => unlink.mutate({ destinationId: destination.id, companyId })}
disabled={disabled || unlink.isPending}
title={`Remove this ${label.toLowerCase()}`}
className="focus-ring inline-flex shrink-0 items-center justify-center rounded-md border border-red-300 p-2 text-red-600 transition-colors duration-150 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-40"
>
{unlink.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
) : (
<Minus className="h-4 w-4" aria-hidden />
)}
</button>
) : (
<button
type="button"
onClick={() => {
if (!value.trim()) return;
create.mutate(
{ type, destination_value: value.trim(), company_ids: [companyId] },
{ onSuccess: () => setValue("") },
);
}}
disabled={disabled || !value.trim() || create.isPending}
title={`Add this ${label.toLowerCase()}`}
className="focus-ring inline-flex shrink-0 items-center justify-center rounded-md border border-green-300 p-2 text-green-700 transition-colors duration-150 hover:bg-green-50 disabled:cursor-not-allowed disabled:opacity-40"
>
{create.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
) : (
<Plus className="h-4 w-4" aria-hidden />
)}
</button>
)}
</div>
{disabled && disabledReason && <p className="mt-1 text-xs text-slate-500">{disabledReason}</p>}
</div>
);
}