mirror of
https://github.com/rommapp/romm.git
synced 2026-10-11 22:40:04 +00:00
master
13814
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ca2a36ca54 |
Move corporate sponsors above friends in README (#5219)
Rename the "Supported By" section to "Corporate Sponsors" and place it before "Our Friends". Claude-Session: https://claude.ai/code/session_011ekeVc4SX98ituuWg5t9SF Co-authored-by: Claude <[email protected]> |
||
|
|
68f71259f7 |
fix(config): migrate retired filesystem folder keys instead of exiting (#5211)
* fix(config): migrate retired filesystem folder keys instead of exiting
5.2 wrote filesystem.roms_folder and filesystem.firmware_folder into
config.yml on every settings save, so instances that never chose a layout
crash-looped on 5.3.1. Appliance installs (TrueNAS, Unraid, CasaOS) have
no way to edit the file.
Translate the keys at load time into the filesystem.structure templates
that reproduce the layout 5.2 resolved them to, using the same disk check
5.2 used to choose between `<folder>/{platform}` and `{platform}/<folder>`.
A declared structure template still wins. The migrated config is written
back when config.yml is writable; otherwise it applies in memory and logs
the replacement once.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* fix(config): refuse folder names a template can't express before migrating
Addresses Greptile review on #5211: a retired folder name containing braces
produced an invalid template after config.yml had already been rewritten.
The migrated templates are now parsed first and startup exits with the
reason, leaving the file untouched. Also trims a test docstring.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* fix(config): code-review fixes for the retired folder key migration
Keep an undetectable layout in memory, accept a '.' folder, honour a
differently cased declared key, stop retrying a failed write, and share
the layout detection with check_library_layout.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* refactor(config): simplify pass over the retired folder key migration
Build the templates in one pass with a parser per key, reuse get_nested
and the library fixture, skip the layout string on the in-memory path,
and memoize a detected layout across get_config() calls.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* test(config): review-polish, drop a docstring that restates the test name
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* fix(config): migrate retired folder keys at startup and write config.yml atomically
Loading the config only translates the retired keys in memory now;
startup writes the migration once, before any worker runs, so processes
no longer race to rewrite config.yml.
config.yml is written to a sibling temp file and renamed over the
original, keeping a symlink and the file mode. When the rename fails
(a config.yml bind-mounted on its own), the in-place write puts the
previous content back if it fails, so a full disk no longer empties it.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* refactor(config): drop the atomic config write and flatten the folder key migration
The temp-file rename, symlink/mode handling and in-place restore are a
separate concern from migrating the retired keys, so config.yml goes back
to the plain write. The translation validates its two templates directly
instead of through a table of parsers.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
5.4.0
|
||
|
|
88b3ebe49d |
refactor(streaming): let the backend pick the resume picker's states (#5210)
* refactor(streaming): let the backend pick the resume picker's states
The player decided which of a ROM's states the resume picker offers from
a core rule the config shipped, and so repeated the rules the claim
checks a pick against.
The player now asks `GET /api/streaming/states/{rom_id}` which states
load natively and which the broker can import, so the rules live only in
the backend. A native state whose filename carries no slot number is
offered only when the broker can import it, since it can't resume as is.
The player asks again only when the ROM's states, the emulator or the
import kinds change, keeps its last answer if a refresh fails, and holds
Play until the first answer for the ROM arrives, so a quick click can't
skip the state about to be preselected.
`StreamingContainerSchema.state_core` and `StateCoreSchema` are removed,
and the config no longer asks each broker for its core.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
* fix(streaming): address Greptile review on the state picks
Refetch the picks when a state is renamed, since the filename decides whether it resumes as it is, and fold the new docstrings to one sentence.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
---------
Co-authored-by: Zach Clendenen <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
5.4.0-beta.1
|
||
|
|
26d83db6ea |
fix(streaming): keep a stream capture Cloud Sync serves from its slot (#5209)
* fix(streaming): keep a stream capture Cloud Sync serves from its slot A stream capture filed in a core's folder is the state Cloud Sync serves for its slot while it is the newest there. Pruning the stream's state history past its limit would delete it, and a device holding it as that slot would lose the slot. The prune now skips a capture a slot serves, until a newer state takes that slot or the capture's file is gone from disk. History can go over the limit by at most one capture per slot. Co-Authored-By: Claude Fable 5.1 <[email protected]> * docs(streaming): fold the prune_state_history docstring to one sentence Greptile review on #5209. Co-Authored-By: Claude Fable 5.1 <[email protected]> --------- Co-authored-by: Zach Clendenen <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]> |
||
|
|
5270c5d31e |
fix(sync): file a screenshot sent ahead of its state past a stream capture (#5208)
* fix(sync): file a screenshot sent ahead of its state past a stream capture A Cloud Sync client sends a slot's screenshot before the state it belongs to. With a stream capture serving the slot, the screenshot was filed under the capture's name and wrote over the capture's own thumbnail. The screenshot now goes to the state the slot's next upload writes to: the slot's newest non-capture state, or the canonical name when the upload will create one. A copy of the capture's own screenshot sent back is left alone. Co-Authored-By: Claude Fable 5.1 <[email protected]> * refactor(sync): address Greptile review on the stream capture checks Move the capture copy checks for a state and a screenshot upload out of the endpoint into the sync handler, and fold the core_library_name, prune_state_history and Stream.vue comments to two lines. Co-Authored-By: Claude Fable 5.1 <[email protected]> --------- Co-authored-by: Zach Clendenen <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]> |
||
|
|
261178cb25 |
fix(streaming): share RetroArch stream saves and states with Cloud Sync (#5197)
A streamed RetroArch exit's save landed in "No slot", and the stream's states were filed under a different emulator than the one Cloud Sync and the other RetroArch clients use, so neither side could see the other's. A streamed exit's save is now stored as an `autosave` version, like the other clients' autosaves. Stream saves and states are filed under the core's RetroArch folder, which the broker reports as `library_name`; a broker that doesn't is filed under the core id. A stream resumes from any state in the booted core's folder, including other clients' states and ones filed under an alias of the folder, and the picker's core rule gains the folder's names so it offers them too. Cloud Sync shares the slot with stream captures but never writes over or deletes one: an upload goes to the slot's newest non-capture state or creates one, sending a capture's own bytes back is a no-op, and a delete removes the slot's other states and leaves captures in place. Co-authored-by: Claude Fable 5.1 <[email protected]> |
||
|
|
1f6b4d11ca |
feat(permissions): gate emulator streaming and in-browser play behind group permissions (#5185)
* feat(permissions): gate emulator streaming behind a group permission Add a streaming permission entity. Read shows streaming and allows joining a session; write allows starting one. Users without read get a disabled streaming config, so every streaming surface hides. Migration 0152 grants read and write to every existing group, so nobody loses access on upgrade. Groups created afterwards start without it. Owner-scoped session controls (save, exit, release) stay ungated so a revoked user can still leave a running game without losing saves. * fix(streaming): pr-ready code-review fixes for the streaming grant Own-only read no longer joins other users' sessions, the config refetches on permissions:changed, the store skips the joinable request when streaming is off, and tests clean up the groups they create. * refactor(streaming): pr-ready simplify pass on the streaming grant Collapse the streaming permission helpers onto assert_can/can_access, return an empty joinable list instead of 403, skip the permission resolve when streaming is off, and add a stream.start action key so the UI hides Stream (but keeps Join) for users without the write grant. * docs(streaming): pr-ready review-polish pass on the streaming grant Trim the 0152 migration docstring to a single summary line, matching 0150 and 0151. * docs(streaming): pr-ready humanizer pass on the streaming grant Plainer wording for the stream.start action key comment and the Permission Groups paragraph, which now says an own-only read shows streaming without the join. * feat(permissions): gate in-browser emulation behind a group permission Adds an emulation entity next to streaming. Read plays in the browser (EmulatorJS, Ruffle, js-dos, PICO-8, EasyRPG) and joins netplay rooms, write hosts them. Migration 0152 now backfills both entities onto every existing group. * fix(streaming): address Greptile review on PR 5185 - Pin the claimed container in the stream player, so a permission refresh that turns streaming off mid-game keeps save, load and disc swap until the session ends. - Empty the Join list when a refreshed config turns streaming off, and discard any joinable request still in flight. - Update the installPermissionsHydration doc to name every layout that calls it. * fix(migrations): renumber play permissions migration to 0153 after master's 0152 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * chore(permissions): drop redundant PermEntity comments Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(streaming): drop the live config refresh on permission changes Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Georges-Antoine Assi <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
0a54359ab4 |
refactor(config): parse every comma-separated env var with one helper (#5207)
* fix(oidc): match any of the comma-separated groups in OIDC_ROLE_* OIDC_ROLE_ADMIN/EDITOR/VIEWER were read as one raw string and checked for membership in the roles claim, so a value like "romm-admin,platform-admins" never matched and the login was rejected with 403. Parse each variable into a set of stripped, non-empty values and grant the role when it intersects the claim. A single-valued string claim is matched whole rather than by substring. Fixes #4302 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(oidc): reject a string role claim that only contains a mapped group Addresses Greptile's review on #5203: the string-claim case only covered a match, which the old substring check would also have passed. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(config): parse every comma-separated env var with one helper Add _get_env_list and build _get_env_set on it, with an optional fallback, so ROMM_CORS_ALLOWED_ORIGINS and DEVICE_INSTALL_EXCLUDED_PLATFORM_SLUGS stop hand-rolling the split/strip/drop-empty parse. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
507f589124 |
fix(v2): keep a save uploaded for a core in autosave (#5205)
RetroArch Cloud Sync now serves a ROM and core's newest autosave .srm and hides an unslotted .srm behind it, so the upload dialog moving a save picked with a core to "No slot" left it invisible to RetroArch once RetroArch had synced that game. The dialog keeps the autosave default, and the slot hint says RetroArch reads autosave. Refs #5158 Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
de62fa3815 |
fix(oidc): match any of the comma-separated groups in OIDC_ROLE_* (#5203)
* fix(oidc): match any of the comma-separated groups in OIDC_ROLE_* OIDC_ROLE_ADMIN/EDITOR/VIEWER were read as one raw string and checked for membership in the roles claim, so a value like "romm-admin,platform-admins" never matched and the login was rejected with 403. Parse each variable into a set of stripped, non-empty values and grant the role when it intersects the claim. A single-valued string claim is matched whole rather than by substring. Fixes #4302 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(oidc): reject a string role claim that only contains a mapped group Addresses Greptile's review on #5203: the string-claim case only covered a match, which the old substring check would also have passed. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
92b6421e65 |
fix(saves): make a deduplicated slot upload the slot head (#5204)
* fix(saves): make a deduplicated slot upload the slot head A slotted upload whose bytes match an older version in the slot returned that version without touching its updated_at, so the slot's newest save stayed one the device had moved away from. A client re-downloading the slot head then restored the wrong save. Bump the matched version before pruning so it becomes the head and survives the cap. A retry of the current head is left alone so other devices don't see a spurious update. Fixes #5200 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(saves): promote a deduplicated save in the handler, past same-second heads Addresses Greptile review on #5204: the head promotion moves out of the endpoint into db_save_handler.promote_to_slot_head, and the promoted timestamp is at least a second past the current head's, so a head written in the same second can no longer win the id tiebreak. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
989bfb8553 |
fix(scan): reassociate a moved ROM by title id when its hashes changed (#5202)
Reassociation matched a moved file to its missing entry by CRC, MD5 and SHA1, and only consulted the binary title id when the hashes were absent. Moving a GameCube iso into its own folder next to a dumpinfo.txt makes it a folder ROM whose hash spans both files, so the hashes no longer matched, the title id was never tried, and the scan created a new entry that showed up under Recently added. Fall back to the title id when the hashes match no missing entry. Hashes still win when they match, and ambiguous hashes still return no match rather than letting the title id break the tie. Fixes #5046 Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
f89f6ef185 |
fix(security): check collection ownership in the ROM filters (#5201)
* fix(security): check collection ownership in the ROM filters The collection endpoints refuse a private collection to anyone but its owner, but the ROM listing endpoints accepted the same id as a collection_id or smart_collection_id filter without checking it. Any user (or a kiosk visitor) could list, randomly pick from and bulk download another user's private collections and Favourites, and re-share one through a public smart collection scoped to it. The membership filters now match only collections that are public or owned by the caller. A collection_id inside smart collection criteria is checked against the smart collection's owner, both when the criteria are saved and when they are evaluated. Reported by Takaya Sato (dagadjno). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(security): clear stale smart collection caches across owners Addresses Greptile's review on #5201. Smart collections cache their membership on the row, so one scoped to another user's private collection kept serving the members it cached before the ownership check. Migration 0152 empties those caches on upgrade, and sharing or unsharing a collection now refreshes the smart collections scoped to it. Also covers a second user listing a public smart collection scoped to its owner's private collection. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(collections): harden smart collection scope refreshes (code-review) Allow re-saving a smart collection whose stored scope was since lost, reject non-object criteria in from_stored_criteria, guard the scoped refresh, and refresh smart collections when a collection's ROMs are replaced or the collection is deleted. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(collections): share the refresh wrapper and trim scope tests (simplify) Move the scoped smart collection refresh next to the existing wrapper, match the scope on the raw criteria before refreshing, batch the migration update, and fold the duplicate unshare test into helpers. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * docs(migrations): describe the cleared smart scopes by current behavior (review-polish) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * docs(roms): state the collection filter comment plainly (humanizer) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
09d8360337 |
perf(frontend): serve the small cover to 1x screens (#5184)
* minimal version * test(v2): cover that the srcset serves the small cover at low DPR Add unit tests for the cover srcset (small at 1x, large at 2x, webp, URL escaping, no srcset for an override or alt art) and a Chromium story that checks the browser picks the small cover at a device pixel ratio of 1 or less. Shorten the DPR comment in useCoverArt. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> * perf(v2): limit the cover srcset to small slots The small cover is a 0.2x or 0.4x downscale of the large one, so it can be narrower than wide slots such as the details page or player. Gate the srcset behind a `responsive` prop that grid cards (xs to md) and the small Home and settings thumbnails opt into. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(v2): address Greptile review on the GameCover srcset story Fill the story covers from colorCoverArt tokens instead of hex literals, expect the candidate that matches the screen's density so the story passes on high-DPR displays, and move the component srcset checks from Vitest into Storybook play() functions, adding a story for the default without srcset. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Sonnet 5.5 <[email protected]> Co-authored-by: Georges-Antoine Assi <[email protected]> |
||
|
|
be6ad905b1 |
fix(sync): serve the newest on-disk RetroArch state in a slot (#5193)
* fix(sync): resolve RetroArch paths across emulators sharing a core folder Several RomM emulator ids map to one RetroArch folder (mednafen_psx_hw, beetle_psx_hw and the raw "Beetle PSX HW" all sync through saves/Beetle PSX HW/). The manifest built paths from whichever id an asset carried, but GET parsed the folder back to the canonical id and matched it exactly, so an asset filed under another alias was advertised and then 404'd. Uploads also started a second autosave history beside the alias's. The manifest now lists the newest asset per folder, and GET, PUT and DELETE resolve a path across every alias. Rewrites of an existing unslotted save or state stay under that row's own emulator. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): prefer on-disk alias saves and share newest-per-key grouping pr-ready step 2 (code-review --fix). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(sync): one alias helper and a lineage computed once pr-ready step 3 (simplify). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(sync): type the alias fixture's asset kind pr-ready step 4 (review-polish). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(sync): isolate the asset dir per RetroArch sync test The alias tests write different bytes to the same asset paths, and user and ROM ids repeat across xdist worker databases, so parallel runs raced on those files. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): serve the newest on-disk RetroArch state in a slot Missing states are dropped before slot grouping for the manifest and reads, so a newer missing state (or alias) no longer hides an older one still on disk. A PUT still matches the missing row and revives it in place. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): fall back past a RetroArch file that vanished unflagged Addresses Greptile's review on #5193: a state whose file was deleted before `missing_from_fs` was set still won its slot. The manifest now rebuilds when hashing flags an asset, and GET flags a missing file and resolves again, so an older asset at the path is listed and served. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
57ebac8efe |
perf(sync): resolve RetroArch core aliases in one query (#5192)
* fix(sync): resolve RetroArch paths across emulators sharing a core folder Several RomM emulator ids map to one RetroArch folder (mednafen_psx_hw, beetle_psx_hw and the raw "Beetle PSX HW" all sync through saves/Beetle PSX HW/). The manifest built paths from whichever id an asset carried, but GET parsed the folder back to the canonical id and matched it exactly, so an asset filed under another alias was advertised and then 404'd. Uploads also started a second autosave history beside the alias's. The manifest now lists the newest asset per folder, and GET, PUT and DELETE resolve a path across every alias. Rewrites of an existing unslotted save or state stay under that row's own emulator. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): prefer on-disk alias saves and share newest-per-key grouping pr-ready step 2 (code-review --fix). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(sync): one alias helper and a lineage computed once pr-ready step 3 (simplify). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(sync): type the alias fixture's asset kind pr-ready step 4 (review-polish). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(sync): isolate the asset dir per RetroArch sync test The alias tests write different bytes to the same asset paths, and user and ROM ids repeat across xdist worker databases, so parallel runs raced on those files. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * perf(sync): resolve RetroArch core aliases in one query The saves handler now filters on a collection of lineages and looks up a file name across several folders, so the RetroArch alias resolvers issue one query each instead of one per alias. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * perf(sync): filter a RetroArch delete to autosave and unslotted saves in SQL Addresses Greptile's review on #5192: saves_at_path loaded every slot of the matching lineages and dropped the named slots in Python. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
79d1feba6c |
fix(sync): resolve RetroArch paths across emulators sharing a core folder (#5191)
* fix(sync): resolve RetroArch paths across emulators sharing a core folder Several RomM emulator ids map to one RetroArch folder (mednafen_psx_hw, beetle_psx_hw and the raw "Beetle PSX HW" all sync through saves/Beetle PSX HW/). The manifest built paths from whichever id an asset carried, but GET parsed the folder back to the canonical id and matched it exactly, so an asset filed under another alias was advertised and then 404'd. Uploads also started a second autosave history beside the alias's. The manifest now lists the newest asset per folder, and GET, PUT and DELETE resolve a path across every alias. Rewrites of an existing unslotted save or state stay under that row's own emulator. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): prefer on-disk alias saves and share newest-per-key grouping pr-ready step 2 (code-review --fix). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(sync): one alias helper and a lineage computed once pr-ready step 3 (simplify). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(sync): type the alias fixture's asset kind pr-ready step 4 (review-polish). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(sync): isolate the asset dir per RetroArch sync test The alias tests write different bytes to the same asset paths, and user and ROM ids repeat across xdist worker databases, so parallel runs raced on those files. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): delete every aliased state in a slot and prefer one on disk Addresses Greptile review on #5191: a slot's state is picked live-first across aliases, so a newer missing alias no longer hides an older one on disk, and DELETE removes every state in the slot so an older alias can't resurface at the same path. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
e232a52656 |
fix(docker): boot when the container runs as a non-default UID (#5189)
* fix(docker): boot when the container runs as a non-default UID Unraid runs the container as 99:100, which can't write to the image's /romm (owned by 1000, mode 755), only to the mounted subfolders. The PSP pending dir at /romm/cache/retroarch_sync was created at import time, so every gunicorn worker failed to boot. - Buffer pending PSP save files under the user's assets folder (assets/users/<user>/retroarch/psp_pending/), beside the other RetroArch Cloud Sync files and outside the client-reachable blob categories. - Move the zip and rom-converto download caches to /tmp/romm/cache, and point nginx's internal /cache/ alias there. - Put gunicorn's control socket in /tmp/gunicorn instead of $HOME/.gunicorn, since HOME is / for that UID. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(docker): honor ROMM_TMP_PATH for caches, explain an unwritable sync folder - The download caches live under $ROMM_TMP_PATH/cache when it's set, so installs with a small Docker disk (Unraid's docker.img) can point them at a share. The entrypoint exports the same path for nginx's /cache/ alias. - With ENABLE_SYNC_FOLDER_WATCHER on and /romm/sync not writable, /init warns and skips the watcher instead of dying on mkdir under errexit. - The compose example lists the /romm/sync mount the sync features need. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(sync): cover reading back and clearing buffered PSP files Addresses Greptile's review on #5189. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): apply code-review findings to PSP pending buffer Remove the emptied psp_pending folder after a merge, define the sync folder path once in init, update the architecture doc's PSP and blob locations, and drop a stale .gitignore entry. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(sync): apply simplify findings to PSP pending cleanup Clear the buffered folder with remove_directory, let the startup check own creating the sync folder, and reword the test fixture docstring. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): cap buffered PSP files and drop them on delete Buffered files for an unresolved save folder now count against the bundle limits, so a folder can't fill the assets share and then fail every merge once it resolves. A DELETE also removes the buffered copy, so a deleted file isn't merged back when the folder resolves. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(docker): check the sync folder for push-pull sync too ENABLE_SYNC_PUSH_PULL keeps SSH keys and device saves under /romm/sync as well, so an unwritable folder now disables it with the watcher and warns at boot instead of failing when the task runs. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(docker): strip whitespace from env vars like the backend does The backend strips every variable it reads, while the init script and nginx templates used raw values, so a padded ROMM_TMP_PATH sent nginx's /cache/ alias somewhere the backend never writes. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * chore(config): drop the CACHE_BASE_PATH comment Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * docs(docker): say which backend config strips env whitespace Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * revert(docker): drop the entrypoint env whitespace trimming Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): apply pr-ready code-review findings Gate push-pull on its SSH keys path rather than a writable sync folder, accept the same truthy values as the backend, and clear only the merged PSP pending files so another worker's buffered file survives. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor: apply pr-ready simplify findings Share one truthy pattern across init's flag checks (the watcher gates now accept the same values as the backend), disable gunicorn's unused control socket instead of placing it, derive the converts redirect from its own cache path, and factor the unknown PSP folder in tests. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * docs(docker): apply pr-ready humanizer pass to the sync mount note Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(fs): create handler base folders on first write, skip leftover temp files FSHandler no longer mkdirs its base path when constructed, so a module-level handler rooted at an unmounted or unwritable folder can't take down boot; every write path already creates its parent folders. Blob listings skip .romm_tmp_ files a crashed write left behind, so they are never merged into a PSP bundle or advertised in the sync manifest. The prefix now lives in one TEMP_FILE_PREFIX constant. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
1060afc1af |
fix(devices): count a device that polls claim as online (#5190)
A client that drains installs by polling claim (Argosy) never holds the /devices socket, so the install dialog showed it offline while awake. Each claim now sets a presence key that lapses after the same 90 seconds as a socket's. Also add TZ to env.template, since save version names and log timestamps follow the container's time zone. Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
e722e96d91 |
docs(readme): add Vita RomM to community projects (#5188)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
ec10e5b930 |
fix(platforms): give platforms missing from IGDB a family (#5187)
* fix(platforms): give platforms missing from IGDB a family Platform families only came from the IGDB platform list, so platforms IGDB doesn't list (win9x, win3x, Sega arcade boards, TI computers, ...) landed in "Other" when grouping the platforms page by family. Platforms IGDB doesn't list now either share the family and category of an IGDB sibling (win9x -> Windows), or get an explicit family, which can override the sibling's (Sega Model 2 -> Sega, Arcade). 108 of the 242 unlisted platforms get a family; the rest are engines, generic groups, single-member makers or ambiguous. Fixes #5178 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(platforms): satisfy mypy when applying a platform family Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(platforms): check every unlisted platform mapping resolves a family Addresses Greptile review on #5187. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
4e8fec6845 |
perf(frontend): serve only the mdi icons the app uses (#5118)
* perf(frontend): serve only the mdi icons the app uses @mdi/font ships a CSS rule and four font formats for every icon. Generate a trimmed stylesheet from the icons named in the source and alias the package import to it. - scripts/build-icons.ts writes src/plugins/materialdesignicons-trimmed.css (woff2 only, LF line endings, no source map pointer). predev and prebuild run it through build:icons, and CI checks the committed file is current. - scripts/trim-mdi-fonts.ts builds the allow list from src/, Vuetify's own icon set and the icons the backend names. Only a plain .mdi-name::before rule can be dropped; anything unsure is kept. - Custom notification icons the app does not draw fall back to the level icon instead of rendering blank. - An "All icons (mdi)" story under Primitives/RIcon shows every shipped icon with search, plus the icons that are available but not shipped. - Vitest keeps ?raw CSS imports instead of emptying them. - build:tokens leaves tokens.css alone when only formatting differs. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> * perf(frontend): trim the mdi icon css in a Vite plugin Replace the generated, committed icon CSS with a transform plugin that trims @mdi/font in memory, in dev, Storybook and build. A watcher reloads dev when the source names a new icon. Drops build-icons.ts, the alias, the CI staleness check and the checked-in CSS. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> * PR feedback * style(frontend): sort imports in notifications test Co-Authored-By: Claude Sonnet 5.5 <[email protected]> * perf(frontend): reduce the mdi icon trim to a build-only plugin Drop the icon browser story, the notification glyph probe, the build:tokens compare and the woff2-only font rewrite. The plugin now only removes unused icon rules in production builds, so dev serves the full set and needs no file watcher. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Sonnet 5.5 <[email protected]> Co-authored-by: Georges-Antoine Assi <[email protected]>5.4.0-alpha.2 |
||
|
|
38dfb208f0 |
fix(streaming): file RetroArch streaming exits as raw saves (#5174)
* fix(streaming): keep foreign saves offered when the import-spec check fails A broker that misses the 5 s import-spec deadline used to read as "imports nothing": the Saves tab vanished and foreign picks were refused with "Save is not a restorable archive". Keep the last good answer in Redis so every worker can fall back on it, cache 404s for five minutes instead of forever, answer a pick that can't be checked with a retryable 503, and refetch the streaming config whenever the Stream page mounts. * fix(streaming): code-review fixes for the import-spec fallback Hold a fallback answer for 10 s so one claim's checks don't each wait out the broker timeout, read the stored answer back through the broker response parser, and warn about a pre-import broker once per worker instead of at every cache expiry. * refactor(streaming): simplify pass on the import-spec fallback Make ImportSpecUnavailable the 503 itself so the save and state pickers no longer convert it by hand, and store the broker's own response body as the last answer instead of rebuilding its shape. * chore(streaming): review-polish on the import-spec fallback Trim the cache comments and the exception docstring, fit the 503 detail within the line limit, and annotate the fresh-worker test dict for mypy. * docs(streaming): humanizer pass on import-spec comments and test docstrings * fix(streaming): tell lasting import-spec failures from passing ones Only timeouts, connection errors, 5xx and 429 fall back to the broker's last answer. A refused request (401, other 4xx) or a reply RomM can't read caches no imports for 30 s and logs an error, so a pick gets a refusal instead of an endless retry. A 404 from a broker that answered before uses that answer, since it is likelier a restart behind a proxy than a downgrade. * fix(streaming): ask for a retry when a picked state can't be checked An older save with the newest state resumes the pick only through an import, so an unanswered import-spec check now returns the 503 instead of launching from the older archive's own state. * fix(streaming): share an in-flight config request App load and the Stream page both fetch the config; two requests could land out of order and leave the older answer in place. (Stability item 5.) * fix(streaming): hold the resume picker until the fresh config lands What the broker imports decides which saves and states are on offer, and the app-load config can predate its answer. The panel shows a spinner until the Stream page's own refetch settles, so a pick can't vanish under the player. (Stability item 4.) * fix(streaming): ask every platform's broker at once on /config The checks ran on the default thread pool (cpu+4 threads), so a broker serving more platforms than that cost one timeout per wave: 10.7 s for 14 platforms on a 6-CPU dev box with a silent broker. The Stream page's picker now waits on /config, so the checks get a pool of their own. * fix(streaming): address Greptile review on the import-spec fallback - Play waits on the fresh config with the resume picker, so it can't send a preselected state the player can't see yet. - The streaming store gets a reset() for logout, and a config reply asked for as the previous account is dropped instead of landing in the next one. - The configFresh write after the refetch checks useIsAlive(). - Trimmed the require_import_spec docstring, the configFresh comment and the /config pool comment to the house length. - Declined: a worker-wide pool for the /config checks. Each request's threads end within the broker timeout, and a shared bounded pool would bring back timeout waves under concurrent loads. * fix(streaming): share /config broker checks across requests Addresses Greptile's re-raised finding on #5174 that each /config request opened its own pool of up to 64 threads, so overlapping page loads during a silent broker multiplied the worker's threads. - The checks run in one worker-wide pool, and a check already in flight for the same container and platform is joined rather than asked again, so concurrent page loads hold no more threads than one does, with no queued-timeout waves. - Callers await the shared run through asyncio.shield, so a request that disconnects doesn't cancel the check others are waiting on. * fix(streaming): file a RetroArch exit's save raw and boot the newest save A RetroArch exit holding one .srm and its states is no longer stored as a .saves.zip: the .srm is filed bare under the core's emulator name with a timestamped name, and each state goes through the state store. Exits with more than one save, a memory card, or unlabelled members still keep the zip. A launch with no pick now boots the newest save, raw or zip. A newer bare .srm goes through the broker's save import when the broker accepts one, and the launch screen defaults to the same save. * fix(streaming): keep the exit zip when a raw save can't be filed safely pr-ready step 2 (code review). - Keep the zip for a core that isn't one plain folder name, a malformed manifest kind, a member that fails to unpack, or a broker that doesn't import saves. - Fall back to the newest archive when the default raw save fails to import. - File exit states oldest first; one unreadable stored state no longer drops the rest. - Rename locals that shadowed the archive module; fix a test return type and a stale comment. * refactor(streaming): simplify the default save comparison pr-ready step 3 (simplify): default_save reuses _newest's ordering, and newestSave takes the first match in the newest-first list. * docs(streaming): trim the raw-save docstrings pr-ready step 4 (review-polish). * fix(streaming): rank saves by their last write and read .ZIP as an archive The web player writes into its existing save row, which only moves updated_at, so ranking by created_at let a stream with no pick boot an older streaming zip over newer web-player progress. The backend default and the launch screen now rank by updated_at, and the picker dates rows by it. Upper-case .ZIP saves are now read as archives on both sides, to match the web player's case-insensitive guard. * feat(states): hash states so duplicate checks skip reading files Adds states.content_hash (migration 0149), filled on every state write: streaming capture, the states upload and update endpoints, the scan and the RetroArch sync push. The streaming duplicate check compares hashes and falls back to reading the file for rows stored before this, which are not backfilled. * fix(streaming): leave an untouched default off the claim and skip missing saves Review items 1 and 2: a default sent as a pick bypassed the zip fallback, and a save marked missing could become the newest. * fix(streaming): code-review fixes for raw exit filing and state hashing pr-ready step 2: keep the zip when a state can't be filed, don't treat a missing state file as a duplicate, fall back to the native archive when a defaulted save fails, hash off the event loop, count filed states as new. * docs(streaming): review-polish, trim the newest-save comment * fix(streaming): bind the background exit pull's states to the swapped disc Decision 1: the exit save job filed states without the session's disc id, so on a multi-disc game its copy could lose the disc binding. The second copy is still dropped by the hash dedupe. * fix(streaming): keep the exit zip when one of its states fails to store Decision 2: a state that threw on store was logged and lost, since the zip holding it was dropped. The zip holds the same .srm, so booting it as the default loses nothing. * fix(sync): keep updated_at when RetroArch sync flags a vanished save or state * fix(streaming): record a legacy state's hash when dedupe reads its bytes * fix(streaming): file a RetroArch exit's auto state raw instead of keeping the zip The auto state now goes into the state history under its own capture stamp, so an exit carrying one files its .srm raw like any other. It is never hydrated, and a resume pick of it is refused, since the broker would file it into its numbered working slot. * fix(streaming): hydrate the newest state still on disk A newer row sync flagged as gone from disk made hydrate push nothing. It now skips such rows the way the save picks already do. * docs(streaming): note the auto-state refusal in resolve_resume_state * fix(streaming): resume a RetroArch auto state like any other state The broker files any pushed state into its working slot, so .state.auto reads as slot 0 like a bare .state. Hydrate and the resume pick now push it instead of skipping or refusing it. * fix(streaming): address Greptile review on raw RetroArch exits - A default .srm the broker refuses at activate now boots the newest native archive instead of failing the launch. Only a default save that rode the import alone falls back; a hand pick still fails. - Exit states the history hasn't seen are filed just under the next state it already holds, so an older slot no longer heads the history over the exit capture teardown filed first. - A raw exit save is skipped only when it matches the newest usable save, so returning to an older save records the play, and a match on a row whose file is gone no longer deletes the new copy. - Stream.test.ts clicks the rendered play button instead of casting the view model to call onPlay. - Trim the raw-exit docstrings to one sentence. * fix(streaming): reserve a free version name for a raw RetroArch exit save Addresses Greptile: the exit's .srm took a second-resolution tag without checking whether another save held that name, so it could overwrite one. Also compares updated_at naively in the mark-missing test, which failed on PostgreSQL's UTC-aware column. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(streaming): file each RetroArch exit state on the disc it was saved on Addresses Greptile: every state in the exit archive took the session's final disc, so a hotkey slot saved before a swap resumed on the wrong one. The session now keeps each swap with its time, and a session that ran more than one disc files each state by its archive timestamp. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Georges-Antoine Assi <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
a1466d9233 |
perf(tasks): batch RetroAchievements status writes, make add_rom_user race-safe (#5179)
* perf(tasks): batch RetroAchievements progress sync reads
Add get_roms_by_ra_ids and get_rom_users_by_rom_ids (single index-backed
IN queries, no eager joins) and fold the per-user feed to {ra_id: status}
so each user costs 2 reads plus writes instead of 2 reads per game.
* fix(tasks): skip joined eager-loads in RA sync batch reads
* style(tasks): trim batch-read comments to house style
* fix(tasks): sync every ROM sharing an ra_id, skip empty rom set early
* fix(tasks): raiseload instead of deprecated noload, fix CI failures
* fix(tasks): apply code-review fixes to RA progress sync batching
pr-ready step 2 (code-review xhigh --fix): load only id/ra_id for the
batched ROM read, flatten the sync loop, isolate per-user status sync
failures, log the ROM id, add a real-row test, and move the batched
RomUser getter next to its siblings.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* refactor(tasks): simplify RA progress sync batching and its tests
pr-ready step 3 (simplify): select (id, ra_id) columns instead of Rom
entities, drop early returns the getters already short-circuit, and
fold the mocked sync tests into shared helpers and one parametrized
status-transition test.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* style(tasks): trim batched RomUser getter comment
pr-ready step 4 (review-polish).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* docs(tasks): tighten unknown-award test docstring
pr-ready step 5 (humanizer).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* perf(tasks): batch RA status writes and make add_rom_user race-safe
set_rom_user_statuses writes one user's statuses in a single
transaction: it inserts missing RomUser rows, then runs one conditional
UPDATE per distinct status. The RETIRED/NEVER_PLAYING guard lives in the
WHERE clause, so a status the user sets mid-sync is never overwritten,
and the per-user cache bumps are queued once.
add_rom_user now inserts inside a savepoint and returns the existing row
when a concurrent writer won the unique (rom_id, user_id) insert, so the
sync no longer needs a catch-all around each user.
A rolled-back savepoint fires after_rollback, which drops every queued
user cache bump, so bumps are queued only after the savepoints finish.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* fix(tasks): skip existing rom_user rows in the batch insert
Addresses Greptile review on #5179. The batch read existing rows before
inserting, so under MariaDB's REPEATABLE READ a row another writer
committed in between raised IntegrityError, and the fallback SELECT (or a
FOR UPDATE read, which MariaDB's snapshot isolation refuses) could not see
it. The insert now skips duplicates itself (ON CONFLICT DO NOTHING /
INSERT IGNORE) with no prior read. The race test commits the competing row
right before the INSERT instead of mocking the lookup. Also trims the
set_rom_user_statuses docstring to one sentence.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* fix(tasks): apply code-review fixes to batched RA status writes
Batch set_rom_user_statuses to stay under PostgreSQL's bind parameter
cap, keep queued cache bumps across a rolled-back savepoint, fold the
single-caller insert helper into add_rom_user, and fix stale comments.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* refactor(tasks): apply simplify pass to batched RA status writes
Group by status before batching to drop a loop level, name the
user-owned statuses, split the dialect insert out of the execute call,
and share a status lookup across the write tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* fix(tasks): type the dialect insert for mypy (review-polish)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---------
Co-authored-by: Shreeniwas <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
|
||
|
|
3b8eabdb77 |
feat(sync): sync RetroArch Cloud Sync saves through save slots (#5170)
* feat(sync): sync RetroArch Cloud Sync saves through save slots RetroArch saves were unslotted and overwritten in place, so they never reached the slot-based sync the native clients use, and a bad upload replaced the only copy. A save path now serves the newest slotted version for its ROM, core and extension, from any slot, under RetroArch's own file name. An upload adds a version to that slot (or autosave), skips unchanged bytes, and prunes to MAX_SAVES_PER_SLOT. A delete removes every save the path covers, so none resurfaces on the next sync. Unslotted saves keep serving until the first slotted upload; PSP bundles and states are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): lock RetroArch save uploads and prune per core and extension Addresses Greptile's review on #5170: - Move the RetroArch save upload into sync_handler.store_save_version, leaving the endpoint to validate and map the result to a status. - Hold a Redis lock per (user, rom, core, extension) across the lookup, write, scan and row update, so concurrent same-second uploads never file two rows over one tagged file. A lock timeout answers 503. - Prune only the uploaded path's core and extension in the slot, so another core's or a .rtc's current save is never dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): pr-ready code-review fixes for RetroArch save slots - Scope an extensionless prune to dotless file names, since every name ends in an empty extension. - Treat only NULL slots as unslotted in the manifest, matching the query it replaced and the PSP bundle lookup. - Share one helper for the slot versions a RetroArch path covers. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(sync): pr-ready simplify pass for RetroArch save slots - Scope a slot prune by save ids, so the versions a RetroArch path covers are decided once in Python instead of again in SQL. - One newest-per-key helper for states and save versions, and resolve_save_by_slot reuses it. - A canonical_save_file_name helper next to the state one. - The upload builds its tagged name once, checks its length in the handler, and finds a same-second version in the list it already loaded. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * chore(sync): pr-ready review-polish pass Drop a comment the canonical_save_file_name helper now says. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): map RetroArch saves onto autosave lineages with unique version names - Only RetroArch's .srm goes through slots, as the autosave versions of its ROM and core. Companion files such as .rtc stay single unslotted saves, so a slot never offers a clock file as the save. - RetroArch reads, writes and deletes in autosave only, leaving named slots alone. - Every slot prune, web uploads included, is scoped to the uploaded save's lineage (emulator and extension), so one core's uploads never drop another's only save. - The manifest lists the newest version under the client's spelling of the game when it still names the ROM. - reserve_version_name moves a taken tag on a second and claims it in Redis, so same-second, concurrent or repeated-hour uploads from either endpoint never share a file. The RetroArch lock and its 503 go. - The manifest reads only the newest version per lineage, through one window query. - An upload is hashed before it is written, so an unchanged re-send writes nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(sync): seed the client-spelled save instead of uploading it Uploading TEST_ROM.srm only resolves to the test_rom ROM under MariaDB's case-insensitive collation, so the test 409'd on PostgreSQL. Seeding the version directly still checks that the manifest keeps the client's spelling of the game. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): resolve RetroArch saves to ROMs case-insensitively on PostgreSQL Cloud Sync matches a save's game name to a ROM's fs_name_no_ext. MariaDB's collation ignores case, but PostgreSQL compared exactly, so TEST_ROM.srm never reached the test_rom ROM. PostgreSQL now compares lowercased names, served by a new expression index (0150), and the endpoint test uploads the differently-cased name again. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): address Greptile review on RetroArch save slots - add_save derives the prune lineage from the tagged name it stores, so a dotted upload such as game.rev.srm is counted against its own versions. - Deleting a RetroArch .srm path also removes unslotted saves in that lineage spelled in another case, which the manifest hid behind the autosave head and PostgreSQL's exact match left behind. - reserve_version_name looks up existing versions off the event loop. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
a874e4d012 |
ci: keep CI jobs off Ubuntu apt mirrors (#5182)
* ci: bound apt network timeouts for Playwright's dependency install `npx playwright install --with-deps` runs its own `apt-get update`, so the `-o` flags from #5181 could not reach it, and a stalled Ubuntu mirror hung the frontend test job until the 15 minute limit cancelled it. Move the retry/timeout options into an apt.conf.d file written by a small composite action, used by setup-backend and the frontend test job. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * ci: install Playwright's headless shell without apt The apt.conf.d timeouts did not stop the hang: on a runner whose Azure mirror is unreachable, apt falls back to archive.ubuntu.com and stalls while downloading the package indexes, slowly enough that no timeout fires. The ubuntu-latest image already ships Chrome's system libraries, so drop --with-deps from the frontend and e2e jobs and keep apt out of their path. Revert the bound-apt-timeouts action, and run the frontend checks when frontend.yml itself changes. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * ci: install setup-backend's apt packages from a cache The MariaDB connector .debs (plus each job's extra apt packages) were fetched from an Ubuntu mirror on every backend job, so a stalled mirror could hang any of them until the 15 minute limit, the same way it hung Playwright's dependency install. Cache the downloaded .debs keyed on the runner image OS and package list, saved from master only, and install from them without apt-get update on a hit. A miss still downloads them with the #5181 retry and timeout options. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * ci: install cached apt packages without re-downloading them With package lists present, apt fetched the mirror's copy of each local .deb instead of installing the file, so even the cached path reached the mirror. Point apt's archives dir at the .deb directory and pass --no-download. Also read ImageOS in a shell step: it is a runner shell variable, not in the `env` context, so the cache key came out empty. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * ci: pin workflow runners to ubuntu-24.04 The apt .deb cache in setup-backend only holds packages built for one Ubuntu release. Pin every job to ubuntu-24.04 so a GitHub move of ubuntu-latest can't restore noble packages onto a newer image, and hardcode the release in the cache key instead of reading ImageOS. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
75e8a9efae |
fix(sync): keep RetroArch Cloud Sync from stalling the event loop (#5171)
* fix(sync): keep RetroArch Cloud Sync from stalling the event loop Basic auth ran its bcrypt check (~250ms) on the event loop for every WebDAV request, and the manifest ran its queries there too, so parallel RetroArch requests queued behind each other. Both now run in a thread. Cloud Sync resolves every save and state to a ROM by fs_name_no_ext, which no index led with; 0149 adds one so unmatched uploads fail fast. The upload dialog now offers a core for saves as well as states, so a manually uploaded save lands in the core folder RetroArch reads, and its slot hint says only unslotted saves reach RetroArch. Refs #5158 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(sync): cover manifest queries running off the event loop Addresses Greptile review on #5171: trim the 0149 migration docstring to its summary, annotate the new MockConnection constructor, and add regression tests that build_manifest and list_manifest_paths query from a worker thread. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(auth): mark Basic-auth users active off the event loop pr-ready step 2 (code-review): run the user lookup, password check and set_last_active in one worker-thread hop, and test that SaveDataTab offers save cores and forwards the picked core on upload. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(v2): build SaveDataTab test saves and states from shared fixtures pr-ready step 3 (simplify): replace the hand-cast UserSaveSchema and UserStateSchema stubs with saveFixture/stateFixture plus toUserSave/toUserState. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * chore(sync): drop restating index comment and type the test spy pr-ready step 4 (review-polish): remove the comment that only restated idx_roms_fs_name_no_ext, and type the manifest test's get_saves spy so mypy passes across the backend. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(auth): check the password grant's credentials off the event loop The OAuth password grant ran bcrypt (~250ms) on the event loop, stalling every other request the worker was serving. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(sync): run RetroArch WebDAV database work off the event loop GET, PUT, DELETE and PROPFIND resolved ROMs, looked up and wrote save, state and screenshot rows, and listed whole platforms synchronously inside async handlers; the PSP bundle helpers did the same. Each now runs that work in a worker thread. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(saves): keep another core's same-named save on upload A slot-less upload matched an existing save by file name alone, so uploading game.srm for a second core rewrote the first core's row to the new folder and deleted its file. The lookup now only matches a save made with the requested core. Clients that send no core still match any, so device conflict detection is unchanged, preferring a save filed under no core. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * feat(v2): file a save picked with a core out of the slots RetroArch Cloud Sync only lists unslotted saves, so picking a core in the upload dialog moves an untouched slot from autosave to "No slot", and clearing the core moves it back. A slot the user picked is kept. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * revert(i18n): restore the original upload slot hint Picking a core now files a save out of the slots, so the hint no longer needs to explain RetroArch Cloud Sync. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
34e994d61e |
ci: add network timeouts and retries to setup-backend apt calls (#5181)
apt-get has no network timeout by default, so a stalled Ubuntu mirror blocked a migrate-mariadb job in `apt-get update` until the 15 minute job limit cancelled it. Retry up to 3 times with a 30s timeout instead. Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
d3e07a3d60 |
docs(readme): refresh community projects list (#5176)
Add RomMulus, RetroVault, Gameflow Deck and RomMix, drop SwitchRomM (no push in over 180 days), and point DeckyRommSync at its new name, Tender. Regroup the list into Mobile and TV, Desktop, Handhelds and Steam Deck, Sync, and Integrations. Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
77f064561a |
fix(streaming): keep a slow broker from hiding save imports (#5172)
* fix(streaming): keep foreign saves offered when the import-spec check fails A broker that misses the 5 s import-spec deadline used to read as "imports nothing": the Saves tab vanished and foreign picks were refused with "Save is not a restorable archive". Keep the last good answer in Redis so every worker can fall back on it, cache 404s for five minutes instead of forever, answer a pick that can't be checked with a retryable 503, and refetch the streaming config whenever the Stream page mounts. * fix(streaming): code-review fixes for the import-spec fallback Hold a fallback answer for 10 s so one claim's checks don't each wait out the broker timeout, read the stored answer back through the broker response parser, and warn about a pre-import broker once per worker instead of at every cache expiry. * refactor(streaming): simplify pass on the import-spec fallback Make ImportSpecUnavailable the 503 itself so the save and state pickers no longer convert it by hand, and store the broker's own response body as the last answer instead of rebuilding its shape. * chore(streaming): review-polish on the import-spec fallback Trim the cache comments and the exception docstring, fit the 503 detail within the line limit, and annotate the fresh-worker test dict for mypy. * docs(streaming): humanizer pass on import-spec comments and test docstrings * fix(streaming): tell lasting import-spec failures from passing ones Only timeouts, connection errors, 5xx and 429 fall back to the broker's last answer. A refused request (401, other 4xx) or a reply RomM can't read caches no imports for 30 s and logs an error, so a pick gets a refusal instead of an endless retry. A 404 from a broker that answered before uses that answer, since it is likelier a restart behind a proxy than a downgrade. * fix(streaming): ask for a retry when a picked state can't be checked An older save with the newest state resumes the pick only through an import, so an unanswered import-spec check now returns the 503 instead of launching from the older archive's own state. * fix(streaming): share an in-flight config request App load and the Stream page both fetch the config; two requests could land out of order and leave the older answer in place. (Stability item 5.) * fix(streaming): hold the resume picker until the fresh config lands What the broker imports decides which saves and states are on offer, and the app-load config can predate its answer. The panel shows a spinner until the Stream page's own refetch settles, so a pick can't vanish under the player. (Stability item 4.) * fix(streaming): ask every platform's broker at once on /config The checks ran on the default thread pool (cpu+4 threads), so a broker serving more platforms than that cost one timeout per wave: 10.7 s for 14 platforms on a 6-CPU dev box with a silent broker. The Stream page's picker now waits on /config, so the checks get a pool of their own. * fix(streaming): address Greptile review on the import-spec fallback - Play waits on the fresh config with the resume picker, so it can't send a preselected state the player can't see yet. - The streaming store gets a reset() for logout, and a config reply asked for as the previous account is dropped instead of landing in the next one. - The configFresh write after the refetch checks useIsAlive(). - Trimmed the require_import_spec docstring, the configFresh comment and the /config pool comment to the house length. - Declined: a worker-wide pool for the /config checks. Each request's threads end within the broker timeout, and a shared bounded pool would bring back timeout waves under concurrent loads. * fix(streaming): share /config broker checks across requests Addresses Greptile's re-raised finding on #5174 that each /config request opened its own pool of up to 64 threads, so overlapping page loads during a silent broker multiplied the worker's threads. - The checks run in one worker-wide pool, and a check already in flight for the same container and platform is joined rather than asked again, so concurrent page loads hold no more threads than one does, with no queued-timeout waves. - Callers await the shared run through asyncio.shield, so a request that disconnects doesn't cancel the check others are waiting on. |
||
|
|
361e18cc56 |
perf(tasks): batch RetroAchievements progress sync reads (#5144)
* perf(tasks): batch RetroAchievements progress sync reads
Add get_roms_by_ra_ids and get_rom_users_by_rom_ids (single index-backed
IN queries, no eager joins) and fold the per-user feed to {ra_id: status}
so each user costs 2 reads plus writes instead of 2 reads per game.
* fix(tasks): skip joined eager-loads in RA sync batch reads
* style(tasks): trim batch-read comments to house style
* fix(tasks): sync every ROM sharing an ra_id, skip empty rom set early
* fix(tasks): raiseload instead of deprecated noload, fix CI failures
* fix(tasks): apply code-review fixes to RA progress sync batching
pr-ready step 2 (code-review xhigh --fix): load only id/ra_id for the
batched ROM read, flatten the sync loop, isolate per-user status sync
failures, log the ROM id, add a real-row test, and move the batched
RomUser getter next to its siblings.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* refactor(tasks): simplify RA progress sync batching and its tests
pr-ready step 3 (simplify): select (id, ra_id) columns instead of Rom
entities, drop early returns the getters already short-circuit, and
fold the mocked sync tests into shared helpers and one parametrized
status-transition test.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* style(tasks): trim batched RomUser getter comment
pr-ready step 4 (review-polish).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* docs(tasks): tighten unknown-award test docstring
pr-ready step 5 (humanizer).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---------
Co-authored-by: Georges-Antoine Assi <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
|
||
|
|
e053234d74 |
feat(v2): show a top-bar indicator while a "Download as" conversion runs (#5157)
* feat(v2): show a top-bar indicator while a "Download as" conversion runs A format download that needs a conversion only flashed a toast, then left the user waiting with nothing on screen until the browser download began. Pending conversions now live in a store, and a "Converting" pill in the top bar lists them until each download starts. The pill shares its look with the scan indicator through a new NavStatusPill. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * chore(v2): shorten converting indicator comments Addresses Greptile review on #5157. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(v2): address code-review findings on the converting indicator Give the Czech converting-downloads string the four plural forms the cs_CZ rule picks from, use a count-agnostic Russian wording, wrap the status pills in an element so their Transition animates, and tighten the format conversions store and its tests. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(v2): simplify the nav status pills Move the pill enter/leave transition into a NavStatusTransition wrapper with scoped styles, show the conversion format with RTag, pass a null scan progress for the indeterminate bar, and trim the format conversions store to what downloadAs uses. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * chore(v2): review-polish pass on the converting indicator Drop CSS comments that restate their rules and use the object props form in the indicator's component mocks. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
48e9b40102 |
perf(scan): replace superseded-folder prefix scans with ancestor set (#5146)
* perf(scan): replace superseded-folder prefix scans with ancestor set Build one ancestor set from discovered fs paths, then flag each missing ROM with an O(1) set lookup. Same flagged set as the old any() prefix scan. Empty paths are skipped with an explicit guard; trailing slashes are normalized on both sides. * fix(scan): rename loop var shadowing Rom-typed rom * refactor(scan): simplify superseded lookup per review * fix(scan): pr-ready code-review pass on superseded-folder check Drop the no-op rstrip, inline the superseded flag, and make the test build real Rom rows plus a shorter-prefix case (Saga vs Saga Deluxe) that a naive startswith check fails. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * refactor(scan): pr-ready simplify pass, extract _ancestor_paths Pull the ancestor walk into a pure helper with its own unit test, and assert the superseded warnings as exact strings instead of parsing them. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Georges-Antoine Assi <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
3f57d11d0b |
perf(scan): batch restored-ROM hydration reads (#5145)
* perf(scan): batch restored-ROM hydration reads Replace one get_rom_simple read per restored ROM with a single get_roms_simple_by_ids call. Same load shape, emit order, and None-skip preserved. * test(scan): assert emitted ROM ids in visit order * fix(scan): return per-ROM ids from SimpleRomSchema mock * refactor(scan): extract restored-ROM emit helper, tighten order test pr-ready step 2 (code-review --fix): scope the hydrated batch to a helper so it is freed before the scan, and make the test catch visit-order and skip bugs. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * test(scan): reuse make_fs_rom and drop dead get_rom mock pr-ready step 3 (simplify). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Georges-Antoine Assi <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
dbf5ae94c3 |
ci: check that PR titles follow Conventional Commits (#5143)
* ci: check that PR titles follow Conventional Commits Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * ci: address Greptile review on the PR title check Give the HLTB updater a Conventional Commits title so its PRs pass the check, and strip line breaks from the title before echoing it so it cannot start a workflow command. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
9d216401b3 | chore(deps): bump postcss-selector-parser in /frontend (#5163) | ||
|
|
7377b3543a | fix(frontend): guard network-quiesced dispatch after DOM teardown (#5167) | ||
|
|
9dcba51122 | chore(deps): bump mako from 1.3.12 to 1.4.2 (#5166) | ||
|
|
a022415d90 | chore(deps): bump multidict from 6.7.1 to 6.9.1 (#5165) | ||
|
|
c9348e19d0 | chore(deps): bump source-map-js from 1.2.1 to 1.2.2 in /frontend (#5164) | ||
|
|
208bfa7889 | test(frontend): run Storybook stories and a11y checks in Chromium via addon-vitest (#5153) | ||
|
|
dcfb4ec86a | chore(v2): switch Storybook docgen to vue-component-meta and enable autodocs (#5152) | ||
|
|
3f13d1e752 |
fix(sync): store RetroArch Cloud Sync blobs under the assets volume (#5155)
The config/, thumbnails/ and system/ blobs lived at /romm/retroarch_sync, which no documented mount covers, so they sat in the image's anonymous /romm volume and were lost on `compose down`/`up`, `-V`, or a docker rm/run upgrade. RetroArch can then treat the missing files as deleted on the server and drop its local copies. They now live at assets/users/<user>/retroarch/<category>/, beside the user's saves and states. The old path never shipped in a release, so no data migration is needed. Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>5.4.0-alpha.1 |
||
|
|
55498ec427 | fix(v2): name dialogs and select listboxes; axe-check story overlays (#5151) | ||
|
|
8983716c62 | chore(v2): let the theme toolbar drive every lib story (#5150) | ||
|
|
b576d9e8b6 | chore(eslint): adopt @vue/eslint-config-typescript and lint with type information (#5148) | ||
|
|
5d73e1d923 | refactor(v2): extract RSortHeader and make the index lists real tables (#5149) | ||
|
|
9ae2cd0e12 | chore(deps): update dependency md-editor-v3 to v7.1.0 (#5142) | ||
|
|
ad0146a88e |
chore(trunk): fix the scheduled trunk check-all failures (#5141)
* chore(trunk): fix the scheduled trunk check-all failures - Ignore hadolint DL3008 on the runtime stage apt-get, matching the emulator-download stage - Build alias-search test ROMs with tests.factories.make_rom instead of the banned add_rom - Drop a bandit B405 ignore that suppresses nothing - Bump fast-uri to 3.1.8 for GHSA-hrr3-gc8f-f4qj Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(tests): type the alias-search rom helper kwargs as Any Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
377405e514 | fix(igdb): harden Twitch auth; test the IGDB client and handler (#5133) |