Skip to content

Patch/6 instance runs fixes - #1065

Open
Emissaryx wants to merge 97 commits into
dalen:mainfrom
Emissaryx:patch/6-instance-runs-fixes
Open

Patch/6 instance runs fixes#1065
Emissaryx wants to merge 97 commits into
dalen:mainfrom
Emissaryx:patch/6-instance-runs-fixes

Conversation

@Emissaryx

Copy link
Copy Markdown

No description provided.

… 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.
Emissaryx and others added 12 commits August 3, 2026 20:46
…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.
@netlify

netlify Bot commented Aug 4, 2026

Copy link
Copy Markdown

Deploy Preview for curious-caramel-c7f0d8 ready!

Name Link
🔨 Latest commit 5289fb4
🔍 Latest deploy log https://app.netlify.com/projects/curious-caramel-c7f0d8/deploys/6a7158adc52f610007889787
😎 Deploy Preview https://deploy-preview-1065--curious-caramel-c7f0d8.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Trevor (via Claude) 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.
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.

1 participant