github rommapp/romm 5.4.0-alpha.1

pre-release4 hours ago

Caution

Give the first start time to finish its migrations.
Several of this release's migrations rewrite the roms table and backfill it in batches, which can take a while on a large library. Don't restart the container while they run.

Warning

Cross-origin requests are denied by default.
An unset or empty ROMM_CORS_ALLOWED_ORIGINS used to allow every origin, and now allows none. If a browser-based client on another origin talks to your RomM, list its origin in ROMM_CORS_ALLOWED_ORIGINS. A * still answers any origin, but without credentials, so a client that signs in with the session cookie needs its origin listed explicitly. #4699

Warning

Add a reverse proxy on a public address to FORWARDED_ALLOW_IPS.
RomM now trusts X-Forwarded-For only from loopback and private ranges. A proxy outside those ranges has to be added, or every request is logged and rate limited as coming from the proxy's address. #4748

Technical changes

🌎 ENVIRONMENT VARIABLES

Notifications and email

variable default description
SMTP_HOST - SMTP server; email stays off until this and SMTP_FROM are set
SMTP_PORT 587 SMTP server port
SMTP_USERNAME - Login for the SMTP server, if it needs one
SMTP_PASSWORD - Password for the SMTP server
SMTP_FROM - Sender address, such as romm@example.com
SMTP_SECURITY starttls starttls, tls (implicit TLS, usually port 465) or none

Audit log

variable default description
AUDIT_LOG_RETENTION_DAYS 90 Days to keep audit events; 0 keeps them forever

Library conversion

variable default description
ROM_CONVERTO_ENABLED false Enable the "Convert library" task and ?format= download conversions
ROM_CONVERTO_TIMEOUT 600 Seconds per rom-converto operation
ROM_CONVERTO_MAX_CONCURRENCY 2 Concurrent conversions per web or task worker

Devices and sync

variable default description
DEVICE_INSTALL_ENABLED true Let users push a ROM to one of their devices for install
DEVICE_INSTALL_REQUEST_TTL_DAYS 2 Days an unfinished install request waits for its device; 0 waits forever
DEVICE_INSTALL_EXCLUDED_PLATFORM_SLUGS win,win3x,win9x,windows-apps Platforms that can never be pushed to a device
SYNC_RETROARCH_PSP_SERIAL_MAP {} JSON map of PSP serial to extensionless ROM file name, for RetroArch PSP saves whose title matches no ROM

Scheduled cleanups

variable default description
ENABLE_SCHEDULED_CLEANUP_NETPLAY true Clean up empty netplay rooms
SCHEDULED_CLEANUP_NETPLAY_CRON */30 * * * * When that cleanup runs
ENABLE_SCHEDULED_CLEANUP_UPLOAD_TMP true Clean up abandoned chunked uploads
SCHEDULED_CLEANUP_UPLOAD_TMP_CRON 0 * * * * When that cleanup runs
ENABLE_SCHEDULED_CLEANUP_ZIP_CACHE true Clean up stale cached ZIP files
SCHEDULED_CLEANUP_ZIP_CACHE_CRON 0 4 * * * When that cleanup runs
ENABLE_SCHEDULED_CLEANUP_SYNC_SESSIONS true Fail sync sessions no client completed
SCHEDULED_CLEANUP_SYNC_SESSIONS_CRON 23 * * * * When that cleanup runs

General

variable default description
⚠️ ROMM_CORS_ALLOWED_ORIGINS - Empty now allows no cross-origin requests; a * allows any origin but never credentials
⚠️ FORWARDED_ALLOW_IPS local/loopback addresses Proxies trusted to report the client's address; was *
DISABLE_EASYRPG false Disable the EasyRPG player for everyone
SEVEN_ZIP_TIMEOUT 180 Timeout for 7-Zip operations in seconds, was 60
📡 API CHANGES | Change | Description | | --- | --- | | ⚠️ CORS denied by default | `ROMM_CORS_ALLOWED_ORIGINS` now defaults to empty instead of `*`; browser clients on another origin must be allowlisted (#4699) | | CORS wildcard drops credentials | With `*` in `ROMM_CORS_ALLOWED_ORIGINS`, responses no longer send `Access-Control-Allow-Credentials` (#4699) | | Invalid credentials are unauthenticated | A malformed Basic header or an invalid/expired JWT bearer no longer 500s; the request proceeds unauthenticated and gets the route's 401/403 (#4799) | | ⚠️ `PermissionGroupSchema.is_system` removed | Replaced by `system_key` (`SystemGroupKey` or null) (#4767) | | Age limits on permissions | `PermissionGroupSchema/Create/Update` and `UserPermissionsSchema/Update` gain `age_limit`, `hide_unrated_roms`; updates apply them only with `set_age_settings: true` (#5081) | | Collection `rom_count` is computed | `rom_count` on collection, smart and virtual collection schemas is now read-only, derived from the visible `rom_ids` (#4930) | | ⚠️ Collection name errors | Duplicate name on create or rename returns 409 (was 500); names over 400 chars return 422 (#4997) | | ⚠️ `GET /api/netplay/list` | Scope `assets.read` - `roms.read`; `game_id` is now an integer ROM id, 404 for a missing or hidden ROM (#4700) | | ⚠️ Netplay socket rooms | `open-room` and password-less `join-room` require a signed-in user with `roms.read` who can see the ROM; the room password moved from `extra.room_password` to top-level `password` (#4700) | | ⚠️ `GET /api/play-sessions` | Without `devices.read`, only allowed when `device_id` equals the calling token's device; no longer defaults `device_id` from the token (#4956) | | ⚠️ `PlaySessionSchema.sync_session_id` removed | Field dropped from play session responses (#4670) | | Page params on play and sync sessions | `GET /api/play-sessions` and `GET /api/sync/sessions` use shared page params (`limit` 1 to 10000, `offset`); sync sessions gains `offset` (#4715) | | ⚠️ Sync `delete` operation | `SyncOperationSchema.action` adds `delete` (slot emptied on the server); `SyncNegotiateResponse` adds `total_delete` (#4674) | | `POST /api/sync/negotiate` payload | New `restore_unlisted` and `emulators` fields to re-offer unlisted saves and filter by emulator (#5091) | | Save sync baselines | `POST /api/saves` and `PUT /api/saves/{id}` take a `content_hash` query param; `POST /api/saves/{id}/downloaded` takes `content_hash` in the body (#4768) | | Save/state uploads validated | `emulator` must be a single folder name (max length enforced); file names over 255 bytes return 400; uploads to a hidden ROM return 404 (#4851, #4997) | | `SaveSchema` / `StateSchema` annotations | New `is_favorite` and `labels`; masked to defaults on another user's shared save or state (#4714) | | `StateSchema.core` | New field naming the RetroArch core that wrote the state (#4862) | | ⚠️ `PUT /api/roms/{id}` rename errors | Renaming onto an existing file returns 409 (was 500); an invalid or overlong `fs_name` returns 400 before any metadata fetch (#4997) | | `PUT /api/roms/{id}` manual metadata | `raw_manual_metadata` is validated, 422 when malformed; `ManualMetadata` gains `alternative_names` (#5111) | | `POST /api/roms/{id}/convert-to-folder` | Also returns 409 when an `.m3u` beside the file lists it; upload and walkthrough routes reject the same case (#5076) | | `GET /api/roms` relevance ordering | Empty `order_by` with a search term ranks by relevance on every database; `char_index` is empty under relevance order (#4996) | | `GET/HEAD /api/roms/{id}/content/{file_name}` | New `?format=` for converted single-file downloads (202 + `Retry-After` while converting, 406 when unavailable) (#4999) | | `GET /api/roms/{id}/content/{file_name}` | New `purpose=download\|play` query param, recorded in the audit log (#4748) | | `DetailedRomSchema.download_formats` | Formats the caller can request with `?format=` (#5000) | | `is_easyrpg_game` on ROM schemas | New boolean on `SimpleRomSchema` and `DetailedRomSchema` (#5077) | | Pinned media | `RomUserSchema.pinned_media` and `RomUserData.pinned_media` (ordered media keys, null for default) (#4764) | | ROM metadata fields | LaunchBox box art `box2d_*`/`box3d_*` (url and path), `video_path` now nullable (#5129); SS `physical_disc`/`physical_extra_discs` (#4885); SS and Hasheous `dump_regions/languages/tags` (#4740); RA `hash_match` (#4816) | | `PlatformSchema` | New computed `abbreviation` and `alternative_names` (#5107) | | `MusicTrackSchema.file_name` | New field (#5079) | | `DeviceSchema.capabilities` | New `capabilities` map on device schema and on `POST`/`PUT /api/devices` payloads (#4835) | | `GET /api/heartbeat` | Adds `EMULATION.DISABLE_EASYRPG` (#5077), `NOTIFICATIONS` (#4735), `DEVICE_INSTALL` (#4835), `CONVERTO` (#4999) | | `GET /api/config` | Adds `CONVERTO` and `CONVERTO_LIBRARY_TARGETS` (#4999) | | `POST /api/tasks/run/{task_name}` | Returns 409 when a single-instance task is already queued or running; `TaskInfo` adds `destructive` (#5000) | | `POST /api/reset-password` | Enforces the password policy with 400, leaving the reset link usable (#5106) | | OIDC callback | A provider error redirects to `/login?bypass_autologin=true` instead of 500 (#4746); an account linked to a different provider identity gets 403 (#4892) | | `POST /api/activity/heartbeat` | Returns 404 for a ROM hidden from the caller (#5117) | | ⚠️ `activity:update` socket event | Sent only to users who can see the ROM, instead of broadcast to every client (#5121) | | ⚠️ `scan` socket event payload | Validated strictly (`ScanPayload`, unknown keys rejected); invalid options, no scan worker, or a scan in flight emit `scan:done_ko` (#5014) | | `scan:scanning_rom` | Payload adds `is_new` (#4867) | | `sync:conflict` | Payload adds `rom_name` (#4552) | | Socket session revocation | Logging out or revoking a session disconnects the sockets it opened (#4732) | | Streaming session routes | Control routes take `container` and `claimed_at` query params; launch socket payloads add `claimed_at`, `core`, `core_tier`, import `refusals`; `StreamingContainerSchema` adds `supports_live_states`, `import_kinds`, `state_core`; 409 bodies use `ContainerBusyDetail` (#4631, #4691, #4796, #4859, #4862) |

Notifications

Method Path Description
GET /api/notifications List the caller's notifications, newest first (#4732)
POST /api/notifications Send a notification to yourself, or to other users with users.write (#4732)
POST /api/notifications/read Mark the given notifications (or all) read (#4732)
DELETE /api/notifications/{notification_id} Dismiss one notification (#4732)
DELETE /api/notifications Dismiss all notifications (#4732)

Notification channels

Method Path Description
GET /api/notification-channels List the caller's channels (Apprise, webhook, email) (#4735)
POST /api/notification-channels Create a channel; email addresses get a confirmation code (#4735)
PATCH /api/notification-channels/{channel_id} Update a channel (#4735)
DELETE /api/notification-channels/{channel_id} Delete a channel (#4735)
POST /api/notification-channels/{channel_id}/test Send a test notification now (#4735)
POST /api/notification-channels/{channel_id}/confirm Confirm an email channel with its code (#4735)
POST /api/notification-channels/{channel_id}/resend-code Resend the email confirmation code (#4735)
GET /api/notification-channels/apprise-services List Apprise services and their fields (admin) (#4769)
POST /api/notification-channels/apprise-services/parse Parse a service or Apprise URL into fields (admin) (#4769)

Audit events

Method Path Description
GET /api/audit-events Paginated audit log, filterable by actor, action, category, target, time range and search; admins with users.read see everyone (#4748)

Device installs

Method Path Description
GET /api/devices/online IDs of the caller's devices with an open /devices socket (#4835)
POST /api/devices/{device_id}/installs Queue a ROM install on a device (#4835)
GET /api/devices/{device_id}/installs List the device's pending and taken install requests (#4835)
POST /api/devices/{device_id}/installs/claim Device claims its pending requests (device-bound token only) (#4835)
PUT /api/devices/{device_id}/installs/{request_id} Device reports done, already_installed or failed (#4835)
DELETE /api/devices/{device_id}/installs/{request_id} Cancel a pending or taken request (#4835)
GET /api/roms/{id}/installs The caller's open install requests for a ROM (#4835)

RetroArch Cloud Sync (WebDAV)

Method Path Description
OPTIONS /api/sync/retroarch/{path} Advertise DAV support (#3904)
PROPFIND /api/sync/retroarch/{path} Read-only browsing of roms/, saves/ and states/ (#3904)
GET, HEAD /api/sync/retroarch/{path} Serve the manifest or a save/state file (#3904)
PUT /api/sync/retroarch/{path} Upload a save/state, matched to a ROM by file name (#3904)
DELETE, MOVE /api/sync/retroarch/{path} Remove a save/state the client dropped (#3904)
MKCOL, LOCK, UNLOCK /api/sync/retroarch/{path} Accepted no-ops for WebDAV clients; uses HTTP Basic with a 401 challenge (#3904)

Saves and states

Method Path Description
PUT /api/saves/{id}/favorite Set is_favorite on a save (#4714)
PUT /api/saves/{id}/labels Replace a save's labels (#4714)
PUT /api/saves/{id}/file-name Rename a save file and its screenshot (#4749)
PUT /api/states/{id}/favorite Set is_favorite on a state (#4714)
PUT /api/states/{id}/labels Replace a state's labels (#4714)
PUT /api/states/{id}/file-name Rename a state file and its screenshot (#4749)

Collections

Method Path Description
PUT /api/collections/{id}/visibility Share or unshare a collection (owner only) (#4749)
PUT /api/collections/smart/{id}/visibility Share or unshare a smart collection (owner only) (#4749)

ROMs, tasks and config

Method Path Description
POST /api/tasks/scan Queue a library scan with a ScanPayload body; 202, 409 if one is in flight, 503 with no worker (#5014)
GET /api/roms/{id}/easyrpg/{path} Serve the EasyRPG index.json and game/RTP files for RPG Maker 2000/2003 games (#5077)
PUT /api/config/converto_settings Update download conversion settings (#4999)

Socket.IO events

Method Path Description
Server to client notifications:new / notifications:read / notifications:dismissed Notification created, marked read, or dismissed for the user (#4732)
Server to client install:updated An install request changed, sent to the owner's tabs (#4835)
Server to client install:queued / install:cancelled Sent to a device on the new /devices namespace (device-bound client token required) (#4835)
Server to client activity:refresh Signals clients to refetch the activity list (#5125)

Highlights

Notifications

RomM now keeps a notification inbox for each user. Scans and tasks that finish or fail, a role change, or someone ending your stream all leave a notification that is pushed to every open tab and still waits for you the next time you open RomM. Admins can send their own from the Send tab on the Notifications page, and any API client can post one through POST /api/notifications. #4732

Each user can also forward their notifications to channels, filtered by level and topic. A RomM webhook sends a JSON payload (signed with HMAC-SHA256 when you give it a secret), and email works once the SMTP_* variables are set, which also sends password reset links by email. Admins can additionally pick any of the services Apprise supports, such as Discord, Telegram, Slack, ntfy, Gotify or Matrix, and fill in that service's fields or paste its URL. #4735 #4769

RetroArch Cloud Sync

RomM is now a Cloud Sync target for RetroArch. Point RetroArch's Cloud Sync at https://<your-romm>/api/sync/retroarch/ (the trailing slash matters) and sign in with your RomM username and password (setup guide). Saves and states sync straight into your library, web player states show up in RetroArch's numbered load slots, and RetroArch registers itself as one of your devices on its first sync. #3904

The config, thumbnails and system file categories sync too, stored per user. PSP save folders are bundled into one save per folder; if a folder's title matches no ROM, map its serial with SYNC_RETROARCH_PSP_SERIAL_MAP. Generic WebDAV clients can browse the same path read-only. #3931

Browser saves sync, and installs on your devices

All five in-browser players (EmulatorJS, EasyRPG, js-dos, PICO-8 and Ruffle) now sync saves through device sync. Each browser profile registers as a device, named like "Firefox on macOS", and negotiates its saves with the server when a game launches, so a save made in one browser is waiting in another and a conflicting copy is archived instead of overwritten. A new Devices page at /devices lets you rename your devices, turn sync off for one, or remove it. #5091

You can also send a game to a device from the web UI. "Install on device" in a game's menu adds it to the download queue of the devices you pick, and the device downloads it the next time it's online. Only devices whose app reports it accepts installs are listed. Turn the feature off with DEVICE_INSTALL_ENABLED=false, and keep platforms out of it with DEVICE_INSTALL_EXCLUDED_PLATFORM_SLUGS. #4835

Parental controls

Give a permission group an age limit, and its members only see games rated for that age or younger. Each user can replace their group's limit, and a "Hide unrated games" switch hides games no rating covers. Admins are never limited. Set them in the group and user dialogs under Administration. #5081 #5083

A game's age comes from the strictest of its IGDB, ScreenScraper, LaunchBox (ESRB) and Steam ratings, and a manual rating list replaces the providers' ratings. The limit applies everywhere a game can show up: the gallery, search, collections, stats, the Jukebox and feeds. See Parental controls for the details.

Audit log

RomM now records who did what: downloads and player launches, play sessions, uploads and edits, collection changes, scans and tasks, and security events like sign-ins, failed sign-ins and permission changes. Admins read it in the new Events tab under Settings › Logs, filtered by user, category and date, with a search over names and IP addresses. Events are kept for 90 days, set by AUDIT_LOG_RETENTION_DAYS (0 keeps them forever). See Audit log for everything that's recorded. #4748

Library conversion with rom-converto

Pick a storage format per platform, and the new "Convert library" task converts every matched game on it in place, so a game keeps its saves and collections. Only lossless conversions are offered, and because the task deletes the originals it asks you to type a confirmation first. Set ROM_CONVERTO_ENABLED=true and choose formats under the new Conversion settings page, or in config.yml (setup guide). #4999

With download_conversion_enabled on, the game page's More menu offers "Download as" in any format the file can be converted to, and clients can ask for one with ?format= on the download URL. #5000

converto:
  download_conversion_enabled: true
  platform_formats:
    psx: chd
    ngc: rvz

RPG Maker 2000/2003 in the browser

RPG Maker 2000 and 2003 games on the rpg-maker platform now play in the browser on the EasyRPG web player. The free EasyRPG RTP ships with it, so games that rely on the RTP start, though some assets (mostly in battles) are missing from it. Games need to be extracted folders, since the web player can't read archives. Disable it with DISABLE_EASYRPG. #5077

Alternative titles and relevance in search

Search now matches the alternative titles your metadata providers know about, including localized names, so a Japanese title finds the game listed under its English name and the other way around. You can add your own in the new "Alternative titles" field of the Edit dialog, such as "ACNH" or a fan translation's name. Already matched IGDB and Hasheous games pick up their regional titles on their next metadata refresh. #5111

The Search page also orders results by relevance until you pick a sort, with exact title matches first. #4995 #4996

Minor changes

  • feat: Offer the desktop shell's emulator from the save/state play page by @sdornan in #4541
  • feat(metadata): take region and language from the dump a hash matched by @sdornan in #4664
  • feat(metadata): read a fan translation's tag and target language by @sdornan in #4665
  • feat(saves): favorite and label saves and states by @zurdi15 in #4714
  • feat(v2): multi-select saves and states by @zurdi15 in #4725
  • feat(metadata): tag a dump from the flags ScreenScraper raises on it by @sdornan in #4666
  • feat(v2): wider state cards and one timestamp across save and state views by @zurdi15 in #4736
  • feat(sync): surface save conflicts detected by negotiate by @sdornan in #4552
  • feat: promote platforms that have games in PlatformSelect by @andest01 in #4599
  • feat: rename saves and states, and one public/private look across v2 by @zurdi15 in #4749
  • feat(v2): surface WebSocket transport fallback in BackendStatusBanner by @andest01 in #4645
  • feat(streaming): reap abandoned streaming sessions every minute by @LoneAngelFayt in #4632
  • feat(sync): tell a device when the save it holds was deleted here by @sdornan in #4674
  • feat(streaming): resume from a foreign emulator's save or state via declared import by @LoneAngelFayt in #4691
  • feat(v2): pin any media to the game overview by @gantoine in #4764
  • feat(streaming): name containers by their label, and harden import archives by @LoneAngelFayt in #4796
  • feat(sync): record a per-device save baseline to prove what did not change by @sdornan in #4768
  • feat(streaming): per-platform RetroArch core override by @LoneAngelFayt in #4859
  • feat(streaming): RetroArch states carry the core that wrote them by @LoneAngelFayt in #4862
  • feat(v2): add an opt-in setting to show the game logo in place of the title on the v2 game page by @sdornan in #4888
  • feat(soundtrack): control playback through the Media Session API by @sdornan in #4904
  • feat(v2): promote platforms with games in the scan picker by @andest01 in #4897
  • feat(search): search the gallery by providers' alternative titles by @sdornan in #4941
  • feat: read play sessions across devices and page sync sessions by @beemines in #4956
  • feat: make netplay and upload cleanup schedules configurable by @beemines in #4957
  • feat(search): rank multi-word searches by phrase on PostgreSQL by @sdornan in #4977
  • feat(input): let the D-pad move through and out of text fields by @gantoine in #4993
  • feat(search): Search provider aliases through search_titles and drop generated_search_aliases by @sdornan in #5003
  • feat(converto): read per-file title ids with rom-converto during scans by @gantoine in #5031
  • feat(i18n): follow the browser's language by default, with an "Auto" language option by @sdornan in #5051
  • feat(api): Add a REST endpoint to start a library scan by @ilyas-hallak in #5014
  • feat(soundtrack): play SPC, VGM, VGZ and GYM soundtracks in the browser by @sdornan in #5079
  • feat(v2): add an active toggle state to RBtn by @sdornan in #5090
  • feat(v2): resume jukebox music after a reload by @gantoine in #5095
  • feat(v2): scope stored preferences to the signed-in user by @gantoine in #5098
  • feat(launchbox): scrape box front, back, spine and 3D box art by @sdornan in #5129
  • feat: search platforms by slug, abbreviation and alternative names by @sdornan in #5107

Fixes

  • fix(cloud-sync): remap web-player states into RetroArch's numbered load slots by @fmustafayaman in #3933
  • fix(roms): order a playlist by disc number, not by file name by @sdornan in #4737
  • fix(v2): run desktop list rows to the screen edges by @zurdi15 in #4739
  • fix(roms): order lettered discs like numbered ones by @sdornan in #4738
  • fix(scan): keep a hash source's tags when a scan skips it by @zurdi15 in #4740
  • fix(docker): stop the watchdog from spawning duplicate RQ workers by @zurdi15 in #4743
  • fix(hltb): retry a separated title with the separator dropped by @sdornan in #4759
  • fix(v2): handle copy to clipboard over plain HTTP in logs and files tab by @sdornan in #4757
  • fix(hltb): match search results by their aliases too by @sdornan in #4760
  • fix(hltb): accept token-only sessions and discover the endpoint when GitHub fails by @sdornan in #4758
  • fix(v2): version ROM file URLs with the file's own timestamp by @gantoine in #4762
  • fix(auth): redirect instead of 500 when the OIDC callback is rejected by @andest01 in #4746
  • fix(streaming): bind every session action to the claim that owns it by @LoneAngelFayt in #4631
  • fix(sync): give a sync session the life of one launch, and drop the dead play-session link by @sdornan in #4670
  • fix(steam): read appdetails envelope keyed by another app ID by @sdornan in #4774
  • fix(scan): keep title-id settled file categories and report id-only changes on refresh by @tmgast in #4720
  • fix(security): deny cross-origin requests by default by @sdornan in #4699
  • fix(backend): detect top-level rom files at any folder depth by @sdornan in #4776
  • fix(soundtrack): fall back to the default cover when a game has no artwork by @sdornan in #4779
  • fix(security): authorize netplay rooms on ROM access by @sdornan in #4700
  • fix(backend): replace the legacy Query API with Select and type the session decorators by @sdornan in #4730
  • fix(frontend): only request platform icons that ship by @gantoine in #4792
  • fix(player): make all Amiga Kickstarts available to PUAE by @yiannias in #4741
  • fix(auth): treat invalid bearer and basic credentials as unauthenticated by @gantoine in #4799
  • fix(auth): rename seeded permission groups to Viewer and Editor by @sdornan in #4767
  • fix(sync): make deleted-save tracking exact, complete, and effective on filled slots by @sdornan in #4789
  • fix(sync): follow-ups to deleted-save tracking by @gantoine in #4807
  • fix(scan): compute RA hash for archives whose member read fails by @sdornan in #4806
  • fix(player): assign a gamepad already connected when EmulatorJS boots by @gantoine in #4808
  • fix(v2): drop the CRT overlay's backdrop grade on phones by @gantoine in #4809
  • fix(playmatch): skip unhashed lookups of folder ROM members by @gantoine in #4812
  • fix(v2): skip the post-upload scan while a scan is already running by @gantoine in #4810
  • fix(v2): keep the focused platform tile in view when moving up the grid by @gantoine in #4811
  • fix(ra): count RomM's own RA hash match as RetroAchievements verification by @sdornan in #4816
  • fix(saves): stamp save and state file names with local time by @gantoine in #4815
  • fix(ra): keep a hash match when RA's details request fails by @sdornan in #4822
  • fix(ra): accept null Developer, Publisher and Genre from RetroAchievements by @sdornan in #4840
  • fix(hasheous): match multi-file roms when one file is unknown by @sdornan in #4846
  • fix(backend): restart the log forwarder and harden three Redis call sites by @sdornan in #4847
  • fix(v2): stop URL query writes from dropping history entries; bump vue-router to 5.3.1 by @sdornan in #4861
  • fix(player): boot cue/gdi/ccd/mds disc sets whole by default by @gantoine in #4848
  • fix(ss): map Win9x to ScreenScraper's generic Windows system by @gantoine in #4849
  • fix(assets): reject emulator names that are not a single folder on upload by @gantoine in #4851
  • fix(v2): keep the checkbox's native input inside its label by @gantoine in #4856
  • fix(scan): raise the 7-Zip timeout default and keep partial bytes out of the fallback hash by @gantoine in #4852
  • fix(v2): mount one PDF viewer at a time in the Media tab by @gantoine in #4853
  • fix(scan): anchor rom file category to the folder below the rom root by @gantoine in #4854
  • fix(v2): move focus by arrow key and D-pad between page regions by @gantoine in #4855
  • fix(v2): show TheGamesDB logo on server stats coverage chips by @sdornan in #4866
  • fix(scan): kill a stalled 7-Zip extractor at the timeout by @gantoine in #4868
  • fix(scan): stop rescans inflating live platform game counts by @sdornan in #4867
  • fix(platforms): stop labeling C128 as Commodore 64 in the platform picker by @gantoine in #4874
  • fix(player): keep disabled EmulatorJS cheats out of the core by @gantoine in #4876
  • fix(player): restart the core after loading a save it did not boot with by @gantoine in #4877
  • fix(roms): pick grouped representatives among siblings the user can see by @gantoine in #4878
  • fix(player): keep arcade BIOS archives whole on EmulatorJS 4.2.3 by @gantoine in #4879
  • fix(tests): redact provider credentials when recording VCR cassettes by @sdornan in #4886
  • fix(netplay): connect over websocket only by @gantoine in #4889
  • fix(ss): keep ScreenScraper disc art for every disc of a multi-disc game by @sdornan in #4885
  • fix(auth): trust OIDC_TLS_CACERTFILE alongside the default CAs by @gantoine in #4891
  • fix(oidc): fetch claims missing from the ID token from the UserInfo endpoint by @gantoine in #4893
  • fix(auth): match OIDC users by their sub claim so an email change still logs in by @gantoine in #4892
  • fix(v2): pin trailing: false on native reprobe throttle by @sdornan in #4928
  • fix(frontend-v2): show the gallery scrollbar and add a back-to-top button by @gantoine in #4894
  • fix(v2): match RSelect stacked label spacing to text fields by @sdornan in #4942
  • fix(scan): reuse unchanged empty files on quick scan by @sdornan in #4949
  • fix: serialize single-file ROM promotion across gunicorn workers by @sdornan in #4898
  • fix(streaming): let the Webstation frame use camera, microphone and clipboard by @LoneAngelFayt in #4923
  • fix(auth): hash passwords with bcrypt directly and upgrade to bcrypt 5 by @sdornan in #4916
  • fix(roms): list the plain filename first in Content-Disposition by @gantoine in #4939
  • fix(backend): keep derived rom state in step with its source by @sdornan in #4931
  • fix(ra): match RetroAchievements by RA hash only, never by Hasheous' game ID by @sdornan in #4962
  • fix(v2): hide the game Achievements tab when the ROM has no achievements by @sdornan in #4968
  • fix(player): EmulatorJS multi-disk boot, Safari fullscreen quit, sparse gamepads, netplay audio, mame2003_plus default by @gantoine in #4953
  • fix(a11y): dark mode typography is WCAG AA safe by @andest01 in #4955
  • fix(tools): make generate_test_data work on PostgreSQL by @sdornan in #4978
  • fix(tests): give each event loop its own fake async Redis client by @sdornan in #4979
  • fix(streaming): let the desktop session own the controller by @LoneAngelFayt in #4974
  • fix(v2): render the v2 pair page and share the auth footer by @gantoine in #4980
  • fix(firmware): store uploads under the name written to disk by @LoneAngelFayt in #4986
  • fix(v2): check the active sort in the gallery sort menu by @sdornan in #4998
  • fix: eight bugs from hand-maintained lists drifting apart by @gantoine in #4991
  • fix(v2): size gallery bulk actions for whole-result selections by @gantoine in #4896
  • fix(api): return 4xx for overlong names and duplicate collections by @LoneAngelFayt in #4997
  • fix(converto): refuse cue sheets that reach outside their folder by @gantoine in #5008
  • fix(player): fix EmulatorJS keyboard-lock listener leak; use VueUse for v2 listener pairs by @sdornan in #5012
  • fix(v2): hide match rename toggle when the file name would not change by @sdornan in #5016
  • fix(backend): disable redis-py maintenance notifications probe by @sdornan in #5015
  • fix(player): let the browser Back button leave a running player by @sdornan in #5020
  • fix(v2): fix useResponsiveColumns observer leak; use VueUse for v2 resize observers by @sdornan in #5019
  • fix(v2): say what a native launch reports when its session expired or a save was deleted by @sdornan in #5021
  • fix(v2): enlarge region and language flags on v2 game cards by @gantoine in #5022
  • fix(docker): print the configured ROMM_PORT in the startup log by @gantoine in #5023
  • fix(streaming): release a failed claim from one place by @gantoine in #5024
  • fix(v2): copy over plain HTTP and skip task polling in hidden tabs by @sdornan in #5040
  • fix(v2): size the re-sort skeleton to the known result count by @sdornan in #5042
  • fix(scan): refresh screenshots when only their order changes by @gantoine in #5029
  • fix(scan): keep an uploaded manual when the rom had no scraped one by @gantoine in #5030
  • fix(input): show the focus ring when arrow keys move focus in Firefox by @andest01 in #5066
  • fix(db): keep iterated query results away from the cyclic GC by @LoneAngelFayt in #5068
  • fix(v2): draw under the iOS status bar and respect safe-area insets by @sdornan in #5075
  • fix(rom): keep a disc an .m3u lists in place when promoting to a folder by @sdornan in #5076
  • fix(v2): unsubscribe from the emitter on unmount via useEmitterEvent by @sdornan in #5085
  • fix(v2): input follow-ups: phantom-pad right stick, LB/RB on gallery routes by @sdornan in #5086
  • fix(v2): validation, spinner, nested button and label bugs by @sdornan in #5088
  • fix(i18n): correct es_ES "Platform updated" string by @sdornan in #5089
  • fix(v2): draw region flags on Chromium/Windows and center them in card chips by @sdornan in #5101
  • fix(auth): validate reset passwords and make the last-admin guard atomic, with tests by @sdornan in #5106
  • fix(v2): vertically align icons in Playable and LIVE chips by @sdornan in #5109
  • fix(v2): don't re-roll header stats when switching platform tabs by @sdornan in #5110
  • fix(sync): use only the device's own SSH key; test push-pull sync and the folder watcher by @sdornan in #5108
  • fix(metadata): one UTC helper for release dates, a lint rule against local-time parsing, and Flashpoint tests by @sdornan in #5114
  • fix(sync, activity): folder scan crash and hidden-ROM heartbeat; test both by @sdornan in #5117
  • fix(patcher): read the patcher's report from its last JSON line; test the patch endpoint by @sdornan in #5120
  • fix(activity): send play activity only to users who can see the ROM by @sdornan in #5121
  • fix(metadata): five provider fixes; test RetroAchievements, SteamGridDB, MobyGames, Playmatch and Pouët by @sdornan in #5123
  • fix(feeds): keep Switch files with uppercase extensions in the Tinfoil feed by @NotAFlightRisk in #5115
  • fix(activity): coalesce activity refreshes and keep one list request in flight by @sdornan in #5125
  • fix(metadata): five provider fixes; test UPC, LaunchBox media, Demozoo, ScreenScraper and HLTB by @sdornan in #5126
  • fix(metadata): four provider fixes; test Hasheous, TGDB, CSDb, gamelist and libretro by @sdornan in #5128
  • fix(igdb): harden Twitch auth; test the IGDB client and handler by @sdornan in #5133
  • fix(sync): store RetroArch Cloud Sync blobs under the assets volume by @gantoine in #5155
  • fix(v2): name dialogs and select listboxes; axe-check story overlays by @sdornan in #5151

Other changes

  • chore(deps): update mariadb docker tag to v13 by @renovate[bot] in #4537
  • test(backend): catch column nullability drift between models and migrations by @sdornan in #4756
  • perf(backend): read the gallery sorts out of an index by @sdornan in #4647
  • perf(sync): batch RetroArch manifest hashing and list PROPFIND paths unhashed by @gantoine in #4761
  • build(deps): bump uvicorn to 0.53 and uvicorn-worker to 0.4 by @sdornan in #4729
  • chore(deps): upgrade vitest and @vitest/ui to v5 by @sdornan in #4747
  • chore(deps): bump fastapi to 0.141.1 and replace fastapi-pagination with a local page model by @sdornan in #4715
  • docs(claude): trim root CLAUDE.md and split stack guidance into nested files by @gantoine in #4790
  • ci: run mypy in its own workflow instead of through Trunk by @sdornan in #4724
  • chore(mypy): require coded ignores and flag stale ones, type protected_route by @sdornan in #4794
  • ci: nightly image, index annotations and SBOMs by @sdornan in #4731
  • chore: delegate llm tasks to static analysis tools. by @andest01 in #4742
  • chore(docker): verify downloads with ADD --checksum and pin RAHasher by commit by @sdornan in #4781
  • refactor(backend): move engine-specific query SQL out of handlers into utils/sql_dialect.py by @sdornan in #4793
  • chore(mypy): require full annotations outside tests, type the setup responses by @sdornan in #4795
  • fix(deps): bump mariadb connector to 1.1.14 for Python 3.14 by @gantoine in #4814
  • chore(mypy): switch to strict mode with listed exceptions by @sdornan in #4817
  • chore(mypy): give every generic its type arguments by @sdornan in #4818
  • test(sync): isolate RetroArch blob and PSP pending dirs per test by @sdornan in #4824
  • refactor(backend): move FORCE INDEX hint and FULLTEXT match into sql_dialect by @sdornan in #4825
  • chore(mypy): drop warn_return_any and validate provider responses with strict JSON by @sdornan in #4826
  • chore(backend): enforce mypy's disallow_untyped_calls and disallow_untyped_decorators by @sdornan in #4827
  • chore(mypy): report untyped imports instead of disabling the check by @sdornan in #4828
  • chore(deps): upgrade Authlib to 1.8 and joserfc to 1.7 by @sdornan in #4829
  • chore(deps): move from httpx to httpx2 by @sdornan in #4830
  • chore(backend): replace zstandard with stdlib compression.zstd by @sdornan in #4831
  • chore(deps): bump uv to 0.12.19 and relax required-version for Dependabot by @sdornan in #4832
  • fix(deps): bump asyncssh, click, pygments and requests for open advisories by @sdornan in #4833
  • perf(gamelist): list each ES-DE media folder once per platform by @sdornan in #4841
  • chore(deps): bump redis to 8.1 and fakeredis to 2.38, drop types-redis by @sdornan in #4844
  • perf(scan): download a rom's media files concurrently by @sdornan in #4842
  • chore(skills): vendor humanizer and run it after pr-ready and the release drafts by @gantoine in #4845
  • chore(mypy): replace type-ignore defaults with typed injection sentinels by @sdornan in #4843
  • perf(backend): pipeline multi-command Redis writes by @sdornan in #4858
  • chore(lint): ban double-cast ROM fixtures in tests by @gantoine in #4863
  • docs: consolidate agent instructions into AGENTS.md and add a shared dev environment setup by @sdornan in #4869
  • ci: cache mypy between runs and install only Playwright's headless shell by @sdornan in #4873
  • chore(trunk): upgrade plugins and linters, automate upgrades, scan all files weekly by @sdornan in #4870
  • chore(trunk): Upgrade trunk by @github-actions[bot] in #4880
  • ci: coverage upload, Docker PR check, API type drift check and other CI maintenance by @sdornan in #4875
  • docs: fix the cloud environment setup script command in AGENTS.md by @sdornan in #4882
  • chore(trunk): remove the no-emdash Trunk linter after a one-time sweep by @gantoine in #4884
  • chore(storybook): Save State support by @andest01 in #4727
  • perf: run the chroma-key placeholder check off the event loop by @sdornan in #4899
  • ci: sign release images, gate on critical CVEs and scan for malware by @gantoine in #4890
  • chore(deps): update dependency @types/node to v24 by @renovate[bot] in #4908
  • chore(deps): update dependency @floating-ui/vue to v2 by @renovate[bot] in #4907
  • chore(deps): update dependency pytest-cov to v7 by @renovate[bot] in #4911
  • chore(deps): update dependency certifi to v2026 by @renovate[bot] in #4915
  • test(tasks): cover scheduled cron description in TasksSection by @sdornan in #4918
  • chore(deps): update dependency cronstrue to v3 by @renovate[bot] in #4917
  • chore(deps): update dependency ipykernel to v7 by @renovate[bot] in #4919
  • chore(deps): update dependency globals to v17 by @renovate[bot] in #4913
  • chore(deps): update dependency streaming-form-data to v2 by @renovate[bot] in #4924
  • chore(deps): update dependency vite-plugin-mkcert to v2 by @renovate[bot] in #4925
  • chore(deps): update dependency md-editor-v3 to v7 by @renovate[bot] in #4920
  • chore(deps): update dependency @vueuse/core to v15 by @renovate[bot] in #4909
  • refactor(frontend): derive state that was stored and synced by hand by @sdornan in #4929
  • chore(deps): update github actions by @renovate[bot] in #4910
  • perf(scan): read only the selected ROMs' gamelist entries by @sdornan in #4940
  • perf(gamelist): validate media tag paths once per directory by @sdornan in #4943
  • test(frontend): pin the timezone for pending-asset capture names by @sdornan in #4948
  • chore(lint): remove the no-emdash-in-comment ESLint rule after a one-time sweep by @gantoine in #4887
  • chore(deps): update dependency pinia to v4 by @renovate[bot] in #4921
  • chore(deps): update md-editor-v3 to v7 and fix its breaking changes by @sdornan in #4922
  • chore(deps): update dependency vuetify to v4 by @renovate[bot] in #4927
  • refactor(db): drop stored columns that duplicate derivable data by @sdornan in #4930
  • ci: grant issues: write to the build scan job by @sdornan in #4934
  • chore(deps): bump markdown-it from 14.2.0 to 14.3.2 in /frontend by @dependabot[bot] in #4960
  • chore(deps): bump urllib3 from 2.7.0 to 2.8.0 by @dependabot[bot] in #4964
  • chore(deps): update dependency axios to v1.20.0 [security] by @renovate[bot] in #4966
  • chore(deps-dev): bump brace-expansion from 2.1.4 to 2.1.7 in /frontend by @dependabot[bot] in #4961
  • test(backend): build test rows through shared factories and enforce them with ruff by @sdornan in #4972
  • chore(deps): update typescript to 6.0.3 and hold below 7 by @gantoine in #4963
  • refactor: remove unused code and deduplicate copied helpers by @sdornan in #4975
  • refactor(db): follow-ups to the derived-state fixes by @sdornan in #4969
  • refactor(db): escape LIKE searches with SQLAlchemy's autoescape by @sdornan in #4976
  • docs: recommend pytest-xdist for local backend test runs by @sdornan in #4983
  • perf(search): keep the FULLTEXT index on searches with short words or stopwords by @sdornan in #4970
  • chore(deps): bump tornado from 6.5.8 to 6.5.9 by @dependabot[bot] in #4985
  • chore(wcag): AA misc fixes by @andest01 in #4971
  • perf: trim idle memory of the backend processes by @gantoine in #4981
  • perf: keep task code out of the cron scheduler and idle RQ workers by @gantoine in #4982
  • chore(deps): update frontend dependencies by @renovate[bot] in #4989
  • chore(deps): update backend dependencies by @renovate[bot] in #4990
  • chore(deps): upgrade SQLAlchemy to 2.1 by @sdornan in #5007
  • refactor(db): replace deprecated noload() with raiseload() by @sdornan in #5009
  • refactor(v2): share popover dismissal across RSelect, RComboboxField, RDateField and RMenu by @sdornan in #5010
  • refactor(v2): route v2 server-backed search boxes through useDebouncedSearch by @sdornan in #5011
  • refactor(v2): route remaining v2 clipboard writes through useClipboard by @sdornan in #5017
  • test(backend): fix order-dependent test_saves / test_states db handler tests by @sdornan in #5018
  • refactor(v2): track AppNav's window scroll with useWindowScroll by @sdornan in #5025
  • test(frontend): auto-unmount every test's wrappers from the global setup by @sdornan in #5026
  • test(frontend): give every test a fresh Pinia from the global setup by @sdornan in #5028
  • test(frontend): share the vue-i18n test mock and let Vitest reset mocks, stubs and env by @sdornan in #5027
  • refactor(v2): use VueUse for v2 debounces, intervals, the page title and avatar preview by @sdornan in #5035
  • refactor(v2): use useTimeoutFn for v2's one-shot timers and useEventListener for hotkeys by @sdornan in #5037
  • refactor(v2): add useRouteQueryParam for URL-backed v2 view state by @sdornan in #5039
  • refactor(v2): load self-fetching v2 components through useFetchState by @sdornan in #5038
  • test(frontend): restore real timers globally and echo params from the shared i18n mock by @sdornan in #5043
  • refactor(v2): use VueUse for v2's rAF loops, listener pairs and grid observers by @sdornan in #5045
  • chore(storybook): fix the RTooltip ParentAttach story so the tooltip renders by @sdornan in #5047
  • refactor(v2): share v2's visible-tab poll and move the last timers and RTooltip to VueUse by @sdornan in #5048
  • refactor(v2): use VueUse for v2's image previews, remaining observers, rAF loops and timers by @sdornan in #5052
  • chore(frontend): tighten frontend typechecking by @sdornan in #5053
  • chore(frontend): enable strictVModel and make RSelect, PlatformSelect and RTabNav generic by @sdornan in #5055
  • chore(converto): bump rom-converto to v0.23.2 in both images by @DevYukine in #5041
  • ci: cache frontend node_modules and skip npm ci on a lockfile match by @sdornan in #5044
  • ci(e2e): copy frontend/assets into the preview build by @gantoine in #5056
  • chore(frontend): split the frontend tsconfig into project references by @sdornan in #5057
  • chore(frontend): prepare the gallery and grid-nav composables for noUncheckedIndexedAccess by @sdornan in #5058
  • refactor(v2): render v2 QR codes with useQRCode from @vueuse/integrations by @sdornan in #5060
  • chore(frontend): prepare the rest of the v2 app code for noUncheckedIndexedAccess by @sdornan in #5061
  • chore(frontend): prepare the shared frontend code for noUncheckedIndexedAccess by @sdornan in #5062
  • chore(frontend): enable noUncheckedIndexedAccess by @sdornan in #5063
  • chore(frontend): prepare v2 and shared code for exactOptionalPropertyTypes by @sdornan in #5065
  • chore(frontend): enable exactOptionalPropertyTypes by @sdornan in #5067
  • perf(scan): skip the second write and reload for newly inserted roms by @LoneAngelFayt in #5070
  • perf(roms): faster smart collection and filter value queries by @LoneAngelFayt in #5069
  • refactor(backend): route ROM visibility through a single RomVisibilityFilter by @sdornan in #5064
  • perf(config): read config.yml values with a plain dict walk by @LoneAngelFayt in #5071
  • test(db): give worktree runs their own temporary test database by @gantoine in #5074
  • perf(tests): faster database setup and cleanup in the backend suite by @LoneAngelFayt in #5073
  • perf(frontend): smaller first load and faster vitest setup by @LoneAngelFayt in #5072
  • refactor(v2): simplify keyboard/gamepad handling by @sdornan in #5084
  • perf(frontend): load md-editor on demand instead of with the app by @LoneAngelFayt in #5080
  • refactor(v2): share the scan form's hash-matcher switches and scan-type select by @sdornan in #5099
  • chore(deps): update frontend dependencies by @renovate[bot] in #5103
  • chore(deps): update backend dependencies by @renovate[bot] in #5104
  • chore(deps): update actions/upload-code-coverage action to v1.4.4 by @renovate[bot] in #5105
  • chore(deps): upgrade apprise to 2.0 by @sdornan in #5102
  • test(tasks): cover the WebP conversion task by @sdornan in #5112
  • test(utils): read tags and covers from real audio files in each format by @sdornan in #5113
  • test: platform fixture factories for the backend and frontend by @sdornan in #5119
  • test: typed ROM fixtures, and no model casts in tests or stories by @sdornan in #5122
  • refactor(metadata): share the indexed filename lookup across IGDB, Moby and SS by @sdornan in #5130
  • refactor(metadata): share provider request retries and error mapping by @sdornan in #5132
  • refactor(metadata): share heartbeats, filename id tags and the Moby rom builder by @sdornan in #5134
  • chore(e2e): Update playwright with smaller focus by @andest01 in #4932
  • chore(deps): update backend dependencies by @renovate[bot] in #5136
  • chore(e2e): check every page for accessibility violations with axe by @andest01 in #4951
  • chore(trunk): pin the CLI download to its sha256 by @gantoine in #5140
  • chore(trunk): Upgrade trunk by @github-actions[bot] in #5137
  • chore(trunk): fix the scheduled trunk check-all failures by @gantoine in #5141
  • chore(deps): update dependency md-editor-v3 to v7.1.0 by @renovate[bot] in #5142
  • chore(v2): let the theme toolbar drive every lib story by @sdornan in #5150
  • refactor(v2): extract RSortHeader and make the index lists real tables by @sdornan in #5149
  • chore(eslint): adopt @vue/eslint-config-typescript and lint with type information by @sdornan in #5148
  • chore(dev): DEV_PROXY_TARGET to run the frontend against a remote RomM by @gantoine in #4763
  • chore(storybook): add an input modality toolbar by @gantoine in #4791
  • chore(locales): it_IT translation by @dms1e4 in #5006

New Contributors

Full Changelog: 5.3.1...5.4.0-alpha.1

Don't miss a new romm release

NewReleases is sending notifications on new releases.