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";