204 lines
8.7 KiB
TypeScript
204 lines
8.7 KiB
TypeScript
import { createSignal, createResource, For, Show, Suspense } from "solid-js";
|
|
import { Title } from "@solidjs/meta";
|
|
import { Sidebar, TopBar } from "~/components/dashboard";
|
|
import { Button, Card, EmptyState, SkeletonCard, SkeletonTable } from "~/components/ui";
|
|
import { api } from "~/lib/api";
|
|
|
|
function BrokerIcon() {
|
|
return (
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10" />
|
|
<path d="M8 12h8" />
|
|
<path d="M12 8v8" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
type EnhancedStats = {
|
|
total: number;
|
|
byStatus: Record<string, number>;
|
|
pending: number;
|
|
completed: number;
|
|
totalListings: number;
|
|
listingsRemoved: number;
|
|
completionRate: number;
|
|
progress: string;
|
|
brokerSuccessRates: Array<{
|
|
brokerId: string;
|
|
brokerName: string;
|
|
status: string;
|
|
successCount: number;
|
|
failureCount: number;
|
|
failureRate24h: number;
|
|
totalOps24h: number;
|
|
isAutoDisabled: boolean;
|
|
}>;
|
|
systemHealth: {
|
|
healthy: number;
|
|
degraded: number;
|
|
broken: number;
|
|
disabled: number;
|
|
total: number;
|
|
systemHealthPercentage: number;
|
|
needsAlert: boolean;
|
|
alertMessage?: string;
|
|
};
|
|
};
|
|
|
|
export default function RemoveBrokersPage() {
|
|
const [sidebarOpen, setSidebarOpen] = createSignal(false);
|
|
const [brokers] = createResource(
|
|
() => api.removebrokers.getBrokerRegistry.query(),
|
|
{ initialValue: [] },
|
|
);
|
|
const [removalRequests, { refetch }] = createResource(
|
|
() => api.removebrokers.getRemovalRequests.query({ page: 1, limit: 20 }),
|
|
);
|
|
const [enhancedStats] = createResource(
|
|
() => api.removebrokers.getEnhancedStats.query(),
|
|
);
|
|
|
|
async function createRequest(brokerId: string) {
|
|
await api.removebrokers.createRemovalRequest.mutate({
|
|
brokerId,
|
|
personalInfo: { fullName: "" },
|
|
});
|
|
refetch();
|
|
}
|
|
|
|
const stats = () => enhancedStats() as EnhancedStats | undefined;
|
|
|
|
return (
|
|
<div class="flex h-[calc(100vh-4rem)] bg-[var(--color-bg)]">
|
|
<Title>RemoveBrokers — Kordant</Title>
|
|
<Sidebar open={sidebarOpen()} onClose={() => setSidebarOpen(false)} />
|
|
<div class="flex-1 flex flex-col min-w-0">
|
|
<TopBar onMenuToggle={() => setSidebarOpen(v => !v)} />
|
|
<main id="main-content" class="flex-1 overflow-y-auto p-6">
|
|
<div class="max-w-4xl mx-auto">
|
|
<h1 class="text-2xl font-bold text-[var(--color-text-primary)] mb-6">
|
|
RemoveBrokers
|
|
<span class="text-sm font-normal text-[var(--color-text-tertiary)] ml-2">
|
|
{stats()?.progress ?? ""}
|
|
</span>
|
|
</h1>
|
|
|
|
<Suspense fallback={<div class="grid grid-cols-4 gap-4 mb-6"><SkeletonCard class="h-24" /><SkeletonCard class="h-24" /><SkeletonCard class="h-24" /><SkeletonCard class="h-24" /></div>}>
|
|
<Show when={stats()}>
|
|
<div class="grid grid-cols-4 gap-4 mb-6">
|
|
<Card class="p-4 text-center">
|
|
<p class="text-2xl font-bold text-[var(--color-brand-primary)]">
|
|
{stats()?.total ?? 0}
|
|
</p>
|
|
<p class="text-xs text-[var(--color-text-tertiary)]">Total Requests</p>
|
|
</Card>
|
|
<Card class="p-4 text-center">
|
|
<p class="text-2xl font-bold text-[var(--color-success)]">
|
|
{stats()?.completed ?? 0}
|
|
</p>
|
|
<p class="text-xs text-[var(--color-text-tertiary)]">Completed</p>
|
|
</Card>
|
|
<Card class="p-4 text-center">
|
|
<p class="text-2xl font-bold text-[var(--color-warning)]">
|
|
{stats()?.pending ?? 0}
|
|
</p>
|
|
<p class="text-xs text-[var(--color-text-tertiary)]">Pending</p>
|
|
</Card>
|
|
<Card class="p-4 text-center">
|
|
<p class="text-2xl font-bold">{stats()?.systemHealth.systemHealthPercentage ?? 100}%</p>
|
|
<p class="text-xs text-[var(--color-text-tertiary)]">System Health</p>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Per-broker success rates */}
|
|
<Show when={(stats()?.brokerSuccessRates?.length ?? 0) > 0}>
|
|
<Card class="mb-6">
|
|
<div class="px-4 py-3 border-b border-[var(--color-border)]/50">
|
|
<h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Per-Broker Success Rates</h2>
|
|
</div>
|
|
<div class="divide-y divide-[var(--color-border)]/50">
|
|
<For each={stats()?.brokerSuccessRates?.slice(0, 10) ?? []}>
|
|
{(broker: EnhancedStats["brokerSuccessRates"][number]) => (
|
|
<div class="px-4 py-2 flex items-center justify-between text-sm">
|
|
<span class="text-[var(--color-text-primary)]">{broker.brokerName}</span>
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-xs text-[var(--color-text-tertiary)]">
|
|
{broker.successCount}s / {broker.failureCount}f
|
|
</span>
|
|
<span class={`text-xs px-1.5 py-0.5 rounded ${
|
|
broker.status === "healthy" ? "text-green-500 bg-green-500/10" :
|
|
broker.status === "degraded" ? "text-yellow-500 bg-yellow-500/10" :
|
|
broker.status === "broken" || broker.isAutoDisabled ? "text-red-500 bg-red-500/10" :
|
|
"text-gray-500 bg-gray-500/10"
|
|
}`}>
|
|
{broker.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</Card>
|
|
</Show>
|
|
</Show>
|
|
</Suspense>
|
|
|
|
<Card class="mb-6">
|
|
<div class="px-4 py-3 border-b border-[var(--color-border)]/50">
|
|
<h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Data Brokers</h2>
|
|
</div>
|
|
<Suspense fallback={<div class="p-4"><SkeletonTable rows={4} columns={2} /></div>}>
|
|
<Show when={brokers().length > 0} fallback={
|
|
<EmptyState
|
|
icon={<BrokerIcon />}
|
|
title="No data brokers found"
|
|
description="Your broker registry is empty. We'll scan for brokers automatically."
|
|
/>
|
|
}>
|
|
<div class="divide-y divide-[var(--color-border)]/50">
|
|
<For each={brokers() as Array<Record<string, unknown>>}>
|
|
{(broker) => (
|
|
<div class="px-4 py-3 flex items-center justify-between">
|
|
<p class="text-sm text-[var(--color-text-primary)]">{String(broker.name ?? "")}</p>
|
|
<Button size="sm" onClick={() => createRequest(String((broker as Record<string, unknown>).id ?? ""))}>
|
|
Opt Out
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</Show>
|
|
</Suspense>
|
|
</Card>
|
|
|
|
<Card>
|
|
<div class="px-4 py-3 border-b border-[var(--color-border)]/50">
|
|
<h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Removal Requests</h2>
|
|
</div>
|
|
<Suspense fallback={<div class="p-4"><SkeletonTable rows={3} columns={2} /></div>}>
|
|
<Show when={(removalRequests()?.items ?? []).length > 0} fallback={
|
|
<EmptyState
|
|
title="No removal requests yet"
|
|
description="Opt out of data brokers above to generate removal requests."
|
|
/>
|
|
}>
|
|
<div class="divide-y divide-[var(--color-border)]/50">
|
|
<For each={removalRequests()?.items ?? []}>
|
|
{(req: Record<string, unknown>) => (
|
|
<div class="px-4 py-3">
|
|
<p class="text-sm text-[var(--color-text-primary)]">{String(req.brokerName ?? "")}</p>
|
|
<p class="text-xs text-[var(--color-text-tertiary)]">Status: {String(req.status ?? "")}</p>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</Show>
|
|
</Suspense>
|
|
</Card>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|