Deleting a platform or collection in the v2 UI showed "Failed to
delete: unknown error" even though the deletion succeeded. The success
handler navigated to a non-existent route name ("platforms" /
"collections"), and Vue Router threw on the unknown name, which the
catch block surfaced as a generic error.
Navigate to the real index routes (PLATFORMS_INDEX / COLLECTIONS_INDEX)
instead.
Fixes#3598
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cv77VUJgPe1H7ipSRitjpH
The miximage hover video had no height bound, so a tall/narrow source
overflowed the cover. Match the miximage frame's box and `object-fit:
cover` it so the clip fills the bezel screen and crops instead of
spilling past it.
Also gate the PWA dev service worker behind DEV_PWA: it intercepted dev
requests and forced full page reloads on every edit (CSS included),
defeating HMR. Default dev now gets working HMR; set DEV_PWA=true to test
the PWA in dev.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removing the aspect_ratio body field left update_platform with a single
scalar Body() param. FastAPI stops embedding a lone scalar body, so the
endpoint began expecting a bare JSON string while the frontend keeps
sending {"custom_name": "..."}, producing a 422 when editing a
platform's display name in v2.
Restore the embedded-key contract with Body(embed=True), matching the
frontend payload and every sibling update endpoint. Regenerate the
frontend types (restores the Body_update_platform model) and add an
endpoint regression test.
AI assistance: written with Claude Code (Opus 4.8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- patcher.js resolves rom-patcher-js from both the relocated sibling
layout (docker/Dockerfile) and the plain node_modules layout (root
Dockerfile), so both build flows work without a manual copy
- apply_patch wraps the node subprocess in asyncio.wait_for with a
timeout and kills it on expiry; a semaphore bounds concurrency, and the
endpoint rejects oversized ROM/patch files to avoid OOM
- report the patch source-checksum validation result via an
X-Patch-Validated header; the patcher UI warns on a mismatch
- return a generic "Patching failed" detail to clients and log the real
error server-side, so node/RomPatcher.js paths don't leak
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
frontend/assets/romm/resources is a symlink into the user's library
(covers, screenshots). Vite's root is frontend/, so chokidar follows the
symlink and tries to watch every file under it. With a large library
(hundreds of thousands of asset files) this exhausts the Node heap and
the dev server crashes with "JavaScript heap out of memory".
Ignore that tree in server.watch; it holds served assets that never need
HMR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the standalone v2 Patcher view into a PatcherTab feature composite
rendered inside GameDetails (gated on the ROM carrying nested files), and
remove the dedicated rom/:rom/patch route along with every navigation
entry that led to it.
v2: remove the FilesTab button, the GameActions menu item, and
useGameActions.patch/canPatch; drop the route binding and named view.
v1 (frozen, removed per request): delete views/Patcher.vue, the AdminMenu
patcher item, and navigation store goPatcher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fill the 9 missing patcher.json keys across all non-English locales
(translated for fr/es/de/it/pt_BR, English placeholders elsewhere) and
exclude the vendored patcherjs library from trunk linting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move default_category_for_non_nested validator onto RomFileSchema so
top-level files default to category "game" (the v2 patcher's base-file
filter relies on this).
- Use Annotated Body() in the patch endpoint; check patcher output via
anyio async Path.
- Drop the now-unused client-side rom-patcher and vite-plugin-static-copy
(patching is server-side); simplify the Storybook plugin filter.
- Regenerate frontend OpenAPI types.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the v1 change that moved patching from a browser web worker to the
server-side /roms/{id}/patch endpoint operating on files already in the
library.
- Rewrite v2 Patcher view to read base/patch files from currentRom and
POST to the patch endpoint, dropping the worker, rom-patcher imports,
window globals, and dropzones.
- Add canPatch + patch() to useGameActions and surface a Patcher entry in
the per-ROM more-menu (gated on nested files).
- Add a Patch button to the GameDetails FilesTab header.
- Remove the now-broken standalone Patcher links (route is per-ROM) from
UserMenu and SettingsSidebar; refresh stale nav doc comments.
- Add patcher.missing-from-fs i18n key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Default orientation now faces right (initialYaw flipped).
- Idle auto-spin actually resumes: the time-based check moved into the RAF
loop, since a computed reading performance.now() cached and never restarted
after the first interaction. Quiet window is 2s.
- Flick momentum without a JS decay loop: the release velocity is handed to
the box as one extra rotation and a CSS ease-out curve coasts it to a stop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019itLXRfJXGGbhPY3JyqnuN
Read the front (box-2D) and spine image natural ratios on mount, not only on
the load event — a cached cover decodes before the listener binds, which left
the box stuck at the default ratio instead of the real box-2D proportions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019itLXRfJXGGbhPY3JyqnuN
Add user-select: none to the box and pointer-events: none to the faces so a
drag always lands on the root (which owns the rotation listeners) and never
starts a text / image selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019itLXRfJXGGbhPY3JyqnuN
Adds an RBox3D primitive that builds a rotatable, fake-3D game box from
three flat ScreenScraper scans (front, back, spine) using CSS 3D
transforms. Box proportions derive from the images themselves; it rotates
via pointer drag, arrow keys / gamepad D-pad, and the right analog stick,
drifts gently when idle, and honours prefers-reduced-motion.
The game detail hero (CoverColumn) upgrades to the spinning box when the
"3D box" boxart style is selected and the rom has the full set of faces,
falling back to the flat cover otherwise.
Backend: persist the box-2D-side (spine) scan locally, mirroring the
existing box-2D-back handling — new BOX2D_SIDE media type + box2d_side_path
on ss_metadata, opt-in via scan.media.
- RBox3D primitive + Storybook story (controls + keyboard-rotation play())
- useBoxFaces composable resolving the three faces + a `complete` gate
- box3d-alt i18n key across all locales
- backend BOX2D_SIDE persistence + tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019itLXRfJXGGbhPY3JyqnuN
Show a "where you left off" screenshot on the Home continue-playing rail and
the live-activity board, with a small cover-art thumbnail (PIP) in the corner
so the game stays identifiable. Both render at the image's natural aspect.
Backend:
- New shared util `continue_playing_screenshot(rom, latest_save)` resolving the
image in priority order: latest save's screenshot, then title screen, then
first gameplay screenshot (None → frontend falls back to cover art).
- `SimpleRomSchema.screenshot_path` populated only on the `last_played` query;
`get_latest_saves_for_roms` batch handler (+ tests).
- ActivityEntry / ActivityEntrySchema gain `screenshot_path`, computed from the
session player's latest save in both the socket and REST heartbeat paths.
Frontend:
- New shared `CoverArtPip.vue` (bottom-right 2D cover thumbnail), reused by
GameCard and ActivityCard.
- Home continue-playing rail uses `screenshot_path` + PIP, natural aspect (no
forced hero/style).
- Activity board: screenshot-forward cover + PIP, and a wrapping flex layout so
cards share a uniform height with natural-ratio widths (gallery-card
behavior).
- GameCover only keys the measured ratio by rom id for the rom's own cover, so
a `coverSrc` override (screenshot) never pollutes the gallery's ratio cache.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking Play at the center of a gallery cover now runs the shared-element
view transition into the /ejs (EmulatorJS) or /ruffle hero, matching the
card → details morph.
- useGameActions gains an optional `coverEl` resolver; `play()` wraps the
navigation in `morphTransition` (cover → player hero, same `rom-cover-<id>`
tag the player paints statically) and awaits the push so the snapshot is
taken after the player renders. GameCard supplies its GameCover box.
- The player heroes only seeded `rom` from `currentRom` (set via GameDetails),
so a direct gallery→play left `rom` null and the `v-if`-gated hero never
rendered — nothing to morph into. Seed a lightweight `heroSeed` SimpleRom
from the gallery store (new `galleryRoms.getRomById`) so the cover paints
its morph tag immediately; `rom` fills in on mount. Play is disabled until
the full payload loads.
- Enable hover-motion on both player heroes so the cover spin / hover video
work there too.
- Arcade systems (arcade / neogeoaes / neogeomvs) skip the cartridge slot-in
animation (new `isArcadeSystem`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework useCoverAnimation to mirror v1's useGameAnimation, which composes
two motions on different CSS properties so they don't fight: the spin
owns `transform: rotate` (set per-frame) while the disc/cartridge slot-in
owns `margin-top` (eased in CSS with an overshoot). A single transform for
both couldn't ease the slide independently of the per-frame spin.
- CD launch spins at max while sliding the cover down into the drive;
cartridge launch seats fully into its bay (1/3 height).
- Drop the cartridge hover animation entirely — matches v1, which only
spins discs and plays the hover video on hover.
- Enable hover-motion on the EmulatorJS and Ruffle player heroes so the
hover spin / video work there too.
- Fix a `.ame-cover__img` typo (missing "g") that was silently breaking
the cover image base styles, and ease `margin` in CSS on `.game-cover__img`
so the slide composes with the bloom reveal instead of overriding it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The re-pack debounce in useGalleryCoverRatios was raised from 150ms to
350ms (c5d844e1) to avoid a re-pack storm during fast scroll, but the
test still advanced fake timers by 150ms and expected the debounced
ratioVersion bump to have fired — so it asserted 1 and got 0. Advance
by the new 350ms delay at all three sites. Pre-existing failure on
master, unrelated to this branch's changes; bundled here to unblock CI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Added "total-sessions" key to various language JSON files for activity localization.
- Updated ActivityCard component to reflect changes in the activity view.
- Enhanced Activity view to display a live session counter with a tooltip.
- Introduced virtual scroll debugging to monitor performance in the gallery.
- Adjusted layout styles for better responsiveness and visual consistency across components.
Cover hugs the title side of its fixed-width column (and the skeleton
matches), so the empty space sits on the left and the cover reads as
attached to its title.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With natural-aspect covers, the list-row cover lived inside the title cell
and its variable width pushed each row's title/meta to a different x. Add a
dedicated fixed-width (64px) cover column to the shared list grid template —
big enough for any portrait→square game cover — so the title column starts
at the same x on every row. Header and skeleton pick it up via the shared
column config; the cover renders left-aligned at its natural aspect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Gallery list view: drop the `fixed` opt-out on its GameCard so the
row thumbnail renders at the cover's natural aspect (fixed height,
natural width) like everywhere else. Removes the now-unused `fixed`
prop / class / CSS branch from GameCard.
- CoverPlaceholder: the title now scales with the cover box (container
query units, clamp(8px, 8cqmin, 18px), em padding) instead of a fixed
12px — readable on a tiny list thumb, proportionate on the detail hero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The overview's screenshots and How-Long-To-Beat blocks rendered with no
label, unlike the related-games sections. Wrap both in the same labelled
section pattern (renamed `overview-tab__related-*` → `__section-*` since it
now covers non-related blocks too). The HLTB heading is gated on a
`hasHltb` computed so it doesn't show when the strip is empty.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The edit dialog's hero used a fixed 240px cover column with the cover
centered, so a natural-width cover left variable leftover space — making
the gap to the fields vary by cover shape. Size the column to the cover
(`auto`) so the gap is exactly the grid gap for any cover, and give it a
touch more room (24px).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend natural-aspect cover rendering beyond the gallery to the rest of
the app (continue-playing stays a fixed 16:9 hero, by request).
- GameCard size tiers now render fixed-height / natural-width like the
default card (size just picks the height); covers EditRomDialog (lg)
and ManageCollectionsDialog (xs) for free. Adds a `fixed` opt-out for
dense aligned tables.
- GameListRow (compact list view) opts into `fixed` so its cover column
stays uniform for row alignment.
- Compact fixed-column thumbnails (ScanPlatformRow, DeleteRomDialog,
RefreshMetadataDialog, ActivityCard) stop cropping (object-fit
cover → contain): the whole cover shows at its true aspect while the
slot stays uniform.
Deliberately unchanged: collection mosaics (composite collage art),
save/state screenshots (16:9, not covers), and the provider cover-picker
comparison grids (uniform tiles aid side-by-side selection).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the hand-rolled RImg + useCoverArt cover resolution in
RandomPickWidget for the shared GameCover, which measures the image's
natural ratio. The thumbnail now renders at a fixed 70px height with
natural width (no crop), matching the gallery, and the widget's cover
plumbing (coverSrc / coverContain / placeholder fallback) collapses into
GameCover.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the measured-ratio map, debounced ratioVersion, onCardRatio handler,
and ratioAt resolver out of GalleryShell into useGalleryCoverRatios. The
composable owns its debounce-timer cleanup (onBeforeUnmount), so the shell
no longer tracks ratioBumpTimer. Behaviour is unchanged; adds a unit test
for the dedup / debounce / position→rom→ratio mapping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>