diff --git a/services/content/src/sync/jobs/homebox.ts b/services/content/src/sync/jobs/homebox.ts index 65f0d01..b37b3d7 100644 --- a/services/content/src/sync/jobs/homebox.ts +++ b/services/content/src/sync/jobs/homebox.ts @@ -5,7 +5,8 @@ * API (Homebox's REST API, v1): * - `POST /api/v1/users/login` with username and password gives a token * (already prefixed `Bearer `), sent as `Authorization`. - * - `GET /api/v1/labels` and `GET /api/v1/locations` name the ids to filter on. + * - `GET /api/v1/labels` (`/api/v1/tags` on newer releases) and `GET /api/v1/locations` + * name the ids to filter on. * - `GET /api/v1/items?labels=&page=&pageSize=` (or `locations=`) * lists the items, each with name, quantity and description. * @@ -84,7 +85,10 @@ export function materialOptions(items: HomeboxItem[]): StockOption[] { } async function call(ctx: SyncContext, url: string, init: RequestInit = {}) { - const res = await ctx.fetch(url, init); + return checked(await ctx.fetch(url, init)); +} + +async function checked(res: Response) { if (res.status === 401 || res.status === 403) throw new Error(`Homebox refused the request (${res.status}); check HOMEBOX_USER and HOMEBOX_PASS`); if (!res.ok) throw new Error(`Homebox answered ${res.status}`); return res.json() as Promise; @@ -110,9 +114,14 @@ export function homeboxJob(o: HomeboxOptions): SyncJob { ...(o.locations ?? []).map((f) => ({ kind: "locations" as const, f })), ]; const ids = { labels: new Map(), locations: new Map() }; - for (const kind of ["labels", "locations"] as const) { - if (filters.some((x) => x.kind === kind)) ids[kind] = parseNamed(await call(ctx, api(`/${kind}`), { headers })); + // Newer Homebox releases call labels "tags" (`/api/v1/tags`, `items?tags=`). + let labelParam = "labels"; + if (filters.some((x) => x.kind === "labels")) { + const res = await ctx.fetch(api("/labels"), { headers }); + if (res.status === 404) labelParam = "tags"; + ids.labels = parseNamed(res.status === 404 ? await call(ctx, api("/tags"), { headers }) : await checked(res)); } + if (filters.some((x) => x.kind === "locations")) ids.locations = parseNamed(await call(ctx, api("/locations"), { headers })); const byGroup = new Map(); const unknown: string[] = []; @@ -124,7 +133,7 @@ export function homeboxJob(o: HomeboxOptions): SyncJob { } const items = byGroup.get(f.group) ?? []; for (let page = 1; page <= 20; page++) { - const p = new URLSearchParams({ [kind]: id, page: String(page), pageSize: "100" }); + const p = new URLSearchParams({ [kind === "labels" ? labelParam : kind]: id, page: String(page), pageSize: "100" }); const r = parseItems(await call(ctx, api(`/items?${p}`), { headers })); items.push(...r.items); if (r.items.length < 100 || page * 100 >= r.total) break; diff --git a/services/content/test/sync/homelab.test.ts b/services/content/test/sync/homelab.test.ts index 23a1b08..4275ad6 100644 --- a/services/content/test/sync/homelab.test.ts +++ b/services/content/test/sync/homelab.test.ts @@ -375,6 +375,32 @@ describe("Homebox", () => { const missing = await scheduler(homeboxJob({ url: "http://homebox:7745", user: "u", pass: "p", labels: parseFilters("vinyl") }), f.fn).runNow("homebox"); expect(missing).toMatchObject({ ok: false, message: expect.stringContaining("vinyl") }); }); + + it("falls back to tags on Homebox releases without /labels", async () => { + r.put({ + type: "product-line", + title: "Wallet", + category: "Leather", + mode: "made to order", + channel: "here", + summary: "s", + slots: { open: 1, total: 2 }, + priceTiers: [], + options: [{ group: "Leather", name: "Chrome tan, Black" }], + publish: true, + }); + r.service.scan(); + const f = fakeFetch((url) => { + if (url.endsWith("/api/v1/users/login")) return ok({ token: "Bearer hb-token", expiresAt: "2026-10-12T00:00:00Z" }); + if (url.endsWith("/api/v1/tags")) return ok(fixture("homebox-labels.sample.json")); + if (url.includes("/api/v1/items?")) { + return new URL(url).searchParams.get("tags")?.endsWith("01") ? ok(fixture("homebox-items.sample.json")) : ok({ items: [], total: 0 }); + } + return ok({}, 404); + }); + const report = await scheduler(homeboxJob({ url: "http://homebox:7745", user: "u", pass: "p", labels: parseFilters("leather,paper") }), f.fn).runNow("homebox"); + expect(report).toMatchObject({ ok: true, counts: { lines: 1, leather: 1, paper: 0 } }); + }); }); // ---------------------------------------------------------------- settings