* 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
* temporary dvctr
* start of work
* fix: timeouts in windows
* fish?
* feat: better, working stuff
* refactor: rename database-seeds to seeds/
* better
* fdsaf
* feat: don't let our container get owned by root
* fix: better user handling
* feat: falling asleep at my desk i yield to you what i have done
* feat: autoadminify
* docs: denote trickshot
* feat: open in browser on launch
* better seeds workings...
* fix: dont kill shell
* docs: mention dev/debs
* fix: whoops
* fix: make seeds load within the century
* feat: rest of the containerisation
* feat: new alias
* style: fix linter errors
* fix: bad call
* fix: better justfile
* feat: clean up this disgusting filter directive thing
* fix: THIS FIX WAS THIS EASY FOR THIS LONG???
* fix: version is obsolete
* feat: much prettier...
* feat: big import warnings
* feat: sour times