diff --git a/frontend/app/bills/[id]/page.tsx b/frontend/app/bills/[id]/page.tsx index 07a0d53..3346b34 100644 --- a/frontend/app/bills/[id]/page.tsx +++ b/frontend/app/bills/[id]/page.tsx @@ -9,7 +9,7 @@ import { ActionTimeline } from "@/components/bills/ActionTimeline"; import { TrendChart } from "@/components/bills/TrendChart"; import { NewsPanel } from "@/components/bills/NewsPanel"; import { FollowButton } from "@/components/shared/FollowButton"; -import { billLabel, congressLabel, formatDate, partyBadgeColor, cn } from "@/lib/utils"; +import { billLabel, chamberBadgeColor, congressLabel, formatDate, partyBadgeColor, cn } from "@/lib/utils"; export default function BillDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); @@ -62,7 +62,11 @@ export default function BillDetailPage({ params }: { params: Promise<{ id: strin {label} - {bill.chamber} + {bill.chamber && ( + + {bill.chamber} + + )} {congressLabel(bill.congress_number)}

@@ -132,7 +136,11 @@ export default function BillDetailPage({ params }: { params: Promise<{ id: strin )} )} - +
diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index 9d43d82..b680b6b 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -137,6 +137,7 @@ export default function SettingsPage() { } | null>(null); const [testing, setTesting] = useState(false); const [taskIds, setTaskIds] = useState>({}); + const [taskStatuses, setTaskStatuses] = useState>({}); const [confirmDelete, setConfirmDelete] = useState(null); const testLLM = async () => { @@ -152,9 +153,26 @@ export default function SettingsPage() { } }; + const pollTaskStatus = async (name: string, taskId: string) => { + for (let i = 0; i < 60; i++) { + await new Promise((r) => setTimeout(r, 5000)); + try { + const data = await adminAPI.getTaskStatus(taskId); + if (["SUCCESS", "FAILURE", "REVOKED"].includes(data.status)) { + setTaskStatuses((prev) => ({ ...prev, [name]: data.status === "SUCCESS" ? "done" : "error" })); + qc.invalidateQueries({ queryKey: ["admin-stats"] }); + return; + } + } catch { /* ignore polling errors */ } + } + setTaskStatuses((prev) => ({ ...prev, [name]: "error" })); + }; + const trigger = async (name: string, fn: () => Promise<{ task_id: string }>) => { const result = await fn(); setTaskIds((prev) => ({ ...prev, [name]: result.task_id })); + setTaskStatuses((prev) => ({ ...prev, [name]: "running" })); + pollTaskStatus(name, result.task_id); }; if (settingsLoading) return
Loading...
; @@ -719,25 +737,36 @@ export default function SettingsPage() {
{name} - {status === "ok" && ( + {taskStatuses[key] === "running" ? ( + + + running + {taskIds[key] && ( + {taskIds[key].slice(0, 8)}… + )} + + ) : taskStatuses[key] === "done" ? ( + ✓ Complete + ) : taskStatuses[key] === "error" ? ( + ✗ Failed + ) : status === "ok" ? ( ✓ Up to date - )} - {status === "needed" && count !== undefined && count > 0 && ( + ) : status === "needed" && count !== undefined && count > 0 ? ( ⚠ {count.toLocaleString()} {countLabel} - )} - {taskIds[key] && ( - queued ✓ - )} + ) : null}

{description}

))} diff --git a/frontend/components/bills/ActionTimeline.tsx b/frontend/components/bills/ActionTimeline.tsx index 01dcae1..c80b6f2 100644 --- a/frontend/components/bills/ActionTimeline.tsx +++ b/frontend/components/bills/ActionTimeline.tsx @@ -4,10 +4,15 @@ import { formatDate } from "@/lib/utils"; interface ActionTimelineProps { actions: BillAction[]; + latestActionDate?: string; + latestActionText?: string; } -export function ActionTimeline({ actions }: ActionTimelineProps) { - if (!actions || actions.length === 0) { +export function ActionTimeline({ actions, latestActionDate, latestActionText }: ActionTimelineProps) { + const hasActions = actions && actions.length > 0; + const hasFallback = !hasActions && latestActionText; + + if (!hasActions && !hasFallback) { return (

@@ -24,22 +29,38 @@ export function ActionTimeline({ actions }: ActionTimelineProps) {

Action History - ({actions.length}) + {hasActions && ( + ({actions.length}) + )}

    - {actions.map((action, i) => ( -
  • -
    + {hasActions ? ( + actions.map((action) => ( +
  • +
    +
    + {formatDate(action.action_date)} + {action.chamber && ` · ${action.chamber}`} +
    +

    {action.action_text}

    +
  • + )) + ) : ( +
  • +
    - {formatDate(action.action_date)} - {action.chamber && ` · ${action.chamber}`} + {formatDate(latestActionDate)} + · latest known action
    -

    {action.action_text}

    +

    {latestActionText}

    +

    + Full history loads in the background — refresh to see all actions. +

  • - ))} + )}
diff --git a/frontend/components/shared/BillCard.tsx b/frontend/components/shared/BillCard.tsx index 4119f55..31b79b2 100644 --- a/frontend/components/shared/BillCard.tsx +++ b/frontend/components/shared/BillCard.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import { TrendingUp, Calendar, User } from "lucide-react"; import { Bill } from "@/lib/types"; -import { billLabel, cn, formatDate, partyBadgeColor, trendColor } from "@/lib/utils"; +import { billLabel, chamberBadgeColor, cn, formatDate, partyBadgeColor, trendColor } from "@/lib/utils"; import { FollowButton } from "./FollowButton"; interface BillCardProps { @@ -22,9 +22,11 @@ export function BillCard({ bill, compact = false }: BillCardProps) { {label} - - {bill.chamber} - + {bill.chamber && ( + + {bill.chamber} + + )} {tags.map((tag) => ( = 11 && lastTwo <= 13) return `${congress}th Congress`; + const suffixes: Record = { 1: "st", 2: "nd", 3: "rd" }; + return `${congress}${suffixes[congress % 10] ?? "th"} Congress`; +} + +export function chamberBadgeColor(chamber?: string): string { + if (!chamber) return "bg-muted text-muted-foreground"; + const c = chamber.toLowerCase(); + if (c === "senate") return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"; + if (c.startsWith("house")) return "bg-slate-100 text-slate-600 dark:bg-slate-700/50 dark:text-slate-300"; + return "bg-muted text-muted-foreground"; +} + export function trendColor(score?: number): string { if (!score) return "text-muted-foreground"; if (score >= 70) return "text-red-500";