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>
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>
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).
OpenTelemetry tries to send traces to a default `localhost:4317`
endpoint if no configuration is provided, which is not what we want if
users don't configure OpenTelemetry explicitly.
This change sets the `OTEL_SDK_DISABLED` environment variable to
`true` if no `OTEL_` prefixed environment variables are found, which
disables the OpenTelemetry SDK.
Guarantee that cache is initialized during startup, and only once,
instead of every time a `MetadataHandler` object is instantiated.
Also, improve logic to determine `fixtures` paths.
For steps that need to run before the web application starts, such as
scheduling tasks, this new `startup.py` script is introduced.
This fixes a recently introduced issue where task scheduling was not
being triggered, because of it being included in the
`if __name__ == "__main__":` block, which is not executed when
the application is run by Gunicorn in production environments.
We do not include this logic as part of FastAPI's lifespan
implementation, as running multiple workers with Gunicorn would
cause this logic to be executed multiple times.
This change introduces OpenTelemetry dependencies, and uses
`opentelemetry-instrument` auto-instrumentation to allow users to
configure OpenTelemetry settings via environment variables [1].
The only custom environment variable added is
`OTEL_SERVICE_NAME_PREFIX`, which allows users to set a prefix to the
service names included by RomM to differentiate between `api`, `worker`,
and `watcher` services.
The instrumentation of RQ workers and file watcher will be added in
subsequent pull requests.
[1] https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
* 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 `watchfiles` library supports event batching, which allows us to
process multiple filesystem changes in a single run.
This change also avoids database calls in the watcher as much as
possible.
This change replaces our custom `scheduler.py` script with the
`rqscheduler` command, allowing us to run the RQ scheduler as a
separate, low-memory process, by avoiding the need to maintain
the Python app in memory.
* Remove `scheduler.py` script.
* Move initialization of scheduled tasks to `worker.py`.
* Update `docker/init_scripts/init` to start the `rqscheduler`
command instead of the custom script.
* Fix scheduled tasks' `func` paths to the new project structure.
* Temporarily use a fork of `rq-scheduler` to support
username and SSL settings in the `rqscheduler` command.