fix: wire catalog add buttons, fix Trans bold rendering, lint cleanup
Some checks failed
CI / ci (push) Failing after 1m44s
CI / e2e (push) Has been skipped
CI / deploy (push) Has been skipped

- CatalogSearchOverlay: replace handleAddStub with real openAddToCollection/openAddToThread routing based on catalogSearchMode
- ConfirmDialog + __root.tsx: swap t() for Trans component on deleteItemMessage, deleteCandidateMessage, pickWinnerMessage — fixes <bold> rendering as literal text
- Biome format pass: fix 23 lint/format errors across scripts, services, tests
- Planning: mark all UAT and verification gaps resolved for phases 07, 11, 16, 20, 21, 22, 24, 32, 34; close debug sessions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 15:36:16 +02:00
parent 16058d0f4d
commit 4ccbb2b070
40 changed files with 807 additions and 227 deletions

View File

@@ -30,8 +30,14 @@ const dryRun = args["dry-run"] === "true";
async function listActiveManufacturers(targetTier: number) {
const res = await fetch(`${GEARBOX_URL}/api/manufacturers`);
if (!res.ok) throw new Error(`Failed to list manufacturers: HTTP ${res.status}`);
const all = await res.json() as Array<{ slug: string; tier: number; active: boolean; name: string }>;
if (!res.ok)
throw new Error(`Failed to list manufacturers: HTTP ${res.status}`);
const all = (await res.json()) as Array<{
slug: string;
tier: number;
active: boolean;
name: string;
}>;
return all.filter((m) => m.active && m.tier === targetTier);
}
@@ -42,9 +48,15 @@ async function main() {
}
const manufacturers = await listActiveManufacturers(tier);
console.log(`Found ${manufacturers.length} active tier-${tier} manufacturers\n`);
console.log(
`Found ${manufacturers.length} active tier-${tier} manufacturers\n`,
);
const results: Array<{ slug: string; status: "ok" | "error"; error?: string }> = [];
const results: Array<{
slug: string;
status: "ok" | "error";
error?: string;
}> = [];
for (const m of manufacturers) {
console.log(`\n${"─".repeat(50)}`);
@@ -52,7 +64,13 @@ async function main() {
try {
const extraArgs = dryRun ? ["--dry-run"] : [];
const proc = Bun.spawn(
["bun", "run", "scripts/crawl-manufacturer.ts", `--manufacturer=${m.slug}`, ...extraArgs],
[
"bun",
"run",
"scripts/crawl-manufacturer.ts",
`--manufacturer=${m.slug}`,
...extraArgs,
],
{ stdout: "inherit", stderr: "inherit", env: process.env },
);
const exitCode = await proc.exited;
@@ -60,7 +78,11 @@ async function main() {
results.push({ slug: m.slug, status: "ok" });
} catch (err) {
console.error(` ERROR: ${(err as Error).message}`);
results.push({ slug: m.slug, status: "error", error: (err as Error).message });
results.push({
slug: m.slug,
status: "error",
error: (err as Error).message,
});
}
}

View File

@@ -38,7 +38,9 @@ const manufacturerSlug = args["manufacturer"];
const dryRun = args["dry-run"] === "true";
if (!manufacturerSlug) {
console.error("Usage: bun run scripts/crawl-manufacturer.ts --manufacturer=<slug>");
console.error(
"Usage: bun run scripts/crawl-manufacturer.ts --manufacturer=<slug>",
);
process.exit(1);
}
@@ -96,7 +98,9 @@ async function fetchPage(url: string): Promise<string> {
// ── Build system prompt ───────────────────────────────────────────
function buildSystemPrompt(manufacturer: Awaited<ReturnType<typeof fetchManufacturer>>) {
function buildSystemPrompt(
manufacturer: Awaited<ReturnType<typeof fetchManufacturer>>,
) {
return `You are a product data extraction agent for GearBox, a gear management app for bikepacking, cycling, and hiking.
Your task: crawl ${manufacturer.name}'s website (${manufacturer.website}) and extract their complete product catalog.
@@ -148,13 +152,16 @@ type CatalogItem = {
tags: string[];
};
async function runCrawlAgent(manufacturer: Awaited<ReturnType<typeof fetchManufacturer>>): Promise<CatalogItem[]> {
async function runCrawlAgent(
manufacturer: Awaited<ReturnType<typeof fetchManufacturer>>,
): Promise<CatalogItem[]> {
const client = new Anthropic({ apiKey: ANTHROPIC_API_KEY });
const tools: Anthropic.Tool[] = [
{
name: "fetch_page",
description: "Fetch the HTML content of a URL. Use this to explore the manufacturer's website and product pages.",
description:
"Fetch the HTML content of a URL. Use this to explore the manufacturer's website and product pages.",
input_schema: {
type: "object" as const,
properties: {
@@ -221,14 +228,20 @@ async function runCrawlAgent(manufacturer: Awaited<ReturnType<typeof fetchManufa
messages.push({ role: "user", content: toolResults });
}
throw new Error(`Agent exceeded ${MAX_TOOL_ROUNDS} tool rounds without finishing`);
throw new Error(
`Agent exceeded ${MAX_TOOL_ROUNDS} tool rounds without finishing`,
);
}
function parseAgentOutput(text: string): CatalogItem[] {
// Handle agent wrapping output in markdown code blocks
const cleaned = text.replace(/^```json\s*/i, "").replace(/\s*```$/i, "").trim();
const cleaned = text
.replace(/^```json\s*/i, "")
.replace(/\s*```$/i, "")
.trim();
const parsed = JSON.parse(cleaned);
if (!Array.isArray(parsed)) throw new Error("Agent output is not a JSON array");
if (!Array.isArray(parsed))
throw new Error("Agent output is not a JSON array");
return parsed;
}
@@ -269,10 +282,12 @@ async function upsertItems(
throw new Error(`Bulk upsert failed (HTTP ${res.status}): ${err}`);
}
const result = await res.json() as { created: number; updated: number };
const result = (await res.json()) as { created: number; updated: number };
totalCreated += result.created;
totalUpdated += result.updated;
console.log(` batch ${Math.floor(i / 100) + 1}: +${result.created} new, ~${result.updated} updated`);
console.log(
` batch ${Math.floor(i / 100) + 1}: +${result.created} new, ~${result.updated} updated`,
);
}
return { created: totalCreated, updated: totalUpdated };

View File

@@ -3,18 +3,18 @@
* These are the only valid values the ingestion agent should use.
*/
export const CATEGORIES = [
"bags", // bikepacking bags, dry bags, stuff sacks
"shelters", // tents, bivys, tarps, hammocks
"sleep", // sleeping bags, quilts, pads, pillows
"cooking", // stoves, cookware, mugs, utensils
"lighting", // headlamps, bike lights, lanterns
"water", // filters, bottles, bladders
"electronics", // power banks, solar panels, GPS, bike computers
"tools", // multi-tools, pumps, repair kits, locks
"clothing", // jackets, base layers, gloves, shoes
"navigation", // GPS devices, maps, compasses
"bikes", // complete bikes
"components", // drivetrain, brakes, wheels, handlebars, saddles, stems
"bags", // bikepacking bags, dry bags, stuff sacks
"shelters", // tents, bivys, tarps, hammocks
"sleep", // sleeping bags, quilts, pads, pillows
"cooking", // stoves, cookware, mugs, utensils
"lighting", // headlamps, bike lights, lanterns
"water", // filters, bottles, bladders
"electronics", // power banks, solar panels, GPS, bike computers
"tools", // multi-tools, pumps, repair kits, locks
"clothing", // jackets, base layers, gloves, shoes
"navigation", // GPS devices, maps, compasses
"bikes", // complete bikes
"components", // drivetrain, brakes, wheels, handlebars, saddles, stems
] as const;
export type Category = (typeof CATEGORIES)[number];

View File

@@ -5,27 +5,65 @@
*/
export const TAGS = [
// Activity
"bikepacking", "cycling", "hiking", "backpacking", "camping", "climbing",
"mountaineering", "road-cycling", "gravel", "running", "trail-running",
"bikepacking",
"cycling",
"hiking",
"backpacking",
"camping",
"climbing",
"mountaineering",
"road-cycling",
"gravel",
"running",
"trail-running",
// Bag subtypes
"handlebar-bag", "framebag", "saddlebag", "top-tube-bag", "stem-bag",
"fork-bag", "feed-bag", "dry-bag", "stuff-sack", "bike-bag",
"handlebar-bag",
"framebag",
"saddlebag",
"top-tube-bag",
"stem-bag",
"fork-bag",
"feed-bag",
"dry-bag",
"stuff-sack",
"bike-bag",
// Shelter subtypes
"tent", "bivy", "tarp", "hammock",
"tent",
"bivy",
"tarp",
"hammock",
// Sleep subtypes
"sleeping-bag", "sleeping-pad", "quilt", "pillow",
"sleeping-bag",
"sleeping-pad",
"quilt",
"pillow",
// Cooking subtypes
"stove", "cookware", "mug", "utensils",
"stove",
"cookware",
"mug",
"utensils",
// Water subtypes
"water-filter", "water-bottle",
"water-filter",
"water-bottle",
// Lighting subtypes
"headlamp", "bike-light", "lantern",
"headlamp",
"bike-light",
"lantern",
// Electronics subtypes
"gps", "bike-computer", "power-bank", "solar-panel",
"gps",
"bike-computer",
"power-bank",
"solar-panel",
// Tools subtypes
"multi-tool", "pump", "repair-kit", "lock",
"multi-tool",
"pump",
"repair-kit",
"lock",
// Clothing subtypes
"rain-jacket", "base-layer", "gloves", "shoe",
"rain-jacket",
"base-layer",
"gloves",
"shoe",
] as const;
export type Tag = (typeof TAGS)[number];