- Ship one hashed Tailwind stylesheet instead of two (one render-blocking)
- Compress responses in-app (brotli/gzip, streaming-safe) and precompress
the node build so local and CI measurements match production
- Validate the Supabase session once per request
- Render the homepage immediately and stream public stats
- Stream a Pokedex's entries with the page instead of fetching after
hydration, running the rows and count queries in parallel
- Shrink the avatar and offline placeholder images, fix layout shift,
contrast, link names and missing meta descriptions
- Add Lighthouse CI (mobile + desktop) with score and metric budgets,
bundle-size budgets in the build tests, and signed-in speed scenarios
- An export now pauses a revoked integration only if its row is
unchanged since the export read it, using the trigger-maintained
updatedAt column as the row version. If the user reconnected in the
meantime, the stale failure no longer disables the fresh credentials
or asks the user to reconnect again. updateExportStatus now reports
whether a row was written.
- The backup status store ignores a response overtaken by a newer
refresh, or by the status being flagged, cleared or set directly, so
a slow response can't overwrite newer state.
- Unit tests cover the whole integration repository, the guarded pause
and stale status responses. Coverage thresholds are raised to the
new baseline.
Google answers a revoked or expired refresh token with invalid_grant.
Every save then retried the dead token, and reconnecting never cleared
the old error, so it kept showing on Backup Settings afterwards.
- The Google Drive and Dropbox OAuth callbacks clear lastError when a
provider is reconnected.
- An invalid_grant, or a missing refresh token, now pauses the
integration with a readable "reconnect" message instead of retrying
it on every catch update. Other failures still retry as before.
- A banner on every page and an alert on the Pokédex page point to
Backup Settings, which shows a "Reconnect needed" badge. The Pokédex
page re-checks backup status after each export, because saving a
catch record also exports on the server and may pause a provider
first.
- Offline sync status and the "Save all artwork" link move from every
page to a new /offline-guide page, linked from the user menu and the
home and welcome pages. Only the offline read-only banner stays
sitewide.
- Unit tests cover every export path and the backup status store. BDD
covers revocation and reconnecting for both providers, and the
offline guide. The mock provider can now reject token refreshes, and
mock control calls fail loudly if a stale mock is reused. Coverage
thresholds are raised to the new baseline.
The resize to 192px isn't worthwhile now that sprites are only downloaded
when viewed or saved on request, and then kept forever. Restore the
original files and the script's no-resize default, drop the detail view
size cap, and regenerate the sprite manifest with the original sizes.
- Sprites are cached as they are viewed instead of bulk-downloaded after
every sign-in, in one shared cache that is never pruned and survives
sign-out and account changes, since sprites never change.
- A "Save all artwork for offline" link saves every remaining sprite on
request, shows the remaining size, and is hidden once all are saved.
- Page loads reuse an offline snapshot under 15 minutes old; edits,
retries and a new sign-in still sync immediately.
- An expired session no longer wipes offline data; only the Sign Out
button does.
Sprites were shipped at 512x512 but render at 44-64px in the box grid.
Resizing to 192px halves each file (median 15.5KB -> 7.6KB); a full dex
of artwork drops from 16.6MB to 7.6MB.
The sprite build now also writes static/sprites-small/manifest.json,
listing every sprite (all forms, shiny and female variants) with its
size, so the offline worker can save the complete set and report what
is missing. A unit test keeps it in step with the files on disk.
@supabase/ssr 0.1.0 never removed the old unchunked session cookie once a
refreshed session grew past one cookie, and always read that stale copy
first. Every load then retried an already-used refresh token, which the
hosted auth server rejects, signing the user out.
Upgrade @supabase/ssr to 0.12 (and supabase-js to match) and move to the
getAll/setAll cookie API, which clears stale chunks when writing.
Each sync created a new artwork cache and re-fetched every sprite with no-cors. Opaque responses are padded to several MB each for storage quota, so a Living Dex grew to tens of GB, syncs never finished, and the offline copy could fail to save.
Artwork now lives in one cache per user that is topped up with only missing sprites, fetched with CORS so they count at their real size, with a per-fetch timeout. The offline copy is committed before any artwork, legacy per-sync artwork caches are removed first, and sprites shown online are cached on first load. Also requests persistent storage.
`npm run check` reported 15 errors and `npm run lint` 20, all pre-existing, so
neither gate could pass. Fixing them turned up three real bugs:
- SignOut destructured `{ error }` off `.then(() => {})`, which resolves to
undefined, so every sign-out threw a TypeError - after the signed-out event had
already been emitted. Sign-out also left the user on the protected page they
were on, still showing its content; it now returns them to the home page and
re-runs the server loads.
- SignUp passed `redirectTo`, which is not a signUp option and was silently
ignored, so the confirmation link has always used Supabase's configured site
URL. Documented rather than changed, since pointing it elsewhere needs an
absolute allow-listed URL.
- The Pokédex page tracked totalRecordsCreated but never passed it to the box
view, so the "Processed N entries so far" progress message never rendered.
The rest is typing and dead code: cookie callback parameters in hooks.server.ts
and +layout.ts, the untyped supabase props, a query-builder type that made
PostgREST rows untyped downstream, an unused session destructure, and
`while (true)` paging loops rewritten as `for (;;)`.
The endpoint overrides are read through `$env/dynamic/private`, so they are
evaluated per request in production, not baked in at build time. That made a
single injected environment variable enough to redirect the authorization-code
and refresh-token POSTs - which carry the OAuth client secret and the user's
refresh token - to an arbitrary host, and to redirect the user's authorize hop
to an arbitrary URL.
Overrides are now ignored unless ALLOW_PROVIDER_ENDPOINT_OVERRIDES is exactly
"true" and the value is a loopback URL. `npm run test:bdd` sets the flag;
nothing else should. resolveProviderEndpoints is pure so the refusals are unit
tested, including near-miss hosts such as http://127.0.0.1.example.
Also drops the unused `pokedex` parameter from buildCsv rather than silencing it
with `void`, and the dead hasGigantamaxed field from its fallback record.
Splits testing into five layers so a failure points at the responsible one:
- tests/unit isolated utility, repository and service tests
- tests/data validates the tracked Pokémon, game, region and dex files
- tests/integration schema, views, constraints, RLS and repositories
- tests/bdd executable Gherkin for user-visible behaviour
- tests/build service worker and manifest artifacts per build variant
Replaces the two Playwright specs in client-test/ and the two Vitest files in
test/. Adds a GitHub Actions workflow running the layers as separate jobs, a
mock OAuth provider server so the Drive and Dropbox scenarios never touch real
accounts, and a wrapper that reads the local Supabase keys from
`supabase status` rather than hard-coding them.
Extracts the pure formatting helpers out of PokedexExportService so they can be
unit tested, and makes the provider endpoints configurable so the mock server
can stand in for Google and Dropbox.
`prettier --check .` failed on 22 files, so gating CI on `npm run lint` was
never going to pass. These changes are whitespace only.
Adds the two remaining generated data exports to .prettierignore so this class
of churn cannot recur, along with machine-local settings files.
These two files are generated exports that prettier was not ignoring, so
`prettier --check .` failed on them. Reformatting is isolated here because the
diff is ~124k lines and would otherwise bury real changes.
The only semantic change in this commit is the apostrophe in Farfetch'd and
Sirfetch'd: U+0027 -> U+2019, matching the spelling the rest of the data
already used. Everything else is whitespace.
When a Pokédex has a game scope set and isFormDex=true, alternate forms
were not appearing because game_pokedex_entries only contains base-form
Pokémon. This adds fetchFormsForGame() to pull supplemental form entries
from pokedex_entries filtered by gamesToCatchIn, then merges and sorts
them using the base form's regional dex number so ordering is preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>