feat: implement reanalyze and distributor discovery endpoints for Stalker products by ASIN

This commit is contained in:
Victor Noguera
2026-05-25 15:57:24 -04:00
parent 313677692b
commit 506e2344b7
3 changed files with 74 additions and 15 deletions

View File

@@ -25,6 +25,8 @@
"Bash(git --no-pager diff -- src/web/frontend.tsx src/web/styles.css 2>&1 || true)", "Bash(git --no-pager diff -- src/web/frontend.tsx src/web/styles.css 2>&1 || true)",
"Bash(bun run build:web 2>&1 || true)", "Bash(bun run build:web 2>&1 || true)",
"Bash(bun run build:web 2>&1 || true)", "Bash(bun run build:web 2>&1 || true)",
"Bash(bun run build:web 2>&1 || true)",
"Bash(bun run build:web 2>&1 || true)",
"Bash(bun run build:web 2>&1 || true)" "Bash(bun run build:web 2>&1 || true)"
] ]
}, },

View File

@@ -531,6 +531,36 @@ async function getProduct(asin: string) {
return { product, observations, analyses, distributorResearch }; return { product, observations, analyses, distributorResearch };
} }
async function findLatestStalkerRunItemIdByAsin(asin: string): Promise<number | null> {
const row = await pgGet<{ id: number }>(
`SELECT ri.id
FROM run_items ri
JOIN runs run ON run.id = ri.run_id
WHERE ri.product_asin = ?
AND run.type = 'stalker'
ORDER BY ri.id DESC
LIMIT 1`,
[asin],
);
return row?.id == null ? null : Number(row.id);
}
async function reanalyzeStalkerProductByAsin(asin: string, useClaude = USE_CLAUDE) {
const runItemId = await findLatestStalkerRunItemIdByAsin(asin);
if (runItemId == null) {
throw new Error("Stalker product item not found");
}
return reanalyzeRunItem(runItemId, useClaude);
}
async function findDistributorsForStalkerProductByAsin(asin: string) {
const runItemId = await findLatestStalkerRunItemIdByAsin(asin);
if (runItemId == null) {
throw new Error("Stalker product item not found");
}
return findDistributorsForStalkerProduct(runItemId);
}
async function reanalyzeRunItem(itemId: number, useClaude = USE_CLAUDE) { async function reanalyzeRunItem(itemId: number, useClaude = USE_CLAUDE) {
const row = await pgGet<Record<string, any>>( const row = await pgGet<Record<string, any>>(
`SELECT ri.id, ri.run_id, ri.product_asin AS asin, r.type, `SELECT ri.id, ri.run_id, ri.product_asin AS asin, r.type,
@@ -1159,6 +1189,30 @@ const server = Bun.serve({
return json({ error: message }, message === "Stalker product item not found" ? 404 : 500); return json({ error: message }, message === "Stalker product item not found" ? 404 : 500);
} }
}, },
"/api/stalker/products/by-asin/:asin/reanalyze": async (req) => {
if (req.method !== "POST") return json({ error: "Method not allowed" }, 405);
const asin = normalizeAsin(req.params.asin);
if (!asin) return json({ error: "Invalid ASIN" }, 400);
const provider = new URL(req.url).searchParams.get("provider")?.trim().toLowerCase();
const useClaude = provider === "claude";
try {
return json(await reanalyzeStalkerProductByAsin(asin, useClaude || USE_CLAUDE));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return json({ error: message }, message === "Stalker product item not found" ? 404 : 500);
}
},
"/api/stalker/products/by-asin/:asin/distributors": async (req) => {
if (req.method !== "POST") return json({ error: "Method not allowed" }, 405);
const asin = normalizeAsin(req.params.asin);
if (!asin) return json({ error: "Invalid ASIN" }, 400);
try {
return json(await findDistributorsForStalkerProductByAsin(asin));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return json({ error: message }, message === "Stalker product item not found" ? 404 : 500);
}
},
"/api/stalker/purge": async (req) => "/api/stalker/purge": async (req) =>
req.method === "DELETE" || req.method === "POST" req.method === "DELETE" || req.method === "POST"
? json(await purgeStalkerData()) ? json(await purgeStalkerData())

View File

@@ -1514,10 +1514,13 @@ function ProductDetails({
}, [asin]); }, [asin]);
async function reanalyze() { async function reanalyze() {
if (effectiveRunItemId == null || reanalyzing) return; if (reanalyzing) return;
setReanalyzing(true); setReanalyzing(true);
try { try {
const res = await fetch(`/api/stalker/products/${effectiveRunItemId}/reanalyze?provider=claude`, { method: "POST" }); const endpoint = effectiveRunItemId == null
? `/api/stalker/products/by-asin/${encodeURIComponent(asin)}/reanalyze?provider=claude`
: `/api/stalker/products/${effectiveRunItemId}/reanalyze?provider=claude`;
const res = await fetch(endpoint, { method: "POST" });
if (!res.ok) { if (!res.ok) {
const body = (await res.json().catch(() => null)) as { error?: string } | null; const body = (await res.json().catch(() => null)) as { error?: string } | null;
window.alert(body?.error ?? "Failed to re-run analysis"); window.alert(body?.error ?? "Failed to re-run analysis");
@@ -1530,16 +1533,18 @@ function ProductDetails({
} }
async function discoverDistributors() { async function discoverDistributors() {
if (effectiveRunItemId == null || findingDistributors) return; if (findingDistributors) return;
setFindingDistributors(true); setFindingDistributors(true);
try { try {
const res = await fetch(`/api/stalker/products/${effectiveRunItemId}/distributors`, { method: "POST" }); const endpoint = effectiveRunItemId == null
? `/api/stalker/products/by-asin/${encodeURIComponent(asin)}/distributors`
: `/api/stalker/products/${effectiveRunItemId}/distributors`;
const res = await fetch(endpoint, { method: "POST" });
if (!res.ok) { if (!res.ok) {
const body = (await res.json().catch(() => null)) as { error?: string } | null; const body = (await res.json().catch(() => null)) as { error?: string } | null;
window.alert(body?.error ?? "Failed to find distributors"); window.alert(body?.error ?? "Failed to find distributors");
} }
} catch { } catch {
// network error or timeout — job may have completed on the server anyway
} finally { } finally {
load(); load();
setFindingDistributors(false); setFindingDistributors(false);
@@ -1552,7 +1557,6 @@ function ProductDetails({
<div className="card"> <div className="card">
<div className="section-header"> <div className="section-header">
<h2>{data?.product.name ?? asin}</h2> <h2>{data?.product.name ?? asin}</h2>
{effectiveRunItemId != null && (
<div className="button-row"> <div className="button-row">
<button onClick={reanalyze} disabled={reanalyzing}> <button onClick={reanalyze} disabled={reanalyzing}>
{reanalyzing ? "Re-running..." : "Re-run analysis"} {reanalyzing ? "Re-running..." : "Re-run analysis"}
@@ -1561,7 +1565,6 @@ function ProductDetails({
{findingDistributors ? "Finding distributors..." : "Find distributors"} {findingDistributors ? "Finding distributors..." : "Find distributors"}
</button> </button>
</div> </div>
)}
</div> </div>
<div className="meta-grid" style={{ marginTop: 12 }}> <div className="meta-grid" style={{ marginTop: 12 }}>
<div className="meta"><strong>ASIN:</strong> <a href={`https://amazon.com/dp/${asin}`} target="_blank" rel="noreferrer">{asin}</a></div> <div className="meta"><strong>ASIN:</strong> <a href={`https://amazon.com/dp/${asin}`} target="_blank" rel="noreferrer">{asin}</a></div>