feat: implement reanalyze and distributor discovery endpoints for Stalker products by ASIN
This commit is contained in:
@@ -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)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user