Encounter stats page + /api/runs/encounter-stats endpoint (#335)#336
Merged
Conversation
…ter-stats Adds a paginated per-encounter view of submitted runs. Each row carries total appearances, fatal count, average damage taken, and average turn count, with an expandable per-character breakdown. Backend: - New aggregation get_encounter_stats() in runs_db_mongo.py walks each run's map_point_history, unwinds acts + rooms, groups by encounter id + character. Fatal detection uses the run's killed_by field (a reasonable approximation; PR #266's extractor handles the same-enemy-twice edge case more precisely). - New endpoint GET /api/runs/encounter-stats with query params: act (comma-separated), room_type (comma-separated), multiplayer (only/exclude), page, limit (max 200, default 50). - Currently live-aggregates per request. Worth materializing into stats_summary if traffic becomes meaningful — left for a follow-up. Frontend: - New page at /leaderboards/encounters with localized mirror at /[lang]/leaderboards/encounters. - Filters: Act (1/2/3 multi-select), Type (monster/elite/boss multi-select), Players (any / solo only / multiplayer only). - 50/page with prev/next pagination + total count. - Click a row to expand the per-character sub-table. - Encounter names resolved via /api/encounters with snake-case fallback for unknown ids. Nav: - 'Encounters' link added to the Stats dropdown between Stats and Scoring. Docs: - contributing/API_REFERENCE.md gets the new endpoint.
ptrlrd
added a commit
that referenced
this pull request
May 22, 2026
… script The /api/runs/encounter-stats endpoint shipped in #336 returned zero rows because the aggregation walks $map_point_history but that field was never stored on the run doc — only denormalized fields (killed_by, deck, relics, card_choices) were. The raw history lives on disk at data/runs/<hash>.json (which the share-run page already reads). Two changes: 1. submit_run now includes map_point_history in the inserted doc. Going forward, every new submission populates the field so the aggregation has data to walk. 2. tools/backfill_run_encounters_mongo.py reads every disk JSON whose corresponding Mongo doc is missing the field and $sets it. Idempotent — safe to re-run, processes ~5K docs/sec. Operator run after deploy: ssh prod 'cd /var/www/spire-codex && docker compose -f docker-compose.prod.yml exec backend python3 -m tools.backfill_run_encounters_mongo' (Beta box runs the same script against the beta compose file.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #335.
New paginated per-encounter view of submitted runs at
/leaderboards/encounters, with public API at/api/runs/encounter-statsfor third-party tools.Filters
Columns
Top-level row: encounter name (linked to its detail page) · Act · room type · Runs (sample size) · Fatal count + % · Avg damage taken · Avg turns. Click any row to expand a per-character sub-table with the same columns scoped per character.
Pagination: 50 per page with prev/next + total count.
API
Returns
{encounters: [{encounter_id, act, room_type, total, fatal, avg_damage, avg_turns, characters: [{character, total, fatal, avg_damage, avg_turns}]}], page, limit, total, has_next}.Backend
get_encounter_stats()inruns_db_mongo.pywalks each run'smap_point_history(acts → rooms), unwinds twice, groups by (encounter_id, act, room_type, character) for the per-character breakdown, then collapses up so the page-limit applies to encounters rather than (encounter, character) pairs. Currently live-aggregates per request — likely OK for now, but worth materializing intostats_summary(alongside the global stats refresher) if traffic warrants. Left as a follow-up so this can ship.Fatal detection: a room counts as fatal when its
model_idmatches the run'skilled_byandwinis falsy. PR #266's extractor refines this to the last matching room — the simpler heuristic here misattributes for runs that hit the same enemy type twice and died on the second instance (rare).Nav + docs
contributing/API_REFERENCE.mdupdated with the new endpointOpen follow-ups (not in this PR)
stats_summaryso the response is sub-msrun_encounter_monstersjoin data from Add run_encounters tables + backfill (roadmap #3 phase 1) #266 once that lands