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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user