This file provides guidance to LLM agents when working with code in this repository.
A client-side Slay the Spire 2 run history dashboard. Parses .run JSON files exported by the STS Collector mod and renders interactive charts and per-run breakdowns. All processing happens in the browser — no server required in production.
Data flow: .run files → _parse.js → _stats.js → _charts-plot.js + _app.js DOM rendering
_parse.ts — Two parsers: parseRuns() (batch summary + all run details for all runs) and parseRunDetailForCache() (per-floor detail for a single run, used internally). Both use cleanId() / fmtId() / normalizeRoomType() helpers. A public parseRunDetail() wrapper exists for backwards compat._stats.ts — Pure functions. computeStats(runs) returns a stats object with 16 aggregated metrics (summary, win_rate_over_time, char_win_rates, card_data, relic_data, floor_histogram, act1_data, deaths, ascension_stats, winning_deck, winning_deck_no_starters, rest_site_data, card_upgrades, card_upgrades_no_starters, ancient_stats, runs). No side effects. Synchronous._charts-plot.ts — Observable Plot wrappers. Shared theme config objects (SC, LC, YH) are refreshed via refreshChartTheme() on theme toggle._idb.ts — IndexedDB helpers. Exports idbGet/idbPut (generic CRUD), named helpers for handles, cache, and run details, plus DB constants (DB_NAME, DB_VERSION, STORE_HANDLES, STORE_DETAILS)._app.ts — Orchestrator. Single state object holds all global state. Manages file I/O (File System Access API + IndexedDB), filtering, lazy chart rendering, and all DOM manipulation. renderAll() is the single re-render entry point.ChartPanel.astro — Astro component wrapping each chart panel <div>. Props: id, title, wide.app.py — Not present in this codebase; ignore if referenced elsewhere.Single state object in _app.ts:
const state = {
dirHandle, // FileSystemDirectoryHandle
rawRunsData, // raw JSON keyed by timestamp
allRuns, // parsed summary objects (ParsedRun[])
currentStats, // result of computeStats()
tableSortKey, // column key for table sort
tableSortDir, // 1 or -1
runTypeFilter, // { normal, daily, custom }
_rtfDebounce, // debounce timer ref
_runDetailCache, // Map of parsed run details (filename -> RunDetail)
_runDetails, // all run details for the current load
currentRun, // currently open run detail (null when modal closed)
};
| Store | Key | Value |
|---|---|---|
handles | "dir" | FileSystemDirectoryHandle |
handles | "cache" | { savedAt, rawRunsData, runDetails } (pre-parsed details) |
details | filename (string) | parsed run detail object |
DB version: 3. If the schema changes, update _idb.ts and add an onupgradeneeded handler.
Note: The details store is keyed by filename (not timestamp). Run details are parsed synchronously during finishLoad() and stored both in the in-memory _runDetailCache and cached to IndexedDB.
Charts are rendered with Observable Plot (imported as @observablehq/plot). Two rendering strategies:
renderAll()IntersectionObserver (rootMargin: 200px) to defer rendering until scrolled into viewChart elements carry a _lazyRender property referencing their render function. This is set in _app.js’s chartIdMap.
Package manager: Use bun for all package operations. Do not use npm or yarn (bun.lock is in version control, not package-lock.json).
Code formatting/linting: Use oxfmt and oxlint for TS/JS. Use stylelint (bun run lint:css) for CSS files. Run them before committing or when requested.
ID handling: Raw game IDs like CARD.STRIKE → cleanId() → STRIKE → fmtId() → Strike. Applied at parse time, not render time.
Room types: normalizeRoomType() in _parse.js maps raw strings to canonical lowercase keys used by ROOM_ICONS in _charts-plot.js.
Run type categorization: Mutually exclusive — custom (has modifiers) > daily (non-standard game_mode) > normal. Filter state persisted in localStorage.
Win rate coloring: winRateColor(pct) — green ≥60%, gold 40–60%, red <40%.
Progressive detail parsing: All run details are parsed synchronously in finishLoad() via parseRuns(), which calls parseRunDetailForCache() for every run. Results are stored in state._runDetails and the in-memory state._runDetailCache. On cache load, pre-parsed details are restored directly without re-parsing.