feat(stats): link stats rows and counts to filtered lists (#65)

Wire the stats page to filter_url()/stats_links:
- Per-row session links on game superlatives, games-by-playtime, platform
  and month rows (game rows keep their detail GameLink, add a session icon).
- Count links: sessions, games, total/refunded/dropped/unfinished/backlog
  purchases.
- Cap preview lists to 5 with a 'View all (N)' link passing ?sort= for order
  parity; remove the redundant 'All Purchases' list.
- stats_data: carry platform_id for platform links; drop the all-time
  games-by-playtime [:10] slice so the view-all count is honest (rendering
  caps the preview).

Also make the filter bar's _extract_labeled tolerate bare choice/multi values
so a programmatically-built filter URL renders instead of crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RF5L4HtbcykTfY9YUYGds3
This commit is contained in:
2026-06-21 15:26:33 +02:00
parent 57980c407f
commit 90c6113772
5 changed files with 323 additions and 34 deletions
+13 -3
View File
@@ -65,9 +65,19 @@ def _filter_parse(filter_json: str) -> dict:
return {}
def _extract_labeled(items: list[dict]) -> list[LabeledOption]:
"""Convert a list of ``{id, label}`` dicts to ``(value, label)`` pairs."""
return [(str(item["id"]), str(item["label"])) for item in items]
def _extract_labeled(items: list) -> list[LabeledOption]:
"""Convert filter values to ``(value, label)`` pairs.
UI-built filters carry ``{id, label}`` dicts; programmatically-built ones
(e.g. stats_links) carry bare ids/choices. A bare value uses itself as its
own label so the bar renders any valid filter instead of crashing."""
pairs: list[LabeledOption] = []
for item in items:
if isinstance(item, dict):
pairs.append((str(item["id"]), str(item["label"])))
else:
pairs.append((str(item), str(item)))
return pairs
def _filter_get_choice(existing: dict, field: str) -> FilterChoice: