What's New
Recent updates to the site, backend and skin pipeline. Each release links to its full notes.
- New
- users: flag hidden osu accounts, drop their stale country and rank
- New
- profile: account unavailable chip
- Fixed
- admin: stray scrollbar next to the tab bar
- Fixed
- community: fall back to the users row when the osu API hides an account
- Fixed
- skins: profile available from repo creation, not first ingest
- New
- add created_at, rank and skin_count to the admin users list
- add HMAC-signed media URL service
- add osc_member flag and group=osc filter to the community roster
- add per-skin revision to the skins read model
- admin activity feed endpoint
- admin: PATCH /api/admin/communities/{slug}
- api: drop osc_member from payloads; swiss_like -> perk_eligible
- auth: viewer community state on /me
- backfill per-skin source_oid from git for pre-rollout tags
- communities: community logo upload and public serving
- communities: join requests
- communities: move community management to /api/communities/{slug}
- db: community management schema
- db: community perk flags, description, multi-country auto-join
- db: slug-keyed community collections
- embed deeper skin page + ?limit for the integration token
- expose per-skin version_tags on the skin detail endpoint
- expose Prometheus /metrics with HTTP request instrumentation
- forgejo reachability health endpoint with osc_forgejo_up gauge
- gate profile picks on the owner's OSC membership and expose osc_member
- include the skin-CI actions in the changelog feed
- inquiries: file support inquiries as Forgejo issues
- instrument business events (downloads, uploads, logins, CI, ingest, upstream, caches, rate limits)
- keep oscbot working under the lock — bot-token picks read + signed osk URLs
- lock all reads behind auth; account-based rate limits
- move community skin to /api/community/{slug}, drop bot API
- multi-community membership model
- og: community link previews
- open uploads to all members, gate render picks behind swiss-like
- periodic DB-derived gauge collector for Prometheus
- perk gating driven by community flags
- picks: community picks name their community
- public OG preview endpoint with signed image URLs
- read-model admin stats with users/skins/ci/storage breakdown
- rebrand user-visible api strings to SkinHub
- serve admin ci-health from local ci_runs, keep Forgejo behind ?deep=true
- tie perks to OSC membership and let admins manage communities
- webhooks: repo-identified community CI events
Fixed- bind oauth state to the session secret
- bind the resolved tag into signed media URLs (no cross-ref reuse)
- bound skin upload/download decompression and archive size
- cap each download-all object fetch at the upload ceiling
- communities: allow appointing auto-joined members as community admins
- community: fall back to osu identity country for roster membership
- derive deep ci-health status from the latest run, not a workflow name
- exempt signed media requests from rate limiting
- gate /api/og behind OG_SHARED_SECRET to block roster scraping
- OG skin preview is thumbnail-only (no gameplay video embed)
- og: embed image URLs no longer expire
- plain ascii in user-facing text
- redact secrets in Settings Debug output
- reject replayed ci-status requests
- require SESSION_SECRET >= 32 bytes in release builds
- saturating sum in download-all size pre-check to prevent i64 overflow
- scope default trusted-proxy CIDRs to the proxy network
- stream .osk overrides rewrite to a tempfile to bound memory
- New
- accent-colour embed bar for link-preview cards
- add a combo colours preview and a fixed-height gallery viewer
- add the /community/osc swiss community page
- admin overview on the new stats/activity API with fast ci-health deep check
- admin: community edit + skin_repo in create form
- admin: dedicated communities tab
- admin: full per-community management panel
- adopt the generalized community API
- advertise the SkinHub Twitch panel extension
- api: community management client surface
- backend-driven community logos and link previews
- badge OSC members and gate profile picks on membership
- catppuccin latte/mocha themes with a red-haze accent
- collapse the navbar into a hamburger menu on phones
- community: data-driven copy, accent color, join requests
- community: description-driven pages and generic OG previews
- community: generic /community/[slug] page + dynamic nav
- community: manage page for community admins
- community: static-convention community logos with glyph fallback
- filter the skin version picker to versions where the skin changed
- forgejo outage banner and admin overview status
- full-size thumbnail gallery on the skin detail page
- lock the site behind a login wall
- manage communities from the admin panel
- manage community membership from the admin users panel
- move community skins to /community/[slug]/skins
- open uploads to everyone, show render picks to swiss-like members only
- profile: make the Twitch extension promo card dismissible
- rebrand to SkinHub with catppuccin themes and a communities menu
- redesign skin detail page with hero banner and tabbed layout
- reframe the homepage as the global members directory
- render and publish CI services in the changelog
- serve OG previews to link crawlers on the locked site
- SH monogram logo (two-tone, vertically centered)
- show live rate-limit budget on /limits
- show per-skin version on skin cards and detail
- support the group=osc filter in the community client
- support: inquiry form backed by the API
- swap in a SkinHub logo; keep the OSC logo for /community/osc
Fixed- admin: allow appointing auto-joined members as community admins
- admin: bust the communities cache on create/delete
- apply hardening headers and a strict CSP to the OG crawler response
- base the skin-limit placeholder on the user's own default cap
- community: validate accent color client-side before styling
- drop og:video from crawler previews (thumbnail-only)
- drop the downloads tab and move the skin version picker to the header
- guard the login redirect target to same-origin paths
- non-technical wording for the outage banner
- point the community-skin client at /api/community/osc
- present OG_SHARED_SECRET when fetching /api/og
- terser copy on the community page
- ui: tighten page copy
- validate url scheme before binding to href/src
- New
- ci-status: include repo name in status payloads
- New
- ci-status: include repo name in status payloads
- Fixed
- deps: update time to v0.3.53
- Fixed
- deps: Update pnpm to v11.8.0 [SECURITY]
- Fixed
- deps: Update Rust crate anyhow to v1.0.103 [SECURITY]
- New
- validate per-skin source_oid in the manifest
- New
- add per-skin source_oid to manifest
- 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.