Skip to content

Per-platform metadata coverage and region breakdown to server stats#3110

Merged
gantoine merged 12 commits intorommapp:masterfrom
cciollaro:feat/stats-metadata-coverage-regions
Mar 11, 2026
Merged

Per-platform metadata coverage and region breakdown to server stats#3110
gantoine merged 12 commits intorommapp:masterfrom
cciollaro:feat/stats-metadata-coverage-regions

Conversation

@cciollaro
Copy link
Copy Markdown

@cciollaro cciollaro commented Mar 10, 2026

Summary

Spent some time working on the stats page. Adds two interesting metrics that didn't take any heavy lifting on the backend. And did some additional visual design for the frontend.

  • Adds two new per-platform statistics to the server stats page: metadata coverage (% of ROMs matched per source) and region breakdown (ROM counts per region with flag emojis)
  • Backend: two new efficient queries — single GROUP BY for metadata sources, Python-side aggregation for regions (DB-dialect agnostic)
  • Frontend: redesigns platform cards with a tabular detail layout, size bar visualization, expandable region chips, and metadata source ordering based on user's SCAN_METADATA_PRIORITY config

Before / After

Screenshot 2026-03-10 at 7 51 44 PM Screenshot 2026-03-10 at 7 51 40 PM

Test plan

  • Verify stats page loads with metadata coverage and region breakdown for each platform
  • Confirm metadata sources are ordered by user's configured SCAN_METADATA_PRIORITY
  • Confirm region chips cap at 5 with expandable "+N" overflow
  • Verify empty states (em dash) render for platforms with no metadata/region data
  • Test sort by name/size/count still works
  • Verify size bar at bottom of each card reflects platform's percentage of total

This PR was developed with AI assistance (Claude Code) per CONTRIBUTING.md disclosure requirements.

🤖 Generated with Claude Code

cc and others added 2 commits March 9, 2026 22:15
…er stats

Enhances the server stats page with two new per-platform statistics:
- Metadata coverage: shows which sources matched ROMs (ordered by user's scan priority config)
- Region breakdown: shows ROM counts per region with flag emojis

Backend adds two new efficient queries (single GROUP BY for metadata, Python-side aggregation for regions).
Frontend redesigns platform cards with a tabular detail layout, size bar visualization, and expandable region chips.

> This PR was developed with AI assistance (Claude Code) per CONTRIBUTING.md disclosure requirements.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@gantoine gantoine self-requested a review March 10, 2026 02:39
@gantoine gantoine changed the title feat: add per-platform metadata coverage and region breakdown to server stats Per-platform metadata coverage and region breakdown to server stats Mar 10, 2026
@cciollaro cciollaro marked this pull request as ready for review March 10, 2026 22:14
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR adds per-platform metadata coverage and region breakdown statistics to the server stats page, alongside a visual redesign of the platform cards (tabular detail layout, size bar, expandable region chips). The backend adds two efficient queries — a single GROUP BY for metadata counts and Python-side region aggregation — and the frontend consumes them via new props passed through the stats view.

Key changes and observations:

  • Backend: _METADATA_SOURCE_COLUMNS is correctly placed at module scope. get_metadata_coverage_by_platform uses a single efficient GROUP BY query. get_region_breakdown_by_platform does Python-side aggregation (acknowledged tradeoff for DB-dialect agnosticism, flagged in previous review threads).
  • Python type annotation mismatch: StatsReturn declares dict[int, ...] for the new fields, but JSON always serializes object keys as strings; the TypeScript side correctly uses Record<string, ...>. This is a minor annotation misleadingness already raised in prior review threads.
  • Frontend — store mutation fix: The prior allPlatforms.value.sort() mutation bug is now correctly fixed with [...allPlatforms.value].sort(...) spread copies.
  • orderedCoverageByPlatform computed: The double-call pattern previously flagged for metadata coverage has been resolved; however, getVisibleRegions(platform.id) is still called twice per platform in the template (once for v-if, once for v-for), representing the same pattern for regions.
  • i18n: The "Games" row label uses t("setup.games") — a key from the setup wizard namespace — rather than a dedicated common.games entry. CSS uppercasing masks the mismatch today, but the borrowing is fragile.
  • Locale files: platforms-size is correctly removed from all 15 locale files. No new translation keys were added, as the new labels reuse existing keys from rom, platform, and setup namespaces.

Confidence Score: 4/5

  • This PR is safe to merge; the changes are additive, well-scoped, and the most significant previously flagged issues have been addressed.
  • The backend queries are efficient and correct. The frontend fixes the prior store-mutation and double-sort bugs. Remaining issues are minor style/performance concerns (double getVisibleRegions call, borrowed i18n namespace) that do not affect correctness or user-facing functionality.
  • frontend/src/components/Settings/ServerStats/PlatformsStats.vue — the getVisibleRegions double call and the setup.games namespace borrowing are both minor but worth a quick look before merge.

Important Files Changed

Filename Overview
backend/handler/database/stats_handler.py Adds two new query methods: get_metadata_coverage_by_platform (single efficient GROUP BY) and get_region_breakdown_by_platform (Python-side aggregation). The _METADATA_SOURCE_COLUMNS dict is correctly moved to module scope. Logic is sound; the region aggregation is O(n ROMs) in memory but acknowledged as a deliberate tradeoff.
backend/endpoints/responses/stats.py Adds MetadataCoverageItem and RegionBreakdownItem TypedDicts and extends StatsReturn. Python annotation uses dict[int, ...] but JSON serializes to string keys; TypeScript side correctly uses Record&lt;string, ...&gt; to match the wire format.
frontend/src/components/Settings/ServerStats/PlatformsStats.vue Major redesign of platform cards with metadata coverage chips, region breakdown chips, and size bar. Previous store-mutation bug in sortedPlatforms is fixed with spread copies. orderedCoverageByPlatform computed avoids double-sort. However, getVisibleRegions is still called twice per platform in the template, and the "Games" label borrows from the setup wizard i18n namespace.
frontend/src/views/Settings/ServerStats.vue Passes new metadataCoverage and regionBreakdown props down to PlatformsStats. Clean and minimal change; initial empty-object defaults are appropriate.
frontend/src/generated/models/StatsReturn.ts Extends the generated StatsReturn type with METADATA_COVERAGE and REGION_BREAKDOWN using Record&lt;string, Array&lt;...&gt;&gt;, correctly reflecting JSON's string-keyed object serialization.
backend/endpoints/stats.py Wires the two new handler methods into the stats endpoint response. Minimal and correct change.
frontend/src/locales/en_US/common.json Removes the now-unused platforms-size key. The new labels (rom.metadata, platform.region, setup.games) are borrowed from existing namespaces rather than being added here; no new keys were added to common.json.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant ServerStats.vue
    participant PlatformsStats.vue
    participant StatsAPI as GET /stats
    participant DBStatsHandler
    participant DB as Database (Rom table)

    Browser->>ServerStats.vue: onBeforeMount
    ServerStats.vue->>StatsAPI: api.get("/stats")
    StatsAPI->>DBStatsHandler: get_metadata_coverage_by_platform()
    DBStatsHandler->>DB: SELECT platform_id, COUNT(igdb_id), COUNT(ss_id), ... GROUP BY platform_id
    DB-->>DBStatsHandler: rows[]
    DBStatsHandler-->>StatsAPI: dict[int, list[MetadataCoverageItem]]

    StatsAPI->>DBStatsHandler: get_region_breakdown_by_platform()
    DBStatsHandler->>DB: SELECT platform_id, regions WHERE regions IS NOT NULL
    DB-->>DBStatsHandler: rows[]
    Note over DBStatsHandler: Python-side aggregation per region per platform
    DBStatsHandler-->>StatsAPI: dict[int, list[RegionBreakdownItem]]

    StatsAPI-->>ServerStats.vue: StatsReturn (JSON, int keys serialized as strings)
    ServerStats.vue->>PlatformsStats.vue: :metadata-coverage="stats.METADATA_COVERAGE"
    ServerStats.vue->>PlatformsStats.vue: :region-breakdown="stats.REGION_BREAKDOWN"
    Note over PlatformsStats.vue: orderedCoverageByPlatform computed<br/>sorts by SCAN_METADATA_PRIORITY
    PlatformsStats.vue-->>Browser: Render platform cards with coverage chips + region chips
Loading

Comments Outside Diff (2)

  1. frontend/src/components/Settings/ServerStats/PlatformsStats.vue, line 246-250 (link)

    getVisibleRegions called twice per platform per render

    getVisibleRegions(platform.id) is invoked once for the v-if guard (line 246) and again for the v-for source (line 250). Each call performs a String(platformId) lookup, a Set.has() check, and a potential slice(). This is the same double-computation pattern as the now-fixed getOrderedCoverage double-call.

    Consider building a computed map (keyed by string platform ID) for visible regions, similar to how orderedCoverageByPlatform was refactored:

    const visibleRegionsByPlatform = computed(() => {
      const result: Record<string, RegionBreakdownItem[]> = {};
      for (const [id, items] of Object.entries(props.regionBreakdown)) {
        const platformId = Number(id);
        result[id] = expandedRegions.value.has(platformId)
          ? items
          : items.slice(0, MAX_VISIBLE_REGIONS);
      }
      return result;
    });

    Then replace the two template calls with visibleRegionsByPlatform[String(platform.id)].

  2. frontend/src/components/Settings/ServerStats/PlatformsStats.vue, line 192 (link)

    Translation key borrowed from unrelated namespace

    The label for the ROM count row uses a key from the setup wizard namespace whose resolved value is the lowercase word "games". The CSS uppercase class makes it render visually as "GAMES" for now, but this borrows from an unrelated i18n context. If that key's value is ever updated to be more setup-wizard-specific, this stats-page label would silently change with it.

    Consider adding a dedicated common.games entry alongside the other new keys in this PR, or using the already-present common.games-n pluralization key with a fixed count to render just "Games".

Last reviewed commit: f8a9740

cc and others added 2 commits March 10, 2026 19:27
- Derive metadata source columns from Rom model instead of hardcoded list
- Replace getOrderedCoverage() function calls with a computed map to avoid
  redundant sorting on each render

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Fallback locale (en_US) covers the two new keys for other languages.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@cciollaro
Copy link
Copy Markdown
Author

i18n should be fixed 🤞

cc and others added 2 commits March 10, 2026 19:39
Reuse rom.metadata and platform.region instead of adding new keys.
Remove orphaned platforms-size key from all 16 locale files.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
JSON serializes object keys as strings, so explicitly convert platform
IDs to strings when accessing metadataCoverage and regionBreakdown maps.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add rom count as detail table row aligned with metadata/region chips
- Use standard chip style for fs_slug matching PlatformListItem
- Size and percentage on two lines for readability
- Adjust platform icon vertical alignment

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Use i18n key for "games" label instead of hardcoded "Roms"
- Remove min-width on size bar fill so zero-size platforms show no bar
- Align Python TypedDict keys to str to match JSON wire format

Co-Authored-By: Claude Opus 4.6 <[email protected]>
cc and others added 2 commits March 10, 2026 20:09
- Spread allPlatforms before sorting to avoid mutating Pinia store
- Move _METADATA_SOURCE_COLUMNS to module level
- Add optional chain on sourceInfo v-img src

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Pydantic enforces response types before JSON serialization, so the
Python dict must use int keys to match the actual data. JSON handles
the int-to-string conversion automatically.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@gantoine gantoine merged commit 9b95e85 into rommapp:master Mar 11, 2026
6 checks passed
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.

2 participants