ab83287b5deaba038adc9f6891c212fcce6dce16
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ab83287b5d |
Move Open Library lookup to the Editions API; stop asking Google Books for its placeholder
Two user-reported shelf-testing symptoms, both a third party answering
misleadingly and the app believing it.
1. Open Library's /api/books?bibkeys=... now 404s for EVERY ISBN, including
books OL demonstrably still holds, with OL's own x-ol-stats header on the
response while /isbn/, /search.json, /api/volumes/brief and covers.* all
serve normally. OL's docs call it the "Legacy Books API" that "may be phased
out" and it is gone from their API index, so this reads as a retirement
rather than an outage. The app had been effectively single-sourced on Google
Books since it broke: every failure the user saw was a book GB lacks.
Lookup now uses /isbn/{isbn}.json — current, non-legacy, edition-level, and
the only option of the three that carries a description. /api/volumes/brief
is a near drop-in for the old response shape and was rejected precisely
because it is also legacy.
Its costs, all handled: authors are references, so AuthorNameCache resolves
and caches them for the process lifetime (books by one author get scanned in
runs off one shelf); an edition may carry NO authors, in which case they live
on the work — 9780898707168 on the user's own shelf is exactly this, so
without the work fallback the move would have silently dropped its author;
author/work requests are best-effort and can only degrade a record, never
turn Found into Unavailable.
404 on this endpoint is authoritative NotFound. The legacy endpoint reported
a miss as 200 with an empty object, which is why every non-2xx there was a
failure. Every other non-2xx still is.
2. Google Books answers zoom=2 with a grey "image not available" PNG at HTTP
200 — not a 404 — for any volume it holds no full preview of. Coil loads it
as a success, so BookCover's placeholder never fires and the cover pipeline
uploads Google's placeholder to PocketBase as the book's cover. Measured over
18 real volumes: 11 placeholders at zoom=2, 0 at zoom=1&w=400. zoom=0/3/6 are
placeholders too. normalizeCoverUrl now pins zoom=1, adds w=400 and strips
edge=curl.
SPEC.md's "Book metadata lookup" is rewritten with both rules and the evidence
for them — it was the source of the zoom=2 instruction, and would otherwise be
the reason someone restores it.
Also fixed, because it blocked verification: LibraryViewModelTest never cleared
the view models it built, and LibraryViewModel's eleven WhileSubscribed(5_000)
flows kept running five seconds into later tests, racing resetMain(). It now
cancels each viewModelScope in tearDown.
NOT fixed, reported instead: AddBookViewModel.performSave's in-flight guard is a
check-then-act and two coroutines can both pass it. Unrelated to this change
(that VM has no metadata dependency) and out of scope. See HAZARD #13.
Verified: assembleDebug exit 0; testDebugUnitTest --rerun-tasks 378 tests,
2 skipped, 0 failures (was 355); verifyPaparazziDebug exit 0, no pixels moved;
0 "always 'false'" warnings; no build files touched. LIVE_METADATA=1 live test
ran (not skipped): 9/9 Found with cover art, with the GB key absent, so Open
Library alone answered through the new endpoint.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
486f6ebc48 |
Use the Google Books API key: the fallback source actually answers now
The user obtained a restricted Google Books key. Keyless requests 429 for every caller on the internet — all anonymous traffic bills to one shared Google Cloud project whose daily quota is permanently exhausted — so the documented fallback has never once answered. Because MetadataRepository.combine turns "a source failed, none found" into Unavailable, that standing failure meant every Open Library hiccup reached the user as "couldn't be reached". The app has been effectively single-sourced since it was written. Build plumbing reads GOOGLE_BOOKS_API_KEY from local.properties (gitignored), falling back to the environment and then to empty. A blank key is a supported state: a fresh clone still builds a working app that falls back to the keyless endpoint, rather than failing to build. GoogleBooksClient appends the key only when non-blank, building the URL with HttpUrl.Builder in a pure requestUrl() so it is testable without a socket. The key is scrubbed from SourceResult.Failed.reason before that string can reach the scan sheet — it is rendered to the user and is our only diagnostic channel from a real phone, and some okhttp/JDK IOExceptions embed the full request URL in their message. Defensive, not a response to an observed leak. Resolves the RATE_LIMITED decision parked in RetryPolicy's KDoc: a keyed 429 is the short per-user rate limit and gets exactly one retry, honouring Retry-After capped at 2s. A keyless 429 is still the dead daily quota and is still never retried. Verified against the live API, not only offline: both ISBNs that failed on the phone (9781883937386, 9781883937676) plus a control return HTTP 200, in the percent-encoded URL shape HttpUrl actually produces. Both books are in Google Books, so the restored fallback now covers precisely the Open Library TLS-reset failure that broke those scans. 189 unit tests (was 172), 0 failures; Paparazzi unchanged; release APK builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7WHnTx2Cso4VV245WDAJY |
||
|
|
d6d02f788c |
Second on-device feedback round: eight fixes, and a measured retry policy
The ghost bookcase was an inset bug, not a data bug. LocationsScreen's list
branch dropped the Scaffold's innerPadding while its empty-state branch applied
it, so the first bookcase row rendered under the top app bar and was invisible.
Both of the user's bookcases were always real; they just could not see the first
one, so they made a second. Every other screen was checked for the same class of
bug — Locations was the only one.
Metadata lookup is now designed against a measurement rather than a guess
(docs/METADATA-SOURCES.md § "Measured again 2026-09-09"):
- Google Books keyless is dead for everyone, permanently. The user's
residential-IP test returned a quota error naming a shared anonymous PROJECT,
not an IP, so the earlier "your phone may well get answers" guess is wrong and
is now marked CORRECTED in place. Because combine() turns any Failed-with-no-
Found into Unavailable, that standing failure meant every Open Library hiccup
surfaced as "one or more sources couldn't be reached". The API key is
deliberately deferred by the user; this commit leaves the source broken.
- Our own timeouts were manufacturing failures. Over 30 live requests, 13%
failed — all fast TLS resets under 2.5s — while successes ran to a median of
4.3s and a max of 22.0s. Two of 26 successes exceeded the old 12s callTimeout,
so ~8% of lookups that were about to work were cancelled and reported as
unreachable. Timeouts are now 25s/20s/20s.
That asymmetry (cheap failures, expensive successes) is what RetryPolicy encodes.
It retries TRANSPORT and SERVER_ERROR with a 250ms/750ms jittered backoff, and
deliberately does not retry TIMEOUT (the budget is already spent) or RATE_LIMITED
(hammering a quota is how an intermittent block becomes a permanent one — this
project's IP has already been refused outright once during research).
SourceResult.Failed now carries a FailureKind alongside its human reason, and the
reason names the specific failure ("tls connection reset, 3 attempts") instead of
a generic "network error". That string was already threaded to the UI and dropped
on the floor; LookupFailedSheet now renders it. It is the only diagnostic channel
we have from a real phone, so nothing may parse it.
Also from the same feedback round:
- Grouped ModalBottomSheet shelf picker, replacing two near-duplicate flat
dropdowns that listed every bookcase x shelf pair. Sections per bookcase,
empty bookcases say so, and the most recently used shelf is pinned on top.
- The recent shelf persists across sessions (SettingsStore.LAST_SHELF_ID) and is
cleared on sign-out. It is offered, never pre-selected: the user weighed that
and chose one tap over the risk of silently mis-shelving a book.
- Locations dialogs and the manual-ISBN dialog auto-focus their first field.
- The library filter menu offers "Add a bookcase to enable filtering" instead of
a lone "All books" that is already the active state and cannot be changed.
- The Locations button is Material Symbols' "shelves" (a bookcase) instead of
Warehouse (a barn). material-icons-extended 1.7.8 has no bookcase glyph.
- The scan sheet drops "you can lower the book" — the ISBN echo already says it.
assembleDebug exit 0; testDebugUnitTest 172 tests, 1 skipped, 0 failures (was
138); verifyPaparazziDebug exit 0; assembleRelease exit 0, signed with the real
release key; zero "always 'false'" warnings on a --rerun-tasks rebuild.
Three soft spots are recorded in docs/HANDOFF.md and are NOT verified: the
ghost-bookcase Paparazzi snapshot renders a lookalike of the screen rather than
the screen, the auto-focus calls swallow their own failure and no emulator exists
here, and the picker opens as a sheet stacked on the save sheet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSnVqWdiQNEcPFRq1hGZAi
|
||
|
|
1ba22a9f36 |
The two books that failed are in Open Library after all
The user supplied the failing ISBNs: 9781883937386 (The Hittite Warrior) and 9781883937676 (Shadow Hawk), both Bethlehem Books. Both resolve against the source the app already queries — title, author, publisher, page count, cover art — and both real responses parse correctly through the app's own OpenLibraryClient. Captured as fixtures with a regression test, because these are the specific books that motivated the research. That kills the coverage hypothesis for these two and demotes Harvard, which holds neither: its 93% in the sample table is inflated by construction (the sample was drawn from Harvard) and misleading in exactly the direction that matters — it is a research library and does not carry small-press children's historical fiction. The failure is upstream of the metadata sources. Documented the three candidates; the leading one is that the barcode never decoded into a valid ISBN-13, which ScanCodeFilter drops silently with no UI feedback whatsoever. Revised the recommendation accordingly: make the app say what happened before adding any source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDcPottghJXEvfYKqFM7zf |
||
|
|
bd24ecbafd |
docs: first on-device test results and metadata-source research
Records the coil3 StateFlow trap (a Kotlin warning, not an error, that silently blanked every book cover) so a future session greps for it, and the two Open Library cover defects behind it. METADATA-SOURCES.md answers the user's research question about alternative lookup sources: measurements over a 60-ISBN sample drawn from a third-party catalogue, the options with costs, and a recommendation to fix diagnosis before buying coverage. Nothing there is implemented — the user asked to be consulted first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDcPottghJXEvfYKqFM7zf |