Conversation
A card whose photo 404s (a listing removed since it was cached, issue #79) used to stay on screen with a broken-image icon and a notice. It now moves on to another unseen card from the pool -- normal mode from feedCache, explore mode from the in-memory batch -- up to three times in a row before falling back to the existing "no longer available" notice. - The failed card is already marked seen when picked, so the only storage write is marking the replacement seen; cards are never evicted, so a transient CDN error can't permanently shrink the pool. - Never recycles seen cards (nextUnseenCard returns null instead), never runs while offline (every photo would fail; issue #69's notice stays), waits for an in-flight refresh so it can't clobber feedCache, and stands down if the failing photo's card has already been replaced. - nextCard() now delegates to nextUnseenCard(); behavior is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…of serving dead listings Refs #79. A light user can take weeks to hit the 85% seen ratio, so a cached pool could sit long enough for some listings to be adopted or removed -- the card still rendered, with a broken photo. There was no age ceiling because the old time-based cutoff (STALE_MS) was removed in b829eed: it advanced `page` and walked users outward through the radius ladder. This adds an age check that does neither. Server: new POST /api/validate-cats { ids } -> { availableIds }. One RescueGroups request (animals.id `equal` + array criteria on the existing available/cats/haspic endpoint, confirmed live to act as an "in" filter that silently omits unknown ids). Ids are validated (1-100 numeric), the route is uncached, and it shares /api/nearby-cats's per-IP rate limit, error mapping and upstream-failure alerting. Purely additive. searchRadius and the new findAvailableIds now share one request/error helper. Extension: once feedCache.validatedAt (falling back to fetchedAt) is 7+ days old, _start validates the pool's ids before rendering (2s timeout, chunks of 100). Dead cards and their seenIds are dropped; every still-available unseen card is kept in order; page/fetchedAt/location are untouched; validatedAt is stamped. refresh() now carries validatedAt forward when it keeps unseen cards (fetchedAt resets on refresh, which would otherwise hide their age). If the whole pool is dead it refreshes from page 1 with nothing kept, writing only if that fetch succeeds. Any failure fails open (cache served as before) and sets a 1h cooldown so a down server can't delay every tab; offline skips validation entirely. A cache with no usable timestamp is left alone. Docs: README behavior/rate-limit notes; PRIVACY.md now discloses that cached public listing ids are sent to the backend (and on to RescueGroups) at most about weekly, with no location attached. Tests (Tier 3, full suite: 325 pass, lint clean): server route + request builder + findAvailableIds; 31 client tests covering when it runs, pruning, seenIds, page/fetchedAt preservation, chunking, cross-tab safety, refresh interplay, all-dead pool, and every failure mode; a live by-id contract test. Mutation-checked seven ways. Also verified end-to-end (real newtab.js against the real local server and RescueGroups API). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-cats Follow-up to the #79 fix, on the same PR. The new route fails open in the extension (a broken route just means stale cards keep being served), so nothing user-visible would flag it breaking in production -- yet none of the pre-merge, CI or post-deploy checks covered it. This closes that, and makes the same class of gap fail the build in future. - deploy-verify.yml: the post-deploy production smoke test now calls /api/validate-cats: ids /api/nearby-cats just listed plus one that cannot exist (12 digits) -- the impossible one must never come back and at least one real one must -- and confirms invalid input is rejected with a 400. The assertion logic was exercised locally for the pass and both failure modes. - test/revalidation-integration.test.js (runs in CI): the real newtab.js in jsdom against the real server over a real socket, with only RescueGroups faked. Covers the client<->server contract, which the two halves' own tests only checked against mocks of each other. Contract mutations (renamed route, renamed response field, wrong filter operation, no pruning) each fail it. - test-live/stale-cache.live.js: the same flow against the real RescueGroups API, starting the server in-process (nothing to run first). Promotes the ad hoc end-to-end script used to verify the fix into a repeatable test. Live tests now use an id that can never be listed (12 digits) instead of 8 digits that could plausibly be assigned someday. - test-support/newtab-harness.js: the shared page harness for both suites. - test/deploy-coverage.test.js: fails if a server route lacks a smoke check in deploy-verify.yml or a mention in README/RELEASE.md, if a file in test-live/ isn't wired into `npm run test:live`, or if one fails to load. - package.json: test:live runs every live file. eslint and .dockerignore cover test-support/. - RELEASE.md: pre-merge now requires `npm run test:live` and a real-browser manual QA pass (with the old-cache simulation for cache changes); post-merge lists /api/validate-cats among the smoke-tested routes. - README.md: CI section says ci.yml runs lint too (it always has) and lists the new smoke check; validation section documents the live suites, the integration test, and a "simulating an old cache" manual QA recipe. Tier 3: lint clean; 339/339 (was 325) on two consecutive runs; 7/7 live tests against the real API. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #79.
Problem
A user's cache was ~5 weeks old, so some cached listings had been adopted or removed. Their cards still rendered, with broken photos (the image URL 404s). There was no age ceiling: the cache refreshes only once it is 5+ minutes old and 85% seen, and a light user can take weeks to get there. Refresh also never rechecks the unseen cards it keeps (it fetches
page + 1, a different window), and it resetsfetchedAt, so a cache can look fresh while holding old cards.The old time-based cutoff (
STALE_MS) was removed in b829eed because it advancedpageand walked users outward through the server's radius ladder. So a plain "refresh after N days" is not the fix; it would reintroduce that.Approach
Three commits on a stack (one merge to
main):1.
fix(newtab): skip to another cat when a photo fails to load(fb0a1ef)Covers the issue's note about broken images. On a photo error the card moves on to another unseen card (normal mode from
feedCache, explore mode from the in-memory batch), up to 3 in a row, then falls back to the existing "no longer available" notice.feedCache, and stands down if that photo's card was already replaced.nextCard()now delegates to a newnextUnseenCard(); behavior unchanged.2.
fix(cache): revalidate a week-old cache against RescueGroups(88a6601)The main fix. Once a pool's
validatedAt(falling back tofetchedAt) is 7+ days old, the next new tab asks the backend which cached ids are still available and drops the rest.Server
POST /api/validate-cats { ids } -> { availableIds }. One RescueGroups request per call:animals.idequalwith an array criteria on the existingavailable/cats/haspicendpoint. I confirmed against the live API that this acts as an "in" filter and silently omits unknown ids (99 of 100 real ids returned, the fake one dropped)./api/nearby-cats's per-IP rate limit, error mapping and upstream-failure alerting. Purely additive.searchRadiusand the newfindAvailableIdsshare one request/error helper.Extension
seenIdsare dropped. Every still-available unseen card is kept, in order.page,fetchedAt, location and radius are untouched;validatedAtis stamped.refresh()carriesvalidatedAtforward when it keeps unseen cards (itsfetchedAtreset would otherwise hide their age).Docs: README behavior and rate-limit notes.
PRIVACY.mdsaid "nothing else is sent"; it now discloses that cached public listing ids are sent to the backend (and on to RescueGroups) at most about weekly, with no location attached, and the "Last updated" date is bumped. WEBSTORE.md and the FAQ stay accurate as written.3.
test(release): close the release safety-net gaps around /api/validate-cats(dae6822)The new route fails open in the extension, so a broken route in production would show up as nothing at all: stale cards would just keep being served. Yet no pre-merge, CI or post-deploy check covered it. This closes that, and makes the same class of gap fail the build in future.
deploy-verify.yml: the production smoke test now calls/api/validate-catswith ids/api/nearby-catsjust listed plus one that cannot exist. The impossible id must never come back and at least one real one must. It also checks invalid input gets a 400. The assertion logic was exercised locally for the pass and both failure modes.test/revalidation-integration.test.js(runs in CI): the realnewtab.js(jsdom) against the real server over a real socket, with only RescueGroups faked. The client and server tests each mock the other side, so this is the only thing that checks the two agree (route, response field, id format, filter operation). Mutations of each were caught.test-live/stale-cache.live.js: the same flow against the real RescueGroups API, with the server started in-process (nothing to run first). This is the end-to-end script I used to verify the fix, promoted to a repeatable test.npm run test:livenow runs every live file. The live tests also switched their "impossible" id to 12 digits, since an 8-digit one could plausibly be assigned someday.test/deploy-coverage.test.js: fails the build if a server route has no smoke check indeploy-verify.ymlor no mention in README/RELEASE.md, if atest-live/file isn't wired intotest:live, or if one fails to load without a key.test-support/newtab-harness.js: the shared page harness. ESLint and.dockerignorecover the new directory.RELEASE.md: pre-merge now requiresnpm run test:liveand a real-browser manual QA pass (with the old-cache simulation for cache changes); post-merge lists the new smoke check. README: documents the above, plus a "Manual QA: simulating an old cache" recipe, and corrects the CI section (ci.ymlhas always run lint too).Options considered
refresh(): never validates kept cards and walks the radius outward. Rejected./api/nearby-catswith anidsfield: saves a route but adds branching to the hottest handler. Rejected in favor of a separate route.Risk and compatibility
validatedAt(plus a transientvalidationRetryAfter) is additive; an older extension ignores both._start, gated to once a week, capped at 2s, wrapped in try/catch.Verification Report
Changes Made
extension/newtab.js.POST /api/validate-cats(server/index.js,server/rescuegroups.js) and client revalidation (extension/newtab.js).Tests Executed
npm run lint: clean.npm test(full suite, Tier 3): 339 / 339 pass (268 onmain), two consecutive runs.rescuegroupstests, about 31 client revalidation tests, 8 new photo-fallback tests, 5 client-to-server integration tests, 9 release-coverage guard tests.photo load failure (issue #69)tests (the handler is now async) and onestarttest's title and comment.npm run test:liveagainst the real RescueGroups API: 7 / 7 pass (4 existing/by-id contract tests, 3 end-to-end stale-cache tests).pageunchanged; a 35-day-old all-dead pool refreshing from page 1; and a 2-day-old pool making no requests.deploy-verify.ymlassertion snippet, run locally for the pass case and both failure modes. The workflow itself only runs on a push tomainthat touchesserver/**, so its first real run is post-merge, and that run is the real check of this step. (A red result there means the check or the deploy is broken, and per RELEASE.md, do not proceed to store submission.)Existing Functionality Confirmed Intact
Before release
fetchedAt/validatedAtback 8 days in devtools and testing with the server stopped.PRIVACY.md.🤖 Generated with Claude Code