* fix: resize profile images on upload and add cache headers
Profile pictures were stored at original upload size (up to 1 MB) but
only ever displayed at 32–128 px, so every page load downloaded the full
original image. Additionally, no Cache-Control headers were set anywhere
in the pipeline, forcing a fresh round-trip through the API redirect and
CDN on every page view.
- Resize JPEG/PNG pfps to max 256×256 and banners to max 1920×1080 via
sharp before storing; re-encode to WebP at 85% quality (GIFs skipped
to preserve the animated-GIF easter egg)
- Hash the resized buffer so the CDN path reflects what's on disk
- Set ContentType and CacheControl: immutable on S3 PutObject so CDN
edges and browsers cache the objects indefinitely (safe because paths
are content-addressed via SHA-256)
- Add Cache-Control: public, max-age=3600, stale-while-revalidate=86400
to the GET /pfp and GET /banner redirect responses so browsers cache
the userId→CDN-URL mapping and skip the API hop on repeat visits
Fixes#1553
* chore: apply biome formatting
* chore: generate ImportTimestop kanel type
* feat: add backfill-media script to migrate existing profile images
One-off script that reads every user's existing pfp and banner from S3,
runs them through the same sharp resize/WebP pipeline as new uploads,
stores the result under the new content-addressed key with immutable
cache headers, and updates custom_pfp_location/custom_banner_location
in the DB.
Features:
- --dry-run flag to preview changes without writing anything
- Skips objects already at optimal size (idempotent)
- Gracefully handles missing S3 objects (logs + skips)
- Summary stats at the end
- Reads S3 config from CLI args or TACHI_CDN_SAVE_LOCATION_* env vars
* chore: apply biome formatting to backfill-media
* fix: resize animated GIFs to animated WebP via sharp animated:true
* test: rewrite change-pfp/banner tests as real S3 + sharp integration
Remove the CDN mock entirely. Tests now pass real image buffers through
sharp and verify the stored S3 object via CDNRetrieve:
- PNG/JPEG input → assert stored object is WebP, dimensions ≤256×256,
smaller than the original
- GIF input → assert stored object is WebP
- Hash returned from the action == SHA256 of the stored WebP bytes
(not the original upload)
- Delete tests upload a real pfp then delete it and verify CDNRetrieve
throws afterwards
Also move change-pfp.test.ts from the isolated vitest project back to
the default pool now that it no longer uses vi.mock.
* chore: apply biome formatting to test file
* fix: use POSTGRES_TEST_HOST env var in bot vitest setup
Both vitest.globalSetup.ts and vitest.setup.ts hardcoded 'tachi-postgres'
(the dev server), but just bot-db-test-template-reset creates the template
on 'tachi-postgres-test' (the tmpfs test server). Workers then failed to
clone the template because they were looking on the wrong host.
Mirrors the pattern already used in the server package:
process.env.POSTGRES_TEST_HOST ?? 'tachi-postgres-test'
* fix: update pfp router tests for WebP resize pipeline
The PUT tests were comparing the stored S3 bytes directly to the original
upload buffer. Since we now resize and re-encode to WebP, this always
fails. Also, following the mockApi redirect to fetch the bytes added a
slow supertest round-trip.
Replace with: read custom_pfp_location from the DB after PUT, fetch the
stored object directly via CDNRetrieve, and assert it is smaller than the
original (non-deterministic WebP compression makes exact byte comparison
inappropriate).
* fix: update banner router tests for WebP resize pipeline
* fix: tests
* fix: ci
* checkpoint
* server compiles again
* fmt
* checkpoint
* last one out, get the lights
* euston station
* like, mostly good. I'm under the ocean right now.
* fix: some nonsense AI stuff
* fix: new eslint rules, and cleanup imports across the board
* feat: folder slug rewrite, email logic change, too
* fix: register must use txn not db
* checkpoint
* feat: i'm really worried about this bit, but empiricism will win
* feat: light driving
* feat: migrate over user game status
* feat: custom scrollbar, and migrate over settings
* feat: eamusement csv movement
* fix: rm debug
* feat: shark reaction
* checkpoint
* feat: get tests passing again
* checkpoint
* feat: bms table sync
* feat: ugs snapshot and backsync bms pms
* feat: checkpoint
* feat: iidx playlists
* manic depression
* continues
* fly
* happy house
* we're happy here
* I think. 6 years is enough for me. This is killing me, and I am dying.