From 8acaeddda5e4af9fbc5720eef8df0a74c2ca1e51 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 10 Oct 2026 13:56:32 -0400 Subject: [PATCH] =?UTF-8?q?fix(web):=20detail=20banner=20=E2=80=94=20a=20f?= =?UTF-8?q?ailed=20annotation=20fetch=20must=20not=20wipe=20its=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetchBannerAnnotations published the rebuilt rows unconditionally, with a failed endpoint's list defaulted to empty — so one failing request (a single handler 500, an auth blip on one call) silently wiped that annotation kind from the banner while the other kind stayed, and the empty state could claim "No notes or highlights on this book yet" over real data. A full outage was already safe (the thrown fetch takes the catch path, which keeps previous rows and toasts) — the partial case was the hole, spotted while mirroring the app's fetch-wipe fix (bookhoard-app 07bbb4c) onto the web. Both responses must now succeed before the rows are published; otherwise the previous rows stay and the next banner open retries (bannerLoaded only sets on success). Verified on the emulator: banner renders 3 rows, a full outage keeps them with bannerError recorded, and a post-recovery refetch re-renders from live data. --- web/src/book-detail.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/web/src/book-detail.ts b/web/src/book-detail.ts index bb35c84f..b1f48d76 100644 --- a/web/src/book-detail.ts +++ b/web/src/book-detail.ts @@ -210,8 +210,20 @@ Alpine.data("bookDetail", () => { headers: { Authorization: getAuthHeader() }, }), ]); - const notes = noteResp.ok ? await noteResp.json() : []; - const highlights = hlResp.ok ? await hlResp.json() : []; + // A partial failure must not read as "no annotations": a failed + // endpoint used to be published as an empty list, silently wiping + // that kind from the banner while the other kind stayed (found + // auditing the app's fetch-wipe fix, bookhoard-app 07bbb4c). + // Keep the previous rows; the next banner open retries. + if (!noteResp.ok || !hlResp.ok) { + console.warn( + `banner annotation load failed ` + + `(notes ${noteResp.status}, highlights ${hlResp.status}) — keeping previous`, + ); + return; + } + const notes = await noteResp.json(); + const highlights = await hlResp.json(); const built = buildAnnotationRows(notes, highlights); this.bannerRows = built.rows; this.bannerSources = built.sources;