fix(ingestion): drain the Grants.gov backlog + source-filtered review queue

ingest-grants spent its 200-detail budget on hits.slice(0, 200) — the
same head of the search results every night; the backlog never drained.
Now new opportunities fill the budget first (serverListGrantSourceUrls
partition), remaining budget refreshes known ones; search cap raised to
2,000. Full eligible pool turns out to be 565 federal opportunities —
drained in two passes, 366 newly embedded.

Review queue gains a source badge column and All/Foundations/Federal
RFPs filter (?source=) — foundation easy-wins otherwise bury posted-RFP
matches, which score lower by design (no precedent, national pools) but
are the deadline-driven sends. Immediate proof: DOJ OVW FY2026 DV
program (closes 9/8) matched five NH domestic-violence orgs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Croissant Le Doux
2026-07-16 22:27:41 -04:00
parent a92b75b315
commit 4fa0bb1c32
5 changed files with 83 additions and 13 deletions

View File

@@ -11,8 +11,10 @@ import type { Route } from './+types/_index.js';
export async function loader({ request }: Route.LoaderArgs) {
if (request.method === 'HEAD') return;
const matches = await serverListPendingReviewMatches(db);
return { matches };
const url = new URL(request.url);
const source = url.searchParams.get('source') ?? undefined;
const matches = await serverListPendingReviewMatches(db, { source });
return { matches, source: source ?? null };
}
export async function action({ request }: Route.ActionArgs) {
@@ -36,10 +38,30 @@ export async function action({ request }: Route.ActionArgs) {
export default function ReviewQueue({ loaderData }: Route.ComponentProps) {
const matches = loaderData?.matches ?? [];
const activeSource = loaderData?.source ?? null;
return (
<main className="container mx-auto p-6">
<h1 className="mb-4 text-2xl font-semibold">Grant Match Review Queue</h1>
<h1 className="mb-2 text-2xl font-semibold">Grant Match Review Queue</h1>
<nav className="mb-4 flex gap-3 text-sm" aria-label="Filter by source">
{[
[null, 'All'],
['irs_990pf', 'Foundations'],
['grants_gov', 'Federal RFPs'],
].map(([value, label]) => (
<a
key={label as string}
href={value == null ? '/' : `/?source=${value}`}
className={
activeSource === value
? 'font-semibold underline'
: 'text-blue-700 underline'
}
>
{label}
</a>
))}
</nav>
{matches.length === 0 ? (
<p role="status" className="text-gray-600">
@@ -55,6 +77,9 @@ export default function ReviewQueue({ loaderData }: Route.ComponentProps) {
<th scope="col" className="p-2">
Grant
</th>
<th scope="col" className="p-2">
Source
</th>
<th scope="col" className="p-2">
Score
</th>
@@ -78,6 +103,11 @@ export default function ReviewQueue({ loaderData }: Route.ComponentProps) {
</Link>
</td>
<td className="p-2">{match.grantTitle}</td>
<td className="p-2">
<span className="rounded bg-gray-200 px-2 py-0.5 text-xs">
{match.source === 'irs_990pf' ? 'foundation' : match.source}
</span>
</td>
<td className="p-2">{match.totalScore}</td>
<td className="p-2">{match.easyWin ? 'Yes' : 'No'}</td>
<td className="p-2">