13814 Commits
Author SHA1 Message Date
41a6efd47e chore(trunk): Upgrade trunk (#5137)
* chore(trunk): Upgrade trunk

* fix(trunk): keep pinact on 4.1.1 until the plugin passes --format

Addresses Greptile's review on #5137: pinact 5 rejects the plugin's
-format flag, so every workflow check reported a config error.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Georges-Antoine Assi <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-05 07:48:06 -04:00
Georges-Antoine AssiandClaude Opus 5.5 fab18a3dc5 chore(trunk): pin the CLI download to its sha256 (#5140)
The Trunk launcher verifies the CLI tarball against cli.sha256 in
.trunk/trunk.yaml, but only when those hashes are present. Add the
published 1.25.0 hashes so every launcher path (trunk-action, the
Copilot setup workflow, dev-setup.sh) refuses a tampered download.

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-05 07:33:46 -04:00
cf6966112b chore(e2e): check every page for accessibility violations with axe (#4951)
* chore(e2e): Update playwright #4772, smaller focus

* chore(e2e) address greptile feedback

* chore(e2e): Add Lighthouse and Axe

* chore(e2e): replace the Lighthouse and axe projects with an axe check per page

- Move PAGES and the page-opening wait into support/pages.ts, so loads.spec.ts
  and the new a11y.spec.ts share one list of every route.
- a11y.spec.ts runs axe on each page as the least privileged role allowed on
  it, and login.spec.ts on the login page. Critical violations fail; every
  violation is attached to the report as JSON. Serious ones (mostly color
  contrast) still fire on several pages, so they stay report-only for now.
- Drop the Lighthouse suite and its dependency tree, the separate axe project,
  the @smoke tag, the sitemap, and build:e2e.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(e2e): wait for skeletons to clear before checking a page

Addresses Greptile's review: openPage returned once the view mounted, so axe
(and the load check) ran while seven data-backed pages still showed
RSkeletonBlock placeholders. Wait until none are in the viewport; virtualized
grids keep them below the fold.

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]>
2026-10-05 07:16:25 -04:00
renovate[bot] 9c23817887 chore(deps): update backend dependencies (#5136) 2026-10-05 04:38:19 -04:00
Sam Dornan 7b1028bedd feat: search platforms by slug, abbreviation and alternative names (#5107) 2026-10-04 22:59:52 -04:00
d8b024bddc chore(e2e): Update playwright with smaller focus (#4932)
* chore(e2e): Update playwright #4772, smaller focus

* chore(e2e) address greptile feedback

* chore(e2e): keep static analysis out of e2e/.output

Everything the suites write (reports, traces, saved sessions) lands in
e2e/.output. It was already gitignored and ESLint skipped it, but the
e2e tsconfig included it through ./**/*.ts, so any .ts file written
there was type-checked, and Trunk skipped it only through the
.gitignore default. Exclude it from the tsconfig and list it in
Trunk's ignore paths explicitly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(e2e): run with zero config locally and typecheck the suite in CI

- Every E2E_* variable now falls back to a default matching
  seed_e2e_users.py, and the shell wins over e2e/.env, which is optional.
- With E2E_BASE_URL unset, the config starts (or reuses) `npm run dev`
  through webServer; CI still sets the URL and serves the static build.
- Drop the migration table for removed variables, the unknown-key checks,
  and the gitignore entries for suite versions that never shipped.
- Add e2e/tsconfig.json to the root project references, so
  `npm run typecheck` (run by Frontend Checks) covers specs/ and setup/.
- Restore `test:e2e:ui`.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(e2e): make seedUiState win over the user's stored preferences

Preferences are now stored per user (#5098), and the app only adopts an
unscoped value when the user has no `user:<id>:` copy. Saved sessions
carry one, so seedUiState(page, "light") was silently ignored. Drop the
scoped copies before seeding, and assert the light theme in the spec
that relies on it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* build(frontend): add build:preview, which copies frontend/assets into dist

Vite leaves frontend/assets out of the build and only the Docker image
copies it in, so a bare `vite preview` serves the SPA fallback for every
icon, logo, font and default cover. Give that copy one home: the e2e
workflow and the local production-build recipe both use the script. It
copies with fs.cpSync so it also runs from cmd.exe and PowerShell.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* ci(e2e): let the Playwright webServer serve CI again

The suite starts its own server once more, so the workflow no longer
needs its own build, serve, wait and teardown steps, nor the E2E_*
variables that now default to the seeded accounts. In CI the webServer
runs `build:preview` and `vite preview` as it did before this PR; locally
it runs (or reuses) `npm run dev`. The default URL is 127.0.0.1, which
the preview binds, rather than localhost, which can resolve to ::1.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): simplify the setup files

- auth.setup.ts signs each account in afresh every run, instead of
  validating saved sessions in a separate browser context. Two sign-ins
  cost about 3s.
- The hand-rolled login retry loop and its CI-specific attempt counts
  give way to `retries: 2` on the setup project, and the timeout
  arithmetic to `setup.slow()`.
- global-setup.ts folds its four check functions into one, and reads the
  library total from the admin's account check instead of fetching it
  twice.
- Drop isSessionValid(), login() and the ESLint exception that only the
  old setup needed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): drop the global-setup preflight

The suite already reports what it checked: a downed backend fails
sign-in through the app-error guard (`GET /api/heartbeat returned 502`),
and a wrong E2E_BASE_URL fails page.goto with the URL. The one case it
explained better, a rejected account, is now an assertion on the
`POST /api/login` status in auth.setup.ts. With no preflight, the suite
never calls the API directly, so its ESLint exception goes too.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* test(e2e): cover every route for both roles, and stop the theme leaking

loads.spec.ts:
- Key PAGES by RouteName from src/plugins/routeNames.ts, with a `skip`
  reason for routes it can't open, so a new route fails the typecheck
  until it is listed. That adds devices and conversion-settings, which
  had drifted out.
- Load each page as every role allowed on it, not only as admin, so a
  viewer-only 403 shows up.
- Drop the wait for in-flight requests, which timed out on pages that
  load slowly on a large library.

Tags and folders now use route names (@page:rom, specs/rom/,
specs/user-profile/).

The app syncs UI settings to the account and reapplies them on sign-in,
so a theme a spec wrote leaked into later sessions: admin ended up on
"auto", and every "dark" spec rendered light. seedUiState now always
stores "auto", the config sets `colorScheme: "dark"`, and the light
spec uses emulateMedia. The saved sessions carry the seeded settings, so
specs no longer call seedUiState themselves.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): keep support/auth.ts to accounts and sign-in

- Navigation helpers (gotoHydrated, gotoFirstRom, gotoOwnProfile) move
  to support/navigation.ts, with a gotoFirstPlatform that loads.spec.ts
  and gotoFirstRom now share.
- openMoreMenu and menuLabels move into rom/actions-menu.spec.ts, their
  only user.
- Role derives from ROLES, and accountFor's return type is inferred, so
  the Account interface goes.
- watchAppErrors folds into the app-error guard, its last caller.
- output.ts exports the results and report paths instead of a
  suiteOutput() that only ever took "specs".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(e2e): don't count the page load against gotoHydrated's waits

The /api/permissions/me and login-form waits are armed before page.goto,
so their 5s local action timeout also covered the navigation, and a slow
page timed out before the request was sent. The test's own timeout now
bounds them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): replace the environment module with plain reads

With every variable defaulted, the parser, its aggregated error and the
e2eEnv fixture guarded against mistakes that can barely happen, and
Playwright reports the rest itself.

- playwright.config.ts loads e2e/.env with process.loadEnvFile (the
  shell still wins) and reads E2E_BASE_URL; support/auth.ts reads the
  accounts into ACCOUNTS. Specs that need the URL use the baseURL
  fixture.
- E2E_WORKERS goes: `--workers N` already does that.
- Drop the e2eEnv fixture, its ESLint import ban and the empty-pattern
  exception it needed, and trim .env.example.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* build(e2e): typecheck the suite with tsconfig.node.json

e2e/tsconfig.json was a looser copy of the Node config: it lacked
noUncheckedIndexedAccess and exactOptionalPropertyTypes, so specs/ and
setup/ escaped flags the rest of the frontend, and the e2e support files
through playwright.config.ts, already obey. The suite runs in Node, so
it joins tsconfig.node.json, and typecheck:e2e goes with its config.
The specs needed no changes under the stricter flags.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): trim the ESLint config and the app-error guard's opt-out

- Drop the Node globals block: typescript-eslint turns no-undef off for
  .ts files, so it changed nothing.
- Drop no-skipped-test's allowConditional: nothing in the suite skips.
- Fold the type-info, floating-promise and no-API blocks into one, and
  move the e2e/.output ignore to the base config's generated-output list.
- Remove failOnAppErrors and E2EOptions: nothing opts out of the guard.

Every remaining rule still fires on a probe spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(frontend): shorten the test:e2e command note

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* build(frontend): copy assets with cp -R in build:preview

Also puts a comma in the test:e2e command note.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(e2e): one set of timeouts, and treat empty variables as unset

- Local runs used 3-10s timeouts against CI's 10-45s, so slow pages on
  a large dev library failed locally while CI passed. Both now use the
  CI values; a debugger still turns them off.
- An empty E2E_BASE_URL left baseURL as "" while still starting the
  server, and an empty account variable signed in as "". Both fall back
  to their defaults now.
- Set the Desktop Chrome device once, point the slow-test comment at
  test.slow(), and anchor the setup project's testMatch.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(e2e): apply code-review findings

pr-ready step 2 (code-review xhigh --fix):
- fall back to E2E_PASSWORD, which the seed script uses, for both accounts
- name the cause when the library has no platform or game to click
- read PWDEBUG=0/false as off, as Playwright does
- widen the API-call lint ban to any uncalled `.request` and its import
- compare the signed-in username case-insensitively
- only allow adminOnly with a URL, so the viewer 404 check always runs
- fix the README grep recipe and the recorded-spec location
- Object.hasOwn in seedUiState, ROLES in role.spec, shorter comments

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): apply simplify findings

pr-ready step 3 (simplify):
- one home for the login-form and signed-in-user locators
- derive ROLES and STORAGE_STATE from ACCOUNTS
- openMediaSubtab returns the panel; upload helpers take a scope
- menuLabels reads the panel openMoreMenu returns
- seed the UI state in login.spec's beforeEach
- one no-restricted-syntax entry for the API-call ban
- one port constant in playwright.config.ts
- drop .gitignore entries for paths nothing writes any more

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(e2e): trim two long comments

pr-ready step 4 (review-polish).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(e2e): plainer README opener and comments

pr-ready step 5 (humanizer).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(e2e): role selectors, one first-game lookup, shared route scopes

- Select by role and label: the account menu's name ("Account menu for
  <user>") replaces the user-name span, the Login button the login form's
  class, the "Page not found" heading the 404 class, and the subtab rail's
  tablist role the subtab button class.
- library.setup.ts clicks through to the first game once per run and saves
  its URL; specs open it with gotoFirstRom() instead of clicking through
  /platforms in every test. loads.spec keeps the click-through.
- Move the router's route scope table to routeNames.ts as ROUTE_SCOPES, so
  loads.spec derives admin-only pages from it: a newly gated route gets the
  viewer 404 check unless it is listed in VIEWER_OPENS.
- `vite preview` serves frontend/assets through a sirv middleware, as the
  dev server and the Docker image do, so build:preview goes away.
- CI builds the frontend in the background while the backend is set up,
  and the Playwright webServer only runs `vite preview` on that build.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(v2): menuitem role on button menu items, tabpanel role on Media panels

- RMenuItem sets role="menuitem" on its button form too, not only on links,
  so every item in an RMenu is a menuitem. Stories give the items a
  role="menu" parent.
- SubtabNav takes an optional idPrefix that gives each rail tab an id and
  aria-controls. MediaTab uses it to make its sections tabpanels labelled
  by their tab; the small-screen sheet variant has no tabs, so its sections
  stay plain.
- The e2e specs read menu labels by the menuitem role and find a Media
  panel by its tabpanel name.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Georges-Antoine Assi <[email protected]>
2026-10-04 20:28:07 -04:00
Sam Dornan 6735a318e6 refactor(metadata): share heartbeats, filename id tags and the Moby rom builder (#5134) 2026-10-04 17:01:39 -04:00
Sam Dornan 97c6a29170 refactor(metadata): share provider request retries and error mapping (#5132) 2026-10-04 16:44:54 -04:00
Sam Dornan b23bc79b46 feat(launchbox): scrape box front, back, spine and 3D box art (#5129) 2026-10-04 16:25:41 -04:00
Sam Dornan 858fd64f7f refactor(metadata): share the indexed filename lookup across IGDB, Moby and SS (#5130) 2026-10-04 16:01:25 -04:00
Sam Dornan c1c3058499 fix(metadata): four provider fixes; test Hasheous, TGDB, CSDb, gamelist and libretro (#5128) 2026-10-04 15:39:48 -04:00
Sam Dornan 02663721c7 fix(metadata): five provider fixes; test UPC, LaunchBox media, Demozoo, ScreenScraper and HLTB (#5126) 2026-10-04 15:28:09 -04:00
Sam Dornan 01b92cb2d2 fix(activity): coalesce activity refreshes and keep one list request in flight (#5125) 2026-10-04 15:06:25 -04:00
Iain 02fbe65893 fix(feeds): keep Switch files with uppercase extensions in the Tinfoil feed (#5115) 2026-10-04 14:47:22 -04:00
Sam Dornan b52d4a1c0e fix(metadata): five provider fixes; test RetroAchievements, SteamGridDB, MobyGames, Playmatch and Pouët (#5123) 2026-10-04 14:26:15 -04:00
Sam Dornan aa1c038004 fix(activity): send play activity only to users who can see the ROM (#5121) 2026-10-04 14:15:46 -04:00
Sam Dornan 11c82aa522 test: typed ROM fixtures, and no ROM casts in tests or stories (#5122) 2026-10-04 14:01:17 -04:00
Georges-Antoine AssiandClaude Opus 5.5 f8b3173bea feat: searchable alternative titles for ROMs (#5111)
* feat: searchable alternative titles for ROMs

Add an editable "Alternative titles" field to manual metadata, folded
into the search titles alongside provider aliases. IGDB localization
titles now join the ROM's alternative names, and the canonical IGDB
name is kept as one when a regional title is displayed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: list hand-added titles in alternative_names, test the field

Address Greptile review on #5111: the response's alternative_names now
appends the hand-added titles to the first provider's, and the Alternative
titles field gets a component test.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* test: type the alternative titles field lookup

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: code-review pass on alternative titles

Validate title lists from client-written metadata in one helper, list
hand-added titles first so the search text cap never drops them, and
let the titles combobox keep commas inside a title.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor: simplify pass on alternative titles

Dedupe IGDB titles once in build_igdb_rom, inline the manual title
source, and reword the combobox comma comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: review-polish pass on alternative titles

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: humanizer pass on alternative titles

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: hand-set alternative titles override, providers join

A non-empty manual list replaces every provider's titles, in the list
and in search; otherwise every provider's titles are joined. IGDB and
Hasheous store every title they know, the canonical name and regional
localizations included, and the ROM's displayed name is left out only
when listing them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: validate manual metadata on ROM update

A malformed raw_manual_metadata is rejected with a 422 instead of being
stored and failing every later response for that ROM.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: list each alternative title once however it is spaced

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-04 13:12:40 -04:00
Sam Dornan c402da2599 test: platform fixture factories for the backend and frontend (#5119) 2026-10-04 13:11:33 -04:00
Sam Dornan 812e0bdb28 fix(patcher): read the patcher's report from its last JSON line; test the patch endpoint (#5120) 2026-10-04 13:05:08 -04:00
Sam Dornan c1f543cda4 fix(sync, activity): folder scan crash and hidden-ROM heartbeat; test both (#5117) 2026-10-04 12:34:13 -04:00
Sam Dornan 9e4b080403 fix(metadata): one UTC helper for release dates, a lint rule against local-time parsing, and Flashpoint tests (#5114) 2026-10-04 11:47:54 -04:00
Sam Dornan 0d9d7a345b fix(sync): use only the device's own SSH key; test push-pull sync and the folder watcher (#5108) 2026-10-04 11:40:37 -04:00
Sam Dornan 88f4540c5d fix(v2): don't re-roll header stats when switching platform tabs (#5110) 2026-10-04 11:24:45 -04:00
Sam Dornan 69a7f7fcb5 test(utils): read tags and covers from real audio files in each format (#5113) 2026-10-04 11:19:10 -04:00
Sam Dornan e835b97c53 test(tasks): cover the WebP conversion task (#5112) 2026-10-04 11:13:13 -04:00
Sam Dornan 30f3f09704 fix(v2): vertically align icons in Playable and LIVE chips (#5109) 2026-10-04 10:42:03 -04:00
Sam Dornan 93c98c71c0 chore(deps): upgrade apprise to 2.0 (#5102) 2026-10-04 10:18:38 -04:00
Sam Dornan 3204037797 fix(auth): validate reset passwords and make the last-admin guard atomic, with tests (#5106) 2026-10-04 10:18:15 -04:00
Georges-Antoine AssiandClaude Opus 5.5 8a243c71e9 Sync saves from every in-browser player as a browser device (#5091)
* feat(player): play RPG Maker 2000/2003 games in the browser with EasyRPG

Embed the EasyRPG web player for extracted game folders on the rpg-maker
platform. The backend generates the player's index.json from the scanned
files and merges in the free EasyRPG RTP under every name the official
and fan RTPs give its assets, so games that lean on the RTP find them.
In-game saves sync with the user's RomM saves through the player's
IndexedDB storage.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): read the free RTP from a fixed path instead of an env var

The full image always keeps the RTP at /var/www/html/assets/easyrpg/rtp,
so the path is a constant beside the web path nginx serves it under. The
dev container links its copy there at startup.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(easyrpg): code-review fixes for the save sync and asset endpoint

- Wait for an in-flight push before the quit push, so the newest save uploads
- Keep Play disabled until the fresh rom (and its saves) is fetched
- Stop onPlay and the rom fetch from acting after the view unmounts
- Leave through usePlayerExit like the other players
- Handle a rejected IDB commit when the run itself fails
- Track uploads by the browser's file name
- Load only visibility and files per asset request, not the full rom
- Serve DEV_MODE files as octet-stream, matching nginx
- Make the traversal test actually reach the endpoint

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): simplify pass over the player and asset endpoint

- Load only file paths per asset request, and look up RTP folders in O(1)
- Reuse RomFile.file_name_for_download and usePlayerFullscreen
- Share one dev/prod serve helper for game and RTP files
- Derive the save sync's game key from the rom
- Have the RTP table tool reuse normalize_name
- Make save-module internals private and tidy the prepare/read loops

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(easyrpg): flag RPG Maker game folders on the rom schemas

Gallery lists carry no files, so the frontend guessed that every multi-file
rom on the rpg-maker platform was playable, including RPG Maker XP/VX/MV
folders. Rom now has a deferred `is_easyrpg_game` subquery (an RPG_RT.ldb at
the folder root), loaded with the other gallery file stats and derived from
loaded files on the with_files path, and the frontend reads it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): drop server save sync, keep saves per user in the browser

Server sync moves to a follow-up that puts the web players on device sync.
Saves now stay in the player's IndexedDB, as js-dos does, with the same note
on the start page. The player is launched as `<rom id>-<user id>` so each
RomM account sharing a browser profile keeps its own saves; nginx and Vite
drop the user id when routing to the backend.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(sync): let a client ask negotiate to restore the saves it lost

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(saves): sync web player saves as a browser device

A shared layer for the in-browser players: each browser profile registers
as a sync device, keeps every account's saves in its own IndexedDB store,
negotiates them with the server at launch (restoring any it lost), and
pushes changes with the device's conflict guard. Hashes match the server's,
including its zip-entry hash.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(emulatorjs): save as the browser's sync device, with the slot guard

EmulatorJS now uploads and downloads as this browser's registered device
instead of the IP-based login device, confirms downloads with their hash, and
lets a write that continues the slot's head be refused when another device
wrote the slot since; that progress is archived and the session keeps
writing to the archive.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(settings): list, rename, pause and remove your devices

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(player): sync EasyRPG, js-dos, PICO-8 and Ruffle saves with RomM

Each player's own storage is restored from the browser's synced copy before
boot and captured back during play and on leave:
- EasyRPG: its IDBFS saves, one slot per in-game slot
- js-dos: its changes zip through fsChanges pull/push, migrating saves it
  kept in OPFS before
- PICO-8: cartdata files, flushed by loading a blank cart on leave
- Ruffle: the game's SharedObjects zipped into one save, kept out of
  localStorage between plays so accounts don't share them

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(easyrpg): address Greptile review

- Start timing the play session once the player page loads
- Use object-form props in the view test's button mock
- Keep the RTP table tool's docstring to one sentence

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(saves): address Greptile review on web device save sync

- Remove EasyRPG save files that sync deleted, so the game cannot load them
- Keep the stale-device guard on device sync's page-close uploads
- Restore only the Ruffle SharedObjects the current SWF can reach

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(saves): code-review pass on web device save sync

Restore master's EasyRPG RTP table path lost in the merge, serialize
DeviceSaveSync pushes, only mark a no_op synced when the server hash
matches, forget the browser device when it is removed, close a late
EasyRPG IndexedDB connection, send content_hash on unload updates, and
share the IndexedDB helpers and slot-conflict check.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(saves): simplify pass on web device save sync

Archive conflicts through uploadArchivedSave and isSlotConflict, skip
hashing unchanged bytes, share bytesEqual from saveSync/hash, pass
restore_unlisted from DeviceSaveSync, use newest() for the EmulatorJS
slot head, and trim one-off Ruffle and PICO-8 helpers.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(saves): review-polish pass on web device save sync

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(saves): humanizer pass on web device save sync comments

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(saves): open every player IndexedDB through openDb

The save sync store now rejects when another tab blocks its upgrade
instead of waiting forever, as the EasyRPG and pending-asset stores
already did.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(sync): let negotiate scope server saves to the client's emulators

A browser player's saves load only in that player, so a native save in
the same slot must not pair with it or download into it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(saves): register the browser device on each launch

Registration returns the existing device, so calling it every time
brings back a device removed from any browser, which EmulatorJS
otherwise kept sending as a dead id. The cached id now serves only when
offline. DeviceSaveSync also negotiates within its own emulator.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(player): share web player save sync through useDeviceSaveSync

The coalesced polled push, the flush before leaving, the page-close
capture and the discard prompt live in one composable, and the player
suites share one DeviceSaveSync fake from test-utils.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(saves): update one archive per slot after another device takes it

A slot refused mid-session archived every later write as a new save.
Later writes now update that archive in place, page-close sends
included, and the next launch settles the slot.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* perf(ruffle): skip unchanged polls and upload saves every 5 minutes

Polls compare the stored SharedObjects with the last read before zipping
them, and keep what changed in the browser, uploading it at most every
5 minutes. Leaving and page close still upload at once.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* perf(ruffle): sync saves like EmulatorJS, polling every second

Polls compare the raw stored strings, so an idle second decodes,
zips and hashes nothing. A change uploads once two polls agree on it,
and a failing upload backs off from 2 to 30 seconds through the retry
backoff EmulatorJS uses, now shared from saveSync. Leaving still
uploads at once.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-04 10:05:19 -04:00
renovate[bot] a0fa9d43a8 chore(deps): update actions/upload-code-coverage action to v1.4.4 (#5105) 2026-10-04 10:01:20 -04:00
renovate[bot] 4f44b7219e chore(deps): update backend dependencies (#5104) 2026-10-04 10:00:40 -04:00
renovate[bot] ab302f8524 chore(deps): update frontend dependencies (#5103) 2026-10-04 09:50:08 -04:00
Georges-Antoine AssiandClaude Opus 5.5 db537728f1 feat(v2): scope stored preferences to the signed-in user (#5098)
* feat(v2): scope stored preferences to the signed-in user

Preferences in localStorage were shared by everyone using the same
browser, so one person's gallery layout, scan providers, chosen cores
and so on carried over to the next account. useUserLocalStorage stores
them under `user:<id>:<key>` while someone is signed in, and under the
bare key while signed out. The first user to sign in after this change
claims any unscoped value, so existing preferences carry over once.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(v2): address Greptile review on user-scoped storage

- Scope the core, BIOS and disc keys the shared EmulatorJS player writes
  (and v1's matching reads), so a game start no longer leaves bare keys
  for the next user to claim.
- Build the test user with userFixture.
- Trim the helper's header comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(v2): store the jukebox session under the user's storage key

The resume session now goes through userStorage like the other stored
preferences, so each user gets their own key instead of one shared key
carrying a userId to check on restore.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(v2): lint against storage that isn't scoped to the user

Adds romm/no-unscoped-local-storage, which rejects raw localStorage and
vueuse's useLocalStorage/useStorage outside frozen v1, so new stored
preferences go through useUserLocalStorage/userStorage. The locale read
at module load stays unscoped with an inline disable.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-04 09:47:02 -04:00
Georges-Antoine AssiandClaude Opus 5.5 a504b0e7c9 feat(v2): resume jukebox music after a reload (#5095)
* feat(v2): resume jukebox music after a reload

An opt-in "Resume music after reload" setting (Settings > User Interface >
Jukebox) saves the soundtrack player's track, queue, shuffle state and
position on the device and restores them on the next load.

Browsers block audio before the first user interaction, so a restored
session loads paused at its position and, if it was playing, starts on the
first click, tap or key press. The session is keyed to the signed-in user,
and a restored track that no longer loads is dropped without an error toast.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(v2): address Greptile review on jukebox resume

- Reset the soundtrack store on sign-out and restore on every sign-in,
  so the next user neither inherits the queue nor saves it as theirs.
- Start a resumed session after the first press is handled, so a first
  press on Play no longer pauses it, and keep waiting while a game
  blocks the music instead of consuming the press.
- Validate the saved session's meta and queue entries before restoring.
- Build the test user with userFixture.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(v2): code-review fixes for jukebox resume

- Re-check the game block when the deferred start runs, and re-arm the
  first-interaction listener when the press is blocked or play() is refused
- Disarm the listener when playback starts some other way, on track change
  and on unmount, including its pending timeout
- Validate isShuffled, activePlaylistRomId and wasPlaying in stored sessions
- Skip deep watching the stored session and only watch it while the
  setting is on
- Make the smAndDown test mock reactive

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(v2): simplify pass on jukebox resume

- Keep a single pendingResume in the store in place of the one-shot
  takePendingResume, awaitingAutoplay and the component's resumedTrack
- Leave the first-interaction listener armed until the sink plays instead
  of re-arming it by recursion
- Read and write the saved session with localStorage directly, so other
  tabs don't re-parse the queue on every save

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(v2): review-polish comments on jukebox resume

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(v2): humanizer pass on jukebox resume comments

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(v2): don't autoplay a restored jukebox session another tab is playing

A new tab picked up the saved session of a tab still playing and started
the same track a second time on the first click. On restore, the tab now
asks the others on a BroadcastChannel whether one is playing and waits
200ms for a reply. A playing tab answers, so the session loads paused;
with no answer (a reload, or every tab closed) it starts on the first
interaction as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(v2): only save the jukebox session a tab changed itself

Every open tab wrote its own session on pagehide and after a restore, so
closing an idle tab, or a tab that restored the session paused, replaced
the session another tab was playing. A tab now saves only after its own
player changed, and not while a restored session is waiting to start.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(v2): fade in a resumed jukebox session

A session restored after a reload now ramps from silence to the user's
volume over a second instead of starting at full volume.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-04 08:41:05 -04:00
Sam Dornan 87b66bc71a fix(v2): draw region flags on Chromium/Windows and center them in card chips (#5101) 2026-10-04 08:19:59 -04:00
Sam Dornan bdaac69f07 refactor(v2): share the scan form's hash-matcher switches and scan-type select (#5099) 2026-10-03 21:49:11 -04:00
Sam Dornan d519534765 Age limit settings in the admin dialogs (#5083) 2026-10-03 19:28:43 -04:00
Sam Dornan 839041c5c5 feat(v2): add an active toggle state to RBtn (#5090) 2026-10-03 18:54:14 -04:00
Sam Dornan ef3128e154 Hide ROMs above a user's or group's age limit (#5081) 2026-10-03 18:26:52 -04:00
Georges-Antoine AssiandClaude Opus 5.5 6f2a361481 Play RPG Maker 2000/2003 games in the browser with EasyRPG (#5077)
* feat(player): play RPG Maker 2000/2003 games in the browser with EasyRPG

Embed the EasyRPG web player for extracted game folders on the rpg-maker
platform. The backend generates the player's index.json from the scanned
files and merges in the free EasyRPG RTP under every name the official
and fan RTPs give its assets, so games that lean on the RTP find them.
In-game saves sync with the user's RomM saves through the player's
IndexedDB storage.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): read the free RTP from a fixed path instead of an env var

The full image always keeps the RTP at /var/www/html/assets/easyrpg/rtp,
so the path is a constant beside the web path nginx serves it under. The
dev container links its copy there at startup.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(easyrpg): code-review fixes for the save sync and asset endpoint

- Wait for an in-flight push before the quit push, so the newest save uploads
- Keep Play disabled until the fresh rom (and its saves) is fetched
- Stop onPlay and the rom fetch from acting after the view unmounts
- Leave through usePlayerExit like the other players
- Handle a rejected IDB commit when the run itself fails
- Track uploads by the browser's file name
- Load only visibility and files per asset request, not the full rom
- Serve DEV_MODE files as octet-stream, matching nginx
- Make the traversal test actually reach the endpoint

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): simplify pass over the player and asset endpoint

- Load only file paths per asset request, and look up RTP folders in O(1)
- Reuse RomFile.file_name_for_download and usePlayerFullscreen
- Share one dev/prod serve helper for game and RTP files
- Derive the save sync's game key from the rom
- Have the RTP table tool reuse normalize_name
- Make save-module internals private and tidy the prepare/read loops

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(easyrpg): flag RPG Maker game folders on the rom schemas

Gallery lists carry no files, so the frontend guessed that every multi-file
rom on the rpg-maker platform was playable, including RPG Maker XP/VX/MV
folders. Rom now has a deferred `is_easyrpg_game` subquery (an RPG_RT.ldb at
the folder root), loaded with the other gallery file stats and derived from
loaded files on the with_files path, and the frontend reads it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): drop server save sync, keep saves per user in the browser

Server sync moves to a follow-up that puts the web players on device sync.
Saves now stay in the player's IndexedDB, as js-dos does, with the same note
on the start page. The player is launched as `<rom id>-<user id>` so each
RomM account sharing a browser profile keeps its own saves; nginx and Vite
drop the user id when routing to the backend.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(easyrpg): address Greptile review

- Start timing the play session once the player page loads
- Use object-form props in the view test's button mock
- Keep the RTP table tool's docstring to one sentence

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(easyrpg): keep the RTP table in a fixtures folder

Matches handler/metadata/fixtures, where the other bundled data files live.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-03 17:51:38 -04:00
65247bf82c perf(frontend): load md-editor on demand instead of with the app (#5080)
* perf(frontend): load md-editor on demand instead of with the app

md-editor's config() ran from the app entry, which kept the library in
the entry's import graph. A Vite and Vitest alias now routes every bare
md-editor-v3 import, v1 included, through plugins/mdeditor.ts, which
applies the XSS config as a side effect of the first load. The
sanitizer stays on for every caller without an entry import.

ChangelogDialog (mounted on every v2 page) and NotesTab imported
MdPreview and MdEditor statically. They now load them through small
MarkdownPreview and MarkdownEditor wrappers with defineAsyncComponent,
so md-editor and CodeMirror download when a note or the changelog is
shown, and the editor only when editing starts.

Based on the lazy-loading approach from #4988 by @andest01.

* fix(storybook): keep the app's alias array when adding @ and @v2

Addresses Greptile: spreading the app's alias array into an object turned
it into numeric keys, so Storybook lost the md-editor-v3 alias and with
it the XSS config.

* refactor(frontend): address code-review findings

- Share one alias list between vite, vitest and (through vite.config.js) Storybook.
- Re-export MdEditor and MdPreview from typed modules instead of wrapper SFCs, so callers keep prop and v-model type checks.
- Block deep md-editor-v3 imports in ESLint, since they would skip the XSS config.
- Pass no-echarts where notes and the changelog render, so md-editor does not fetch echarts from unpkg.

* refactor(frontend): simplify the shared md-editor module

Simplify pass:
- Merge the editor and preview re-exports into one shared/markdown module; both pulled the same md-editor chunk and stylesheet.
- MarkdownViewer imports MdPreview from it too, and gets no-echarts like the other v2 previews.
- Drop the ESLint ignore for mdeditor-dist.d.ts, which has no deep import.

* style(frontend): trim the shared md-editor module comment (review-polish)

* fix(frontend): keep the md-editor preview and editor in separate modules

The simplify pass merged them into one module, which put MdEditor and CodeMirror in the preview's chunk: the changelog and note previews went from 78 KB to 290 KB gzip. MarkdownViewer keeps using the shared preview module.

* feat(frontend): show a spinner while the note editor loads

* fix(frontend): cover md-editor chunk loads with the existing spinners (code-review)

ChangelogDialog waits on the renderer chunk with the releases fetch, so a
load failure lands in the dialog's error state instead of blank bodies.
NotesTab's preview gets the same loading spinner as the editor. Document
the @v2 and md-editor-v3 aliases.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(frontend): share the async md-editor components (simplify)

Define the async MdPreview/MdEditor wrappers and their attr-dropping
spinner once in shared/asyncMarkdown.ts instead of per NotesTab instance
and again in ChangelogDialog. Keep the md-editor alias out of the tsconfig
paths block in the architecture doc.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(frontend): let TypeScript follow the md-editor-v3 alias

Only Vite knew about the alias, so go-to-definition on md-editor-v3 opened
the package types instead of src/plugins/mdeditor.ts, where the sanitizer
config lives. A tsconfig path now points there too.

* refactor(frontend): turn off md-editor's CDN renderers once for v2

The shared markdownPreview/markdownEditor modules wrap the components with
noHighlight, noKatex, noMermaid and noEcharts, so v2 call sites no longer
repeat the flags and a new one cannot miss 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]>
2026-10-03 17:04:09 -04:00
Sam Dornan 6d4c1d7ec9 fix(i18n): say "Plataforma actualizada" for es_ES platform.updated (#5089) 2026-10-03 16:54:52 -04:00
Sam Dornan 4f14c64dd7 fix(v2): validation, spinner, nested button and label bugs (#5088) 2026-10-03 16:38:38 -04:00
Sam Dornan 8c154be288 fix(v2): input follow-ups: phantom-pad right stick, LB/RB on gallery routes (#5086) 2026-10-03 16:18:49 -04:00
Sam Dornan 0498a4f158 fix(v2): unsubscribe from the emitter on unmount via useEmitterEvent (#5085) 2026-10-03 15:55:52 -04:00
Sam Dornan dc32cfcec6 refactor(v2): simplify keyboard/gamepad handling (#5084) 2026-10-03 15:26:05 -04:00
Sam Dornan 1dcda140e2 fix(rom): keep a disc an .m3u lists in place when promoting to a folder (#5076) 2026-10-03 13:37:24 -04:00
Sam Dornan 655976faa9 feat(soundtrack): play SPC, VGM, VGZ and GYM soundtracks in the browser (#5079) 2026-10-03 13:12:28 -04:00
a0ede752f4 perf(frontend): smaller first load and faster vitest setup (#5072)
* perf(frontend): keep the inactive UI and the markdown renderer out of the first load

vue-router fetches every named view of a route before entering it, so a v2 visit also downloaded the v1 view tree. The inactive UI's views are now deferred until rendered. The changelog dialog, mounted on every v2 page, loads md-editor-v3 on first open.

* perf(frontend): set up Storybook annotations only for the story test

Every vitest file paid for the Storybook preview (full Vuetify and the Storybook runtime) though only storybook.test.ts renders stories.

* fix(frontend): address pr-ready code review

* style(frontend): pr-ready polish

* docs(frontend): pr-ready humanizer pass

* fix(frontend): address Greptile review

- router.test.ts: cover an in-place UI switch, so a deferred view that renders blank fails the suite.
- storybook.test.ts: narrow the preview setup comment to the invariant it guards.

* refactor(frontend): drop the view-loader check from the deferred-view guard

Every route view is a lazy import loader, so a typeof check finds the
inactive UI's views without copying vue-router's own heuristic.

* chore(frontend): restrict direct md-editor-v3 imports

md-editor-v3 now loads lazily on v2 pages outside the game details route,
so an eager import anywhere else would quietly pull it back into a shared
chunk. ESLint now flags it and points at the MarkdownPreview wrapper. Type
imports stay allowed, the frozen v1 trees are skipped, and the list is
shared with the v2/lib block so primitives keep their own restrictions.

* fix(frontend): pr-ready code review

Prefetch every deferred view of the current route on a UI switch, lint
static imports of the md-editor wrappers, make the hand-back test
self-contained, and drop redundant mock resets.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(frontend): pr-ready simplify

Render MarkdownViewer through the shared MarkdownPreview wrapper, inline
the route-entering check, and set the test UI version once in beforeEach.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* perf(frontend): load md-editor config with its consumers and skip echarts

The md-editor config module now applies itself on import, and each
md-editor consumer (v1 details, NotesTab, MarkdownPreview) imports it,
so md-editor's config, json5 and the XSS filter leave the entry chunk.
Lint flags eager imports of the config module.

Echarts fences render as code and a stub instance stops md-editor from
fetching echarts from unpkg whenever a preview mounts.

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]>
2026-10-03 12:55:05 -04:00