Skip to content

Emeroteca: HTTP caching, supplements, truncated lists, and clearer 404s - #35

Merged
fabiodalez-dev merged 4 commits into
mainfrom
fix/emeroteca-review
Sep 9, 2026
Merged

fabiodalez-dev merged 4 commits into
mainfrom
fix/emeroteca-review

Conversation

@fabiodalez-dev

Copy link
Copy Markdown
Owner

Summary

Four improvements to the Emeroteca (periodicals) section, from a full review of the client/server contract against the Pinakes plugin.

  • HTTP cache: the server sends an ETag and Cache-Control: private, max-age=0, must-revalidate on every /api/v1/periodicals/* GET, but the client never sent If-None-Match — no OkHttp cache was configured, so every refresh re-downloaded the full payload. A 10 MB disk cache now makes revalidation transparent. The cache is keyed by URL and the app is single-account, so isolation rests on eviction: clearHttpCache() runs on sign-out and on instance switch (AuthRepository.logout() / forgetInstance()), inside runCatching on Dispatchers.IO so a cache IO failure can never break sign-out.
  • Supplements: the server exposes the issue's supplements note and the app dropped it. Now shown in the issue header, reusing the same InfoRow component as the periodical detail screen.
  • Truncated issue lists: the server caps a year at 400 issues; a new meta.truncated flag (nullable, so older servers still read as "complete") drives a non-blocking banner. The subtitle interpolates the real item count instead of hardcoding the cap.
  • Missing record vs disabled section: the detail ViewModels showed a generic error for both. They now probe the health endpoint on a 404 only, and tell "this issue no longer exists" (retryable) apart from "the section is no longer available" (the same empty state the list screen uses).

Tests

./gradlew testDebugUnitTest157 tests, 0 failures. PeriodicalsUiStateTest grew 12 → 24: the new cases cover the truncated-flag decision (true / explicit false / omitted / null meta), 404 detection, both branches of the failure classifier including the guarantee that a non-404 never degrades to Gone, and the error-state builder. Per the repo rule the ViewModels are not faked — the decision logic lives in pure functions in PeriodicalsUi.kt and is tested there. Each of the first three commits also compiles on its own, so the history bisects cleanly.

Strings

Six keys added to all four locales (en/it/fr/de) in i18n/*.json, inserted next to their siblings so key order stays identical across files.

Server side

meta.truncated is being added to the Pinakes plugin in a parallel PR. The field is optional on the wire, so this app works against current servers as-is.

The server already tags its cacheable GETs — the whole periodicals
surface among them — with an ETag and `Cache-Control: private,
max-age=0, must-revalidate`, but the client never sent `If-None-Match`
back: no okhttp3.Cache was configured, so OkHttp had no stored
validator to attach and every navigation re-downloaded a body the
device already had.

I give the OkHttp client a ~10 MB disk cache in the app cache dir.
Revalidation is now transparent and, since `max-age=0` keeps every
request going to the server, this trades bandwidth for nothing —
a 304 replays the stored body instead of serving stale data.

Cache entries are keyed by URL alone, so I evict the whole cache on
logout and on instance switch, next to the Room purge that already
happens there. The eviction runs on the IO dispatcher and swallows
its own failures: an unpurgeable cache is a bandwidth problem and
must not turn signing out into an error the user has to fight.
The issue detail endpoint returns a nullable `supplements` string —
the free-text note on inserts bound with a fascicolo — and the app
was dropping it on decode.

I add it to the DTO and render it in the issue header, reusing the
masthead detail's InfoRow so the label/value pair reads identically
on both screens (InfoRow goes from private to internal for that).
The row appears only for a non-null, non-blank value, like every
other optional field in this section.
`GET periodicals/years/{id}/issues` caps its result at 400 fascicoli
and now reports the cut in `meta.truncated`. Without that flag the app
silently presented a partial year as the whole thing.

I add the field to the shared Meta DTO as a nullable with a null
default, so an instance that predates it keeps decoding: absent must
read as "complete", never as "truncated". The decision lives in a pure
`isTruncatedList(meta)` so it is testable without a ViewModel, and I
deliberately do not infer truncation from the item count — that would
cry wolf on a year sitting exactly on the cap and would be wrong
outright the day the cap moves.

When the flag is set the issue list shows an informational banner
above the rows. It is not an error state: the issues below are real
and browsable, so nothing is blocked and there is nothing to retry.
PeriodicalsViewModel already degraded gracefully on a 404 — re-probe
health, and if the plugin is really off show a terminal "section
unavailable" state instead of a retryable error. The three detail
screens did not: a masthead, a year or an issue that 404'd produced
a generic failure with a retry button that could only 404 again.

I extend the same pattern to them, but a 404 on a detail endpoint is
ambiguous in a way the list's never was: the one record may have been
deleted while the section is perfectly alive. So health stays the
oracle — I probe it only on a 404, and only a 404 from health too
means the section is gone. Anything else stays "no longer exists",
which sends the user back to browsing rather than telling them the
whole archive vanished.

The decision is a pure function over the failure plus the probe
answer, unit-tested for both 404 branches and for the guarantee that
a non-404 can never degrade to "gone" even if a stale probe says so.
The gone/not-found states drop the server's bare "Not found." message
on purpose: resolvedMessage() prefers a non-blank message, so keeping
it would have shown that instead of the wording that distinguishes
the two cases.
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 42c5f10d-8fd0-4369-8105-4f22747b02e5


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@fabiodalez-dev
fabiodalez-dev merged commit 99c13bc into main Sep 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant