* 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
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
Tachiin VSCode or Cursor. - Other editors can be used, but you will need to set up devcontainers yourself.
- Install the
Dev Containersextension - 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 startto start Tachi, useseedsto 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) Formerlyzkldi/tachi-beatoraja-ir, brought into the monorepo per #1515.

