fix(news): auto-retry news fetch when backend Celery task is in-flight

When a bill page opens with no stored articles, the backend queues a
fetch_news_for_bill Celery task and returns immediately. Added a retry
loop (up to 3x, 6 s apart) driven off newsArticles state so articles
populate without a manual refresh. Fixed broken useEffect dependency
([billId] → [newsArticles]) that caused the timer to never fire.
News is now fetched via a separate useBillNews query (staleTime: 0)
independent of the cached bill detail response.

Co-Authored-By: Jack Levy
This commit is contained in:
Jack Levy
2026-03-01 03:17:17 -05:00
parent d5711312b8
commit 12a3eac48f
2 changed files with 21 additions and 4 deletions

View File

@@ -1,9 +1,9 @@
"use client"; "use client";
import { use } from "react"; import { use, useEffect, useRef } from "react";
import Link from "next/link"; import Link from "next/link";
import { ArrowLeft, ExternalLink, User } from "lucide-react"; import { ArrowLeft, ExternalLink, User } from "lucide-react";
import { useBill, useBillTrend } from "@/lib/hooks/useBills"; import { useBill, useBillNews, useBillTrend } from "@/lib/hooks/useBills";
import { BriefPanel } from "@/components/bills/BriefPanel"; import { BriefPanel } from "@/components/bills/BriefPanel";
import { ActionTimeline } from "@/components/bills/ActionTimeline"; import { ActionTimeline } from "@/components/bills/ActionTimeline";
import { TrendChart } from "@/components/bills/TrendChart"; import { TrendChart } from "@/components/bills/TrendChart";
@@ -17,6 +17,23 @@ export default function BillDetailPage({ params }: { params: Promise<{ id: strin
const { data: bill, isLoading } = useBill(billId); const { data: bill, isLoading } = useBill(billId);
const { data: trendData } = useBillTrend(billId, 30); const { data: trendData } = useBillTrend(billId, 30);
const { data: newsArticles, refetch: refetchNews } = useBillNews(billId);
// When the bill page is opened with no stored articles, the backend queues
// a Celery news-fetch task that takes a few seconds to complete.
// Retry up to 3 times (every 6 s) so articles appear without a manual refresh.
// newsRetryRef resets on bill navigation so each bill gets its own retry budget.
const newsRetryRef = useRef(0);
useEffect(() => { newsRetryRef.current = 0; }, [billId]);
useEffect(() => {
if (newsArticles === undefined || newsArticles.length > 0) return;
if (newsRetryRef.current >= 3) return;
const timer = setTimeout(() => {
newsRetryRef.current += 1;
refetchNews();
}, 6000);
return () => clearTimeout(timer);
}, [newsArticles]); // eslint-disable-line react-hooks/exhaustive-deps
if (isLoading) { if (isLoading) {
return <div className="text-center py-20 text-muted-foreground">Loading bill...</div>; return <div className="text-center py-20 text-muted-foreground">Loading bill...</div>;
@@ -85,7 +102,7 @@ export default function BillDetailPage({ params }: { params: Promise<{ id: strin
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<TrendChart data={trendData} /> <TrendChart data={trendData} />
<NewsPanel articles={bill.news_articles} /> <NewsPanel articles={newsArticles} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -31,7 +31,7 @@ export function useBillNews(id: string) {
return useQuery({ return useQuery({
queryKey: ["bill-news", id], queryKey: ["bill-news", id],
queryFn: () => billsAPI.getNews(id), queryFn: () => billsAPI.getNews(id),
staleTime: 10 * 60 * 1000, staleTime: 0, // Always fetch fresh — news arrives async after brief generation
enabled: !!id, enabled: !!id,
}); });
} }