OHTB · Product notes

Back to Matches

Why this exists

The match-history tool for the job I actually have

OHTB is for one player or coach who owns a deep history and wants to interrogate it quickly. I built the whole console around that person. Conveniently, that person is me.

Different jobs, different winners

Mass-market League sites have a giant job: cover millions of players, surface current ranks, support a broad ecosystem, and pay for it with ads. OP.GG is built for that breadth. OHTB is deliberately selfish. It can spend the entire screen on one player’s teams, accounts, matches, and questions.

That narrower target is why OHTB is better for this use case. I can move through years of my own games, change the question without losing my place, and open a full scoreboard without leaving the console. I do not need global player search while reviewing one player. I need my history to behave like a dataset instead of a stack of receipts.

OHTB does not reproduce OP.GG’s global scale or rank ecosystem. That trade is the point, not a footnote.

The URL is the notebook

Queue, date, champion, and lane-opponent filters live in the URL. Pick Solo/Duo, Flex, ARAM, or every queue; narrow the history to seven days, one month, three months, one year, all time, or an inclusive custom date range. The champion and opponent controls use the full champion catalog, while the player’s most-played champions stay near the top where they belong.

A question such as ?queue=420&from=…&to=…&champ=…&opponent=… has one durable address. Changing a filter clears pagination and preserves the rest of the question. Server rendering, navigation, cache keys, refreshes, and the back button all read the same state. React does not get a secret second opinion.

A matchup, or no matchup

For supported Summoner’s Rift queues, OHTB shows a lane opponent only when the match payload identifies one player on each team in the same recognized position. The stored queue and champion also have to agree with the raw match. The opponent filter uses the same rule, so the badge and the query tell the same story.

ARAM has no honest lane mapping. Messy or ambiguous position data does not get a made-up opponent, either. In both cases the matchup stays blank. A missing badge is mildly annoying; a confident lie is much worse.

Forty rows at a time; ten players when I care

The first history page is cached by account and filter set. Older matches arrive in batches of forty as the scroll approaches the end, and the virtualized feed keeps the browser focused on the rows around the viewport. That combination matters more than a heroic benchmark screenshot: deep history stays practical instead of turning into a very tall DOM monument.

Opening a standard match expands into both teams: player names, champions, roles, K/D/A, damage, gold, CS, vision, objectives, and item builds. The compact row is for scanning. The ten-player scoreboard is where I stop and inspect what actually happened.

Fresh data, with the limits in plain English

Refresh is manual. I paste a temporary Riot developer key and OHTB checks the account’s newest ten match IDs, fetches only unseen matches, stores them, and invalidates the account and team caches. The key field is cleared when the run finishes. This is a newest-ten check, not historical backfill; an older gap stays an older gap.

Live game is another manual action. It uses Riot’s Spectator-v5 endpoint, displays the current teams when the player is in game, clears the pasted key, and applies a short cooldown before the next check. The request and failure paths have automated coverage. The first checks backed by a real Riot key are still operator-run, so I call these flows implemented rather than field-proven.

Raw truth, derived speed

SQLite keeps the raw Riot match JSON alongside derived participant rows and a scoreboard payload. Normal browsing reads the smaller shapes the filters and components need. If a derived scoreboard is missing, OHTB can rebuild it from the raw match instead of asking Riot for history it already owns.

Keeping raw and derived data together is intentionally boring. The raw payload is the recovery point; the derived payload is the working copy. I can evolve how the console reads a match without pretending the first parser was divinely inspired.

Shipping without cowboy theatre

Product feedback becomes a bounded job in an isolated worktree. An agent makes the source change, targeted checks and the production build exercise it, and the runner accepts one exact clean commit. Deployment creates and validates a versioned release before atomically switching the active release. The public app is never the scratchpad.

That feedback → isolated agent → checks → release loop gives me iteration speed without treating unreviewed output as authority. Fast typing is cheap now. Judgment about scope, claims, and what deserves to ship is still the work.

The rough edges are real

The public demo auto-logs into one configured user. That makes the console easy to inspect and says nothing useful about a finished onboarding flow.

Refresh sees the newest ten matches, temporary developer keys are still a manual input, and the first real key-backed refresh and live-game checks remain with the operator. Match-detail deep links, the mobile experience, and clearer player-perspective controls are queued for more work.

OHTB remains a focused history console, not a global ranking network. I would rather name that boundary now than discover a fake platform hiding inside a good personal tool later.

The human part of agentic engineering

Models have written a lot of this project. My job is to bring the problem, constrain the change, inspect what came back, reject clever nonsense, and make the result prove itself. The useful part is the loop between taste and implementation, not a percentage of keystrokes.

AI can produce JSX faster than I can. It cannot decide which claims deserve to survive contact with the current product. The code arrives faster; responsibility does not. Annoying, but fair.