Adopt master's ROM schema design (sibling_roms + files, batched
get_files_for_roms / get_siblings_for_roms) while preserving the v2-branch
features master lacks: per-user is_main_sibling on siblings and audio_meta
on rom files.
Conflict resolution:
- responses/rom.py: keep master's sibling_roms/files fields; re-graft
is_main_sibling via SiblingRomSchema.from_rom(rom, is_main_sibling=...);
restore the eager-relationship fallback in
SimpleRomSchema.from_orm_with_request (None sentinel) so the v2
/{id}/simple endpoint still returns siblings/files.
- roms_handler.py: get_siblings_for_roms now left-joins RomUser and returns
(Rom, is_main_sibling) tuples; keep both branch and master file helpers.
- drop the redundant branch-only sibling_ids field and
get_sibling_data_for_roms.
- generated types resolved to match (sibling_roms + files; RomFileSchema
keeps audio_meta and gains archive_members).
- update v2 components and the RelatedGameCard mock to read sibling_roms.
- fix stale exclude={"siblings"} -> "sibling_roms" in scan emit payloads.
- re-chain the audio_meta migration as 0083 (after master's 0082) to keep a
single Alembic head.
- package.json: union of branch tooling + master dependency bumps; lock
regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR #3388 added hardlink-based asset import/export (os.link with a
shutil.copy2 fallback) to avoid duplicating disk space. The Dockerfile
VOLUME instruction listed each /romm subdirectory (resources, library,
assets, config, sync) separately, which makes Docker create an
independent mount point for each one — even when the user bind-mounts a
single parent at /romm. Each mount point is its own st_dev, so every
cross-directory os.link() failed with EXDEV and silently fell back to a
full copy, defeating the optimization.
Declare the parent /romm directory instead so all subdirectories share a
single filesystem and hardlinks can succeed when paths reside on the same
underlying host filesystem.
The default Docker image symlinked /romm/assets into the nginx static web
root (/assets/romm/assets), where it was served by an unauthenticated
`location /assets { try_files ... }` block. /romm/assets holds private user
data (save files, save states, screenshots, avatars) that is meant to be
accessible only through the authenticated /api/raw/assets/{path} route
(Scope.ASSETS_READ). The static symlink bypassed that protection, letting any
unauthenticated caller read another user's files given a (guessable) path.
Avatar URLs leaked the hex user ID through the same static route, making path
construction straightforward.
Fix:
- Drop the /romm/assets symlink from the Docker image build and both
entrypoint scripts; only /romm/resources (public cover art, screenshots,
manuals) remains statically served.
- Point the frontend avatar URLs at the authenticated /api/raw/assets/ route
instead of /assets/romm/assets/. Browser <img> loads authenticate via the
existing session cookie.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the sync staging folder out of ROMM_BASE_PATH so it lives on a
dedicated writable mount. This lets the container run with a read-only
root filesystem without losing in-flight save uploads, and keeps
app-owned state separate from the user-curated library volume.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rewrite frontend/src/views/Patcher.vue around library ROM/patch pickers
(v-autocomplete with debounced search) and call POST /api/roms/{id}/patch
with responseType: blob; download and/or re-upload the patched output.
- Drop the rom-patcher npm dep and vite-plugin-static-copy from the frontend;
remove the now-unused web worker, type decls, and viteStaticCopy plugin.
- Move the Node patcher into a self-contained npm project at
backend/utils/rom_patcher/ (dir uses underscore so it's a valid Python
package). Patcher.js loads RomPatcher.js from its sibling node_modules.
- Extract the heavy subprocess logic into backend/utils/rom_patcher/patcher.py
(apply_patch, PatcherError, SUPPORTED_PATCH_EXTENSIONS); slim patch.py to
HTTP/DB plumbing and fix a stale .parent.parent path.
- Wire up Docker: dev Dockerfile installs the new npm project; prod
Dockerfile adds a backend-node-build stage and copies rom-patcher-js into
the production image; install nodejs at runtime.
- Add new patcher i18n keys to en_US (other locales fall back).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Related `mod_zip` issue https://github.com/evanmiller/mod_zip/issues/90
has been fixed in commit
288d66541c
By upgrading `mod_zip` to include this fix, we can remove the workaround
that involved using a separate internal location and server to serve
files for zipping.
* Added `linux-headers` back, but only for development stage.
* Fixed initialization script, as `uv` is not included in the final
Docker image.
* Initialize variable `ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA`.
The `production-stage` stage was depending on the `python3` package from
Alpine, which at the moment of writing is still Python 3.12.
To avoid relying on Alpine's package and releases, we now copy the
Python installation directly from its official Docker image.
Other options were tested but did not work:
- Trying to install Python 3.13 using `apk`. As mentioned, Alpine does
not support Python 3.13 yet.
- Installing Python 3.13 using `pyenv`. The installation worked, but the
generated image was too large. Related `pyenv` discussion:
https://github.com/orgs/pyenv/discussions/2868
- Installing Python 3.13 using `uv`. Only worked for `amd64`
architecture, but the installation failed on `arm64`.