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>
atob() decodes base64 to a JS string, which r.return() then re-encodes
as UTF-8. For filenames with non-ASCII characters (e.g. Pokémon), bytes
above 0x7F get double-encoded — serving different content than what the
backend computed the CRC32 over, causing mod_zip to report CRC failure
on the .m3u file.
Buffer.from(value, 'base64') decodes directly to a byte array and
r.return() sends it verbatim, matching the CRC exactly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All callers declare a fresh `local -a wrap=()` before invoking, so the
in-function reset is unnecessary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapse `otel_prefix` and `otel_prefix_str` into a single nameref-based
helper. Watchfiles call sites embed the array as a shell-quoted prefix
via `${wrap[*]@Q}`, which also fixes a quoting bug where an
`OTEL_SERVICE_NAME_PREFIX` containing a single quote would produce an
invalid command string and break the watcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapse the duplicated OTEL_SDK_DISABLED / opentelemetry-instrument
branches in run_startup, start_bin_gunicorn, start_bin_watcher, and
start_bin_sync_watcher into two small helpers:
- otel_prefix: emits the wrapper as NUL-delimited argv tokens (for
direct process invocation).
- otel_prefix_str: emits the wrapper as a shell-string prefix (for
embedding inside `watchfiles --target-type command`).
Each call site becomes a single command instead of a 2- or 3-way
branch with a fully duplicated command body. As a side effect, the
watcher functions now also gain the `command -v opentelemetry-instrument`
fallback that the gunicorn/startup paths added.
The shell fallback was assigned locally but never exported, so
sync_watcher.py and the Python config layer never saw the resolved
value. They happened to land on the same /var/lib/romm/sync default by
coincidence; export it so the shell and Python defaults stay linked
through a single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prod Dockerfile creates /var/lib/romm/sync at build time, but if a
user overrides SYNC_BASE_PATH to a path that doesn't exist (or runs the
dev entrypoint, which never created the default), watchfiles fails to
start because its target directory is missing. Have both entrypoints
mkdir -p the resolved path before handing it to watchfiles.
Co-Authored-By: Claude Opus 4.7 (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>
The init script ran `sed -i` against /etc/gunicorn/logging.conf, which
fails both on read-only root filesystems and when the container runs
as a non-root UID (since /etc/gunicorn is not chmod'd writable). Copy
the config to /tmp/gunicorn/logging.conf at startup and edit/use that
copy instead, leaving the image file untouched.
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>
The gunicorn logging config hard-coded level=INFO, ignoring LOGLEVEL.
Patch it at startup via sed. Also pass --logging_level to rq worker
and rqscheduler so their framework logging respects LOGLEVEL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When OTEL_SDK_DISABLED=true (set automatically when no OTEL_ env vars
are present), the opentelemetry-instrument wrapper does not properly
pass through the WATCHFILES_CHANGES environment variable to watcher.py.
This causes the filesystem watcher to silently fail - watchfiles detects
changes but watcher.py receives an empty WATCHFILES_CHANGES and exits
immediately without scheduling any rescans.
The fix skips the opentelemetry-instrument wrapper when OTEL is disabled,
allowing watchfiles to pass WATCHFILES_CHANGES directly to watcher.py.
Fixes automatic rescan on filesystem change for users who don't configure
OpenTelemetry (the majority of self-hosted deployments).