* 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]>
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]>
* 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]>
* 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]>
* 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]>
* 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]>
* 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]>
* 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]>
* 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]>
* 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]>
* 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]>