|
|
|
|
@@ -17,6 +17,9 @@ import {
|
|
|
|
|
Bell,
|
|
|
|
|
Shield,
|
|
|
|
|
Zap,
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
Wrench,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { settingsAPI, adminAPI, notificationsAPI, type AdminUser, type LLMModel, type ApiHealthResult } from "@/lib/api";
|
|
|
|
|
@@ -134,6 +137,7 @@ export default function SettingsPage() {
|
|
|
|
|
const [taskIds, setTaskIds] = useState<Record<string, string>>({});
|
|
|
|
|
const [taskStatuses, setTaskStatuses] = useState<Record<string, "running" | "done" | "error">>({});
|
|
|
|
|
const [confirmDelete, setConfirmDelete] = useState<number | null>(null);
|
|
|
|
|
const [showMaintenance, setShowMaintenance] = useState(false);
|
|
|
|
|
|
|
|
|
|
const testLLM = async () => {
|
|
|
|
|
setTesting(true);
|
|
|
|
|
@@ -316,6 +320,7 @@ export default function SettingsPage() {
|
|
|
|
|
{ label: "AI briefs generated", value: stats.briefs_generated, color: "text-green-600 dark:text-green-400", icon: "✅" },
|
|
|
|
|
{ label: "Pending LLM analysis", value: stats.pending_llm, color: stats.pending_llm > 0 ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground", icon: "🔄", action: stats.pending_llm > 0 ? "Resume Analysis" : undefined },
|
|
|
|
|
{ label: "Briefs missing citations", value: stats.uncited_briefs, color: stats.uncited_briefs > 0 ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground", icon: "⚠️", action: stats.uncited_briefs > 0 ? "Backfill Citations" : undefined },
|
|
|
|
|
{ label: "Briefs with unlabeled points", value: stats.unlabeled_briefs, color: stats.unlabeled_briefs > 0 ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground", icon: "🏷️", action: stats.unlabeled_briefs > 0 ? "Backfill Labels" : undefined },
|
|
|
|
|
].map(({ label, value, color, icon, note, action }) => (
|
|
|
|
|
<div key={label} className="flex items-center justify-between py-2.5 gap-3">
|
|
|
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
|
|
|
@@ -637,82 +642,9 @@ export default function SettingsPage() {
|
|
|
|
|
{/* Manual Controls */}
|
|
|
|
|
<section className="bg-card border border-border rounded-lg p-6 space-y-4">
|
|
|
|
|
<h2 className="font-semibold">Manual Controls</h2>
|
|
|
|
|
<div className="divide-y divide-border">
|
|
|
|
|
{([
|
|
|
|
|
{
|
|
|
|
|
key: "poll",
|
|
|
|
|
name: "Trigger Poll",
|
|
|
|
|
description: "Check Congress.gov for newly introduced or updated bills. Runs automatically on a schedule — use this to force an immediate sync.",
|
|
|
|
|
fn: adminAPI.triggerPoll,
|
|
|
|
|
status: "on-demand",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "members",
|
|
|
|
|
name: "Sync Members",
|
|
|
|
|
description: "Refresh all member profiles from Congress.gov including biography, current term, leadership roles, and contact information.",
|
|
|
|
|
fn: adminAPI.triggerMemberSync,
|
|
|
|
|
status: "on-demand",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "trends",
|
|
|
|
|
name: "Calculate Trends",
|
|
|
|
|
description: "Score bill and member newsworthiness by counting recent news headlines and Google search interest. Updates the trend charts.",
|
|
|
|
|
fn: adminAPI.triggerTrendScores,
|
|
|
|
|
status: "on-demand",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "actions",
|
|
|
|
|
name: "Fetch Bill Actions",
|
|
|
|
|
description: "Download the full legislative history (votes, referrals, amendments) for recently active bills and populate the timeline view.",
|
|
|
|
|
fn: adminAPI.triggerFetchActions,
|
|
|
|
|
status: "on-demand",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "backfill-actions",
|
|
|
|
|
name: "Backfill All Action Histories",
|
|
|
|
|
description: "One-time catch-up: fetch action histories for all bills that were imported before this feature existed. Run once to populate timelines across your full bill archive.",
|
|
|
|
|
fn: adminAPI.backfillAllActions,
|
|
|
|
|
status: stats ? (stats.bills_missing_actions > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.bills_missing_actions,
|
|
|
|
|
countLabel: "bills missing action history",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "sponsors",
|
|
|
|
|
name: "Backfill Sponsors",
|
|
|
|
|
description: "Link bill sponsors that weren't captured during the initial import. Safe to re-run — skips bills that already have a sponsor.",
|
|
|
|
|
fn: adminAPI.backfillSponsors,
|
|
|
|
|
status: stats ? (stats.bills_missing_sponsor > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.bills_missing_sponsor,
|
|
|
|
|
countLabel: "bills missing sponsor",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "metadata",
|
|
|
|
|
name: "Backfill Dates & Links",
|
|
|
|
|
description: "Fill in missing introduced dates, chamber assignments, and congress.gov links by re-fetching bill detail from Congress.gov.",
|
|
|
|
|
fn: adminAPI.backfillMetadata,
|
|
|
|
|
status: stats ? (stats.bills_missing_metadata > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.bills_missing_metadata,
|
|
|
|
|
countLabel: "bills missing metadata",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "citations",
|
|
|
|
|
name: "Backfill Citations",
|
|
|
|
|
description: "Regenerate AI briefs that were created before inline source citations were added. Deletes the old brief and re-runs LLM analysis using the already-stored bill text — no new Congress.gov calls.",
|
|
|
|
|
fn: adminAPI.backfillCitations,
|
|
|
|
|
status: stats ? (stats.uncited_briefs > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.uncited_briefs,
|
|
|
|
|
countLabel: "briefs need regeneration",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "resume",
|
|
|
|
|
name: "Resume Analysis",
|
|
|
|
|
description: "Restart AI brief generation for bills where processing stalled or failed (e.g. after an LLM quota outage). Also re-queues document fetching for bills that have no text yet.",
|
|
|
|
|
fn: adminAPI.resumeAnalysis,
|
|
|
|
|
status: stats ? (stats.pending_llm > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.pending_llm,
|
|
|
|
|
countLabel: "bills pending analysis",
|
|
|
|
|
},
|
|
|
|
|
] as Array<{
|
|
|
|
|
|
|
|
|
|
{(() => {
|
|
|
|
|
type ControlItem = {
|
|
|
|
|
key: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description: string;
|
|
|
|
|
@@ -720,7 +652,9 @@ export default function SettingsPage() {
|
|
|
|
|
status: "ok" | "needed" | "on-demand";
|
|
|
|
|
count?: number;
|
|
|
|
|
countLabel?: string;
|
|
|
|
|
}>).map(({ key, name, description, fn, status, count, countLabel }) => (
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderRow = ({ key, name, description, fn, status, count, countLabel }: ControlItem) => (
|
|
|
|
|
<div key={key} className="flex items-start gap-3 py-3.5">
|
|
|
|
|
<div className={`w-2.5 h-2.5 rounded-full mt-1 shrink-0 ${
|
|
|
|
|
status === "ok" ? "bg-green-500"
|
|
|
|
|
@@ -757,13 +691,133 @@ export default function SettingsPage() {
|
|
|
|
|
disabled={taskStatuses[key] === "running"}
|
|
|
|
|
className="shrink-0 flex items-center gap-1.5 px-3 py-1.5 text-xs bg-muted hover:bg-accent rounded-md transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
>
|
|
|
|
|
{taskStatuses[key] === "running" ? (
|
|
|
|
|
<RefreshCw className="w-3 h-3 animate-spin" />
|
|
|
|
|
) : "Run"}
|
|
|
|
|
{taskStatuses[key] === "running" ? <RefreshCw className="w-3 h-3 animate-spin" /> : "Run"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const recurring: ControlItem[] = [
|
|
|
|
|
{
|
|
|
|
|
key: "poll",
|
|
|
|
|
name: "Trigger Poll",
|
|
|
|
|
description: "Check Congress.gov for newly introduced or updated bills. Runs automatically on a schedule — use this to force an immediate sync.",
|
|
|
|
|
fn: adminAPI.triggerPoll,
|
|
|
|
|
status: "on-demand",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "members",
|
|
|
|
|
name: "Sync Members",
|
|
|
|
|
description: "Refresh all member profiles from Congress.gov including biography, current term, leadership roles, and contact information.",
|
|
|
|
|
fn: adminAPI.triggerMemberSync,
|
|
|
|
|
status: "on-demand",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "trends",
|
|
|
|
|
name: "Calculate Trends",
|
|
|
|
|
description: "Score bill and member newsworthiness by counting recent news headlines and Google search interest. Updates the trend charts.",
|
|
|
|
|
fn: adminAPI.triggerTrendScores,
|
|
|
|
|
status: "on-demand",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "actions",
|
|
|
|
|
name: "Fetch Bill Actions",
|
|
|
|
|
description: "Download the full legislative history (votes, referrals, amendments) for recently active bills and populate the timeline view.",
|
|
|
|
|
fn: adminAPI.triggerFetchActions,
|
|
|
|
|
status: "on-demand",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "resume",
|
|
|
|
|
name: "Resume Analysis",
|
|
|
|
|
description: "Restart AI brief generation for bills where processing stalled or failed (e.g. after an LLM quota outage). Also re-queues document fetching for bills that have no text yet.",
|
|
|
|
|
fn: adminAPI.resumeAnalysis,
|
|
|
|
|
status: stats ? (stats.pending_llm > 0 ? "needed" : "on-demand") : "on-demand",
|
|
|
|
|
count: stats?.pending_llm,
|
|
|
|
|
countLabel: "bills pending analysis",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const maintenance: ControlItem[] = [
|
|
|
|
|
{
|
|
|
|
|
key: "backfill-actions",
|
|
|
|
|
name: "Backfill All Action Histories",
|
|
|
|
|
description: "One-time catch-up: fetch action histories for all bills that were imported before this feature existed.",
|
|
|
|
|
fn: adminAPI.backfillAllActions,
|
|
|
|
|
status: stats ? (stats.bills_missing_actions > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.bills_missing_actions,
|
|
|
|
|
countLabel: "bills missing action history",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "sponsors",
|
|
|
|
|
name: "Backfill Sponsors",
|
|
|
|
|
description: "Link bill sponsors that weren't captured during the initial import. Safe to re-run — skips bills that already have a sponsor.",
|
|
|
|
|
fn: adminAPI.backfillSponsors,
|
|
|
|
|
status: stats ? (stats.bills_missing_sponsor > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.bills_missing_sponsor,
|
|
|
|
|
countLabel: "bills missing sponsor",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "metadata",
|
|
|
|
|
name: "Backfill Dates & Links",
|
|
|
|
|
description: "Fill in missing introduced dates, chamber assignments, and congress.gov links by re-fetching bill detail from Congress.gov.",
|
|
|
|
|
fn: adminAPI.backfillMetadata,
|
|
|
|
|
status: stats ? (stats.bills_missing_metadata > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.bills_missing_metadata,
|
|
|
|
|
countLabel: "bills missing metadata",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "citations",
|
|
|
|
|
name: "Backfill Citations",
|
|
|
|
|
description: "Regenerate AI briefs created before inline source citations were added. Deletes the old brief and re-runs LLM analysis using already-stored bill text.",
|
|
|
|
|
fn: adminAPI.backfillCitations,
|
|
|
|
|
status: stats ? (stats.uncited_briefs > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.uncited_briefs,
|
|
|
|
|
countLabel: "briefs need regeneration",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: "labels",
|
|
|
|
|
name: "Backfill Fact/Inference Labels",
|
|
|
|
|
description: "Classify existing cited brief points as fact or inference. One compact LLM call per brief — no re-generation of summaries or citations.",
|
|
|
|
|
fn: adminAPI.backfillLabels,
|
|
|
|
|
status: stats ? (stats.unlabeled_briefs > 0 ? "needed" : "ok") : "on-demand",
|
|
|
|
|
count: stats?.unlabeled_briefs,
|
|
|
|
|
countLabel: "briefs with unlabeled points",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const maintenanceNeeded = maintenance.some((m) => m.status === "needed");
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="divide-y divide-border">
|
|
|
|
|
{recurring.map(renderRow)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Maintenance subsection */}
|
|
|
|
|
<div className="border border-border rounded-md overflow-hidden">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowMaintenance((v) => !v)}
|
|
|
|
|
className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium bg-muted/50 hover:bg-muted transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<span className="flex items-center gap-2">
|
|
|
|
|
<Wrench className="w-3.5 h-3.5 text-muted-foreground" />
|
|
|
|
|
Maintenance
|
|
|
|
|
{maintenanceNeeded && (
|
|
|
|
|
<span className="text-xs font-normal text-red-600 dark:text-red-400">⚠ action needed</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
{showMaintenance
|
|
|
|
|
? <ChevronDown className="w-4 h-4 text-muted-foreground" />
|
|
|
|
|
: <ChevronRight className="w-4 h-4 text-muted-foreground" />}
|
|
|
|
|
</button>
|
|
|
|
|
{showMaintenance && (
|
|
|
|
|
<div className="divide-y divide-border px-4">
|
|
|
|
|
{maintenance.map(renderRow)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|