Commit Graph

1766 Commits

Author SHA1 Message Date
zurdi
7d45795408 fix(permissions): address gantoine review + Postgres-safe single migration
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>
2026-06-26 22:10:00 +00:00
zurdi
5303130d15 Merge remote-tracking branch 'origin/master' into chore/permissions-system-rework 2026-06-26 15:35:41 +00:00
zurdi
4e686e1d74 fix(permissions): address PR review (Copilot + adversarial) findings
Resolves the CI blocker and a cluster of opt-out visibility "fail-open"
gaps surfaced in review of the granular permission system.

Security / correctness:
- admin oauth_scopes projection keeps canonical FULL_SCOPES order
  (order_scopes) instead of sorting alphabetically, fixing the red
  test_user.py::test_admin on MariaDB + Postgres.
- default-group hides no longer fail open: the resolver resolves the
  effective (own-or-default) group before the hidden-entity lookup.
- /roms/by-hash and /roms/by-metadata-provider now 404-mask hidden roms.
- USERS-entity grant no longer enables admin creation: add_user and
  invite-link require a real admin to mint admin accounts.

Visibility leaks closed on secondary read paths:
- feeds, sibling roms (list query + single-rom schemas), /stats counts
  and per-platform breakdowns, collection rom_ids/rom_count, search_rom.

Hardening / cleanups:
- firmware/platform PUT 404-mask hidden entities; group rename conflict
  returns 400 not 500; guard against removing the last default group;
  kiosk read-only enforced at the fine layer; add_hidden_entity rejects
  non-cascading entity types.

Frontend:
- permissionGroups.ensureLoaded coalesces concurrent callers on one
  in-flight request; permissions.setGrants resets isAdmin/hidden;
  CreateUserDialog no longer orphans a user when group assignment fails;
  HiddenGamesPicker search rows are native buttons (keyboard/gamepad);
  invite-role labels and group swatch aria-label use i18n; drop dead code
  (originalRole, unused permissionsApi export).

AI assistance: changes authored with Claude Code (Claude Opus), driven by
the Copilot review and a multi-agent adversarial review, then verified
(backend pytest, frontend typecheck/vitest, i18n parity, trunk).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 12:37:58 +00:00
Georges-Antoine Assi
0af24b3fab Merge pull request #3607 from rommapp/claude/github-issue-3599-ye41l1
fix(scan): make scan tracker totals reflect platforms actually scanned
2026-06-25 20:40:22 -04:00
Georges-Antoine Assi
8405e574be fix(scan): reuse fetched platforms for NEW_PLATFORMS totals
Compute the "new platforms" totals from the platforms already loaded via
get_platforms() instead of issuing one get_platform_by_fs_slug query per
platform. Fix the test fixture to report the existing platform through
get_platforms() so the mocked data matches the code path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:59:52 -04:00
Georges-Antoine Assi
f818586b7d fix(scan): use get_platform_by_fs_slug for NEW_PLATFORMS totals
Compute the "new platforms" totals from the same existence check used
per-platform in _identify_platform, so the tracker totals match what is
actually scanned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:58:13 -04:00
Georges-Antoine Assi
009725e585 cleanup 2026-06-25 19:50:23 -04:00
Georges-Antoine Assi
ba6d0ef9db cleanup 2026-06-25 19:17:26 -04:00
Claude
e4660aca4c fix(scan): prevent duplicate ROM entries from racing scans
The patcher uploads the patched ROM and then fires a platform scan to
register it. When a second scan runs against the same platform around the
same time (a filesystem-watcher rescan, a scheduled rescan, or another
manual scan on a multi-worker setup), both scans could see the new file as
absent from the DB and each insert it, producing two identical library
entries for one patched file.

A platform folder can't physically hold two entries with the same name, so
a ROM is uniquely identified by (platform_id, fs_name). Enforce that with a
unique index instead of the previous plain index, which makes the duplicate
impossible. The scan's early ROM insert now adopts the row created by a
concurrent scan (catching the integrity error and skipping) instead of
failing, and ROM rename pre-checks for a name collision so it returns a
clean 409 rather than hitting the constraint.

Includes a migration that removes any pre-existing duplicates (keeping the
lowest id; dependents cascade) before upgrading the index to unique.

Fixes #3590

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0135UV8Xn2XHkRhjzhm9UptP
2026-06-25 23:01:37 +00:00
Claude
fb6f6affad fix(scan): make scan tracker totals reflect platforms actually scanned
The scan tracker computed total_platforms and total_roms over every
filesystem platform, ignoring both the selected platforms and the scan
type. For a "new platforms" scan, existing platforms are skipped inside
_identify_platform, so their ROMs never count toward scanned_roms, yet
they were all included in total_roms. This made the tracker wildly
overcount (the whole library instead of just the new platform).

Resolve the platform list before computing totals and, for NEW_PLATFORMS
scans, exclude platforms that already exist in the database so the totals
match what is actually processed.

Fixes #3599

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158RJnc7MmAbwRz6Qiyrj5a
2026-06-25 22:53:46 +00:00
zurdi
bf801cb314 feat(permissions): narrow user role to admin/user and adapt v1
Collapse the Role enum from viewer/editor/admin to two kinds (admin/user);
non-admin access now comes entirely from permission groups + overrides.

Backend:
- Role -> StrEnum {USER, ADMIN}, VARCHAR-backed (native_enum=False); Role.coerce
  maps legacy/unknown strings (incl. in-flight invites) to USER.
- Resolver: group-less users fall back to the default group (dropped the
  role-based legacy fallback); kiosk caps all non-admins to read-only.
- OIDC editor/viewer claims both map to USER (env var names unchanged).
- Migration 0092 converts the native enum to VARCHAR and normalizes
  ADMIN->admin, VIEWER/EDITOR->user.
- Updated endpoints, conftest fixtures, role/oidc/parity/db-handler tests, tools.

Frontend:
- v2 + v1 user management now use admin/user (dropdowns, defaults, getRoleIcon).
- Regenerated types (Role = 'user' | 'admin'); added role-user i18n to all locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 22:42:51 +00:00
zurdi
de3f35df6e feat: added foundational work for the new permissions system 2026-06-25 21:49:00 +00:00
zurdi
8d75474890 Merge remote-tracking branch 'origin/master' into chore/permissions-system-rework 2026-06-25 13:44:38 +00:00
Georges-Antoine Assi
0158097389 fix(platform): accept embedded custom_name body on platform update
Removing the aspect_ratio body field left update_platform with a single
scalar Body() param. FastAPI stops embedding a lone scalar body, so the
endpoint began expecting a bare JSON string while the frontend keeps
sending {"custom_name": "..."}, producing a 422 when editing a
platform's display name in v2.

Restore the embedded-key contract with Body(embed=True), matching the
frontend payload and every sibling update endpoint. Regenerate the
frontend types (restores the Body_update_platform model) and add an
endpoint regression test.

AI assistance: written with Claude Code (Opus 4.8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:21:10 -04:00
Georges-Antoine Assi
6e3ef32815 fix(roms): gate sidecar caching on all active filters
The char index and rom id index sidecars are cached under a key that
encodes only user/order/grouping. is_unscoped previously excluded only
scope and search, so metadata/tag/status filters and the bool flags
applied to the query bypassed the gate: a filtered all-games request
stored a narrowed id list under the shared "all" key and later
unfiltered (or differently-filtered) requests read it back, showing the
wrong set and count of games.

Treat any narrowing parameter as scoped so those sets compute live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:10:58 -04:00
Georges-Antoine Assi
bd22a46eda refactor(roms): extract unscoped sidecar cache key into a helper
Deduplicate the identical cache key expression used by the char index,
filter values, and rom id index sidecars so the key scheme stays
consistent across them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:05:05 -04:00
Georges-Antoine Assi
67f3474d3f perf(roms): speed up the gallery/search list endpoint on large libraries
The /api/roms list endpoint did several O(library) computations on every
request. On a 100k-rom library each request took 4-5s. This addresses the
dominant costs, all measured on a real 100k-rom MariaDB.

- Cache rom_id_index: the full ordered id list backing virtual scroll was
  recomputed (the sibling-dedup window over the whole library) on every
  request, even limit=1, and shipped uncached. Memoise the unscoped scan
  under the same versioned cache as the other sidecars. 2815ms -> 7ms on hit.

- Slim the sibling-dedup query: the inner derived table materialized all of
  Rom (including JSON metadata blobs) for 100k rows, and carried a wide unused
  fs_name_no_ext through the window's temp table (spilling the sort to disk),
  plus a pointless inner ORDER BY. Select only the columns the window needs.
  2.79s -> 0.86s, identical results, no schema change.

- Rewrite with_char_index: replace row_number() over the whole library (full
  materialization + double filesort) with a per-letter COUNT and an
  accumulate. Identical output, drops a filesort layer.

- Add idx_roms_sibling_cover covering index for the sibling_roms view
  self-join, so the 7-way metadata-id OR resolves from the index instead of
  reading wide rows per parent. ~8x on dense pages warm, far more cold.

AI assistance: written with Claude Code (diagnosis, query rewrites, migration,
tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:29:09 -04:00
zurdi
649ddce2aa feat: Implement fine-grained permission checks and admin CRUD for permission groups
- Added new permission handling in `backend/handler/auth/dependencies.py` to support fine-grained, DB-backed permission checks.
- Enhanced user role update logic in `backend/endpoints/user.py` to prevent demotion of the last admin.
- Introduced `hidden_platform_ids` and `hidden_rom_ids` parameters in various database handlers to manage visibility based on admin settings.
- Created new endpoints for managing permission groups, user memberships, and hidden entities in `backend/tests/endpoints/test_permissions_admin.py`.
- Added tests for permissions visibility and CRUD operations in `backend/tests/endpoints/test_permissions_visibility.py` and `backend/tests/endpoints/test_permissions_me.py`.
- Updated archive handling in `backend/utils/archives.py` to improve error logging and timeout management during extraction.
2026-06-24 08:45:45 +00:00
Georges-Antoine Assi
c8055ac973 Address self-review on patcher PR
- 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>
2026-06-23 21:07:27 -04:00
Georges-Antoine Assi
a3176494c9 Patcher backend refinements, dep cleanup, regenerated types
- 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>
2026-06-23 09:17:52 -04:00
Georges-Antoine Assi
52163633e5 Merge branch 'master' into claude/server-side-rom-patching-EaQck 2026-06-23 07:01:07 -04:00
Georges-Antoine Assi
af4b057894 feat(v2): screenshot-forward covers + cover-art PIP for continue-playing & activity
Show a "where you left off" screenshot on the Home continue-playing rail and
the live-activity board, with a small cover-art thumbnail (PIP) in the corner
so the game stays identifiable. Both render at the image's natural aspect.

Backend:
- New shared util `continue_playing_screenshot(rom, latest_save)` resolving the
  image in priority order: latest save's screenshot, then title screen, then
  first gameplay screenshot (None → frontend falls back to cover art).
- `SimpleRomSchema.screenshot_path` populated only on the `last_played` query;
  `get_latest_saves_for_roms` batch handler (+ tests).
- ActivityEntry / ActivityEntrySchema gain `screenshot_path`, computed from the
  session player's latest save in both the socket and REST heartbeat paths.

Frontend:
- New shared `CoverArtPip.vue` (bottom-right 2D cover thumbnail), reused by
  GameCard and ActivityCard.
- Home continue-playing rail uses `screenshot_path` + PIP, natural aspect (no
  forced hero/style).
- Activity board: screenshot-forward cover + PIP, and a wrapping flex layout so
  cards share a uniform height with natural-ratio widths (gallery-card
  behavior).
- GameCover only keys the measured ratio by rom id for the rom's own cover, so
  a `coverSrc` override (screenshot) never pollutes the gallery's ratio cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:40:56 -04:00
Georges-Antoine Assi
662dc89a24 Merge branch 'master' into aspect-ratio-no-no 2026-06-21 19:20:09 -04:00
Georges-Antoine Assi
1f98b2df14 run fmt 2026-06-21 17:47:06 -04:00
Georges-Antoine Assi
4724361f6d refactor: remove per-platform cover aspect-ratio setting
The platform aspect_ratio setting is dropped from the UI and the API
(platform update body + response schema) — nothing consumed it for
rendering, and covers now size to their image's natural aspect.

- SettingsTab: remove the cover-style / aspect-ratio picker (and its
  now-dead helpers, CSS, and unused imports); collapse to a single column.
- update_platform: drop the `aspect_ratio` body field; PlatformSchema no
  longer returns it; utils/platforms stops seeding the default.
- Regenerate the affected frontend types (PlatformSchema, update body).

The DB column stays (out of the update/response scope; dropping it would
be a separate destructive migration) but is no longer read or written
through the API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:43:40 -04:00
Georges-Antoine Assi
389025942f changes from bot review 2026-06-21 17:42:25 -04:00
Georges-Antoine Assi
77970f6379 Merge branch 'master' into feature/device-flow-authorization 2026-06-21 16:25:39 -04:00
Zurdi
ee33bf1b71 Merge pull request #3569 from rommapp/feat/share-savestates
feat: share saves & states with other users
2026-06-21 21:18:34 +02:00
Georges-Antoine Assi
0460133992 Secure activity identity, cut Redis churn, remove v1 activity
Backend:
- Resolve the acting user from the authenticated socket session on
  connect instead of trusting the client-supplied user_id, so a client
  can no longer spoof a "now playing" session for another user. Only
  rom_id/device_id come from the payload.
- Emit activity:update/clear through the already-initialised socket
  server instead of opening (and leaking) a fresh AsyncRedisManager per
  REST heartbeat.
- Collapse get_all_active's per-key GET into a single MGET.
- Drop the pure pass-through _build_activity_entry helper.

Frontend:
- Remove all activity emits from the v1 EmulatorJS Player; the v2 shell
  is the single driver of the activity lifecycle.
- Remove activity from the v1 UI entirely (Activity view, ActivityBtn,
  ActivePlayers on game details, navigation, and the now-v2-only route).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:12:18 -04:00
zurdi
9106c3721e feat: add visibility toggle for saves and states, enhance community sections
- Introduced new API endpoints for updating visibility of saves and states.
- Added `is_public` property to `SaveSchema` and `StateSchema`.
- Created new models for user saves and states with visibility attributes.
- Updated the `SaveDataTab` component to differentiate between "Mine" and "Community" sections.
- Implemented visibility toggle functionality for user saves and states.
- Enhanced localization files to include new strings for visibility actions.
2026-06-21 14:00:02 +00:00
nendo
812491f68c Refine device authorization flow
- device/init returns a relative verification_path; the client joins it
  with its own origin
- Render the v2 approval screen via the named v2 router outlet (was blank)
- DevicePair: RSpinner, keyboard-accessible scope chips, scrollable scopes;
  DevicePairShell uses always-light overlay tokens (no hex)
2026-06-21 10:57:48 +09:00
Georges-Antoine Assi
429e718a14 add ui for v2 2026-06-20 21:09:11 -04:00
Georges-Antoine Assi
4b091bfb34 Resolve activity device_type from the device record
Instead of always inferring "web" for browser-emitted activity events, look
the device up by device_id and use its client type, falling back to "web"
(the browser default) when no device record exists. Mirrors the REST
heartbeat endpoint, which derives device_type from device.client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:56:03 -04:00
Georges-Antoine Assi
892a1b18e1 Merge branch 'master' into claude/user-game-activity-monitoring-Mqb1v 2026-06-20 13:42:11 -04:00
copilot-swe-agent[bot]
bd0bc65e92 fix: allow ROM deletion when file is missing
Co-authored-by: zurdi15 <34356590+zurdi15@users.noreply.github.com>
2026-06-20 16:23:24 +00:00
zurdi
78cac11913 feat: add DISABLE_LOGS_VIEWER configuration and implement checks in logs endpoints and frontend 2026-06-20 15:48:58 +00:00
zurdi
78eef900b9 feat: update logs endpoint to require 'logs.read' scope and adjust related authorization checks 2026-06-20 09:55:10 +00:00
Georges-Antoine Assi
0e016eca9b Merge branch 'master' into claude/user-game-activity-monitoring-Mqb1v 2026-06-19 21:11:05 -04:00
zurdi
8b70e31828 refactor: address Copilot review on logs feature
- main.py: await the cancelled log-forwarder task (suppressing
  CancelledError) so its pubsub/lock cleanup finishes before shutdown.
- forwarder: only heartbeat the Redis lock while we still own it; if a
  stall let another worker take it, relinquish forwarding to avoid
  duplicate lines (the outer loop re-contends).
- endpoints/logs.py: derive MAX_LOG_LIMIT from LOG_BUFFER_SIZE so the
  REST backfill never drifts from the producer's ring buffer.
- Logs.vue: append the download <a> to the DOM before click() (matches
  the Patcher pattern) for cross-browser reliability.
- Add tests/endpoints/test_logs.py: non-admin 403, limit clamping to
  [1, MAX_LOG_LIMIT], oldest-first ordering, and malformed-entry skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:53:58 +00:00
zurdi
02c6fc8d3f fix: suppress bandit B110/B112 on intentional log-stream swallows
The log-stream handler and forwarder deliberately swallow exceptions: the
handler is a best-effort mirror that must never raise into the app, and the
forwarder can't log its own failures without feeding back into the stream.
Annotate these with `# nosec` (with justification) so Trunk's bandit check
passes, keeping lines within black's width.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:17:45 +00:00
zurdi
ad543d93ee feat: add logs feature with internationalization support
- Introduced a new logs view for admin users, allowing real-time monitoring of backend logs.
- Implemented a log entry streaming mechanism using Socket.IO.
- Added filtering and searching capabilities for log entries.
- Created localized log messages in Spanish, French, Hungarian, Italian, Japanese, Korean, Polish, Portuguese, Romanian, Russian, Simplified Chinese, and Traditional Chinese.
- Updated router and sidebar components to include the new logs route.
- Enhanced user interface with tooltips and buttons for copying and downloading logs.
2026-06-19 16:06:50 +00:00
zurdi
54b403a38e Merge remote-tracking branch 'origin/master' into feat/add-user-avatar-to-notes 2026-06-19 07:48:36 +00:00
zurdi
d5c985f634 Merge remote-tracking branch 'origin/master' into feat/screenshot-CRUD-endpoints 2026-06-19 07:35:07 +00:00
zurdi
5714ba3cec refactor: move screenshot file validation logic to utils and clean up endpoints 2026-06-19 07:30:07 +00:00
Georges-Antoine Assi
8bf7d18afe cleanup 2026-06-18 11:48:51 -04:00
Georges-Antoine Assi
a38ebe29b5 Preserve custom name_sort_key; gate derivation on "still derived"
Drop the name_sort_key_custom flag/migration in favour of a flagless rule: a
key is "custom" when it no longer equals compute(name). Apply that consistently
across all three write paths so a manual sort key survives renames while a
derived key keeps following the name:

- @validates re-derives on name assignment only when the stored key still
  matches the derived value; direct name_sort_key assignment stores a
  normalized custom key (or reverts to derived when cleared). Handles both
  kwarg orders at construction.
- update_rom mirrors the same check for the bulk update() path it bypasses.
- The edit endpoint only writes the key when the user actually changed the
  field, delegating the untouched case to update_rom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:30:40 -04:00
Georges-Antoine Assi
cd19d723fa Merge sort_name into name_sort_key with custom-override flag
Collapse the separate `sort_name` column into `name_sort_key`, which is now
the single user-settable sort field: always normalized and indexed for fast
ordering, derived from `name` by default, and overridable. A new
`name_sort_key_custom` boolean marks user/metadata overrides so they survive
renames and rescans.

- Drop the `roms.sort_name` column; repurpose migration 0085 to add
  `name_sort_key_custom`.
- Derive the key via `@validates("name")` unless pinned custom; the edit
  dialog, unmatch flow, and ES-DE gamelist <sortname> set custom keys.
- update_rom / scan_rom keep the columns in sync explicitly (bulk update and
  construction bypass / reorder the validator).
- Frontend: edit field drives name_sort_key (empty when auto), api sends the
  override only when custom, regenerated types updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:34:21 -04:00
zurdi
d4a6d97acb feat: add user avatar and updated timestamp to community screenshots 2026-06-18 14:20:54 +00:00
zurdi
c0b44cd2ef Merge branch 'feat/screenshot-CRUD-endpoints' into feat/add-user-avatar-to-notes 2026-06-18 14:17:32 +00:00
Georges-Antoine Assi
0d8a0a2ab2 Merge branch 'master' into copilot/support-sortname-tag-es-de 2026-06-18 09:04:17 -04:00