Skip to content

fix(home): stop scanning the library filesystem on every home page load - #4069

Open
Spinnich wants to merge 1 commit into
rommapp:masterfrom
Spinnich:fix/home-setup-library-scan
Open

fix(home): stop scanning the library filesystem on every home page load#4069
Spinnich wants to merge 1 commit into
rommapp:masterfrom
Spinnich:fix/home-setup-library-scan

Conversation

@Spinnich

@Spinnich Spinnich commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #4063

The v2 home page called GET /api/setup/library on every page load, even on a
fully populated library. That endpoint walks the ROM library on disk (iterdir()
per platform directory) to build a first-run "here's what we see on disk" hint,
and the home page threw the result away because the library isn't empty.

Measured cost of the wasted walk:

  • 17.0s on an 83,000-game instance with the DB on SSD (16.8s on a second run, so
    the OS page cache doesn't rescue it)
  • 38.3s on a 23,698-game instance with a 5200 RPM HDD

Root cause. loadLibraryInfo() was driven by watch(isEmpty, ..., { immediate: true }).
An immediate watcher runs during setup(), which is strictly earlier than
onMounted, so at that moment no data request has even started: every store
fetching* flag is still false and every store array is still empty. isEmpty
therefore read true for any library, and the walk fired unconditionally.

Fixed on both sides of the stack, since either one alone leaves a sharp edge:

  1. Frontend. onMounted now collects its conditional fetches and awaits
    Promise.allSettled before flipping a new initialLoadDone flag. The
    watcher and the empty-state v-if both gate on
    showEmptyState = initialLoadDone && isEmpty, so the filesystem hint is
    requested only once the library is confirmed empty. allSettled rather
    than all so a rejected fetch still releases the gate.
  2. Backend. /setup/library returns early with existing_platforms: []
    when the database already holds at least one ROM, so any other caller (or an
    older frontend) can't trigger the walk either.

Note on scope of the user-visible symptom. This is a wasted-request fix, not
a visible-flash fix. I verified with a requestAnimationFrame frame sampler that
the empty-library hero rendered in 0 frames on a populated library both before
and after the change: onMounted sets the fetching flags before the first paint,
so only the setup-time watcher was ever wrong. Please don't expect a visual
difference on a populated library; the difference is one fewer multi-second
request per page load.

Design note for reviewers. The backend bail-out is gated on ROMs only,
deliberately not on "platforms and ROMs". A ROM row implies a platform row via
FK, and a database with platform rows but zero ROMs is precisely the case the
filesystem hint exists to serve, so that case must still walk. Both behaviours
are pinned by tests.

Files modified

File Change
frontend/src/v2/views/Home.vue onMounted awaits Promise.allSettled of the initial fetches, then sets initialLoadDone. New showEmptyState computed gates both the watcher and the empty-state v-if. immediate: true removed from the watcher.
frontend/src/v2/views/Home.test.ts New. Covers the populated-library case (no request, at setup time or after settle), the empty-library case (exactly one request), and the template gate.
backend/endpoints/heartbeat.py get_setup_library_info returns early with an empty existing_platforms when db_rom_handler.has_any_rom(). Placed after the existing scope check, and still returns detected_structure + supported_platforms.
backend/handler/database/roms_handler.py New has_any_rom() existence probe (SELECT id LIMIT 1, not COUNT(*)).
backend/tests/endpoints/test_heartbeat.py Two tests: the walk is skipped when ROMs exist (asserts fs_platform_handler.get_platforms is never called), and the walk still happens for platform rows with no ROMs.

Testing notes

  • Backend: uv run pytest → 2704 passed, 2 skipped.
  • Frontend: npm run test → 615 passed across 48 files. vue-tsc --noEmit clean.
  • trunk fmt && trunk check clean.
  • Browser-verified against a real dev stack in both light and dark themes:
    • Populated library: 0 /api/setup/library requests (pre-fix: 1, firing in
      the same millisecond as the other initial requests).
    • Empty library: 1 correctly-deferred request, with the "9 platforms
      detected" / "16 games detected" chips still rendering as before.
    • The template gate removes an intermediate paint for empty-library users
      rather than adding one (rows:5 → rows:4 → hero @377ms became
      rows:5 → hero @271ms).
  • No API contract change: the endpoint has no response_model and the response
    keys are unchanged, so no npm run generate is needed.
  • No new user-visible strings, so no locale changes.
  • No v1 files touched.

Reviewer attention

  • The ROMs-only gating described above is the one judgement call in this PR
    that the issue text did not specify. If you'd rather it also considered
    platform rows, that flips the behaviour of
    test_get_setup_library_info_walks_when_platforms_have_no_roms.
  • Setup wizard is unaffected, but it's the natural regression risk and worth
    a second pair of eyes: SHOW_SETUP_WIZARD is only true when there are zero
    admin users, at which point there are no ROMs, so the bail-out cannot trigger
    mid-wizard. The early return also preserves detected_structure and all 460
    supported_platforms entries the wizard reads.
  • Promise.allSettled semantics: a failed initial fetch still flips
    initialLoadDone, which is intentional. A user whose requests all failed will
    see the empty state and one filesystem-hint request, which is the same thing
    they'd have seen before.

Checklist

  • I've tested the changes locally
  • I've updated relevant comments
  • I've assigned reviewers for this PR
  • I've added unit tests that cover the changes

AI assistance disclosure

Per CONTRIBUTING.md: this change was written with AI assistance (Claude Code).
The AI wrote the tests and implementation, ran the test suites, and performed the
browser verification described above. I reviewed the diff, and the measurements in
the linked issue are my own from my instances.

The v2 home page requested GET /api/setup/library on every visit, which
walks every platform directory under the library path to count files.
That hint only exists for brand-new instances, but it fired for everyone:
17s per home page load on an 83k-game library, 38s on slower storage, and
the result was thrown away because the library wasn't empty.

The frontend watcher was wired to `isEmpty` with `immediate: true`, so it
ran during setup, before any of the page's data requests had started. All
the `fetching*` flags were still false and the stores were still empty, so
the library looked empty and the walk kicked off.

Frontend: onMounted now awaits the initial fetches and flips an
`initialLoadDone` flag. The watcher and the empty-state `v-if` both gate on
`initialLoadDone && isEmpty`, so the request and the render can no longer
disagree about whether the library is empty.

Backend: get_setup_library_info() returns early with an empty
`existing_platforms` once the database holds ROMs, since the on-disk hint is
meaningless then. Gated on ROMs alone, not platforms: platform rows with
zero ROMs are exactly the case the hint exists for, so those still walk.

Fixes rommapp#4063

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR prevents the v2 home page from requesting an expensive filesystem snapshot until its initial data requests have settled and the library is confirmed empty.

  • Adds an initialLoadDone gate around the home-page empty state and filesystem-hint request.
  • Adds an efficient ROM existence probe and skips filesystem enumeration when the database already contains ROMs.
  • Adds frontend and backend regression coverage for populated and empty library states.

Confidence Score: 5/5

The PR appears safe to merge with no concrete blocking or independently actionable non-blocking issues identified.

The frontend now waits for its launched initial requests before evaluating the empty state, while the backend preserves the setup response contract and avoids enumeration only after a valid ROM existence probe succeeds.

Important Files Changed

Filename Overview
frontend/src/v2/views/Home.vue Defers empty-state rendering and filesystem discovery until all conditionally started home-page requests settle; no actionable defect identified.
frontend/src/v2/views/Home.test.ts Covers setup-time ordering, populated-library suppression, empty-library discovery, and template gating.
backend/endpoints/heartbeat.py Preserves the endpoint response keys while short-circuiting costly filesystem enumeration after any ROM has been persisted.
backend/handler/database/roms_handler.py Adds a correct SELECT id LIMIT 1 existence probe using the repository’s established session-management pattern.
backend/tests/endpoints/test_heartbeat.py Verifies that ROM presence skips enumeration while platform-only database state continues to inspect the filesystem.

Reviews (1): Last reviewed commit: "fix(home): stop scanning the library fil..." | Re-trigger Greptile

@Spinnich
Spinnich requested a review from gantoine August 2, 2026 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Home page runs the first-time-setup filesystem scan on every load, even with a full library

1 participant