Patch/6 instance runs fixes - #1065
Open
Emissaryx wants to merge 97 commits into
Open
Conversation
… stats Share view links now bake in an absolute start/end timestamp instead of a relative range, so a copied link keeps showing the same window. Standouts and roster-preview column order now matches the full Scenario detail page. Expanded match rows in Scenario Breakdown show the score. Dropped the invented Overall contribution stat - Standouts now sort by real stats directly, and Top contributor is now Top killer with a tooltip. Added tooltips for Balance thresholds. Filter selects use Bulma's select component.
Averaging or taking the median over 1-2 scenarios lets a single lucky match dominate the ranking regardless of which stat is selected. Reproduced live: Destruction Standouts ranked by Kill damage, Average view, put a 1-scenario player at the top. Default the minimum-scenarios filter to 10+ whenever View is Average or Median; Totals keeps the old 1+ default. An explicit lbMin in the URL always overrides this.
Reworked from one flat sortable table into two team sections grouped by role (Tank/Melee DPS/Ranged DPS/Healer) with a team totals row, matching the roster preview pattern and the maartenson.net reference. Every stat cell also shows the player's share of their team total for that stat, e.g. Kill dmg 27K (11%), so healer/dps/tank contribution can be compared honestly without a combined score. Sorting still works, applied within each role group.
…d' in it Clearing the box while navigateOnSubmit is set built `/search/guild/` (empty trailing segment) and navigated there. React Router has no route for that exact shape, but bare /search/guild does match the player route /search/:query with query="guild" - so it landed back on a search box, just the player one, pre-filled with the literal text "guild" instead of going back to the guilds/players home page. Now an empty value navigates straight to '/' (player) or '/guilds' (guild) instead of building a URL with a dangling empty segment.
…tion navigateOnSubmit boxes (Home's player/guild tabs, the Search and SearchGuild pages) can jump between pages mid-search: a debounced commit navigates from e.g. /guilds to /search/guild/:query, which is a different route element - React unmounts the old input and mounts a brand-new one, taking focus with it. The user had to click back into the box to keep typing past whatever they'd typed when the debounce fired. Focus (and restore the cursor to the end of the value) on mount for navigateOnSubmit boxes. This only fires once per page visit: staying on the same search results page for further keystrokes just updates the URL param, it doesn't remount.
Inspired by maartenson.net's per-dungeon pages (Bastion Stairs etc), which show tabs for recent Runs, the Characters who've run it, and a Leaderboards view. That site tracks its own historical data with hourly graphs and a multi-day date-compare tool - replicating that would need a scraper and years of storage, so this is a scoped-down first pass: one GraphQL request for the most recent 50 runs of an instance (the API already supports filtering instanceRuns by instanceId, with each run's full scoreboard inline), and all three tabs are derived from that single batch client-side. No new backend, cron, or cache. - Runs: recent runs with duration/deaths, plus the API's own average duration/deaths for the batch. - Characters: everyone who's run it recently, sorted by run count, with total damage/healing/protection/deaths. - Leaderboards: best single-run performance per character for a chosen metric (damage/healing/protection), filterable by role and realm. New route /instance/:id (+ /characters, /leaderboards). The Instances list page's name link and 'Runs' button, which already pointed at /instance/:id with no matching route, now land here instead of 404ing.
InstanceRunFilterInput.instanceId expects the custom UnsignedShort
scalar, not Int - declaring the query variable as $instanceIdNum: Int!
failed GraphQL's variable-type-compatibility check at query time
('CombinedGraphQLErrors: The variable instanceIdNum is not compatible
with the type of the current location'), so the Runs tab never loaded.
Declaring it as UnsignedShort! (matching InstanceRunsFilters.tsx's other
uses of this same field) fixes it.
The connection-level averageDuration field is a Duration scalar that
comes back as an ISO-8601 string ('P19DT16H26M27.5S'), not milliseconds -
new Date(Math.round(iso)) silently produced Invalid Date, so the stat
rendered blank. It's also an all-history average (skewed by a small
number of runs in the underlying data with bogus multi-day end times),
which didn't match the page's own 'most recent N runs' framing anyway.
Now both averageDuration and averageDeaths are computed client-side from
the same loaded batch shown in the runs table below, so the summary card
and the table always agree.
…ustom)
No new Worker/D1 needed - instanceRuns already supports start:{gte,lte}
filtering server-side, so this reuses the same windowed-loading trick as
ScenarioList's time-range picker: page through the live API in batches
for the selected window instead of the fixed most-recent-50 request.
The GraphQL API's Duration scalar serializes as an ISO-8601 duration string (e.g. "P19DT16H26M27.52S"), not a millisecond count. Passing it straight into new Date(x) produces an Invalid Date / NaN. This parses the day/hour/minute/second components directly into a date-fns Duration object for formatDuration(), with no Date math involved.
The Average Duration card rendered blank because instanceRuns.averageDuration is an ISO-8601 duration string, not a millisecond count - new Date(Math.round(...)) silently produced NaN. Switched to parseIsoDuration(). Also added click-to-sort headers to the Instance Runs table (start time, instance, duration, encounters, deaths, item rating min/avg/max, tank/healer/DPS counts), matching the pattern already used on Instance Hub and Creatures.
Every encounter row on /instance-statistics/:id was failing with a GraphQL 400: the start filter is a DateTime field, but the code was passing the raw number 0. Replaced with an ISO-8601 epoch string. Also fixed medianDuration rendering NaN for the same ISO-8601-duration-as-milliseconds reason as the Instance Runs average duration bug, using the new parseIsoDuration() helper.
✅ Deploy Preview for curious-caramel-c7f0d8 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
added 17 commits
August 3, 2026 21:22
instanceRuns.averageDuration can be dragged into the thousands of days by rows that never got a proper end timestamp written (abandoned/never-closed sessions). Rather than display a nonsensical '7030 days', show a short explanatory message when the parsed value exceeds a plausible bound (< 1 day). The underlying data-quality issue belongs on the API-features list for dalen.
The API's averageDuration aggregate includes runs that never got a proper end timestamp written, dragging the value into the thousands of days for instances with a lot of history. Sample the 500 most recent matching runs, exclude any with a computed duration over 7 hours (implausible for a dungeon clear), and average the rest, with a note showing how many were excluded. Falls back to the guarded API value if the sample query hasn't returned data yet.
The sample query was requesting first: 500, which the API rejects outright (HC0051 - max 50 items per page), silently returning a null result and making the average duration card always fall back to 'Not available'. Capping at 50 avoids that error entirely, and avoids paginating across multiple requests to build a bigger sample.
…style Nobody uses the Min/Avg/Max item rating filters or columns on Instance Runs or Instance Statistics - drop them entirely (InstanceRunsList, InstanceRunsFilters, InstanceEncounterRunsFilters). Both filter components were also still on raw Bulma columns/field markup instead of the site's .filter-grid convention already used on Instance Hub, Creatures, and Quests - rebuilt them on .filter-grid so all instance pages share the same filter look and feel. Also extended the outlier-excluded average-duration approach (7h cutoff, excluded-count note) to the Instance Hub Runs tab, which was previously just averaging the loaded batch's raw durations despite a comment claiming otherwise.
The Min/Avg/Max item rating stat block and columns on /instance-run/:id matched the item-rating clutter already removed from Instance Runs and Instance Statistics - drop them here too. Also swap the skull-icon Deaths column header for a plain text label, matching the rest of the site's headers.
Several 'instances' the API returns are really just a single wing or named boss of a larger dungeon modeled as its own instance record - Bastion Stair's four named bosses (Thar'Ignan, Lord Slaurith, Kaarn the Vanquisher, Skull Lord Var'Ithrok) each have their own instance ID with a single encounter, on top of Bastion Stair's own 16; same story for Gunbad's Nursery/Lab/Squig Boss/Baracks wings and the three Altdorf Sewers / three Sacellum instance IDs. Players think of these as one dungeon, so introduce a shared INSTANCE_GROUPS config other instance-related pages can use to show/query them as one combined entry instead of a wall of near-duplicate entries.
Replaces the old hand-maintained 21-option list (which listed every
dungeon wing/boss as its own entry, including two with zero encounters)
with the curated instance groups. Picking a grouped dungeon now filters
via instanceId: {in: [...]} across every underlying instance ID, so
e.g. selecting Bastion Stair includes runs recorded under any of its
four named-boss instance IDs too.
Instance Hub's Runs tab now resolves the URL's instance ID to its
dungeon group and queries every underlying instance ID together
(instanceId: {in: [...]}), so a grouped dungeon's Runs/Characters/
Leaderboards tabs show combined data instead of just one wing. Falls
back to a live instance-name lookup for any instance ID outside the
curated group list.
Also fixes the Completed/Wiped label: it was reading the InstanceRun's
own completed field, which tracks whether the session was closed
properly server-side (same root cause as the average-duration bug
fixed earlier), not whether the group actually downed what they
fought - the vast majority of runs are completed: false regardless of
outcome, so nearly every row read 'Wiped'. Derive the label instead
from the run's own encounters: cleared only if every distinct boss
attempted was eventually downed.
/instance-statistics/:id now queries every instance ID in the URL's dungeon group and merges their encounter lists into one table, instead of only ever showing whichever single wing the URL happened to name.
Collapses the 22 raw instance records (several of which were just a wing or named boss of a bigger dungeon, and two with zero recorded encounters) into one card per curated dungeon group, with a de-duplicated encounter count across the group's underlying instance IDs. There are only ~10 groups total, so the whole list now fits on one page - no more pagination. Also drops the redundant 'Home /' crumb from the breadcrumb since the top nav already covers that.
Two related readability fixes, both driven by the same underlying data: separate InstanceRun/instanceEncounterRun rows that are really just noise around one meaningful outcome. Instance Hub Runs tab: separate InstanceRun rows recorded within 20 minutes of each other are almost always the same group's session - a few short wiped attempts followed by the run that actually cleared everything. Cluster them, show the cleared run (or the last attempt, if none cleared) as the visible row, and tuck the rest into a collapsible 'N wiped attempts' toggle underneath instead of flat- listing every attempt as its own row (which made the tab look like it was mostly failures). Instance Statistics: Bastion Stair's four named bosses (Thar'Ignan, Lord Slaurith, Kaarn the Vanquisher, Skull Lord Var'Ithrok) and similar cases like Gunbad's Squig Boss are each their own instance ID under the hood. Keep the group's main dungeon encounters as the primary list and move the named-boss instance IDs into a collapsible 'Named Bosses' section instead of mixing them into one flat table.
Individual encounter attempts sharing the same encounterId (repeat pulls on a boss that took a few tries) were flat-listed as separate rows. Group them like the Instance Hub Runs tab: the attempt that finally killed the boss (or the last attempt, if it never went down) is the visible row, with the earlier wiped pulls nested in a collapsible 'N wiped attempt(s)' toggle underneath instead of cluttering the table with every failed pull as its own peer row.
…ncounters - InstanceRunsList.tsx (/instance-runs table): swap the skull-icon Deaths column header for plain text, matching the fix already made on the other instance pages. - Instance Run, Instance Runs, and Instance Encounter Run breadcrumbs now link back to /instances instead of /instance-runs, so every page past the Instances listing consistently returns to it. - /instances now sorts dungeon cards by encounter count, most first.
Each leaderboard entry only showed a character's single best-performing run for the selected metric. Add a Runs count next to it so it's clear whether that number came from one lucky run or a character who shows up constantly.
Instances is reachable from the top nav, so the leading Home crumb was just noise on Instance Statistics, Instance Run, Instance Runs, and Instance Encounter Run pages.
- Drop the redundant Home crumb, matching the other instance pages. - 'Runs' is now selectable in the Leaderboards Metric dropdown, ranking characters by how many matching runs they appear in. The always-on Runs column is hidden when Runs is the selected metric to avoid showing the same number twice.
Encounters with a median duration under a minute (including the zero-duration 'no data' rows) are trash pulls or bugged/incomplete fights, not real boss encounters - drop them from the list. This applies to every instance's statistics page (Lost Vale, Bastion Stair, etc.) since it's the shared per-encounter row component.
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.
No description provided.