import index from "./web/index.html";
import { getDb, initDb } from "./database.ts";
import { fetchKeepaDataBatch } from "./keepa.ts";
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "./sp-api.ts";
import { analyzeProducts } from "./llm.ts";
import type { EnrichedProduct, ProductRecord, SpApiData } from "./types.ts";
type ProcessType = "lead_analysis" | "category_analysis";
type RunRecord = {
processType: ProcessType;
runId: number;
timestamp: string;
status: string;
jobType: string;
source: string | null;
output: string | null;
totalProducts: number;
fbaCount: number;
fbmCount: number;
skipCount: number;
};
type ProductListRecord = {
processType: ProcessType;
runId: number;
asin: string;
product_name: string | null;
brand: string | null;
category: string | null;
verdict: "FBA" | "FBM" | "SKIP";
confidence: number | null;
sellability_status: string | null;
monthly_sold: number | null;
seller_count: number | null;
amazon_is_seller: number | null;
amazon_buybox_share_pct_90d: number | null;
sales_rank: number | null;
current_price: number | null;
avg_price_90d: number | null;
reasoning: string | null;
fetched_at: string;
};
const DB_PATH = process.env.RESULTS_DB_PATH || "./results.db";
const DEFAULT_PAGE_SIZE = 25;
const MAX_PAGE_SIZE = 200;
const ASIN_PATTERN = /^[A-Z0-9]{10}$/;
initDb(DB_PATH);
const db = getDb(DB_PATH);
function json(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { "content-type": "application/json; charset=utf-8" },
});
}
function csv(text: string, filename: string): Response {
return new Response(text, {
status: 200,
headers: {
"content-type": "text/csv; charset=utf-8",
"content-disposition": `attachment; filename="${filename}"`,
},
});
}
function parseIntParam(value: string | null, fallback: number): number {
if (!value) return fallback;
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
return parsed;
}
function normalizeAsin(value: string): string {
return value.trim().toUpperCase();
}
function isValidAsin(value: string): boolean {
return ASIN_PATTERN.test(value);
}
function parseSort(
sortParam: string | null,
allowed: Set,
fallback: string,
): string {
if (!sortParam) return fallback;
const clauses = sortParam
.split(",")
.map((chunk) => chunk.trim())
.filter(Boolean)
.map((chunk) => {
const [fieldRaw, dirRaw] = chunk.split(":");
const field = fieldRaw?.trim();
const dir = dirRaw?.trim().toUpperCase() === "DESC" ? "DESC" : "ASC";
if (!field || !allowed.has(field)) return null;
return `${field} ${dir}`;
})
.filter((value): value is string => value !== null);
return clauses.length > 0 ? clauses.join(", ") : fallback;
}
function parseResultSort(
sortParam: string | null,
allowed: Set,
fallback: string,
): string {
if (!sortParam) return fallback;
const clauses = sortParam
.split(",")
.map((chunk) => chunk.trim())
.filter(Boolean)
.map((chunk) => {
const [fieldRaw, dirRaw] = chunk.split(":");
const field = fieldRaw?.trim();
const dir = dirRaw?.trim().toUpperCase() === "DESC" ? "DESC" : "ASC";
if (!field || !allowed.has(field)) return null;
if (field === "monthly_sold")
return `CAST(COALESCE(monthly_sold, 0) AS INTEGER) ${dir}`;
if (field === "amazon_is_seller")
return `CAST(COALESCE(amazon_is_seller, 0) AS INTEGER) ${dir}`;
if (field === "amazon_buybox_share_pct_90d") {
return `CAST(COALESCE(amazon_buybox_share_pct_90d, 0) AS REAL) ${dir}`;
}
return `${field} ${dir}`;
})
.filter((value): value is string => value !== null);
return clauses.length > 0 ? clauses.join(", ") : fallback;
}
function escapeCsvValue(value: unknown): string {
if (value === null || value === undefined) return "";
const text = String(value);
const escaped = text.replaceAll('"', '""');
return /[",\n]/.test(escaped) ? `"${escaped}"` : escaped;
}
function parseResultFilters(
processType: ProcessType,
runId: number,
filters: URLSearchParams,
) {
const q = filters.get("q")?.trim() || "";
const verdict = filters.get("verdict")?.trim();
const sellabilityStatus = filters.get("sellabilityStatus")?.trim();
const minConfidence = filters.get("minConfidence")?.trim();
const maxConfidence = filters.get("maxConfidence")?.trim();
const conditions: string[] = ["run_id = ?"];
const params: Array = [runId];
if (verdict) {
conditions.push("verdict = ?");
params.push(verdict);
}
if (sellabilityStatus) {
conditions.push("sellability_status = ?");
params.push(sellabilityStatus);
}
if (minConfidence) {
conditions.push("confidence >= ?");
params.push(Number(minConfidence));
}
if (maxConfidence) {
conditions.push("confidence <= ?");
params.push(Number(maxConfidence));
}
if (q) {
const wildcard = `%${q}%`;
if (processType === "lead_analysis") {
conditions.push(
"(asin LIKE ? OR product_name LIKE ? OR brand LIKE ? OR category LIKE ? OR reasoning LIKE ?)",
);
params.push(wildcard, wildcard, wildcard, wildcard, wildcard);
} else {
conditions.push(
"(asin LIKE ? OR name LIKE ? OR brand LIKE ? OR category LIKE ? OR reasoning LIKE ?)",
);
params.push(wildcard, wildcard, wildcard, wildcard, wildcard);
}
}
return {
where: `WHERE ${conditions.join(" AND ")}`,
params,
};
}
function getRuns(filters: URLSearchParams) {
const q = filters.get("q")?.trim() || "";
const processType = filters.get("processType")?.trim();
const status = filters.get("status")?.trim();
const startDate = filters.get("startDate")?.trim();
const endDate = filters.get("endDate")?.trim();
const page = parseIntParam(filters.get("page"), 1);
const pageSize = Math.min(
parseIntParam(filters.get("pageSize"), DEFAULT_PAGE_SIZE),
MAX_PAGE_SIZE,
);
const offset = (page - 1) * pageSize;
const allowedSort = new Set([
"timestamp",
"status",
"totalProducts",
"fbaCount",
"fbmCount",
"skipCount",
"runId",
"jobType",
]);
const orderBy = parseSort(
filters.get("sort"),
allowedSort,
"timestamp DESC, runId DESC",
);
const conditions: string[] = [];
const params: Array = [];
if (processType === "lead_analysis" || processType === "category_analysis") {
conditions.push("processType = ?");
params.push(processType);
}
if (status) {
conditions.push("status = ?");
params.push(status);
}
if (startDate) {
conditions.push("timestamp >= ?");
params.push(startDate);
}
if (endDate) {
conditions.push("timestamp <= ?");
params.push(endDate);
}
if (q) {
conditions.push(
"(jobType LIKE ? OR source LIKE ? OR output LIKE ? OR CAST(runId AS TEXT) LIKE ?)",
);
const wildcard = `%${q}%`;
params.push(wildcard, wildcard, wildcard, wildcard);
}
const where =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const baseUnion = `
SELECT
'lead_analysis' AS processType,
id AS runId,
timestamp,
'completed' AS status,
'lead_file_analysis' AS jobType,
input_file AS source,
output_file AS output,
COALESCE(total_products, 0) AS totalProducts,
COALESCE(fba_count, 0) AS fbaCount,
COALESCE(fbm_count, 0) AS fbmCount,
COALESCE(skip_count, 0) AS skipCount
FROM runs
UNION ALL
SELECT
'category_analysis' AS processType,
id AS runId,
run_timestamp AS timestamp,
status,
category_label AS jobType,
CAST(category_id AS TEXT) AS source,
NULL AS output,
COALESCE(top_asins_checked, 0) AS totalProducts,
COALESCE(fba_count, 0) AS fbaCount,
COALESCE(fbm_count, 0) AS fbmCount,
COALESCE(skip_count, 0) AS skipCount
FROM category_analysis_runs
`;
const totalRow = db
.query(`SELECT COUNT(*) as total FROM (${baseUnion}) all_runs ${where}`)
.get(...params) as { total: number };
const items = db
.query(
`SELECT * FROM (${baseUnion}) all_runs ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`,
)
.all(...params, pageSize, offset) as RunRecord[];
return {
items,
page,
pageSize,
total: totalRow.total,
totalPages: Math.max(1, Math.ceil(totalRow.total / pageSize)),
};
}
function getProductList(filters: URLSearchParams) {
const q = filters.get("q")?.trim() || "";
const verdict = filters.get("verdict")?.trim();
const page = parseIntParam(filters.get("page"), 1);
const pageSize = Math.min(
parseIntParam(filters.get("pageSize"), DEFAULT_PAGE_SIZE),
MAX_PAGE_SIZE,
);
const offset = (page - 1) * pageSize;
const conditions: string[] = [];
const params: Array = [];
if (verdict === "FBA" || verdict === "FBM" || verdict === "SKIP") {
conditions.push("verdict = ?");
params.push(verdict);
}
if (q) {
const wildcard = `%${q}%`;
conditions.push(
"(asin LIKE ? OR product_name LIKE ? OR brand LIKE ? OR category LIKE ?)",
);
params.push(wildcard, wildcard, wildcard, wildcard);
}
const where =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const allowedSort = new Set([
"asin",
"verdict",
"monthly_sold",
"seller_count",
"amazon_is_seller",
"amazon_buybox_share_pct_90d",
"sales_rank",
"current_price",
"product_name",
"brand",
"category",
"avg_price_90d",
"confidence",
"reasoning",
"fetched_at",
]);
const orderBy = parseResultSort(
filters.get("sort"),
allowedSort,
"CAST(COALESCE(monthly_sold, 0) AS INTEGER) DESC, fetched_at DESC",
);
const baseUnion = `
SELECT
'lead_analysis' AS processType,
run_id AS runId,
asin,
product_name,
brand,
category,
verdict,
confidence,
sellability_status,
monthly_sold,
sellers AS seller_count,
amazon_is_seller,
amazon_buybox_share_pct_90d,
sales_rank,
current_price,
avg_price_90d,
reasoning,
fetched_at
FROM results
UNION ALL
SELECT
'category_analysis' AS processType,
run_id AS runId,
asin,
name AS product_name,
brand,
category,
verdict,
confidence,
sellability_status,
monthly_sold,
seller_count,
amazon_is_seller,
amazon_buybox_share_pct_90d,
sales_rank,
current_price,
avg_price_90d,
reasoning,
fetched_at
FROM product_analysis_results
`;
const totalRow = db
.query(`SELECT COUNT(*) as total FROM (${baseUnion}) all_products ${where}`)
.get(...params) as { total: number };
const items = db
.query(
`SELECT * FROM (${baseUnion}) all_products ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`,
)
.all(...params, pageSize, offset) as ProductListRecord[];
return {
items,
page,
pageSize,
total: totalRow.total,
totalPages: Math.max(1, Math.ceil(totalRow.total / pageSize)),
};
}
function getRun(processType: ProcessType, runId: number) {
if (processType === "lead_analysis") {
const run = db
.query(
`SELECT
id AS runId,
timestamp,
'completed' AS status,
'lead_file_analysis' AS jobType,
input_file AS source,
output_file AS output,
COALESCE(total_products, 0) AS totalProducts,
COALESCE(fba_count, 0) AS fbaCount,
COALESCE(fbm_count, 0) AS fbmCount,
COALESCE(skip_count, 0) AS skipCount
FROM runs WHERE id = ?`,
)
.get(runId);
return run ?? null;
}
const run = db
.query(
`SELECT
id AS runId,
run_timestamp AS timestamp,
status,
category_label AS jobType,
CAST(category_id AS TEXT) AS source,
NULL AS output,
COALESCE(top_asins_checked, 0) AS totalProducts,
COALESCE(fba_count, 0) AS fbaCount,
COALESCE(fbm_count, 0) AS fbmCount,
COALESCE(skip_count, 0) AS skipCount,
error_message AS errorMessage,
available_asins AS availableAsins
FROM category_analysis_runs WHERE id = ?`,
)
.get(runId);
return run ?? null;
}
function getRunResults(
processType: ProcessType,
runId: number,
filters: URLSearchParams,
) {
const page = parseIntParam(filters.get("page"), 1);
const pageSize = Math.min(
parseIntParam(filters.get("pageSize"), DEFAULT_PAGE_SIZE),
MAX_PAGE_SIZE,
);
const offset = (page - 1) * pageSize;
const tableName =
processType === "lead_analysis" ? "results" : "product_analysis_results";
const productNameSelect =
processType === "lead_analysis" ? "product_name" : "name AS product_name";
const sellerCountSelect =
processType === "lead_analysis"
? "sellers AS seller_count"
: "seller_count";
const salesRankAvgSelect =
processType === "lead_analysis"
? "rank_avg_90d AS sales_rank_avg_90d"
: "sales_rank_avg_90d";
const { where, params } = parseResultFilters(processType, runId, filters);
const allowedSort = new Set([
"asin",
"product_name",
"brand",
"category",
"current_price",
"avg_price_90d",
"sales_rank",
"seller_count",
"amazon_is_seller",
"amazon_buybox_share_pct_90d",
"monthly_sold",
"verdict",
"confidence",
"fetched_at",
]);
const orderBy = parseResultSort(
filters.get("sort"),
allowedSort,
"CAST(COALESCE(monthly_sold, 0) AS INTEGER) DESC, asin ASC",
);
const totalRow = db
.query(`SELECT COUNT(*) as total FROM ${tableName} ${where}`)
.get(...params) as { total: number };
const items = db
.query(
`SELECT
id,
run_id,
asin,
${productNameSelect},
brand,
category,
unit_cost,
current_price,
avg_price_90d,
avg_price_90d_sheet,
selling_price_sheet,
sales_rank,
${salesRankAvgSelect},
${sellerCountSelect},
amazon_is_seller,
amazon_buybox_share_pct_90d,
monthly_sold,
rank_drops_30d,
rank_drops_90d,
fba_fee,
fbm_fee,
referral_percent,
can_sell,
sellability_status,
sellability_reason,
verdict,
confidence,
reasoning,
fetched_at
FROM ${tableName}
${where}
ORDER BY ${orderBy}
LIMIT ? OFFSET ?`,
)
.all(...params, pageSize, offset);
return {
items,
page,
pageSize,
total: totalRow.total,
totalPages: Math.max(1, Math.ceil(totalRow.total / pageSize)),
};
}
function deleteRun(processType: ProcessType, runId: number) {
if (processType === "lead_analysis") {
const resultRows = db
.query("DELETE FROM results WHERE run_id = ?")
.run(runId);
const runRows = db.query("DELETE FROM runs WHERE id = ?").run(runId);
return {
deletedRun: runRows.changes > 0,
deletedResults: resultRows.changes,
};
}
const resultRows = db
.query("DELETE FROM product_analysis_results WHERE run_id = ?")
.run(runId);
const runRows = db
.query("DELETE FROM category_analysis_runs WHERE id = ?")
.run(runId);
return {
deletedRun: runRows.changes > 0,
deletedResults: resultRows.changes,
};
}
function exportRunResultsCsv(
processType: ProcessType,
runId: number,
filters: URLSearchParams,
) {
const tableName =
processType === "lead_analysis" ? "results" : "product_analysis_results";
const productNameSelect =
processType === "lead_analysis" ? "product_name" : "name AS product_name";
const sellerCountSelect =
processType === "lead_analysis"
? "sellers AS seller_count"
: "seller_count";
const salesRankAvgSelect =
processType === "lead_analysis"
? "rank_avg_90d AS sales_rank_avg_90d"
: "sales_rank_avg_90d";
const { where, params } = parseResultFilters(processType, runId, filters);
const allowedSort = new Set([
"asin",
"product_name",
"brand",
"category",
"current_price",
"avg_price_90d",
"sales_rank",
"seller_count",
"amazon_is_seller",
"amazon_buybox_share_pct_90d",
"monthly_sold",
"verdict",
"confidence",
"fetched_at",
]);
const orderBy = parseResultSort(
filters.get("sort"),
allowedSort,
"CAST(COALESCE(monthly_sold, 0) AS INTEGER) DESC, asin ASC",
);
const rows = db
.query(
`SELECT
run_id,
asin,
${productNameSelect},
brand,
category,
unit_cost,
current_price,
avg_price_90d,
${salesRankAvgSelect},
${sellerCountSelect},
amazon_is_seller,
amazon_buybox_share_pct_90d,
monthly_sold,
sellability_status,
verdict,
confidence,
reasoning,
fetched_at
FROM ${tableName}
${where}
ORDER BY ${orderBy}`,
)
.all(...params) as Array>;
const headers = [
"run_id",
"asin",
"product_name",
"brand",
"category",
"unit_cost",
"current_price",
"avg_price_90d",
"sales_rank_avg_90d",
"seller_count",
"amazon_is_seller",
"amazon_buybox_share_pct_90d",
"monthly_sold",
"sellability_status",
"verdict",
"confidence",
"reasoning",
"fetched_at",
];
const lines = [headers.join(",")];
for (const row of rows) {
lines.push(headers.map((h) => escapeCsvValue(row[h])).join(","));
}
return lines.join("\n");
}
type ReanalyzeSourceRow = {
asin: string;
product_name: string | null;
brand: string | null;
category: string | null;
unit_cost: number | null;
sales_rank: number | null;
avg_price_90d_sheet: number | null;
selling_price_sheet: number | null;
fba_net_sheet: number | null;
gross_profit_dollar: number | null;
gross_profit_pct: number | null;
net_profit_sheet: number | null;
roi_sheet: number | null;
moq: number | null;
moq_cost: number | null;
qty_available: number | null;
supplier: string | null;
source_url: string | null;
asin_link: string | null;
promo_coupon_code: string | null;
notes: string | null;
lead_date: string | null;
};
function getReanalyzeSourceRow(
processType: ProcessType,
runId: number,
asin: string,
): ReanalyzeSourceRow | null {
if (processType === "lead_analysis") {
return (
(db
.query(
`SELECT
asin,
product_name,
brand,
category,
unit_cost,
sales_rank,
avg_price_90d_sheet,
selling_price_sheet,
fba_net_sheet,
gross_profit_dollar,
gross_profit_pct,
net_profit_sheet,
roi_sheet,
moq,
moq_cost,
qty_available,
supplier,
source_url,
asin_link,
promo_coupon_code,
notes,
lead_date
FROM results
WHERE run_id = ? AND asin = ?
LIMIT 1`,
)
.get(runId, asin) as ReanalyzeSourceRow | null) ?? null
);
}
return (
(db
.query(
`SELECT
asin,
name AS product_name,
brand,
category,
unit_cost,
sales_rank,
avg_price_90d_sheet,
selling_price_sheet,
NULL AS fba_net_sheet,
NULL AS gross_profit_dollar,
NULL AS gross_profit_pct,
NULL AS net_profit_sheet,
NULL AS roi_sheet,
NULL AS moq,
NULL AS moq_cost,
NULL AS qty_available,
NULL AS supplier,
NULL AS source_url,
NULL AS asin_link,
NULL AS promo_coupon_code,
NULL AS notes,
NULL AS lead_date
FROM product_analysis_results
WHERE run_id = ? AND asin = ?
LIMIT 1`,
)
.get(runId, asin) as ReanalyzeSourceRow | null) ?? null
);
}
function toProductRecord(row: ReanalyzeSourceRow): ProductRecord {
return {
asin: row.asin,
name: row.product_name ?? row.asin,
unitCost: row.unit_cost ?? 0,
brand: row.brand ?? undefined,
category: row.category ?? undefined,
amazonRank: row.sales_rank ?? undefined,
avgPrice90FromSheet: row.avg_price_90d_sheet ?? undefined,
sellingPriceFromSheet: row.selling_price_sheet ?? undefined,
fbaNet: row.fba_net_sheet ?? undefined,
grossProfit: row.gross_profit_dollar ?? undefined,
grossProfitPct: row.gross_profit_pct ?? undefined,
netProfitFromSheet: row.net_profit_sheet ?? undefined,
roiFromSheet: row.roi_sheet ?? undefined,
moq: row.moq ?? undefined,
moqCost: row.moq_cost ?? undefined,
totalQtyAvail: row.qty_available ?? undefined,
supplier: row.supplier ?? undefined,
sourceUrl: row.source_url ?? undefined,
asinLink: row.asin_link ?? undefined,
promoCouponCode: row.promo_coupon_code ?? undefined,
notes: row.notes ?? undefined,
leadDate: row.lead_date ?? undefined,
};
}
function unknownSpApiData(): SpApiData {
return {
fbaFee: 5.0,
fbmFee: 1.5,
referralFeePercent: 15,
estimatedSalePrice: 0,
canSell: null,
sellabilityStatus: "unknown",
sellabilityReason: "Sellability check returned no result",
};
}
function refreshRunCounts(processType: ProcessType, runId: number): void {
if (processType === "lead_analysis") {
const stats = db
.query(
`SELECT
COUNT(*) AS total,
SUM(CASE WHEN verdict = 'FBA' THEN 1 ELSE 0 END) AS fba,
SUM(CASE WHEN verdict = 'FBM' THEN 1 ELSE 0 END) AS fbm,
SUM(CASE WHEN verdict = 'SKIP' THEN 1 ELSE 0 END) AS skip
FROM results
WHERE run_id = ?`,
)
.get(runId) as {
total: number;
fba: number | null;
fbm: number | null;
skip: number | null;
};
db.query(
`UPDATE runs
SET total_products = ?, fba_count = ?, fbm_count = ?, skip_count = ?
WHERE id = ?`,
).run(
stats.total ?? 0,
stats.fba ?? 0,
stats.fbm ?? 0,
stats.skip ?? 0,
runId,
);
return;
}
const stats = db
.query(
`SELECT
COUNT(*) AS available,
SUM(CASE WHEN verdict = 'FBA' THEN 1 ELSE 0 END) AS fba,
SUM(CASE WHEN verdict = 'FBM' THEN 1 ELSE 0 END) AS fbm,
SUM(CASE WHEN verdict = 'SKIP' THEN 1 ELSE 0 END) AS skip
FROM product_analysis_results
WHERE run_id = ?`,
)
.get(runId) as {
available: number;
fba: number | null;
fbm: number | null;
skip: number | null;
};
db.query(
`UPDATE category_analysis_runs
SET available_asins = ?, fba_count = ?, fbm_count = ?, skip_count = ?
WHERE id = ?`,
).run(
stats.available ?? 0,
stats.fba ?? 0,
stats.fbm ?? 0,
stats.skip ?? 0,
runId,
);
}
async function reanalyzeSingleAsin(
processType: ProcessType,
runId: number,
asin: string,
): Promise<{
asin: string;
runId: number;
processType: ProcessType;
fetchedAt: string;
}> {
const row = getReanalyzeSourceRow(processType, runId, asin);
if (!row) {
throw new Error("Result row not found");
}
const productRecord = toProductRecord(row);
let keepa = null;
try {
const keepaMap = await fetchKeepaDataBatch([asin]);
keepa = keepaMap.get(asin) ?? null;
} catch {
keepa = null;
}
const sellabilityMap = await fetchSellabilityBatch([asin]);
const sellability = sellabilityMap.get(asin) ?? {
canSell: null,
sellabilityStatus: "unknown" as const,
sellabilityReason: "Sellability check returned no result",
};
const spApi = await fetchSpApiPricingAndFees(asin, sellability);
if (keepa?.currentPrice && spApi.estimatedSalePrice === 0) {
spApi.estimatedSalePrice = keepa.currentPrice;
}
const enriched: EnrichedProduct = {
record: productRecord,
keepa,
spApi: spApi ?? unknownSpApiData(),
fetchedAt: new Date().toISOString(),
};
const verdicts = await analyzeProducts([enriched]);
const verdict = verdicts[0] ?? {
asin,
verdict: "SKIP" as const,
confidence: 0,
reasoning: "LLM analysis returned no verdict",
};
const amazonIsSeller =
keepa?.amazonIsSeller == null ? null : keepa.amazonIsSeller ? 1 : 0;
const fetchedAt = enriched.fetchedAt;
if (processType === "lead_analysis") {
db.query(
`UPDATE results SET
current_price = ?,
avg_price_90d = ?,
sales_rank = ?,
rank_avg_90d = ?,
sellers = ?,
amazon_is_seller = ?,
amazon_buybox_share_pct_90d = ?,
monthly_sold = ?,
rank_drops_30d = ?,
rank_drops_90d = ?,
fba_fee = ?,
fbm_fee = ?,
referral_percent = ?,
can_sell = ?,
sellability_status = ?,
sellability_reason = ?,
verdict = ?,
confidence = ?,
reasoning = ?,
fetched_at = ?
WHERE run_id = ? AND asin = ?`,
).run(
keepa?.currentPrice ?? null,
keepa?.avgPrice90 ?? null,
keepa?.salesRank ?? row.sales_rank ?? null,
keepa?.salesRankAvg90 ?? null,
keepa?.sellerCount ?? null,
amazonIsSeller,
keepa?.amazonBuyboxSharePct90d ?? null,
keepa?.monthlySold ?? null,
keepa?.salesRankDrops30 ?? null,
keepa?.salesRankDrops90 ?? null,
spApi.fbaFee,
spApi.fbmFee,
spApi.referralFeePercent,
spApi.canSell == null ? null : spApi.canSell ? 1 : 0,
spApi.sellabilityStatus,
spApi.sellabilityReason ?? null,
verdict.verdict,
verdict.confidence,
verdict.reasoning,
fetchedAt,
runId,
asin,
);
} else {
db.query(
`UPDATE product_analysis_results SET
current_price = ?,
avg_price_90d = ?,
sales_rank = ?,
sales_rank_avg_90d = ?,
seller_count = ?,
amazon_is_seller = ?,
amazon_buybox_share_pct_90d = ?,
monthly_sold = ?,
rank_drops_30d = ?,
rank_drops_90d = ?,
fba_fee = ?,
fbm_fee = ?,
referral_percent = ?,
can_sell = ?,
sellability_status = ?,
sellability_reason = ?,
verdict = ?,
confidence = ?,
reasoning = ?,
fetched_at = ?
WHERE run_id = ? AND asin = ?`,
).run(
keepa?.currentPrice ?? null,
keepa?.avgPrice90 ?? null,
keepa?.salesRank ?? row.sales_rank ?? null,
keepa?.salesRankAvg90 ?? null,
keepa?.sellerCount ?? null,
amazonIsSeller,
keepa?.amazonBuyboxSharePct90d ?? null,
keepa?.monthlySold ?? null,
keepa?.salesRankDrops30 ?? null,
keepa?.salesRankDrops90 ?? null,
spApi.fbaFee,
spApi.fbmFee,
spApi.referralFeePercent,
spApi.canSell == null ? null : spApi.canSell ? 1 : 0,
spApi.sellabilityStatus,
spApi.sellabilityReason ?? null,
verdict.verdict,
verdict.confidence,
verdict.reasoning,
fetchedAt,
runId,
asin,
);
}
refreshRunCounts(processType, runId);
return { asin, runId, processType, fetchedAt };
}
const server = Bun.serve({
port: Number(process.env.PORT || "3000"),
routes: {
"/": index,
"/products": index,
"/runs/:processType/:runId": index,
"/api/runs": (req) => {
const url = new URL(req.url);
return json(getRuns(url.searchParams));
},
"/api/products": (req) => {
const url = new URL(req.url);
return json(getProductList(url.searchParams));
},
"/api/runs/:processType/:runId": (req) => {
const processType = req.params.processType as ProcessType;
const runId = Number(req.params.runId);
if (
!(
processType === "lead_analysis" || processType === "category_analysis"
) ||
!Number.isInteger(runId)
) {
return json({ error: "Invalid run identifier" }, 400);
}
if (req.method === "DELETE") {
const deleted = deleteRun(processType, runId);
if (!deleted.deletedRun) return json({ error: "Run not found" }, 404);
return json(deleted);
}
const run = getRun(processType, runId);
if (!run) return json({ error: "Run not found" }, 404);
const summary = {
totalProducts: (run as { totalProducts: number }).totalProducts,
fbaCount: (run as { fbaCount: number }).fbaCount,
fbmCount: (run as { fbmCount: number }).fbmCount,
skipCount: (run as { skipCount: number }).skipCount,
};
return json({ processType, ...run, summary });
},
"/api/runs/:processType/:runId/results": (req) => {
const processType = req.params.processType as ProcessType;
const runId = Number(req.params.runId);
if (
!(
processType === "lead_analysis" || processType === "category_analysis"
) ||
!Number.isInteger(runId)
) {
return json({ error: "Invalid run identifier" }, 400);
}
const url = new URL(req.url);
const payload = getRunResults(processType, runId, url.searchParams);
return json(payload);
},
"/api/runs/:processType/:runId/asins/:asin/reanalyze": async (req) => {
const processType = req.params.processType as ProcessType;
const runId = Number(req.params.runId);
const asin = normalizeAsin(req.params.asin);
if (
!(
processType === "lead_analysis" || processType === "category_analysis"
) ||
!Number.isInteger(runId)
) {
return json({ error: "Invalid run identifier" }, 400);
}
if (req.method !== "POST") {
return json({ error: "Method not allowed" }, 405);
}
if (!isValidAsin(asin)) {
return json({ error: "Invalid ASIN" }, 400);
}
try {
const result = await reanalyzeSingleAsin(processType, runId, asin);
return json(result);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message === "Result row not found") {
return json({ error: message }, 404);
}
return json({ error: message }, 500);
}
},
"/api/runs/:processType/:runId/export.csv": (req) => {
const processType = req.params.processType as ProcessType;
const runId = Number(req.params.runId);
if (
!(
processType === "lead_analysis" || processType === "category_analysis"
) ||
!Number.isInteger(runId)
) {
return json({ error: "Invalid run identifier" }, 400);
}
const url = new URL(req.url);
const csvText = exportRunResultsCsv(processType, runId, url.searchParams);
return csv(csvText, `run-${processType}-${runId}.csv`);
},
},
fetch() {
return json({ error: "Not found" }, 404);
},
development: {
hmr: true,
console: true,
},
});
console.log(`Results viewer running on http://localhost:${server.port}`);