* fix: various backsync fixes, assorted changes
* automated: Backsync BMS+PMS Songs/Charts 2026-05-23T22:16:28.469Z
* fix: some patches, but all good!
---------
Co-authored-by: Test Name <test@example.com>
* 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
* docs: migrate from mkdocs to mdbook
- Rename old mkdocs docs/ to old-docs/ for reference
- Set up new docs/ with mdbook (book.toml + src/ tree)
- Mirror full nav structure from mkdocs.yml into SUMMARY.md
- Add Justfile-docs with docs-serve, docs-build, docs-check, docs-install recipes
- Import Justfile-docs from root Justfile
- Rewrite .github/workflows/docs.yml: build step uses taiki-e/install-action
to install mdbook, split into separate build + deploy jobs, PR builds
run the check step too
* ci(docs): pin actions to SHAs, install mdbook via release binary
* ci(docs): install mdbook from apt instead of curling a release binary
* dev: replace mkdocs python stack with mdbook in dev image
* ci(docs): apt only works on Debian; restore release binary install for Ubuntu CI
* docs: fix duplicate file entries in SUMMARY.md
* docs: remove docs-install recipe
* docs: remove site-url from book.toml to fix asset loading
* dev: install mdbook from upstream release binary, not Debian apt
The Debian package (0.4.x+ds) strips bundled font assets, leaving the
built site without fonts/fonts.css. Use the upstream tarball (same as CI)
so the theme is complete. Handles x86_64 and aarch64.
* docs: vendor mdbook tarballs in dev/mdbook/, install from there
Dockerfile.dev uses COPY + tar to install the right arch at build time.
CI extracts the x86_64 tarball directly from the checkout.
No network access required for either — and no stripped-fonts Debian package.
* fix: unwritten