Commit Graph
13288 Commits
Author SHA1 Message Date
Claude e979b3e9ee chore(skills): move humanizer scope out of the step list (simplify)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XXuG7JWgxP6DPLB182yD9v
2026-09-27 18:27:05 +00:00
Claude a4186eb4c0 chore(skills): tighten humanizer step scope and directives (code-review)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XXuG7JWgxP6DPLB182yD9v
2026-09-27 18:24:49 +00:00
Claude c6b6d4668e chore(skills): vendor humanizer and run it as the last pr-ready step
Vendors blader/humanizer (MIT) unmodified at 9862685f, excluded from Trunk
because its examples quote the em dashes it removes. pr-ready gains a fifth
step that runs it over the prose a change adds, with RomM's comment limits and
house formatting taking precedence over its patterns.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XXuG7JWgxP6DPLB182yD9v
2026-09-27 15:46:00 +00:00
Sam Dornan a89f7167a2 fix(deps): bump asyncssh, click, pygments and requests for open advisories (#4833) 2026-09-27 10:23:29 -04:00
Sam Dornan 864a0769fc chore(deps): bump uv to 0.12.19 and relax required-version for Dependabot (#4832) 2026-09-27 10:23:19 -04:00
Sam Dornan 23191407c1 chore(backend): replace zstandard with stdlib compression.zstd (#4831) 2026-09-26 21:20:34 -04:00
Sam Dornan 3206bc541f chore(deps): move from httpx to httpx2 (#4830) 2026-09-26 21:17:13 -04:00
Sam Dornan 0c6e85f923 chore(deps): upgrade Authlib to 1.8 and joserfc to 1.7 (#4829) 2026-09-26 21:10:35 -04:00
Sam Dornan 3679c42732 chore(mypy): report untyped imports instead of disabling the check (#4828) 2026-09-26 20:41:04 -04:00
Sam Dornan e0df54cba5 chore(backend): enforce mypy's disallow_untyped_calls and disallow_untyped_decorators (#4827) 2026-09-26 20:21:54 -04:00
Sam Dornan 7c2b049b70 chore(mypy): drop warn_return_any and validate provider responses with strict JSON (#4826) 2026-09-26 20:13:55 -04:00
Sam Dornan 84726ba9a2 refactor(backend): move FORCE INDEX hint and FULLTEXT match into sql_dialect (#4825) 2026-09-26 19:08:25 -04:00
Sam DornanandClaude Opus 5.5 0afe3d2176 test(sync): isolate RetroArch blob and PSP pending dirs per test (#4824)
Blob uploads and pending PSP files land on disk under the user id, and
nothing removed them between tests. User ids restart in every fresh test
database, including each pytest-xdist worker database, so a later test
whose admin reused an id picked up a stale config/retroarch.cfg in its
manifest. Point both handlers at the test's tmp_path.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 16:39:05 -05:00
Sam DornanandClaude Opus 5.5 5a6c895d33 chore(mypy): give every generic its type arguments (#4818)
* chore(mypy): switch to strict mode with listed exceptions

Replace the hand-picked flags with `strict = True`, turning off only the
four flags that aren't clean yet (disallow_any_generics,
warn_return_any, disallow_untyped_calls, disallow_untyped_decorators).
Any other strict flag, including those future mypy releases add to the
preset, is on by default.

That turns on no_implicit_reexport, strict_equality and extra_checks:
- Import each name from the module that defines it. About 240 test
  sites patched a dependency through the module that imported it
  (`scan_module.fs_rom_handler`); they now import the same object from
  its source, so every patch targets what it did before. Tests that
  replace a module attribute are unchanged.
- handler.streaming.memory_cards re-exports store_memory_card_version
  explicitly, since endpoints call it there and tests patch it there.
- mutagen gets implicit_reexport, as it re-exports without __all__.
- Test comparisons build typed values (CoverResult, a typed expected
  dict, enum .value) and the CORS lookup casts for its identity check.
- One ss_handler ** spread keeps a coded ignore: extra_checks wants its
  NotRequired dump_* keys spelled out, and the media dict never has them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PLzXae5up2pYXRoBjfjn2G

* chore(mypy): give every generic its type arguments

Drop disallow_any_generics from the strict-mode exceptions and fix the
347 bare generics it reports, 230 of them in app code.

- Bare `dict` becomes `dict[str, Any]`: update payloads, raw provider
  JSON and endpoint bodies map string keys to mixed values, so the Any is
  real and now written down. The OpenAPI document is unchanged.
- Rom query filters take and return RomSelect; _music_facet_joins and
  _exclude_hidden are generic over the Select they're given, and
  _build_hash_selects returns id-only selects.
- Redis clients are typed by what they return: the decoding caches are
  Redis[str], the RQ connection and binary cache Redis[bytes].
- The rest get explicit parameters: Row, Pattern, ColumnElement,
  QueryableAttribute, SpooledTemporaryFile, Task, Future and friends.

Precise types reached callers that had been getting Any, which surfaced:
- The LaunchBox cache writer is fed binary-cache pipelines.
- The flashpoint and IGDB request helpers return raw JSON of varying
  shape, not always a dict.
- The rom search endpoint returned dicts under a list[SearchRomSchema]
  annotation; it now validates them into that schema.
- The audit filter's id subquery selects strings.
- In tests the binary cache is the non-decoding fake shared with
  async_cache, so it is cast to Redis[bytes] rather than changed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PLzXae5up2pYXRoBjfjn2G

* test(upload): assert cleanup against the patched upload directory

The 409 test checked config's ROM_UPLOAD_TMP_BASE while the fixture patches
the endpoint's copy to tmp_path/uploads, so it passed even if cleanup failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PLzXae5up2pYXRoBjfjn2G

* chore(mypy): keep the row type through the ROM filter helpers

filter_roms and its helpers also receive select(Rom.id) membership queries,
so they now take and return the caller's Select type instead of narrowing to
RomSelect. merged_ra_metadata mixes scalar and list values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PLzXae5up2pYXRoBjfjn2G

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 16:32:29 -05:00
Sam DornanandClaude Opus 5.5 e6a022790f fix(ra): keep a hash match when RA's details request fails (#4822)
* fix(ra): keep a hash match when RA's details request fails

The hash index names the game before get_rom asks RA for its details. When
that request failed, the empty reply hit the KeyError fallback and came back
as ra_id=None, which a hashes scan read as a confirmed miss and used to clear
an existing hash_match. An empty reply now raises unavailable, so the scan
marks RA inconclusive and keeps the flag, as it already did for a failed
index download.

Also reads fs_resource_handler from its defining module, which strict mypy
requires (same change as #4821).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PLzXae5up2pYXRoBjfjn2G

* style(ra): keep only the provider behavior in the empty-details comment

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PLzXae5up2pYXRoBjfjn2G

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 16:31:54 -05:00
Georges-Antoine Assi fbff4347ac Merge pull request #4815 from rommapp/fix/local-time-asset-names
fix(saves): stamp save and state file names with local time
2026-09-26 17:22:51 -04:00
Sam DornanandClaude Opus 5.5 bd16648c9a chore(mypy): switch to strict mode with listed exceptions (#4817)
* chore(mypy): switch to strict mode with listed exceptions

Replace the hand-picked flags with `strict = True`, turning off only the
four flags that aren't clean yet (disallow_any_generics,
warn_return_any, disallow_untyped_calls, disallow_untyped_decorators).
Any other strict flag, including those future mypy releases add to the
preset, is on by default.

That turns on no_implicit_reexport, strict_equality and extra_checks:
- Import each name from the module that defines it. About 240 test
  sites patched a dependency through the module that imported it
  (`scan_module.fs_rom_handler`); they now import the same object from
  its source, so every patch targets what it did before. Tests that
  replace a module attribute are unchanged.
- handler.streaming.memory_cards re-exports store_memory_card_version
  explicitly, since endpoints call it there and tests patch it there.
- mutagen gets implicit_reexport, as it re-exports without __all__.
- Test comparisons build typed values (CoverResult, a typed expected
  dict, enum .value) and the CORS lookup casts for its identity check.
- One ss_handler ** spread keeps a coded ignore: extra_checks wants its
  NotRequired dump_* keys spelled out, and the media dict never has them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PLzXae5up2pYXRoBjfjn2G

* test(upload): assert cleanup against the patched upload directory

The 409 test checked config's ROM_UPLOAD_TMP_BASE while the fixture patches
the endpoint's copy to tmp_path/uploads, so it passed even if cleanup failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PLzXae5up2pYXRoBjfjn2G

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 15:45:37 -05:00
Sam DornanandClaude Opus 5.5 cbfc904a48 fix(ra): count RomM's own RA hash match as RetroAchievements verification (#4816)
* fix(ra): count RomM's own RA hash match as RetroAchievements verification

The RetroAchievements verification chip, the header Verified badge and the
library verified filter only read Hasheous' `ra_match` flag. RetroAchievements
hashes only part of some ROMs (NDS, PSP), so Hasheous never flags those from
their file hashes, and a ROM RomM matched to its RA game by RA hash still
showed a grey chip.

The RA handler now records `hash_match` in `ra_metadata` whenever the ROM's
RA hash is one RetroAchievements lists for the matched game, whichever route
found the game (hash lookup, Hasheous-supplied id, filename tag, update scan
or a manual edit). The frontend verification helper and the backend verified
filter count it alongside Hasheous' flag. Existing ROMs pick it up on their
next scan.

Fixes #4801

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LfQbq2ZUFK53NMXPNktdaM

* style: satisfy trunk isort and no-emdash-in-comment

Sort imports with the repo's .isort.cfg and replace the comment em-dashes
in MetadataTab.vue that trunk flags once the file is modified.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LfQbq2ZUFK53NMXPNktdaM

* fix(ra): address code-review findings on the RA hash match

- A HASHES rescan whose RA lookup ran and found nothing clears
  `hash_match`, mirroring the Hasheous guard; a lookup that errored keeps it.
- A manual RA id change that can't be resolved no longer keeps the previous
  game's `hash_match`.
- `_search_rom` no longer caches a failed hash-list download ({}) as an
  empty index for the whole refresh window.
- Update the `verified` filter description and trim the romVerification
  header comment.

Step: pr-ready code-review (xhigh --fix)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LfQbq2ZUFK53NMXPNktdaM

* fix(ra): don't let a failed hash check block an RA ID lookup

`get_rom_by_id` now treats the hash-list check as secondary: on failure it
logs, keeps a match already recorded for the same game, and still returns
the game. A failed hash-list download raises "RetroAchievements
unavailable" instead of reading as "no match", so scans treat RA as
inconclusive and keep the stored flag.

Addresses the Copilot and Greptile review findings on ra_handler.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LfQbq2ZUFK53NMXPNktdaM

* refactor(ra): simplify the RA hash-match verification

- Cache the parsed RA hash index per platform, keyed by the cache file's
  mtime, so a scan parses it once instead of once per ROM.
- Check the hash only after `get_rom_by_id` has resolved the game.
- Give each frontend verification database its own `matches(rom)`, which
  drops the `ra_match` special case in the shared helper along with
  `matchesDatabase` and `VERIFICATION_KEYS`.

Step: pr-ready simplify

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LfQbq2ZUFK53NMXPNktdaM

* style(v2): polish romVerification naming and comment

Rename the Hasheous matcher factory to `anyHasheousFlag` and trim the
VERIFICATION_DATABASES comment to two lines.

Step: pr-ready review-polish

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LfQbq2ZUFK53NMXPNktdaM

* fix(ra): keep a recorded hash match only for the same RA hash

When the hash-list check fails, `get_rom_by_id` now keeps a stored
`hash_match` only if both the game and the ROM's RA hash are unchanged, so
a rehashed ROM isn't left verified by a hash nobody checked. The
romVerification tests build on the shared typed `SimpleRom` fixture
instead of casting a bare object.

Addresses the Greptile review findings on ra_handler.py and
romVerification.test.ts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LfQbq2ZUFK53NMXPNktdaM

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 15:33:55 -05:00
Georges-Antoine Assi 8c3874a5b0 Merge remote-tracking branch 'origin/master' into HEAD
# Conflicts:
#	backend/tests/endpoints/test_saves.py
2026-09-26 16:08:30 -04:00
Sam DornanandClaude Opus 5.5 5b3d0bf544 feat(sync): record a per-device save baseline to prove what did not change (#4768)
compare_save_state decides a side changed from its timestamp, and
emulators rewrite a save on exit, so an untouched save looks new and a
server change elsewhere turns into a false conflict.

device_save_sync now records what each side held at the last sync
(last_sync_hash from the device, last_sync_server_hash from the server,
migration 0140). A baseline match proves that side unchanged; a mismatch
or NULL falls back to the timestamp logic, so this only removes
conflicts. It runs after the removed_at check in compare_save_state.

- Baselines are recorded only for versions the device provably holds:
  confirm_download pins to the served version, update_save records the
  client hash only when the file was uploaded, deduplicated uploads and
  pushed saves record against the surviving save, and push-pull no-ops
  record hashes only for identical content.
- Identical-content no-ops in negotiate and the watcher record a
  boundary without re-tracking untracked saves.
- The SSH push-pull and file-transfer watcher paths hash device saves
  through hash_save_file, matching compute_content_hash for zipped
  saves, and treat unreadable saves as unknown.

Squashes rommapp/romm#4768 onto master.



Claude-Session: https://claude.ai/code/session_01WAKUdfLb6fyduEqaFbf5A8

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 15:05:57 -05:00
Georges-Antoine Assi c19dca87e7 Merge pull request #4811 from rommapp/fix/wrap-grid-nav-scroll-up
fix(v2): keep the focused platform tile in view when moving up the grid
2026-09-26 16:00:40 -04:00
Georges-Antoine AssiandClaude Opus 5.5 e877ec71ee fix(v2): clear the fixed bars when centring index tiles
Addresses Greptile review on #4811:
- IndexShell gives focus-keyed tiles a scroll-margin matching the top bar,
  pinned toolbar and bottom tab bar, so a centred tile taller than the
  uncovered band no longer lands under the chrome on short viewports.
- Cover the saved-focus restore path with a test.
- Drop the comparison with "nearest" from the scroll comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 15:55:42 -04:00
Georges-Antoine Assi 8f505f129e Merge pull request #4810 from rommapp/fix/upload-scan-while-scanning
fix(v2): skip the post-upload scan while a scan is already running
2026-09-26 15:41:51 -04:00
Georges-Antoine Assi 6ff1c7b047 Merge pull request #4814 from rommapp/fix/mariadb-connector-py314
fix(deps): bump mariadb connector to 1.1.14 for Python 3.14
2026-09-26 15:41:13 -04:00
Georges-Antoine Assi 2b09bcb297 Merge pull request #4812 from rommapp/fix/playmatch-hashless-folder-members
fix(playmatch): skip unhashed lookups of folder ROM members
2026-09-26 15:40:42 -04:00
Georges-Antoine Assi 26c6c181d5 Merge pull request #4796 from LoneAngelFayt/streaming-patches-8
feat(streaming): name containers by their label, and harden import archives
2026-09-26 15:39:39 -04:00
Sam Dornan 3750d6ff70 chore(mypy): require full annotations outside tests, type the setup responses (#4795) 2026-09-26 15:31:00 -04:00
Georges-Antoine Assi 71776206a1 Merge pull request #4809 from rommapp/fix/crt-mode-mobile-images
fix(v2): drop the CRT overlay's backdrop grade on phones
2026-09-26 14:27:33 -04:00
Georges-Antoine Assi da76615164 Merge pull request #4808 from rommapp/fix/emulatorjs-gamepad-rescan
fix(player): assign a gamepad already connected when EmulatorJS boots
2026-09-26 14:22:30 -04:00
Zach Clendenen d0f801283f style(streaming): field comments above their fields, and the library_path example wording from review
The streaming response models were the only ones using attribute docstrings; they are now # comments above each field like the rest of endpoints/responses. Pydantic never read them (use_attribute_docstrings is unset), so the OpenAPI schema is unchanged.
2026-09-26 13:18:22 -05:00
Georges-Antoine AssiandClaude Opus 5.5 4ec4247e20 fix(player): address Greptile review on gamepad replay
Shorten the replay comment to two lines, and make the replay test assert
the pad lands in a player slot through a listener shaped like EmulatorJS's,
not just that dispatchEvent was called.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 14:10:43 -04:00
Georges-Antoine AssiandClaude Opus 5.5 b6c03e79eb fix(saves): stamp save and state file names with local time
State names were built with toISOString() in the browser, and save,
streaming save and memory card names with datetime.now(timezone.utc) on
the server, so every timestamp in an asset's file name was UTC. States
now use the browser's local time and server-named assets use the
server's local time (the container's TZ). Matching and ordering read the
stored file_name and the DB timestamps, so existing files are unaffected.

Fixes #4726

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 13:58:54 -04:00
Georges-Antoine AssiandClaude Opus 5.5 0f219b268b fix(deps): bump mariadb connector to 1.1.14 for Python 3.14
1.1.12 predates Python 3.14 support and has NULL-dereference crashes
(CONPY-318, CONPY-306) fixed in 1.1.13 and 1.1.14. The image moved to
Python 3.14 in 5.3.0, which matches reports of alembic segfaulting inside
_mariadb.cpython-314 at startup.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 13:58:33 -04:00
Georges-Antoine AssiandClaude Opus 5.5 383893a388 fix(playmatch): skip unhashed lookups of folder ROM members
Wii U is a non-hashable platform, so a Wii U title's Playmatch lookup
carried only the file name and size of its largest content file. Those
names (00000005.app, 0000001a.app) repeat across every title in the
Wii U DATs, so Playmatch matched unrelated games by name and size and
hundreds of titles got the same IGDB/SGDB id. Skip the lookup when the
selected file has no hashes and lives inside a folder ROM; a single-file
ROM is still asked about by its own name.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 13:55:23 -04:00
Georges-Antoine AssiandClaude Opus 5.5 bca922d608 fix(v2): skip the post-upload scan while a scan is already running
The upload view and the patcher's "upload to RomM" emitted a scan
unconditionally. With a library scan in flight the backend refuses it with
scan:done_ko, which flipped the UI out of the scanning state and showed a
failure until the running scan's next progress event flipped it back.
Both now go through useScanTrigger, which warns instead of emitting.

Fixes #4690

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 13:55:04 -04:00
Georges-Antoine AssiandClaude Opus 5.5 11b34ae18c fix(v2): center the row the wrap-grid nav moves to
Moving up the platforms/collections grid scrolled with block "nearest",
which parks the row at the viewport top edge, under the fixed top bar and
pinned toolbar, so focus ended up hidden. Vertical moves and focus
restoration now center the target row, matching useGridNav.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 13:55:01 -04:00
Georges-Antoine AssiandClaude Opus 5.5 e7ee22f51c fix(v2): drop the CRT overlay's backdrop grade on phones
On Chromium for Android, the full-viewport backdrop-filter on the CRT
overlay left platform and collection images unpainted until touched.
The xs breakpoint keeps scanlines, vignette and flicker but skips the
saturation/contrast grade. Also swaps the file's em-dashes for
punctuation so it passes the no-emdash lint.

Fixes #4770

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 13:54:12 -04:00
Georges-Antoine AssiandClaude Opus 5.5 16005c9474 fix(player): assign a gamepad already connected when EmulatorJS boots
EmulatorJS's GamepadHandler polls navigator.getGamepads() once inside its
constructor, before the emulator registers its "connected" listener. A pad
the page already knows about (such as the one that pressed Play) lands in
the handler's list without ever being assigned to a player slot, so its
input is dropped until it disconnects and reconnects.

Replay the "connected" event for those pads from the EJS_emulator
assignment trap, right after the constructor returns.

Fixes #4797

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 13:53:52 -04:00
Sam Dornan 9c1e6b2d86 refactor(backend): move engine-specific query SQL out of handlers into utils/sql_dialect.py (#4793) 2026-09-26 13:48:33 -04:00
Georges-Antoine Assi cdbced8d53 Merge pull request #4764 from rommapp/feat/pinned-media
feat(v2): pin any media to the game overview
2026-09-26 13:40:34 -04:00
Sam Dornan 0b98e5ff63 fix(scan): compute RA hash for archives whose member read fails (#4806) 2026-09-26 13:35:27 -04:00
Georges-Antoine AssiandClaude Opus 5.5 81ff4599a7 fix(v2): show media pin button only on hover, even when pinned
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 13:21:37 -04:00
Georges-Antoine Assi c573d328a8 Merge pull request #4807 from rommapp/claude/sync-deleted-saves-followup
fix(sync): follow-ups to deleted-save tracking
2026-09-26 13:17:24 -04:00
Georges-Antoine AssiandClaude Opus 5.5 a06ec55060 refactor(sync): simplify pass over deleted-save tracking
- Move unrecorded_hash onto FSAssetsHandler, so sync_watcher and the
  push-pull task no longer import handler.asset_store (and with it
  scan_handler and every metadata provider, ~80 modules)
- Drop the PSP bundle's unrecorded_hash call: bundles are never slotted
- Skip the removal-times query when the device's copy already matches
- Restore per-row deletes in prune_slot: the single `id IN (...)` delete
  from the review pass deadlocked the threaded removal test 3/10 runs on
  MariaDB (0/25 per row)

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 13:11:37 -04:00
Georges-Antoine AssiandClaude Opus 5.5 f0d42102d0 fix(sync): code-review fixes for deleted-save tracking
Follow-up to #4789 from the code-review pass:
- PUT /api/saves/{id} records the replaced bytes of a never-hashed save
- rehash_save keeps updated_at, so a recompute is not read as a write
- unstamped legacy versions keep their removal time when another is lost
- prune_slot deletes its locked rows in one statement
- 0138 creates the slot index after the collation change

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 12:59:17 -04:00
Georges-Antoine AssiandClaude Opus 5.5 9815723844 style(v2): drop em-dashes from pinned media comments
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 12:54:40 -04:00
Georges-Antoine Assi c6ffa3ca57 Merge remote-tracking branch 'origin/master' into feat/pinned-media
# Conflicts:
#	backend/tests/test_migrations.py
2026-09-26 12:45:12 -04:00
Sam Dornan d1ce4e3db2 fix(sync): make deleted-save tracking exact, complete, and effective on filled slots (#4789) 2026-09-26 12:33:54 -04:00
Sam Dornan 6002b8fdaa chore(docker): verify downloads with ADD --checksum and pin RAHasher by commit (#4781) 2026-09-26 11:35:02 -04:00
Georges-Antoine AssiandClaude Opus 5.5 14e03181a6 fix: stop whole rom_user writes from reverting pinned media
The player last-played updates now send an empty payload, the v1 main
sibling toggle sends only its field, and the v1 personal tab leaves
pinned_media out of its debounced save.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 11:24:03 -04:00