Feature/meta build aggregator#20
Open
314159DD wants to merge 40 commits into
Open
Conversation
Placeholder page reachable at /meta. NavBar shows the new entry. Next: backend SQL aggregations.
Code review flagged em-dash + "PD2 Tools" — fork convention is "Page - pd2.tools" (hyphen, lowercase, matches About + Builds). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Temporary notes file — captures schema + patterns we'll need for the meta route SQL. Delete at sprint close.
Request shape (IMetaQuery), per-aggregation row types, combined IMetaResponse. Shapes mirror api.pd2.tools' public /stats/* responses since /meta replaces those calls.
- Replace open-ended `... | string` itemType union with a named `ItemType` so typos surface at compile time - Rename level-bucket field `count` -> `numOccurrences` for consistency with the other row interfaces; introduce `ILevelBucket` so the same shape isn't inlined twice Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MetaDB_Postgres class with findCohort(filter). Mirrors the existing getFilteredCharacters CTE pattern: subquery-resolves game_mode_id from name lookup, JOIN Classes for className, EXISTS-clause per required skill against CharacterSkills joined to SkillsDefinitions. Returns character_db_id[] — downstream aggregation queries (Tasks 7-8) take this list as their starting set.
GameMode type already constrains values to lowercase literals; the runtime call was redundant. Aligned `unknown[]` -> `any[]` to match the existing query-param convention in economy.ts and postgres/index.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Counts equipped Unique/Set/Runeword items across the cohort. Mirrors analyzeItemUsage's CASE classification for item-type (promotes is_runeword rows to 'Runeword' label). Rare/Magic/Crafted items skipped — they have unique random names and can't be name-aggregated; affix-mod aggregation in Task 17 handles their statistical patterns instead.
Four more methods on MetaDB_Postgres against the cohort:
- aggregateSkillUsage — CharacterSkills JOIN SkillsDefinitions
- aggregateMercType — CharacterMercenaries.description
- aggregateMercItems — MercenaryItems, same CASE/IGNORED pattern
as aggregateItemUsage (mirrors analyzeMercItemUsage)
- aggregateLevelDistribution — Characters grouped by level, returns
{hardcore:[], softcore:[]} populated on one side only
Schema mirrors what was documented in .meta-recon-notes.md.
Sanity-tested against the dev Postgres.
Code review noted Task 9's wiring could be confused by the param — it never reaches the SQL, only picks which side of the return shape gets populated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GET endpoint that takes gameMode + className + minLevel + skills +
season query params, fans out to 5 aggregations in parallel via
Promise.all, returns a combined IMetaResponse.
15-minute Redis cache via autoCache(900) — matches every other read
endpoint in the API. validateSeason middleware applied.
Query param parsing is strict — invalid gameMode/className/minLevel/
skills return 400 with a clear message using the error envelope
pattern { error: { message } } used elsewhere.
Code review caught two issues: - gameMode `as string` cast silently passed `string[]` through on duplicate `?gameMode=hardcore&gameMode=softcore` requests. Now an explicit typeof guard rejects non-string forms. - No success log line on the GET handler. Added one with className, gameMode, season, minLevel, skillsCount, cohortSize — matches the visibility level the other routes maintain in prod. Also dropped a redundant `as string` cast on seasonRaw. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports Sprint 2.2's PD2 parity assertions to the fork as Jest + supertest integration tests against the live Express app and dev Postgres. 7 canonical builds × 6 parity sub-tests = 42, plus 4 validation tests = 46 total. Tests assert internal consistency (totalSample agreement, math correctness, no duplicates) and validation paths (missing className, bad gameMode, out-of-range minLevel, malformed skills JSON), not absolute population values — so they pass regardless of ladder state in the dev DB. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds web/src/api/meta.ts (metaAPI.fetchMeta) and web/src/types/meta.ts mirroring api/src/types/meta.ts — duplicated for the package boundary since web/ and api/ are independent npm packages with no shared types module. Adds API_ENDPOINTS.meta = "/meta" to web/src/config/api.ts. Exports metaAPI from api/index.ts and meta types from types/index.ts. Uses the existing apiClient singleton + encodeURIComponent(JSON.stringify) pattern for skills, matching how characters.ts serialises requiredSkills.
Thin wrapper over metaAPI.fetchMeta. queryKey includes the full filter so different builds get distinct cache entries. Inherits staleTime (5 min) / retry (1) / refetchOnWindowFocus (false) from the QueryClientProvider defaults in App.tsx. Also exports useMetaData from the hooks barrel (hooks/index.ts).
Hardcoded softcore Hammerdin filter for now; placeholder JSON dump of the top 10 items. Validates the full pipeline: React Query -> APIClient -> Express -> Postgres -> response. Task 15 replaces the hardcoded filter with the FilterForm.
Files ported (no changes except import-path fixes): - web/src/lib/shape/buildSheet.ts - web/src/lib/shape/topItems.ts - web/src/lib/filter.ts - web/src/lib/diff.ts - web/src/lib/slot.ts - web/src/lib/buildPresets.ts - web/src/lib/types.ts - web/src/lib/url-state.ts - web/src/data/skill-prereqs.json - web/src/data/item-slots.json - web/src/data/builds.json - web/src/data/mod-dictionary.json Added web/src/lib/api.ts — thin type bridge mapping PD2's lib/api.ts type names (CommonFilter, GameMode, ItemUsageRow, SkillUsageRow, MercTypeUsageRow, MercItemUsageRow, LevelDistribution) to the fork's existing types/meta.ts equivalents. No fetch logic — only type aliases. AffixMod / AffixModsBySlot types moved inline into diff.ts since aggregate/affixMods.ts is not ported (aggregation runs server-side). Skipped (no longer used on FE — backend does the aggregation now): - aggregate/affixMods.ts, avgStats.ts, charms.ts, skillUsage.ts, index.ts, types.ts These can be ported back if a future feature needs client-side aggregation.
Mode toggle (guide/diff), game mode pills, class selector, build preset row (conditional on class), min-level slider, skill picker with selected chips + scrollable available list. URL-state hydration on mount, URL update on submit. Skill names sourced from data/skill-prereqs.json. TODO: future iteration could fetch class skills from a server endpoint instead to maintain a single source of truth.
Tabbed table per slot (weapon, offhand, helm, armor, gloves, belt, boots, amulet, ring). Rarity badges color-coded. Uses shapeTopItemsBySlot from the ported lib. ScrollArea wraps each table for mobile responsiveness.
Backend: aggregateAffixMods fetches Rare/Magic/Crafted equipped items via jsonb_array_elements on full_response_json->'items', then aggregates in Node. Parsing ported verbatim from PD2's affixMods.ts: zone gate (Equipped only), slot mapping from SLOT_BY_EQUIPMENT, skill-tab bucket key collapsing magnitude prefix, median/p75 using nearest-rank method. Denominator is per-slot item count (not cohortSize) to match standalone's pct semantics. Suppresses mods with <3 occurrences. Frontend: Mantine tab-per-slot table, top 20 mods per slot. Display labels resolved via mod-dictionary.json; skill-tab keys strip the "item_addskill_tab|" prefix to show the tab name directly. Tests: 46 → 60 (added shape + pct-math assertions for affixMods). All 60 pass. tsc + lint clean both sides. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: aggregateSkillUsageClassified — classifies each cohort member's 1-point skills as prereq-only when another skill at >1pt requires it. Returns numAsBuild + numAsPrereq + pctBuild alongside the existing counts. Replaces aggregateSkillUsage in the route (kept as a private helper for potential future use). skill-prereqs.json copied to api/src/data/ (clean package boundary). Frontend (Mantine): - BuildSheet: top skills with prereq-only toggle + mini level- distribution bar chart using Mantine Box bars (no extra lib) - CharmPanel: placeholder for now — full charm aggregation will come in a follow-up PR (significant scope; needs /api/v1/meta/charms) - DataFreshness: cohort size + 'fetched X ago' badges, ticks every 30s - MatchBanner: 'N <Class> characters match this filter' header Meta.tsx wiring: MatchBanner -> DataFreshness -> BuildSheet -> ItemFrequencyTable -> AffixFrequencyTable -> CharmPanel. fetchedAt tracked in state, set on data arrival. Tests: 60 -> 67 (+7 classified-skill row shape assertions, one per canonical build). All 67 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In diff mode, looks up the named character via charactersAPI, adapts to the PD2 Character shape, runs diffCharacter against the current cohort's top items, renders a per-slot comparison table. Gracefully handles missing characters with an error alert.
- Loading: replace Loader with Skeleton blocks (more visible during long cohort queries) in both hydration guard and data-fetch gate - Error: Alert with Retry button wired to React Query refetch() - Empty: friendly Alert when cohortSize=0 with filter suggestions - FilterForm slider: marks (1/50/99) + tooltip label for level selection - FilterForm Group: explicit wrap="wrap" on class, preset, game-mode button rows so they reflow at 375px - Tables already wrap in ScrollArea; no change needed - Mantine Tabs/Table/Slider are semantic-HTML by default; no extra ARIA work needed
Recon complete — schema documented in code comments throughout api/src/database/postgres/meta.ts.
Iterated based on live testing against the dev cohort: Backend: - aggregateSkillUsageClassified now also exposes numAtTwenty + pctAtTwenty (matches pd2.tools/builds' >= 20 hard-points threshold). Tests assert the new fields and subset invariant. Frontend: - FilterForm fetches class-only baseline meta data so the skill picker can show real cohort percentages next to each skill name. - Skill rows ported to the pd2.tools /builds SkillCard pattern: top-bordered Paper rows form dividers, absolute background fill bar shows percentage, hover state. - Bigger touch targets throughout (32px skill icons, size=md buttons, uppercase section labels, fw=600 button text). - BuildSheet primary column is now Hard% (pctAtTwenty); Any% kept as secondary context column. - useMetaData hook is enabled-gated on className so empty class doesn't fire a doomed 400. - DEFAULT_UI_STATE no longer preselects Paladin; lands on a "Pick a class above" prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three pre-PR polish fixes: 1. Affix mod magnitude was wrong for two-value mods. PD2 modifier shapes are [magnitude] for single-value mods (item_fastercastrate) but [id, magnitude] for two-value mods (item_singleskill, item_addskill_tab, item_addclassskills). Old code used values[0] for all, so "+X to a Skill" rows showed avg=100+ (the skill ID). Now uses values[values.length - 1] which is always the magnitude. 2. Affix bucketing extended: item_singleskill and item_addclassskills now bucket by label-derived target (skill name / class name) so "+3 to Ice Blast" and "+3 to Meteor" stay in separate rows instead of collapsing. AffixFrequencyTable.resolveLabel handles any "<modKey>|<label>" form generically. 3. Level distribution chart: removed overflowX scroll (was producing a stray scrollbar at the chart edge), bumped bars from 6px to flex-grow with 28px max width and 96px max height, drops empty buckets, labels every level (not just multiples of 10). Reserved fixed height so the page doesn't reflow when cohort changes. 4. DataFreshness "FETCHED: -15s AGO" bug: useState lazy-init captured a timestamp before the parent's setFetchedAt fired. Added an effect that resets `now` whenever fetchedAt changes, plus a Math.max(0) clamp as a defensive backup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- FilterForm skill list: selected skills float to the top (within groups still sorted by pctAtTwenty desc). Gives immediate visual feedback when clicking a skill or a build preset. - ItemFrequencyTable: section header 'Top items' with subtitle 'Unique, Set, and Runeword items'. - AffixFrequencyTable: section header 'Affix patterns' with subtitle 'Rare, Magic, and Crafted items only' (hover-tooltip explains why counts differ from the Top items table). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The selected-row background bar overlapped the percentage-width fill bar, producing two stacked shades of green. Now selected rows use a single solid darker green across the full row (no fill bar) and the percentage stays visible on the right edge. Click the row to deselect (X icon removed — the whole row is the toggle). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-skill presets were too loose — "Hammerdin" matching every character with Blessed Hammer at 20 hard points doesn't pin down the build. 3-skill signatures (primary + key synergy/aura + extra) match the conventions PD2 community uses to describe a build: Hammerdin = Blessed Hammer + Concentration + Vigor Smiter = Smite + Holy Shield + Fanaticism FoH = Fist of the Heavens + Holy Bolt + Conviction ... etc, across all 7 classes Verified every preset skill name resolves to a real SkillsDefinitions row in the DB (caught 4 PD2-renamed skills: "Raise Skeleton Warrior" -> "Raise Skeleton", "Enchant Fire" -> "Enchant", and removed "Poison Explosion" / "Poison Dagger" which PD2 doesn't have). Production cohorts will be meaningful for every preset. The dev DB sample happens to lack data for Hammerdin/Smiter specifically (no characters with maxed Blessed Hammer or Smite among ~114 scraped Paladins), but FoH/Zealot/Auradin/Charger all return real cohorts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Was only there as a copy-paste source for the GitHub PR body — not part of the feature, doesn't belong in the merged tree.
Set Group wrap="nowrap" and shrink Button size to xs so all 7 classes fit on a single row at typical sidebar widths instead of wrapping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UI polish - Class buttons stay on one line; shorten to Sorc/Pala/etc. then to initials at narrow viewports so the row never overflows - HC/SC pills shrunk one size step; form widened to 1050px - Top items and Affix patterns tables use fixed column widths so type/count/% columns stay anchored across slot tabs - Tab rows are center-aligned and wrap to a second centered row on narrow viewports - Page title centered; banner alert constrained to form width Collapsible sections - New CollapsibleSection wrapper with rotating chevron and bottom border, mirroring the pd2-aggregator Section component - Core Skills, Level distribution, Top items, Affix patterns, Charms, and Average build stats are all collapsible Average build stats (new section) - Backend: aggregateAvgStats() reads character.attributes plus character.life and character.mana directly. These are the stats-page totals (base, gear, leveling already baked in). Returns six fixed rows: str, dex, vit, energy, life, mana. Resistances are skipped since every end-game build caps at 75 - AvgStatsPanel renders six stat cards - Affix patterns gained an "All slots" tab; TopAffixAveragesPanel reuses that aggregation to show the most common build-defining affixes. Resists and skill/proc mods are excluded since they do not aggregate meaningfully across the pool Core Skills synergy filter - Default Core Skills view hides skills that appear as synergies of other displayed core skills, using skill-prereqs receivesBonusesFrom. A new "Show synergies (N)" toggle parallel to "Show prerequisites" exposes them with a SYNERGY badge Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Core Skills now show a per-skill Type bubble (Synergy or Prereq; cores get no badge to keep visual noise down) and a single popularity-sorted list. Synergies are hidden by default behind the "Show synergies (N)" toggle for a compact entry view. Classification source: scripts/build-skill-classification.py scrapes 21 per-tree pages from wiki.projectdiablo2.com, assigns a default role per tree (Combat Skills -> core, Defensive Auras -> synergy, etc.), and falls back to the existing receivesBonusesFrom heuristic for six trees that mix the two roles (Offensive Auras, Warcries, Shape Shifting, Cold/Fire/Lightning Spells, Poison and Bone, Druid Summoning). A small OVERRIDES dict patches the obvious edge cases (Cold Mastery -> synergy, Teleport -> synergy, Skeleton/Golem/Curse Mastery -> synergy, Maul -> core, etc.). Output is checked into web/src/data/skill-classification.json so the FE doesn't need network access at runtime; rerun the script when PD2 patches change skill rosters. The "Show synergies" toggle defaults to off so the entry view stays short. Top-three cores still get the yellow text highlight regardless of how synergies interleave when the toggle is on. Also: - BETA pill on the /meta nav link and the page title (violet light Badge) - Welcome banner gains a dimmed second line asking for feedback on Discord (@tekk0n) since the tool is new Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shrinks the PR diff per the review ask. Net -264 lines.
- Remove the Python wiki scraper from the repo. Generated JSON stays.
- Remove the CharmPanel placeholder card; charms ship in a follow-up
when the backend route is built.
- Remove the "Resistances aren't shown" note and the "Prereq detection
uses skill-tree data" citation. Both were noise.
- Level slider now spans 80-99 to match the backend's indexed range.
- SkillIcon switches from the wiki Special:FilePath URL to /icons/{name}.png
so Prayer (and every other skill) renders, and icons have uniform
20px sizing instead of variable wiki dimensions. Fixes the missing
Prayer image and the spacing drift Cole called out.
- Top items table wraps each row name with the existing ItemTooltip
from components/builds/shared/ItemHelpers so hover gets the same
item-card behavior as the /builds page.
- Comment cleanup pass across meta.ts (backend + types), BuildSheet,
AvgStatsPanel, TopAffixAveragesPanel, CollapsibleSection,
AffixFrequencyTable. Kept the WHY-comments, trimmed paragraphs that
a reviewer can derive from the code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trims ~2k lines off the PR diff. - web/src/data/skill-prereqs.json: web only needed Object.keys() of the skill list, so it now reads skill-classification.json (same 220 skill names across the 7 classes). API keeps its copy since it still uses the prereqs graph for the 1-pt-prereq classifier. - web/src/lib/filter.ts: not imported anywhere. Was ported from the standalone for client-side cohort filtering; with backend aggregation the FE doesn't filter raw characters. - web/src/lib/shape/buildSheet.ts: not imported anywhere. The BuildSheet component now reads the API's IClassifiedSkillRow shape directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Net -191 lines. No behavior changes.
- types.ts: drop the 150-odd "// CONFIRMED: ..." snapshot-inspection
annotations. The types themselves document the shape.
- Strip "Sprint X" / "Task X" / "PD2 standalone" / "mirrors the
standalone" / "ported from PD2/..." references across backend and
frontend comments.
- Replace em dashes in comments and user-facing empty-state JSX
("No skill data" instead of "- no skill data -" style).
- Consolidate api.ts comment block and diff-adapter.ts docstring.
- Trim verbose JSDoc on aggregateItemUsage / aggregateAffixMods.
- Hoist BuildSheet imports above the top-level const.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
314159DD
added a commit
to 314159DD/pd2-aggregator
that referenced
this pull request
May 12, 2026
Sprint 2.3 shipped to coleestrin/pd2-tools#20 on 2026-05-11. This commit captures the doc-side closeout (status line, roadmap entry, archived sprint file) plus the build tooling + classification data that the PR ports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
314159DD
added a commit
to 314159DD/pd2-aggregator
that referenced
this pull request
May 12, 2026
Brings pd2-aggregator.vercel.app to feature parity with coleestrin/pd2-tools#20 while Cole reviews the upstream port. Same data layer, adapted to the standalone's Tailwind/shadcn UI. - Min-level slider clamped to 80-99 with marks (and URL parser clamps too) - "All slots combined" affix view now hides resists + skill/proc procs; avgStats does the same (resists cap at 75, averaging is misleading) - New "Average build stats" section above Top items: - AvgStatsPanel: Str/Dex/Vit/Energy/Life/Mana from character.attributes - TopAffixAveragesPanel: cross-slot mod averages, resist-filtered - Level distribution rendered as a vertical bar chart with empty buckets dropped and per-bar title tooltips, replacing the flat pill row - Skill table gains Core/Synergy/Prereq classification via data/skill-classification.json, plus "Chars 20+" and "Hard %" columns with explanatory tooltips. Synergies and prereqs hidden behind toggles - Item names get hover tooltips with image + req level + attribute lines via a mirrored public/items.json (640 KB; refresh on PD2 patches) All 180 tests still pass; tsc + next build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles the work for LAMP's second review round into a single commit since the changes are interlocking. Net -2225 LoC across 26 files (6 deletions). Architectural changes - Drop `web/src/types/meta.ts`: the duplicates of `ItemUsageStats` / `SkillUsageStats` / `MercTypeStats` / `LevelDistribution` already exist in `web/src/types/character.ts`. Reuse those; the genuinely-new meta types (`IMetaResponse`, `IMetaQuery`, `IClassifiedSkillRow`, `IAffixModRow`, `IAvgStatRow`, `ISkillRequirement`, `Slot`) move into `types/character.ts`. `ItemUsageStats.itemType` widened to include Rare / Magic / Crafted. `ILevelBucket` field renamed `numOccurrences → count` to match existing `LevelDistribution`. - Drop `web/src/lib/diff-adapter.ts`, `web/src/lib/types.ts` (the PD2- shape Character / Item universe), and `web/src/lib/api.ts` (the type shim). `lib/diff.ts` is rewritten to consume `FullCharacterResponse` / `IItem` directly. The "userHas affix" check shifts from `modifiers[].name` (not present on `IItem`) to `properties[]` string match against the pool affix display label — same source the /builds UI uses. - Drop `web/src/data/item-slots.json` (1591 entries) and the `slotFromItemName` lookup. Backend now derives slot from `item.base.type` in each character's JSON, builds a lazy `Map<itemName, slot>` cache on first call, and returns `slot` on every `IItemUsageRow`. FE `shape/topItems` reads `row.slot` directly. UI changes - `/meta` link moves into the Tools dropdown (next to Character Exporter and Corrupted Zones). BETA badge dropped from nav; page header keeps it. - DiffView pool-top and user-equipped item names get hover tooltips, reusing the `/builds` page's `ItemTooltip` + `/items.json` mechanism. Pool item type now flows through `SlotDiff.poolTopItemType` for correct tooltip border colors. - FilterForm class-button responsive shrink range extended from ≤768px to ≤992px to fix overflow on widths between mobile and full-tablet (LAMP's responsive observation). Cleanup - Header block comments removed from `api/src/types/meta.ts`, `api/src/routes/meta.test.ts`, `web/src/components/meta/FilterForm.tsx` and other meta components. JSDoc retained where field semantics are non-obvious (modKey suffix bucketing, pctAtTwenty threshold, the zone-gate behavior on `location.equipment`). Verification - API tsc + jest meta.test (67 tests, all green) - Web tsc + eslint clean
Follow-up to e1eb0f4 addressing issues caught in self-review. Bugs fixed - `DiffView.tsx` was multiplying `m.pct` by 100 a second time (`(m.pct * 100).toFixed(0)`) — every affix row would have rendered "10000% of pool". Other consumers in the codebase print `pct.toFixed(N)` directly; `m.pct` is already 0-100. - `lib/diff.ts` "userHas affix" check resolved non-piped modKeys (the common case: `item_fastercastrate`, `item_armorpercent`, …) to the raw mod name, which never appears in a `properties[]` string. Now resolves through `mod-dictionary.json` first (same dict `AffixFrequencyTable` uses) and falls back to the raw key. Matching also tightened from substring to word-boundary regex so a pool top of "Resist" no longer false-positives on "Cold Resist 10%". - `lib/diff.ts` `SLOT_BY_EQUIPMENT` was mapping both `Right Hand`/`Right Hand Switch` to "weapon" (and same for offhand), so `items.find()` could return a weapon-swap item instead of the active-set weapon. Removed Switch positions — diff considers the active loadout only. Type tightening - Narrowed `ItemType` to `Unique | Set | Runeword` (was 6 values). SQL never emits the other 3; the wider union was a lie. Dead branches in `ItemFrequencyTable.rarityColor` dropped. - `IAffixModRow.slot` is now `Slot` instead of `string` on both sides. Backend `inferSlot` and `SLOT_BY_EQUIPMENT` typed accordingly. Drops the `as Slot` cast in `bucketAffixModsBySlot`. - `ItemUsageStats.slot` widened to `Slot | null` to match the actual wire format (backend writes explicit `null` for non-equippable items like charms). Docs updated. - Added `zone?: string` to `IItem.location`. The previous `(loc as { zone?: string }).zone` cast was a workaround for the type being wrong, exactly the kind of accommodation-smell the bridge-deletion was supposed to remove. Backend - Slot cache wrapped in `Promise<Map>` so concurrent first-callers share a single in-flight JSONB scan instead of each issuing one. Comments - Dropped remaining AI-prose: 9× `// Test N:` comments in `meta.test.ts`, 9× `{/* Section name */}` JSX dividers in `FilterForm.tsx`, 3× `// ----` block separators in `meta.ts` and `meta.test.ts`, and the dangling "Mirrors X" / "Ported verbatim" / "(Tasks 7-8)" references in `meta.ts` (which referred to a standalone codebase the fork's readers have no context for). - Updated stale `modKey` docstrings to mention all three pipe-bucketed mods (`item_addskill_tab`, `item_singleskill`, `item_addclassskills`), not just the first one. Frontend - `.catch(() => {})` on `/items.json` fetches in `DiffView` and `ItemFrequencyTable` replaced with `console.error` to match the `UniqueCard` / `MercItemCard` convention. Logging - Switched the items.json catch handlers to console.error. Verification - API tsc + jest meta.test (67/67 green) - Web tsc + eslint clean - Live API spot-check: `slot` populated correctly per item, level distribution emits `{ level, count }`, charms map to `slot: null`.
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.
Add /meta build aggregator page
Adds a new top-level page at
/metathat aggregates Project Diablo 2ladder build data: top items per slot, affix mod patterns,
skill usage with prereq classification, mercenary, and level
distribution, plus a "diff my character" mode.
This implementation is a port of https://pd2-aggregator.vercel.app —
discussed in Discord. All public-API code paths have been replaced
with direct Postgres aggregations via a new Express route + autoCache.
What's new
Backend
api/src/routes/meta.ts— single GET endpoint with strict query-param parsingapi/src/database/postgres/meta.ts— 7 aggregation methods onMetaDB_Postgres:findCohort— class/level/gameMode/skill filteraggregateItemUsage— equipped Uniques/Sets/RunewordsaggregateSkillUsage— base counts per skill (kept as private helper)aggregateSkillUsageClassified— adds prereq classification (1pt skills required by a higher skill are bucketed as "prereq" not "build")aggregateMercType+aggregateMercItems— mercenary dataaggregateLevelDistribution— character count per levelaggregateAffixMods— Rare/Magic/Crafted modifier patterns with avg/median/p75 per (slot, modName)api/src/types/meta.ts— request + response typesapi/src/routes/meta.test.ts— 67 Jest+supertest tests (validation paths + parity assertions across 7 canonical builds)autoCache(900)+validateSeasonmatching the rest of the APIFrontend
web/src/pages/Meta.tsx— top-level pageweb/src/components/meta/*— 8 components (FilterForm, BuildSheet, ItemFrequencyTable, AffixFrequencyTable, CharmPanel, DiffView, DataFreshness, MatchBanner) — all Mantine v7web/src/api/meta.ts— typed API clientweb/src/hooks/useMetaData.ts— React Query hook (first one in the codebase — RQ was already wired in App.tsx)web/src/lib/*— pure logic ported from pd2-aggregator (slot map, build presets, URL state, diff function, types)web/src/data/*— skill prereq + synergy data, item-slot map, build presets, affix mod dictionaryWired in
web/src/App.tsx—<Route path="/meta">web/src/components/layout/NavBar/index.tsx— "Meta" entry between Builds and Economyweb/src/config/api.ts—meta: "/meta"endpoint constantapi/src/routes/index.ts—router.use("/meta", metaRoutes)api/src/database/index.ts— exportsmetaDBapi/src/types/index.ts— re-exports meta typesVerification
cd api && npm test -- --testPathPattern=meta.test)cd web && npx tsc --noEmit && npm run lint)Hard %column matches the>= 20 hard pointsthreshold thatanalyzeSkillUsageinCharacterDB_Postgresuses, affix table shows real magnitudes (avg/median/p75 of the rolled value, not the skill-ID index)Notes on the iteration
After the initial implementation we ran the live stack and tightened a few things based on what we saw:
aggregateSkillUsageClassifiednow exposespctAtTwentyalongside the existingpctandpctBuild. FilterForm + BuildSheet's primary column usepctAtTwentyso the numbers match what/buildsshows.values[0]which is the skill/tab/class ID for two-value mods (item_singleskill,item_addskill_tab,item_addclassskills); now usesvalues[values.length-1]which is always the magnitude. Bucketing also extended so+3 to Ice Blastand+3 to Meteorno longer collapse into the same row.Open follow-ups (not in this PR)
/api/v1/meta/charmsin a follow-up since the fork doesn't load raw character JSON FE-side.web/has no vitest/jest setup currently. We lean ontsc --noEmit+ manual testing. Happy to open a follow-up PR if you'd like vitest added.autoCachecaches 400 responses — pre-existing across the whole API; a 400 response gets returned as 200 on cache hit. Not introduced by this PR but worth flagging.Data sources
A note on architecture
The aggregator's "look-and-feel" runs Mantine v7 (your stack). The "engine" is direct SQL queries against your existing Postgres schema. We deliberately did NOT preserve the standalone's public-API HTTP path or its IndexedDB cache — those don't add value in a server-rendered environment with
autoCache.Happy to iterate on review feedback.
🤖 Generated with Claude Code