What's New
Recent updates to the site, backend and skin pipeline. Each release links to its full notes.
- Fixed
- bind manifest media oid+size to the rendered artifact bytes
- bind publish artifact osu_id to the trusted repo identity
- Fixed
- tag a skin repo only when the ci run created a commit
- Fixed
- harden publish trust gate against untrusted artifact fields
- New
- country-based default skin limits (Swiss 20, others 5)
Fixed- degrade rate limiter to in-process governor fallback on DB error
- Fixed
- check ownership before CI-lock on skin mutations
- reject non-zip .osk uploads before parsing
- New
- send X-Content-Type-Options: nosniff on all responses
Fixed- guard commit file paths against repo-root traversal
- New
- name generated assets by dir_name (not the live skin.ini name)
- New
- flat media endpoints /media/{kind}/{dir} (no ext, osk kind, Content-Disposition)
- Fixed
- drop the dead .gitea paths from tag-diff + cleanup (fleet is .forgejo-only)
- New
- pretty-print manifest.json (2-space indent)
- Fixed
- reap stuck 'running' CI rows whose done callback was lost
- Fixed
- dispatch deploy-ci on skins-template (moved there from reusable-actions)
- Fixed
- point the skins.json help text at .forgejo/workflows
- Fixed
- write skins.json to .forgejo/workflows (fleet migrated off .gitea)
- Fixed
- prefix ./ on osk member paths so a leading-dash filename isn't a zip flag
- Faster
- index skins on (osu_id, tag) for the roster and pick joins
- New
- recognize .forgejo workflow paths in tag-diff + cleanup
- New
- read skins.json + classify workflow changes under .gitea or .forgejo
- Fixed
- self-bump the report sub-path pin alongside the publish pin
- Fixed
- remove the downloaded artifact dir before staging (never commit _artifact)
- Fixed
- read osc-meta.json (drop dot — upload-artifact globs out hidden files)
- Fixed
- rename osc-meta.json (drop dot — upload-artifact globs out hidden files)
- Fixed
- cross-device-safe move for prepare/previews (EXDEV on /workspace↔/app/danser)
- Fixed
- resolve the artifact root robustly (handle upload path nesting)
- Fixed
- stage only this run's rendered outputs into _artifact
- Fixed
- cap the admin download-all archive size
- pin rate-limit client IP to the socket peer for untrusted hits
- rate-limit the ci-status tag ingest to bound amplification
- shut down gracefully on SIGTERM
- stop leaking internal error detail in 5xx responses
Faster- single-flight the osu! app-token refresh
- Fixed
- make the report action self-contained (no .. escape in main)
- New
- trusted publish half of the skin CI (allowlist-validate, commit, tag)
- New
- untrusted render half of the skin CI (discover, render, manifest)
Fixed- stop sending the notify token as a bearer header
- Fixed
- require a signed ci-status request and drop the legacy bearer
- Fixed
- anchor the What's New popover under its button
- New
- back the rate limiter with Postgres for multi-replica budgets
- fan out live CI frames via Postgres LISTEN/NOTIFY
Fixed- add a replay-safe signature scheme to the ci-status webhook
- bail instead of caching an empty osu! access token
- bound push-triggered tag ingests and prune atomically on reproject
- build media response headers without panicking on bad metadata
- cap admin uploads instead of leaving the spool unbounded
- guard CI phase merge against a non-object snapshot
- guard the LFS S3 key against a malformed oid
- key XFF-less direct callers by peer IP for rate limiting
- re-check the admin allowlist on every request
- reject non-release tags on the ci-status ingest path
- stop following redirects and cap response size in the avatar proxy
Faster- batch pick validation into one skin lookup
- index community_skins by tag
- Fixed
- ci-banner: clear the build banner on a no-change run
- clone tags in SkinsEditor reset so edits don't alias the prop
- footer: grouped-column layout; drop Source link
- invalidate instead of full-reload on pick save and clear the copy timer on unmount
- order changelog markers by date then version for chronological compare
- re-stream a repeat deep-link to the same CI run after close
- seed TagSelector default month from today instead of a literal
- skins-editor: auto-scroll the page when dragging a row to the edge
- ui: dvh heights + overscroll-contain for clean mobile modal scroll
- ui: keep version + What's New dropdowns on-screen on mobile
- ui: lock page scroll behind open modals and popovers
- whats-new: portal the popover so it sits on-screen and taps off close it
- New
- add unified What's New popover and /changelog page
- filterable changelog timeline with per-service theming
- wayback-style calendar version picker
- New
- add /api/changelog merging Forgejo release notes
- paginate changelog releases for full stable history
Fixed- exclude prerelease tags from the changelog feed
- Fixed
- exclude phantom runs from the render-load runner count
- reject path traversal in skin folder names
Faster- skip LFS smudge when committing skins so saves/deletes don't pull every skin's content
- Fixed
- stop rendering errored phantom CI runs as 'about to start' in the build panel
- Fixed
- commit skin saves/deletes via git push in the background to avoid the Forgejo bulk-commit timeout
- Fixed
- forward the session cookie on internal SSR fetches so login survives the origin rewrite
- New
- forward X-Forwarded-For on internal SSR fetches so rate limits stay per-client
Fixed- resolve the CI watcher to a stalled state on timeout instead of spinning forever
- Fixed
- reap unclaimed CI phantoms every 5 min instead of every 6h
- Fixed
- poll user fresh in the CI watcher so fast-run version banners aren't masked by the nav cache
Faster- make the admin pick target a typeahead instead of a 100-cap list
- pause CI-load polling while the tab is hidden
- route SSR API fetches to the internal backend when configured
- New
- dispatch CI with a self-callback URL so beta builds report to beta
- dispatch skin-save CI with a self-callback instead of a bare push
Fixed- add connect/read timeouts to the shared HTTP client
- authorize ci-status webhook before full body deserialization
- cap user download-all zip at 4 GiB to bound disk use
- stop ci-status stream registry from leaking senders
Faster- assemble download-all zip on a blocking thread
- batch osu! user lookups 50 per call
- index ci_runs on (repo, run_number) and (repo, created_at)
- offload .osk parsing to a blocking thread
- prune old ci_runs and expired cache_entries in maintenance
- run user-page reads concurrently
- serve ci-load from the ci_runs read model and rate-limit the route
- New
- admin: backfill per-repo CI notify secrets
- set a per-repo CI notify secret on repo creation
- Faster
- batch roster identity cache reads into one query
- bound the Postgres connection pool with an acquire timeout
- load the membership row once in get_user
- resolve bot picks with a single-row lookup
- Faster
- fetch the current user in a server load to avoid a duplicate request
- lazy-load the skins editor and CI panel
- New
- admin: add re-run-CI toggle to the deploy-CI panel
- New
- admin: pass skip_ci through to the deploy-ci dispatch
- New
- ci: add run_number/event/progress columns to ci_runs
- webhooks: add push-based ci-status endpoint + broadcast hub
- webhooks: ingest on the CI tag event; drop ci-done; org hook invalidate-only
Fixed- avatar: only seed upstream avatars for members or the caller
- ci: cap concurrent live CI streams per user or IP
- ci: guard SSE log delta against a non-char-boundary offset on source switch
- ci: require owner or admin to read CI runs, logs, and streams
- upload: cap .osk decompression by actual bytes, not declared sizes
- upload: scope the large request body limit to the upload route
Faster- community: single-flight the roster cache rebuild
- zip: stream each .osk to disk instead of buffering it in memory
- New
- ci: drive build panel from pushed status, drop log view
Fixed- ui: show a friendly 404 page for a user with no profile
- Fixed
- csp: drop git.sulej.net from img-src/media-src
- download: mark download-all as a native download so the nav bar clears
- New
- ops: add a container HEALTHCHECK on /api/health
- rate-limit: rate-limit skin-detail and pick endpoints, add picks_save bucket
Fixed- db: track applied read-model migrations in a ledger
- download: stream download-all .osk from Garage by OID, not Forgejo media
- picks: keep owner_kind on picks embedded in the user page
- rate-limit: compare integration token in constant time
- rate-limit: key on the rightmost X-Forwarded-For hop
- upload: stream multipart parts to disk and bound batch size
Faster- auth: resolve session and user in a single query
- ci: render only new log lines in the live stream
- ci: resolve dispatch run id in the background, return immediately
- community: count member skins in one grouped query
- media: skip ref resolution when the tag already matches
- rate-limit: shard the token-bucket map across 16 locks
- reads: resolve repo tags once per page instead of three times
- upload: spool decompressed .osk members to disk instead of RAM
- zip: fetch skin .osk files concurrently when building zips
- New
- ops: sweep expired sessions and reap stranded CI runs
Fixed- auth: verify OAuth state nonce to block login CSRF
- rate-limit: scope integration token to user_get and meter download-all
- New
- list all version tags, drop main from the version picker
- ui: dedicated /osc-skins collection routes + community render picks
- ui: infinite-scroll community + skin grids, paginated admin users
- ui: per-tag download-all, /limits page, queued high-load banner
Fixed- ui: drop redundant community-skin labels from picks views
- ui: gate CI and skin-folder Forgejo links to admins
- ui: gate Forgejo repo links to admins; correct privacy copy for private repos
- New
- api: centralize rate-limit catalog + public GET /api/rate-limits
- api: model the OSC community skin as its own collection, drop virtual user 0
- api: paginate /api/community, user skins, and admin users
- ci: GET /api/health/ci-load (running/queued) for the high-load banner
- restore per-tag browsing in the read model
- skins: download-all honors the selected tag (?ref=)
Fixed- forgejo: create skin repos private to close direct-clone bypass
- read-model: ingest osc_skins as virtual user 0 (was dropped by numeric repo filter)
- Fixed
- ui: honest version banner on main + disable overscroll past footer
- Fixed
- ingest: take tag from the ci-done/webhook payload, not the manifest
- New
- adopt the skins read model — loading bar, reproject admin control, drop cache panel
- New
- skins read model (v3) — serve pages from Postgres, decommission unused endpoints
- New
- adopt the Rust backend's OpenAPI (Scalar docs, regenerated types)
- New
- rewrite the backend in Rust (axum + sea-orm)
- Fixed
- health: derive degraded signal from leaf Forgejo calls, not the community composite
- New
- ui: banner when the git backend is degraded
Faster- nav: cache user/skin/community reads to remove refetch-on-navigation
- New
- health: expose /api/health/forgejo degraded signal for the UI banner
Faster- avatar: cache avatar bytes in-process in front of Garage
- cache: serve stale Forgejo data on restart, cache community payload, fix roster invalidation
- Fixed
- ci-gate: record pending CIRun before commit so resolver matches
- Fixed
- docs: vendor redark for Redoc dark mode (replaces hand-rolled theme)
- docs: vendor unwrapped Amoenus SwaggerDark, inline into /api/docs
- Fixed
- privacy: mention /api/redoc as a jsdelivr consumer too
- Fixed
- footer: move version row below nav links
- New
- add /license page and footer link
- New
- docs: dark mode for /api/docs (Swagger) and /api/redoc
- Fixed
- api: revert /api/community asyncio.gather (overran DB pool); back to serial
- New
- ui: add 'API docs' link to admin panel header (-> /api/docs)
- ui: add /privacy and /rules pages, link from footer
- ui: add API docs link to footer (-> /api/docs Swagger)
- ui: hide manual rebuild form from non-admins; owners trigger CI implicitly via upload/edit/delete
- ui: per-page Open Graph / Twitter embeds (main, user, skin)
- ui: split previews toggle into gameplay/panel/thumbnail
- ui: typed ApiBackoffError for 429/409 + getRateLimits + hide force_rebuild from non-admins
Fixed- skins-editor: disable row drag while editing so dblclick selects text
- ui: drop docs phase from CI progress visualizer (no longer emitted)
- ui: drop the non-admin explanatory line from CI panel
- ui: main page logo back as small embed; skin page large embed, title is just skin.ini name
- ui: privacy and rules pages in first-person, no em-dash, escape {your_osu_id}
- ui: remove Cloudflare from privacy (no proxy on sulej.net, direct IP)
- ui: smaller Discord embeds (twitter:card summary); drop main page logo+counts; drop og:video from skin page
Faster- ui: hero img eager+fetchpriority high; avatar grid lazy+decoding async; explicit width/height to skip CLS
- New
- api: ci_lock service + no_active_ci dependency (reject 409 when CI in-flight)
- api: dispatch_ci is admin-only (owners trigger CI via upload / edit / delete)
- api: gate writes on no-active-CI + apply rate limits + admin-only force_rebuild
- api: GET /api/users/{osu_id}/rate-limits snapshot for the caller
- api: in-memory token-bucket rate limiter + rate_limit dependency factory
- api: INTEGRATION_TOKEN bypasses rate limits on read endpoints (for trusted proxies)
- api: list_ci_runs only returns runs from the last 24h
- api: rate-limit public read endpoints (media w/ own-repo bonus, avatar, community, users, ci runs/log/stream)
- api: serve swagger ui + redoc + openapi spec under /api/
- api: split do_previews into do_gameplay/do_panel/do_thumbnail
- api: structured skin detail endpoint with author + build history
- semver: rank CalVer tags newer than legacy semver in sort_desc
Fixed- api: read version from package metadata so Swagger shows the real release tag
- ci-lock: drop stale rows + close all non-terminal rows on ci-done webhook
Faster- api: parallelize skin_count lookups in /api/community (gather)
- Fixed
- svelte: clear all vite-plugin-svelte build warnings
- Fixed
- layout: pin footer to viewport bottom on short pages
- New
- footer: redesign with brand, links, and build metadata
- New
- footer: show app version with link to release notes
- Fixed
- avatar: log non-2xx upstream responses at warning for operator visibility
- Fixed
- config: correct stale garage_bucket default from "gitea" to "forgejo"
- New
- security: F-5 frontend — attach CSRF token to every mutation
- New
- security: F-6 CSP nonces + bump JSON editor height
- New
- security: F-10 add HSTS header
- New
- security: re-add CSP with Anubis-compatible allowlist
- New
- security: add CSP + companion hardening headers
- Fixed
- json-editor: visible cursor + dark-theme lint tooltips
- New
- user-page: link avatar + username to osu! profile
- Fixed
- install with --legacy-peer-deps for openapi-typescript
- New
- security: F-5 backend — double-submit CSRF tokens
- New
- security: F-8 + F-14 quick wins
- Fixed
- auth: admin role tracks ADMIN_OSU_IDS in both directions
- Fixed
- pregen: log.exception instead of log.warning(..., e)
- Fixed
- nh3: drop 'rel' from <a> attribute allowlist
- New
- security: sanitize rendered skin docs with nh3
- Fixed
- avatar: pass through upstream content-type instead of hardcoding png
- New
- api: proxy + cache a.ppy.sh avatars through /api/avatar/{osu_id}
- Fixed
- upload: clear target Skins/<dir>/ before writing the new files
Showing the most recent releases.