Daniel Gentile

Chess Dashboard

My tournament games from Lichess and my official ratings from US Chess, reconciled on one page.

2026. Live

  • Python
  • Plotly Dash
  • pandas
  • Docker
  • Fly.io
  • GitHub Actions

Problem

A tournament player’s record lives in two systems that never talk. My games and notes sit in Lichess Studies. My official ratings, crosstables, and event results sit on the US Chess Federation site. Neither knows the other exists, and when they disagree about a result or a rating, nothing tells you. I wanted one page that holds both, with engine analysis, plain-English summaries, and a coach’s review layered on top, and that shows me every disagreement instead of quietly picking a side.

Approach

Lichess Studies are the source of truth, named by study identifier rather than pulled by username, so casual games never pollute the stats. Lessons and tags live in the Lichess comments themselves under a small convention, so the app has no database of its own. US Chess data is enrichment, never a dependency: a sync that reaches Lichess but not US Chess still succeeds, and immutable responses like crosstables are cached for good. Engine evaluations are read from the annotations Lichess already embeds in the export, so there is no bundled engine.

The matching engine is where the honesty lives. US Chess game records are matched to Lichess games by opponent member identifier and result first. A fallback by normalised name, result, and a date window applies only when it is strictly unambiguous. Colour and date break ties; they are never requirements. The stated bias is to prefer a missed match over an invented one, and the engine is validated against a 63-record ground-truth fixture.

All of the logic lives in pure modules with no framework imports. The Dash pages are thin. There is one in-memory store per user and exactly one server worker, and the configuration flag that would add workers was removed so nobody trips the constraint by accident.

Tech

Python 3.11, Plotly Dash, pandas, the python-chess library, Gunicorn. Docker on a single small Fly.io machine with a persistent volume. GitHub Actions runs lint, tests with a 92 percent coverage floor, type checks, and a Docker boot smoke test. 1,175 tests over the pure core modules. Seven decision records. A guest mode lets anyone read the live site, and a demo command boots from an anonymised seed with no network.

My contribution

I owned the requirements, the architecture, the seven decision records, and the glossary. The decisions that shape the code are mine: Lichess as truth by study identifier, US Chess as enrichment only, no database, one worker, the match-or-abstain rule, and the pure-core layout that keeps every rule testable without a browser. Agents implemented the plan in vertical slices as pull requests. I reviewed each one and merged it. The one bug that hard-locked a browser tab, an observer loop that kept remounting the game viewer, was mine to diagnose: the real cause was the viewer’s mount contract, not the loop.

AI usage

Claude Code did the planning and most of the implementation from my product requirement documents, one slice per branch. Codex did a first-pass code review on each. Nearly every one of the 114 commits carries a co-author trailer for the model that wrote it, and I keep those on purpose. The agent guide in the repository is the contract they work under.

Lessons

An agent will build exactly what you asked for. The infinite remount loop was a correct implementation of a wrong instruction, and the fix came from reading the viewer’s source, not from asking the agent again. Eager syncing of every user at boot looked clean and grew linearly until the host was about to kill the worker; the first request per user now pays for its own sync. And the no-database, one-worker design cannot scale horizontally. That is written down as a correctness requirement so a future me does not flip it in a config file.

Back to projects