zk ae5e72aa89 fix: resize profile images on upload and cache redirect responses (#1564)
* 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
2026-05-23 13:48:49 +01:00
2026-05-23 10:07:48 +01:00
2026-04-20 23:11:16 +01:00
2026-05-08 16:54:34 +00:00
2026-05-08 16:54:34 +00:00
2026-03-11 00:06:28 +00:00
2023-02-11 03:08:23 +00:00
2026-03-11 00:06:28 +00:00
2026-03-11 00:06:28 +00:00
2026-03-28 19:38:00 +00:00
2026-03-11 00:06:28 +00:00
2026-05-23 10:07:48 +01:00
2026-05-23 10:07:48 +01:00
2026-05-08 16:54:34 +00:00
god
2026-03-28 17:23:34 +00:00
2026-05-20 00:35:33 +01:00
2026-04-20 23:11:16 +01:00
2026-05-16 13:42:22 +00:00
2026-05-08 16:54:34 +00:00
god
2026-03-28 17:23:34 +00:00
2026-03-11 00:06:28 +00:00

Tachi

This is the main monorepo for Tachi.

What is Tachi?

Tachi is a modern, powerful, modular Rhythm Game Score Tracker.

In short, it does the things that people would otherwise make spreadsheets for.

Tachi is a score tracker and analyser for various rhythm games. It was designed out of a dislike for existing websites that display your scores. I think that scores are integral to the rhythm game experience, and that displaying them properly is just as important!

By using Tachi, you get access to powerful, novel rhythm game score-tracking features, like automatically breaking your scores into sessions, setting goals and rivals, and more!

Quick Setup For Experienced Programmers

  • Open Tachi in VSCode or Cursor.
  • Other editors can be used, but you will need to set up devcontainers yourself.
  • Install the Dev Containers extension
  • Run Dev Container: Rebuild and Open in Container
  • (It can take a couple minutes to build and bootstrap)
  • You should be placed inside a vscode instance called Tachi (dev-container)
  • Inside there, you'll have a perfectly set up shell with fancy utils and fancy graphics. Use just start to start Tachi, use seeds to run seeds scripts, etc.

Doing Tachi dev outside of the devcontainer is unsupported. The container is set up perfectly with the right versions of everything and is the expected way to do things. Please do not report bugs in the local setup if you are not using the devcontainer.

Repository Info

This monorepo contains the following codebases:

  • typescript/client/, Which is a React frontend for Tachi. (AGPL3)

The client and the server are fairly decoupled. Someone could trivially create their own frontend client for Tachi.

  • typescript/server/, Which is an Express-Typescript backend for Tachi. (AGPL3)

This contains all of our API calls, and interfaces with our database, and powers the actual score import engine.

  • seeds/, Which is a git-tracked set of data to be synced with Tachi. (unlicense)

This is the source of truth for the songs, charts, and more on the site! By submitting PRs to this, you can fix bugs on the website, add new charts, and more.

  • typescript/bot/, Which is a discord bot frontend for Tachi. (MIT)

  • typescript/common/, Which contains common types, utils and functions shared between all other packages. (MIT)

This is also published to NPM when it hits production.

  • docs/, Which contains Tachi documentation. (MIT)

  • typescript/sieglinde/, Which contains our BMS/PMS analysis functions. (MIT)

  • java/beatoraja-ir/, Which is the Tachi Internet Ranking implementation for the LR2oraja variant of beatoraja. (MIT) Formerly zkldi/tachi-beatoraja-ir, brought into the monorepo per #1515.

S
Description
No description provided
Readme
1.4 GiB
Languages
TypeScript 91.3%
Shell 3%
SCSS 2.6%
JavaScript 2.2%
Java 0.3%
Other 0.5%