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
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
=== G-diagnostics attempts=2 quota_waits=1 hard_fails=0 ===
|
||||
cost=$0.28007899999999997 turns=6 err=?
|
||||
--- result (tail) ---
|
||||
` case. `byIsbnCoverUrl` untouched.
|
||||
|
||||
**AppContainer.kt** — `metadataHttpClient` now has `callTimeout(12s)/connectTimeout(10s)/readTimeout(10s)`. PocketBase `okHttpClient` untouched.
|
||||
|
||||
**ui.scan:**
|
||||
- `ScanCodeFilter.kt` — `accept()` returns `ScanOutcome { Isbn, NotAnIsbn, Ignored }`; one shared debounce mechanism throttles repeats of valid *and* rejected codes alike.
|
||||
- `ScannerController.kt` — new `rejectedCodes: SharedFlow<String>` routed alongside `scanResults`.
|
||||
- `ScanModels.kt` — `ScanSheetState.NotFound` gained `viaLookupFailure: Boolean = false`; added `ScanSheetState.LookupFailed(isbn13, reason)`; `ScanMetadataOutcome.from` maps all three `LookupResult` cases.
|
||||
- `ScanViewModel.kt` — `retryLookup`/`enterByHand` added; lookup logic factored so Retry bypasses the "sheet already showing" guard; `rejectedMessage: StateFlow<String?>` with a 3s auto-clear timer.
|
||||
- `ScanScreen.kt` — new `LookupFailedSheet` (Retry/Enter by hand/Skip, dismissible, never claims the book unknown); `ManualEntrySheet` reworded via `authoritative: Boolean`; new `RejectedBarcodeBanner` shown over the camera.
|
||||
|
||||
**Tests:** classify() cases for both clients, the full 3×3 `combine` matrix, `ScanCodeFilter` throttle tests with an injected clock, `ScannerController` rejected-code routing, updated `ScanModelsTest`, and two new Paparazzi snapshots (`LookupFailedSheet`, rejected-barcode banner) recorded in both light and dark — all visually verified.
|
||||
|
||||
No warnings (Kotlin or otherwise) were introduced in any file I touched; a clean `--rerun` compile shows only pre-existing, unrelated deprecation warnings elsewhere in the codebase.
|
||||
|
||||
Nothing was left undone relative to the original instructions, and no constraints (forbidden files, no commits, foreground builds, no MockWebServer) were violated.
|
||||
Reference in New Issue
Block a user