Visibility-coverage gaps (404-mask hidden entities, mirroring the existing
delete/read paths):
- update_rom (PUT /roms/{id}) and update_rom_user (PUT /roms/{id}/props)
- add_firmware: platform-hide now cascades to firmware uploads
- patch_rom: resolve the parent rom of both the base and patch files and
404 when hidden, so a hidden rom's bytes can no longer be streamed back
- activity feeds (get_all_activity / get_rom_activity): drop sessions whose
rom is hidden from the caller
Migration: make the role enum -> varchar narrowing Postgres-safe. The cast
now uses postgresql_using, the orphaned native role type is dropped on
upgrade, and downgrade recreates it explicitly (create_type=False) before
re-typing the column. Verified up/down/up on Postgres 16 and MariaDB.
Also collapses the two permission migrations into a single 0092 and notes
the override own_only replacement granularity limit in the resolver.
AI assistance: implemented with Claude Code (review-fix pass).
Co-Authored-By: Claude Opus 4.8 <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>
- 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>
- 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>
Introduces POST /api/roms/{id}/patch that applies patch files to ROM files
server-side, enabling third-party apps to request ROM patching using games
and patches already in the library without needing client-side JS support.
- backend/utils/patcher.js: Node.js helper that loads RomPatcher.js and
applies a patch file to a ROM, writing the result to an output path
- backend/endpoints/roms/patch.py: FastAPI endpoint that looks up ROM and
patch files by ID, invokes the Node.js patcher via subprocess, and
streams the patched ROM back as a download
- Supports all 9 patch formats: IPS, UPS, BPS, PPF, RUP, APS, BDF, PMSR, VCDIFF
- Requires roms.read scope for authentication
- Temp files are cleaned up via Starlette BackgroundTask after response
https://claude.ai/code/session_01HS6ZvAiBjmLPVB3gGw8eEt