# Changelog
All notable changes to this project are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Single sign-on (OpenID Connect)
- **Sign in with Authentik, Keycloak or any OpenID Connect provider.** Set
`OIDC_ISSUER`, `OIDC_CLIENT_ID` and `OIDC_CLIENT_SECRET`, and the login page
gets a button whose label you choose with `OIDC_BUTTON_TEXT`. Authorization
code flow with PKCE, state and nonce, through `openid-client`. Nothing changes
until those variables are set. Guide: [docs/OIDC.md](docs/OIDC.md).
- **SSO-only mode.** `OIDC_ONLY=true` removes password login, sign-up,
"forgot password" and password reset, in the interface and on the server.
Set by mistake without a working SSO configuration, it is ignored with a
warning instead of locking everyone out.
- **No setup wizard on an SSO-only install.** A fresh install started with
`OIDC_ONLY=true` skips the wizard, whose password nobody could use: sign-up
is closed, the coach is left for *Admin → System*, and the browser goes
straight to the login page. The first SSO login creates the admin, even
with automatic accounts off.
- **Admins from a provider group.** With `OIDC_ADMIN_GROUP=patzer-admins`,
members of that group are admins and everyone else signing in through SSO
is a normal user, re-checked at every login. While no admin exists, only
group members can sign in, so on a fresh install the first admin is the
first group member rather than whoever is fastest. A provider that sends no
`groups` claim leaves roles alone, with a warning in the log.
- **Logging out of Patzer logs you out of the provider too.** A session that
came from SSO remembers its ID token. Logout sends the browser to the
provider's end-session endpoint, which sends it back to the login page, so
the next person at a shared computer has to sign in again.
- **Your choice of who gets in.** `OIDC_AUTO_PROVISION=true` creates an
account for someone the provider lets in. It takes their username (or the
next free `name-2`), display name, email and browser language, and gives
the account no password. `OIDC_MATCH_BY=username|email` links a first SSO
login to an existing account instead: username ignoring case, email only
when the provider marks it verified. An identity stays linked to its
account from then on, and an account takes only one identity per provider,
so a second match is refused rather than guessed.
- **Linked accounts get the provider's email** if they have none and no other
account uses it, so password resets and notifications reach them too.
Existing emails are never overwritten.
- **Admin → Users shows how each account signs in:** *Password*, *Password*
linked to SSO, or *SSO* for accounts created by a single sign-on login.
- **Clear errors.** A failed SSO login returns to the login page with the
reason in the user's language (provider unreachable, cancelled, took too
long, no account, already linked, waiting for the admin, too many
attempts), and an `[auth] sso_…` line in the log.
- **Hardened against abuse.**
- SSO callbacks are rate-limited per IP, so Patzer can't be used to flood
the provider.
- The redirects are `no-store`.
- Control characters and bidi overrides are stripped from provider names.
- Outside values are sanitized before they reach the log.
- Patzer warns at startup when SSO runs without `PUBLIC_BASE_URL`.
- What this protects and where its limits are: the *Security* section of
[docs/OIDC.md](docs/OIDC.md).
- **Tests.**
- `server/test/oidc.test.ts`, `oidc-first-run.test.ts` and
`oidc-mixed.test.ts` cover account matching, emails, the admin group, both
kinds of fresh install and the SSO-only guards.
- `web/src/pages/Login.test.tsx` and `web/src/pages/admin/Users.test.tsx`
cover the screens.
- `npm run test:oidc` (also in CI) runs the whole thing against a real
OpenID provider.
## [7.19.0] — 2026-10-18
### Tactic Trainer: see your move, and hear why the better one is better
- **Your move on the board.** In Train → From your games the move you played
in the game is drawn as a red arrow on the puzzle position, so you no longer
have to work out from "you played Qd3" which move that was.
- **The coach explains the difference.** Once the puzzle is over (solved, or
the solution shown — never before), the coach explains in plain words why
the engine's move beats the one you played: the same engine-checked
explanation as in Game Review, in your language and pitched at your
audience setting.
### Merged contributions (thanks, @eric-gpu and @Suven-p)
- **Train → From your games: puzzles come back, and go on (#82, @eric-gpu).**
A puzzle you don't solve at the first try comes back the next day, marked
*Review*, with the same simple daily queue as the opening trainer: solved at
the first try on a later day, it's done; missed again, it's back tomorrow.
Trying again the same day is practice and changes nothing. The header shows
how many are due. When the engine's line goes on with forcing moves — a
check, a capture, a promotion, a mate — the puzzle goes on too, up to three
of your moves. A move that isn't the engine's but is just as good (within
2 % win chance) counts as solved, any checkmate counts, and your own move
from the game never does. Hint and "Show solution" work like the Lichess
puzzles.
- **Puzzle of the day, with a streak (#83, @eric-gpu, roadmap #12).** The
"Today's puzzle" card on Home is one puzzle from the Lichess set near your
puzzle rating — the same puzzle all day, a new one tomorrow — with a plain
counter of the days in a row you did it. It opens on Train → Puzzles and is
rated like any other puzzle (first try counts). The next puzzle from your
own games is no longer on Home; it stays on Train → From your games.
- **Play from here (#84, @eric-gpu, roadmap #13).** In Game Review the move
card has "Play from here against Patzer" at every move, and the Lab has the
same button for the position on its board. You play your own colour, pick
the bot's strength and the clock, and the game is saved like any bot game —
unrated, marked "from a position", and kept out of the opening tree, the
opening trainer's repertoire and Insights. Also fixed on the way: the Lab's
"Start" went back to the normal starting position after you had set a FEN.
- **Admin → System: a saved AI model the server doesn't have (#85,
@eric-gpu).** When the saved model wasn't in the list the server sent back,
the dropdown showed another model while the old one stayed saved, and the
coach failed with a 404. The saved model now shows as its own entry, marked
"not on this server".
- **Game Review: the Elo rating is always visible (#81, @Suven-p).** On wide
screens the rating next to each player's accuracy was pushed out of sight;
the player card now stacks them.
## [7.18.1] — 2026-10-05
### Moves land the instant you drop them
- **Fixed: a dropped piece jumped back to its square until the server
answered.** In a timed game the page redraws ten times a second for the
clock, and every redraw put the board back to the last position the server
had confirmed, so your move only appeared after a round trip. A drag still
in progress was reset on the next tick too, and arrows you had drawn
vanished. On bullet that felt like input lag. The board now only updates
when something on it actually changes, and your move is on the board, with
its sound, the moment you let go. The server's answer just confirms it, or
puts the board back if it rejects the move.
- **Premoves right after your move.** Until the server confirmed your move
the board still thought it was your turn, so a quick premove was sent as a
real move and rejected. It is now queued as a premove.
### Merged contributions (thanks, @Fristail27)
- **Russian Learn, puzzles and opening trainer.** Every lesson text (kid
versions included), the tactic puzzles and the opening trainer are now in
Russian, and the locale check covers Russian lesson texts like German ones.
## [7.18.0] — 2026-10-03
### Game Review explains itself
- **Why a move got its label.** The Moves tab now opens with a card for the
current move: whose move it was, its label, the win chance before and after
("62% → 30%"), one sentence on why the engine called it that (a blunder:
the win chance collapsed by 32%, about 3 pawns; a miss: the position was
winning and this move let it slip; …), the move the engine wanted with how
its line goes on, and then the coach in plain words — what the move did or
allowed, and what the better move would have achieved. (The coach used to
live in its own tab; it is now right here, so the separate Coach tab is
gone.)
- **Pick a category, walk its moves.** The counts in the Game Report table
(Black's 2 misses, White's 3 inaccuracies, …) are now clickable: the move
list highlights those moves, dims the rest, jumps to the first one, and
‹ › arrows step through them. Clicking the category name picks both sides.
- **The AI report knows whose move it is.** Key moments used to be written as
if every move was yours, so the opponent's blunder read as "you played …".
Each moment is now marked *Your move* / *Opponent's move*, and an opponent's
moment is about what it gave you and whether your answer took the chance.
Key moments also get the coach's facts (what the move allowed, the best
answer to it, the better move), so they say something concrete.
- **A summary with something in it.** The summary used to get nothing but
accuracy and Elo, so all it could do was repeat them. It now gets the story
of the game — the result, the turning point, your costliest moves with the
better ones, your best finds, the opponent's gifts and whether you used
them, your weakest phase — and is told not to repeat numbers the page
already shows. Phase texts no longer get cut off after three lines, and key
moments are shown in full. Reports written by an older version are
rewritten on request ("Write it again" under every report).
### Fixed
- **The eval bar was upside down for Black.** With the board turned for
Black, the bar put White's share at the top in Black's colour: when Black
was better, the white part grew. The colours now stay with their side.
- **Material count like chess.com.** The number next to the captured pieces
was the total value one side had captured; it is now the material lead on
the board, and only next to the side that is ahead (a knight for two pawns
is +1, not +3).
### Merged contributions (thanks, @eric-gpu)
- **Real-board move sounds from the very first move.** With "Real board"
chosen, the first move after opening a page still played the synthesized
knock — the recordings only started loading with the first sound. Most
noticeable where Patzer moves first by itself, like the opponent's first
move in the opening trainer, or the first move in a lesson. The recordings
now download as soon as the setting is known, and a move sound that comes
while they're still loading waits for them (at most 0.4 s).
- **Insights: move quality.** A new card counts your own moves by
classification — brilliant, great, best, … down to blunders — across your
analyzed games, with a bar and a share for each, and lists your most recent
brilliant moves; one click opens Game Review on that very move.
- **Opening trainer: deviations.** Real opponents rarely stay on the main
line. Every built-in line now comes with the opponent's most common other
moves and a few tempting ones a beginner meets (2...f6 against the Italian,
3...Nd4, the Albin Counter-Gambit, 2.Qh5 against 1...e5) — 47 in all. After
a line, or straight from "Do you know this line?", choose *Practise the
deviations*: each starts just before the other move, Patzer plays it, and
you find how to go on (or how to punish it). Every one of your moves in them
is checked with Stockfish at depth 18: none is more than half a pawn worse
than the engine's best. Lines from your own repertoire get their deviations
from your games: where your opponents played something else, and — where
the analysis called your reply a mistake — the engine's move instead, so
you practise what you should have played. Missed moves go into the daily
review like any other.
- **Train: puzzles (beta).** A second tab next to the puzzles from your own
games: about 4,000 popular Lichess puzzles (CC0) from rating 400 to 2600,
shipped with Patzer, no network needed. Each profile gets a puzzle rating
(Glicko-1, like the game ratings, starting at 1200); the next puzzle is one
you haven't tried, close to your rating. Like on chess.com the first try
counts — a wrong move, a hint or "show solution" and it counts as not
solved, but you can still finish it. Multi-move solutions, the opponent's
replies played for you, any checkmate accepted on the last move, and filters
for checkmate, forks, pins, discovered attacks, sacrifices, hanging pieces,
defence and endgames. When your own puzzles run out, the empty page points
here. The set is one JSON file (`server/src/chess/tacticsSet.json`,
460 KB); `npm run build:tactics` rebuilds it from the Lichess puzzle
database, and the page credits the database in its footer.
- **Learn: 15 new lessons** (69 in all), each with an explanation and six
tasks. Beginner: *Defend!*, *The weak point f7* and *Pawn on the run*.
Intermediate: *Discovered check*, *The exposed king*, and three more mating
patterns — the Opera mate, the hook mate and Pillsbury's mate. Advanced:
*The only move*, a new course *Rare mating patterns* (Boden's mate, the
dovetail, the bishop pair, mate in four) and a new course *Queen and
minor-piece endgames*. As before, every task is a Lichess puzzle (CC0) that
Stockfish confirmed at depth 20 to have exactly one right answer, and no
puzzle appears twice; texts in English and German, with kid versions.
- **Learn:** the overview now explains how stars and levels work.
## [7.17.1] — 2026-09-27
### Finished games no longer hang on "Saving game…"
- **Fixed: after a game against the bot, the game-over card spun on "Saving
game…" forever.** The game *was* saved — it just couldn't be reached from
there. Since 7.14.0 the page closes the bot socket as soon as the game is
over, and the server announced the end (`game_over`) a few milliseconds
before confirming the save (`game_saved`), so the confirmation arrived on a
socket that was already closed. In a real browser that happened every
time. The server now saves first and puts the game's id on `game_over`
itself, and the page closes the socket only once it knows the id.
- **Fixed:** `[classify-user-move] Error: engine not started` in the server
log at the end of a bot game — the analysis engine was shut down while the
last move was still being classified.
Tests: a browser scenario that finishes five games in a row and opens each
one's review (0/5 on 7.17.0, 5/5 now), and a protocol check that `game_over`
carries the saved game's id.
## [7.17.0] — 2026-09-27
### The coach coaches
The coach used to narrate: it named the move, repeated its label and stopped.
It saw nothing but the one move — not why it was bad, not what was better,
not the player's own games. Worse, it could contradict the engine: in
discussion #37 Game Review called 3…Nf6?? (which allows 4.Qxf7#) a blunder
while the coach called it a great move. That wasn't a small model's fault —
a 27B model did the same, three times out of three, because the prompt gave
it nothing to explain and an example sentence that praised development.
- **Fixed: the coach can no longer contradict the engine.** Every answer is
checked against the verdict before it is shown. Praise for a mistake gets
one retry with a correction, and if the model does it again the player gets
a plain answer built from the facts instead. The prompt now puts the
verdict first, forbids praise for mistakes outright, and the example
sentences that small models copied word for word are gone in every
language. (#37)
- **New: why a move is good or bad.** The server works out what a move
allows, leaves hanging, wins, forks, stops or threatens (chess.js), and
the opponent's best answer as a short line (Stockfish) — "this lets the
queen take on f7 with checkmate", not "a blunder".
- **New: the better idea, and alternatives.** The best move and the other
moves that are nearly as good, each with what it achieves ("the pawn to g6:
it stops the checkmate threat and attacks the queen").
- **New: the coach remembers your games.** Your recurring mistake types over
the last 30 analysed games (allowing a quick mate, leaving a piece
unprotected, missing a mate, a win or a tactic), your weakest phase, your
best finds, and positions you got wrong in the opening trainer — brought
up when the move on the board is the same kind.
- **New: a rule to remember and a next step.** Each mistake ends with a rule
of thumb for that kind of error, and the coach links the matching Learn
lesson and your puzzles.
- **New: hints that coach.** A hint now knows the position — a mate threat,
pieces under attack, a loose enemy piece, undeveloped pieces, an uncastled
king, and what the engine's best move achieves — and points you there
without naming the move.
- **Play gets the same coach as Game Review.** Play never sent an evaluation
or engine line; the coach now runs its own short engine search, so a move
in a live game is explained as fully as one in Game Review. The in-game
auto-coach uses the same pipeline and guard.
- **Fixed: Bulgarian and Russian "better move" read as praise.** Found while
testing the guard against every verdict in every language.
- The coach panel no longer shows a server error message as if it were
coaching, and keeps paragraph breaks.
Tests: 46 new — tactics detection, the coaching facts, the guard in all five
languages (including every verdict phrase), the player memory (and its speed:
30 games in under a second on a real account, down from 17.8 s during
development), and the #37 regression end to end through the real route with a
fake LLM that praises the blunder. Against the 7.16 code that suite fails.
## [7.16.1] — 2026-09-27
### vLLM works again, and CI now runs the regression suites
- **Fixed: the vLLM coach broke in 7.15.0.** Adding DeepSeek moved vLLM's
model list from `/v1/models` to `/models`, which vLLM doesn't serve — so
*Test* in **Admin → System** failed on every existing vLLM setup. Adding
`/v1` to the URL made the model list work but broke everything else
(`/v1/v1/chat/completions`): *Test ALL models*, the coach and Game Review.
vLLM's routes are back under `/v1` of the bare server URL, and a URL that
already ends in `/v1` works too, so nobody has to change their setting back.
Ollama and DeepSeek are unchanged.
- **New: a regression suite for the LLM providers**
(`server/test/llm-providers.test.ts`, 33 tests). Fake Ollama, vLLM and
DeepSeek servers over real HTTP, each serving only the routes the real one
has; every entry point (model list, model test, coach stream, JSON review,
the Admin → System buttons) for every provider and every way of writing the
URL. Against the 7.16.0 code it fails 10 tests — exactly the two symptoms
above.
- **CI runs the regression suites on every PR**: `test:e2e`, `test:resume`
and the two real-browser suites `test:ui` and `test:resume-ui`, a boot
check of the Docker image, and — when lesson content changes —
`verify:lessons`. CONTRIBUTING and the PR template now ask for a
regression test that fails without the fix with every bug fix.
## [7.16.0] — 2026-09-27
### A Learn section, Russian, and a round of fixes
Patzer can now teach chess, not just review it: **54 interactive lessons** from
"how the knight moves" to rook endgames, in English and German. It also speaks
**Russian** now, its fifth language. Thank you to
**[@eric-gpu](https://github.com/eric-gpu)** (Learn, opening-trainer follow-ups,
Docker coach setup, two security and hardening fixes — #46–#51, #53, #54) and to
our newest contributor **[@Fristail27](https://github.com/Fristail27)** (Russian —
#52).
Both the Learn section and the opening trainer are marked **beta**: they work,
but their texts are still being refined. That is also why Russian shows the
trainer and Learn screens in English for now; they get translated once the
wording settles.
### Added
- **Learn section (beta).** A new *Learn* page with 54 interactive lessons in four
levels — New to chess, Beginner, Intermediate, Advanced: how the pieces
move and every rule, how moves are written down, basic checkmates, forks,
pins and the other tactics, mating patterns, opening principles, pawn and
rook endgames. A lesson is a short explanation with animated examples, then
tasks: collect stars, tap squares, find the move (Patzer answers), play an
ending out against Stockfish, or answer a question. A wrong move gets a
reason where the position shows one (stalemate, a check that isn't mate);
"Show the solution" draws the arrows and you play the moves yourself; the
explanation can be looked up again during the tasks. Each lesson earns one
to three stars, stars add up to levels, and there are three new
achievements. Progress is stored per profile on the server, an unfinished
lesson resumes where you left it, kid mode shows the same lessons in
simpler words, and a Home tile suggests the next lesson. The tactic, mate
and endgame tasks are puzzles from the Lichess puzzle database (CC0). Every
task is checked twice: by the test suite (legal moves, real mates, star
tasks solvable in the stated number of moves) and by
`npm run verify:lessons`, which asks Stockfish whether each answer really is
the one clearly best move. Lesson texts in English and German. Marked beta
while it is being refined. (Roadmap #7; #49, #50, #51)
- **Russian (`ru`).** UI, setup wizard, coach persona and prompts, move
narration and Game Review prose, with all four Russian plural forms (one /
few / many / other). A key-parity test keeps `ru.json` complete; the
beta trainer and Learn texts (plus a few strings from this same release) are
listed as pending and fall back to English until they're translated.
(#20, #52)
### Changed
- **Opening trainer is marked beta.** It works, but it is still being
refined, so the *Trainer* tab carries a small "Beta" badge. (#48)
- **Opening trainer: easier first steps, fairer misses.** A new line starts
with "Do you know this line already?": watch it first (arrows on every
move, nothing counted) or practise right away. One wrong try no longer
lands in the review — the second one (or "Show me the move") does. "Practice
this line" from your repertoire stops before one of your own moves that the
analysis calls a mistake in most of your games. The review queue turns over
at your own midnight instead of UTC's, and a move you no longer play can be
removed from it. Built-in line names are translated. In the review, a
first wrong try gets a hint and one more go; after the answer it moves on
by itself. (#48)
### Fixed
- **Connecting the AI coach from Docker.** The setup wizard rejected
`http://host.docker.internal:11434` — the very address the README
recommends — as `invalid_url`, and a failed test only said "unreachable" or
"fetch failed". Now that address is accepted, and a failed test in the setup
wizard and *Admin → System* explains the usual cause: inside Docker
`localhost` is the container itself, and Ollama has to listen on the network
(`OLLAMA_HOST=0.0.0.0`). `docker-compose.yml` has an opt-in (commented-out)
line that maps `host.docker.internal` on Linux too, and the README
troubleshooting entry spells out both steps. (#37, #53)
- **German admin texts for DeepSeek and the hosted engine.** The DeepSeek
fields and the chess-api.com note in *Admin → System* were hard-coded in
English; they now come from the locale files, with German translations,
and the German coach description mentions DeepSeek like the English one. (#54)
- **Lichess import: one broken game no longer fails the whole import.** A
line of the export that is not a whole game is skipped instead of answering
500, and an account that already has an import waiting gets "an import is
already running" instead of queueing more requests behind it. (#47)
### Security
- **The GitHub star count no longer sends the page address to GitHub.** The
star badge also shows on the sign-up and password-reset pages, whose URLs
carry an invite code or a reset token; the request to api.github.com now
goes out without a Referer. On top of that the server now sends
`Referrer-Policy: strict-origin-when-cross-origin` instead of
`no-referrer-when-downgrade`, so no link or request to another site
carries a page's path and query any more. (#46)
## [7.15.0] — 2026-09-26
### German, Lichess, invites, an opening trainer — and two new contributors
The biggest community release so far: seven pull requests from two first-time
contributors. **Thank you, [@eric-gpu](https://github.com/eric-gpu)** (German,
Lichess import, invite-only sign-up, both sound sets and the opening trainer —
#39, #40, #41, #42, #43, #45) **and [@aminghuf](https://github.com/aminghuf)**
(DeepSeek, the opt-in hosted engine and automatic Chess.com sync — #44).
Nothing changes unless you ask for it: the new sounds default to *Classic*,
sign-up keeps whatever mode you had, and Game Review stays on the bundled
Stockfish. The one thing that runs on its own is the Chess.com sync below — it
only touches profiles that already have a Chess.com username.
### Added
- **German (`de`).** The whole app — UI, setup wizard, coach persona and hard
rules, move narration, Game Review prose — in German. The coach prompt is
tuned for small local models: it asks for the informal "du" without using
it as a form of address (mistral:7b otherwise ended most answers' first
sentence with "…, du!"), and it has no example sentences, which such
models copied word for word — "Solide Entwicklung" even for a blunder.
German is also the first
coach language whose piece names need an article and a grammatical case
("der Springer zieht", but "schlägt den Bauern", "du hast einen Turm
mehr"), so move and material phrasing decline properly instead of gluing
bare nouns together. (#18)
- **Soft check and game-end sounds.** A new *Check & game-end sounds* choice
under **Settings → Sound & assistance**: *Classic* keeps the original bells
(and stays the default), *Soft* swaps those two for marimba-style wooden
bars that sit closer to the wood-knock move sounds — two short rising notes
for check, and the same G → E → C cadence as before on marimba with a faint
bell underneath for game end. Still fully synthesized, no audio files, with
a preview button next to each option. (#36)
- **Real-board move sounds.** A second choice under **Settings → Sound &
assistance**, *Move sounds*: *Classic* keeps the synthesized wood knocks
(default), *Real board* plays recordings of real pieces set down on a
wooden board for moves, captures and castling (the move click twice, king
then rook). Check, promotion and game end keep their sounds and still follow
the check & game-end choice. The two clicks (~30 KB each) are cut from a CC0
recording on Freesound and only load once *Real board* is picked; until
they have loaded — or if they can't — moves fall back to the synthesized
knocks. (#36)
- **Import games from Lichess.** Next to the Chess.com importer: set a
Lichess username in **Settings** and *Import from Lichess* appears in Game
Review. Uses Lichess's public export API (no token), sends a real
User-Agent, makes one request at a time from the server and tells you to
wait a minute instead of retrying when Lichess answers 429. Standard chess
only — variants, aborted and unfinished games are skipped — and importing
twice doesn't duplicate anything. Correspondence games land in the same
*daily* pool as chess.com's. Import errors (unknown player, Lichess down)
are shown now instead of failing silently, for Chess.com too. (#28)
- **Invite-only sign-up.** Admin → System now has three answers to "who can
create an account": anyone, invite only, nobody. Admin → Users gets an
Invites list: each invite is a code and a `/signup?invite=…` link, limited
to a number of accounts and a lifetime (one account, one week unless you
pick otherwise), and can preset the language and coach level of the
accounts made with it — so a coach can hand a class a "Spanish, beginner"
link. Copy the link again later, withdraw it, and see who signed up with
it. The sign-up page checks an invite as soon as it's entered, so a dead
link is reported before anyone fills in the form. Existing installs keep
what they had: on becomes "anyone", off becomes "nobody". (#26)
- **Opening trainer.** The Openings page has a second tab, *Trainer*. Pick a
line — one of 16 built-in main lines for White or Black, or any position in
your own repertoire tree ("Practice this line", continued the way most of
your games went on) — and Patzer plays the other side while you find your
moves. A wrong move is taken back so you can try again; "Show me the move"
draws the arrow. Every move you miss is stored on your profile and comes
back in a daily review queue until you've got it right on three different
days. (Roadmap #6)
- **DeepSeek as a coach provider.** A third tab next to Ollama and vLLM in
**Admin → System**, for machines too small to run a model. The API key comes
from `DEEPSEEK_API_KEY` (wins, Docker-secret friendly) or the Admin field,
and is never sent back to the browser — only whether one is set. Ollama and
vLLM stay the local-first default. (#44)
- **Optional hosted analysis engine.** `ENGINE_BACKEND=chessapi` sends Game
Review positions to chess-api.com (Stockfish 18 NNUE) instead of the bundled
binary — meant for a Pi/NAS or a public try-it instance. **Off by default**:
`local` stays the default and nothing leaves your machine unless you set it.
On an API failure it retries once, then finishes that game locally, so one
review never mixes two engines. (#44)
- **Automatic Chess.com sync and review.** Profiles with a Chess.com username
get their new games pulled in the background (every 15 minutes by default,
`CHESSCOM_SYNC_MINUTES`, `0` turns the timer off), the newest ones analyzed
(at most three per tick), and — when an LLM is configured — the AI Game
Review written without a click. Finished bot and friend games get their
review the same way. A failed review never blocks the import; it is retried
on the next sweep. (#44)
### Changed
- **`games.source` accepts `lichess`.** SQLite can't alter a CHECK
constraint, so existing installs rebuild the `games` table once on startup:
in one transaction, with foreign keys off for the swap so no analysis,
puzzle or rating history is cascade-deleted, and with the row count and
`foreign_key_check` verified before it commits. If anything goes wrong it
rolls back, logs, and the server starts as before without the Lichess
import.
### Fixed
- **Admin → System's Save button didn't save the sign-up settings.** It only
saved the coach and engine fields; the sign-up switches were saved by
"Save email settings" further down, so changing them and pressing Save
quietly did nothing. Save now saves the whole page.
- **English text on translated screens.** Around 140 strings were hard-coded
in components (Settings descriptions, the greeting, phase names, the game
list, key moments, admin pages…) or used a translation key that existed in
no locale file, so they showed English in every language. They are locale
keys now. Achievement titles and weekly plan goals come from the server in
English; they are now translated in the browser from their id/kind, with
the server text as fallback. Bulgarian and Spanish fall back to English for
the new keys, exactly as before, until someone translates them.
- **Language pickers showed every language in the current one.** After
switching to Bulgarian, the Settings select read "Немски" for German — no
way back for someone who can't read Cyrillic. Settings, the setup wizard
and Admin → Users now list each language in itself (English, Български,
Español, Deutsch), like the command palette already did.
## [7.14.0] — 2026-09-23
### Your game is still there when you come back
Switching to another app mid-game and coming back used to lose the game. Not
"lose the connection" — lose the game. This release fixes that for bot games and
for games against a friend, which failed the same way for two different reasons.
- **Fixed: a game against the bot was never written down until it ended.** It
lived entirely inside the WebSocket that created it, so closing the tab — or a
phone freezing the browser while you answered a message — destroyed it with no
trace. Every move is now snapshotted server-side, and reconnecting picks the
game up mid-position. It survives the tab closing, a reload, and a server
restart. **Your clock is not charged for the time you were away**: PvP charges
it because a human is waiting, but the bot is a process that wasn't even
running, and flagging you for closing a tab would punish the exact thing this
release is about. Bot games are unrated, so there's nothing to protect.
- **Fixed: the page never reconnected.** `onclose` was literally
`/* ignore */` — so after a phone sleep the board still looked alive, the clock
kept counting down, and nothing you did reached the server. The socket now
retries with a backoff, and retries *immediately* on the events that actually
fire when a phone wakes up. This is what was killing longer games with a
friend: the server kept those games correctly all along, but the browser had
quietly stopped talking to it.
- **New: "Continue where you left off."** Until now there was no route back into
an unfinished game at all — a friend game could only be reached if you still
had its URL. The Play screen now lists the bot game you left and every
unfinished game with a friend, with how far in you are and which colour you're
playing. One button to resume, one to discard.
- **New: the clock measures real time.** It used to count interval ticks rather
than elapsed time, and browsers throttle timers in background tabs. It now
measures real time, and freezes while the socket is down instead of counting
down a game you aren't connected to.
- **Fixed: the GitHub star count from 7.13.0 never appeared.** The page's own
Content-Security-Policy blocked the request and logged a violation on every
page load. Found by the new browser test.
Two new test suites, because "it reconnects" is not something to take on trust:
`npm run test:resume` drives the protocol against a real server (kill the socket,
reconnect, keep playing), and `npm run test:resume-ui` does it in a real browser,
including pulling the network out from under a live game.
## [7.13.0] — 2026-09-20
### Star on GitHub link
Patzer is free and self-hosted, so there is exactly one thing it can ask for in
return. This release adds a **Star on GitHub** link — in three places that stay
out of the way, and nowhere near the board.
- **In the footer, beside the version chip.** On every page, at the same size
as the version, with the repo's live star count next to it. Deliberately not
in the top bar: that bar already measures itself and collapses nine nav pills
to icons when the labels don't fit a laptop, so a star button up there would
have cost somebody a destination.
- **At the bottom of the changelog.** Having just read what the last few
releases handed you for free is the one moment to ask.
- **Under the login card.** That is where someone sizing Patzer up for their
own box lands — and until now nothing in the product said where the source
lives. The word `github` appeared exactly once in the whole web app, in a
help string on the admin page.
The count comes from GitHub's public API once a day per browser and is cached in
`localStorage`. If the box has no internet, the call fails or you are
rate-limited, the link still works and simply shows no number. Click it once and
it goes muted — *Starred* — and stops nudging on that device for good. The
browser cannot actually check whether you starred without your GitHub
credentials, which Patzer will never ask for, so it takes the click at its word.
Bulgarian and Spanish strings included. Five new tests cover the cache, the
offline path and the once-only nudge; `npm test` is at 100.
## [7.12.0] — 2026-09-19
### Premove queues, an instant board, and a clock you can read at a glance
Three changes aimed squarely at playing fast.
- **New: queue up to six premoves.** chessground holds exactly one premove, so
the queue is Patzer's own. While the opponent is thinking you can keep
playing: the board shows the position *as if* every queued move had been
made — your pieces are already sitting where you put them, with an arrow per
move and the move list under the board — which is the trace you actually want
rather than a single ghost arrow. Each move is re-checked against reality the
instant the opponent moves: if it's still legal it's sent immediately, if it
isn't, the whole queue is dropped rather than half-played. Escape cancels, so
does clicking the chip.
The edge cases it handles, because a premove queue is mostly edge cases: a
queued move that gives check ends the queue (the position after it isn't one
you can plan from); the queue clears on takeback, on game over, and whenever
the server rejects anything; premoves are ignored while you're browsing back
through the game; promotion works (the picker runs against the queued
position, not the live one); and a premove skips the blunder warning, because
the point of premoving is speed.
- **Removed: the piece slide animation.** It was 200ms of dead time on every
move — the board knew the position before you could see it, which reads as
lag on a fast time control. Pieces now appear where they are.
- **The clock grows under 30 seconds.** The side actually on the clock doubles
in size below 30s (18px → 36px) so it's readable without looking away from
the board, and keeps the red pulse it already had below 10s. The idle side
stays small — a big number on a clock that isn't running is just noise.
## [7.11.0] — 2026-09-19
### It tells you when it's out of date, and the header stops falling off the screen
Four community pull requests landed today — thanks to **@Vaibhavs25**, who
picked up four of the open issues in one sitting.
- **New: Patzer notices when a newer release is out.** A self-hosted app can't
update itself and shouldn't try, but it can stop you discovering six months
later that the thing you wanted was fixed in July. Patzer now asks GitHub at
most once every six hours whether there's a newer tag, and shows one quiet
line at the top of the page when there is, with a link to the release notes
and a reminder to pull the image. Dismiss it and that version never nags you
again. It sends nothing about you or your games — it's an anonymous read of a
public page — a failed check is silent, and you can switch the whole thing
off in **Admin → System** or with `UPDATE_CHECK=0`.
- **Fixed: the header overflowed the window.** With nine nav items, an admin
account and the language toggle, the top bar wrapped onto two lines and
pushed the `ES` button and the user chip off the right-hand edge — on a
1440px laptop, which is most laptops. Two things were wrong: the bar was
capped at 1280px wide no matter how big your monitor was, so the nav never
had room even at 1920; and the pills overflowed their container instead of
giving way, so they painted over the user menu. The bar now uses the width
it has, and the nav *measures itself* — when the labels don't fit, the pills
become icons with tooltips, so every destination stays one click away instead
of some being hidden. Measuring rather than picking a breakpoint matters
because an admin carries two more items than a regular profile and Bulgarian
labels are wider than English ones: any fixed width would be wrong for
somebody. `Users` and `System` also moved into an Admin dropdown. (#22, PR by
@Vaibhavs25, finished off afterwards)
- **Fixed: the coach's mute button didn't always mute.** Two separate bugs,
both in the gap between "you press mute" and "sound comes out". The panel
read a stale copy of the mute flag when a reply finished streaming, so muting
mid-answer still read the answer aloud. And the speech helper has two
deferred paths — a one-tick delay to dodge a Chrome bug, and a wait for the
OS voice list, which on Windows is usually empty on first use — neither of
which was cancellable, so a mute landing in either window was simply ignored
and the coach carried on talking. Both now check a generation counter that
`cancel()` bumps. Covered by tests that fail against the old behaviour.
- **New: the phone moves sheet is draggable.** Swipe up on the *Moves* button
to open it, drag the handle down to dismiss, with the sheet following your
finger. Tapping still works, the move list inside still scrolls, and
`prefers-reduced-motion` disables the gesture rather than just shortening the
animation. (#23, PR by @Vaibhavs25)
Also: the Spanish/Bulgarian strings for the new UI, and `npm test` is now at 89
tests.
## [7.10.1] — 2026-09-19
### Black could not move in a PvP game
Playing a full game against another person through two browsers — the one
test nobody had run — found this immediately. As Black, you could see the
opponent's moves arrive, the clock ran, "Your turn" showed, and clicking your
pieces did **nothing**. A page refresh fixed it, which is exactly why it read
like a flaky connection.
chessground caches where the board sits on screen and only re-measures on a
window resize or scroll. Black's board mounts while the opponent is still
connecting, so the "waiting for them to come back" banner is on screen; the
moment they arrive the banner disappears and the board slides up by its
height. chessground keeps the old position, so every click is mapped to the
square *below* the one you clicked — usually an empty square, so nothing gets
selected and the board looks dead. White never saw it, because White's board
mounts after the opponent is already online.
The board now re-measures itself whenever it moves or changes size (and a
ResizeObserver covers a resize that comes with no re-render, like a rotating
phone). Two new tests: a component test that pins the re-measure, and
`npm run test:ui` — two real browsers playing each other through the actual
interface, which is the only thing that would have caught this. Nothing else
changed.
## [7.10.0] — 2026-09-18
### Spanish, tests, PvP negotiation, and two bugs nobody could have seen
This release closes every open issue on the tracker (#10–#16) and merges the
first external feature PR. Thanks to **@fiedri** for the Spanish translation
and — more than that — for restructuring the coach prompts so the *next*
language is a table entry instead of a branch in every function.
- **New: Spanish.** The whole app — UI, setup wizard, coach persona and
hard rules, move narration, Game Review prose — in `es`. Every language
toggle now iterates one registry (`web/src/lib/languages.ts`), so adding a
fourth language touches one list, one JSON file and the server enums; the
contributor guide has the recipe. Text-to-speech picks a Spanish voice
and previews in Spanish. (#14, #17 by @fiedri)
- **New: PvP draw offers, takebacks and one-click rematch.** Offer a draw
(crossing offers count as agreement), ask for a takeback (rolls back to
before your last move, one or two plies as needed), and from the game-over
card start a rematch with colours swapped and the same clock — no trip
back through the lobby. Offers are relayed live, survive a refresh, and are
cancelled by any move. (#10)
- **New: "What's the threat?" in Game Review and Lab.** If you passed, what
would the opponent play? A null-move engine probe rendered in plain words
from the move's own facts — "Nxf7, takes the pawn on f7, winning a piece",
"Qxf7#, checkmate" — never an invented line, no LLM involved. (#12)
- **New: master-game stats.** A "Master games" card under the engine lines
with win/draw/loss, game count, ECO name and the top continuations from the
Lichess Opening Explorer, proxied and cached server-side, shown next to —
never instead of — the engine. `LICHESS_EXPLORER_URL` points it at a
self-hosted explorer. Heads-up: Lichess is currently refusing third-party
API clients (lila#19610), so the card shows an "unreachable" line until
they reopen it or you run your own. (#11)
- **New: phone layout for Play.** Full-width board, a sticky bottom bar
(back/forward, moves, flip, hint, resign, PvP actions in a menu) and a
tap-to-expand moves sheet. Desktop also gains a board flip. (#13)
- **New: `setup.sh`.** `npm run setup` now works on Linux and macOS too —
detects OS/CPU, fetches the matching official Stockfish 17 build into
`./bin/`, and is safe to re-run. (#16)
- **New: a real test suite.** `npm test` (vitest) runs in CI on every PR:
the classification ladder and Elo calibration, the Glicko-1 math, the PGN
save/restore cycle PvP reconnects depend on, the explorer client against a
mocked upstream, and the threat/takeback/time-control helpers.
`npm run test:e2e` boots a real server and plays a PvP game over two
WebSockets. (#15)
- **Fixed: two-player games never relayed a single move.** Each player has
their own `games` row, but PvP sessions were keyed by that row id — so the
two humans sat in two separate sessions, each seeing "opponent
disconnected". Sessions are now keyed by the shared game id. Found by the
new end-to-end test; nobody had reported it because the live instance has
never had two people online at once.
- **Fixed: timed PvP games ran without clocks.** The stored time control is
`600+0` but the socket looked it up as the keyword `rapid`; it also made
"classical" a 10-minute game. Both formats are understood now and
classical is 30 minutes again.
- **Fixed: Brilliant moves could not exist.** The sacrifice test compared
your material before and after *your own* move, which never changes —
4,482 classified moves in the reference database and not one Brilliant.
Sacrifices are now measured with a static exchange evaluation on the
position after the move, net of what it captured, with the engine-line
recapture filter comparing material *balance*. Roughly 1 in 100 engine-top
moves now qualifies before the filter, which is about what chess.com shows.
Scoring version 8 → 9: cached analyses re-run on next view.
- **Changed: Game Review prose is table-driven.** `review.ts` joins
`prompts.ts` in keeping every language string in one lookup — English and
Bulgarian output is byte-identical (verified across 26,776 prompt
renderings).
## [7.9.0] — 2026-09-06
### vLLM support, three bug fixes, and a security pass
A community-driven release — every change here started from a GitHub issue
or pull request. Thanks to **@qrakhen**, **@souorosco**, **@ryanchristle**
and **@bashir-abdelwahed** for the reports and the diagnosis work.
- **New: vLLM as an AI-coach backend.** The coach can now talk to an
OpenAI-compatible vLLM server as an alternative to Ollama, picked with a
provider toggle in **Admin → System**. Ollama and vLLM keep independent
URL + model settings, so switching the toggle never clobbers the other's
config — point one at a small local model and the other at a bigger box,
and flip between them freely. Reasoning models served via vLLM (Qwen3 and
friends) have their chain-of-thought suppressed the same way Ollama's do,
so a tight response budget doesn't get eaten by thinking tokens before the
model ever answers. Existing deployments need no migration — the provider
defaults to Ollama. (#1)
- **Fixed: board frozen when playing Black.** Starting a bot game (or
joining a PvP game) as Black could leave the board permanently unplayable
— pieces would visually move but no move ever reached the server, with no
error shown. Root cause: the board's drop handler was only wired up when
it mounted on your turn; mounting on the opponent's turn (always the case
for Black at game start) left it disconnected for the rest of the game.
Premoves had the same gap. (#3, reported and diagnosed by @souorosco)
- **Fixed: PvP games failing to connect for one side.** Accepting a
challenge could leave one player stuck on "Connecting to game…" forever
while the other saw a permanent "opponent disconnected" — every game
needed two database rows (one per player), but only one of them was ever
handed out as the game to join, so whichever player got the other row
could never connect. (#2, reported by @ryanchristle)
- **Fixed: bot difficulty tiers playing at nearly the same strength.** Every
difficulty from "kid" to "stockfish" shared Stockfish's effective rating
floor, so a weak setting still played like a strong club player — just
with less thinking time. Rebalanced the depth/time/rating curve per tier
and added slight per-move timing variance so the bot feels less
metronomic. (#4, reported and fixed by @qrakhen in #6)
- **Security:** updated dependencies flagged by `npm audit` (several
severe/critical advisories, mostly in the optional SMTP mailer path) and
resolved the one remaining non-breaking finding (`postcss-selector-parser`
DoS). Three moderate/high findings remain that require major-version
bumps (nodemailer, react-router, vite/esbuild) and real regression
testing — tracked for a future release rather than forced through here.
(#6, contributed by @qrakhen)
## [7.8.0] — 2026-06-24
### Players — browse the people you play
Patzer gains a social surface. Until now you could challenge other accounts
from the Play tab, but you couldn't *see* anyone — no record, no ratings, no
sense of who's who. This release adds a proper players directory and public
profiles, so a family/club instance feels like the small social network it is.
- **New "Players" page (`/players`).** A directory of everyone on the instance:
avatar, online dot, lifetime games, win-rate and best rating at a glance.
Search by name/username, sort by Rating / Win % / Games / Name, online
players always float to the top, and the top three on the rating board get a
crown. You appear in the list too, badged "You".
- **Public player profiles (`/players/:id`).** Full record for any player —
W/L/D, win-rate, average accuracy, current streak, per-time-class ratings
(bullet/blitz/rapid/daily, provisional-aware), and their last ten games.
- **Your head-to-head.** Each profile shows your personal score against that
player (wins / draws / losses), computed from your own game rows.
- **Challenge from the profile.** Pick a colour and time control and send a
live invitation without leaving the page; a pending challenge shows a
"waiting…" state you can cancel.
- **Missed invitations.** The Players page surfaces challenges that expired
before you answered them, with a one-tap "Challenge back".
- **Wired into the chrome.** New nav pill (desktop + mobile), command-palette
entry, and a `g u` keyboard jump. Full EN/BG localization.
- **New API:** `GET /api/players` (directory) and `GET /api/players/:id`
(profile), plus `GET /api/challenges/history` for invitation activity. All
read-only and auth-scoped; no schema changes — built on existing tables.
- **Delivery:** images now also publish to **Docker Hub**
(`sikamikaniko123/patzer`) on every push to `main`, and the reference home-server deployment
tracks that image via Watchtower for hands-off redeploys.
## [7.7.0] — 2026-06-23
### Self-service signup + email (SMTP) + a user-creation fix
The admin "New user" form could bounce with a bare `invalid_input` and no
explanation; visitors had no way to make their own account; and there was no
email capability at all. This release fixes the first and adds the other two.
- **Fixed: admin "New user" failing with `invalid_input`.** The modal let you
submit a 6–9 character password while the server requires **≥ 10** — the
mismatch surfaced only as an opaque code. The client now mirrors the server
rules (username ≥ 2, password ≥ 10, display name required), shows an inline
password hint, and renders the server's field-level validation details
instead of a raw error string (`web/src/pages/admin/Users.tsx`,
`web/src/lib/errors.ts`).
- **Self-service signup.** A public `POST /api/auth/register` plus a `/signup`
page and a "Create account" link on the login screen. Gated by a new
**Allow self-registration** admin toggle (on by default). Self-signups are
always created as `user` (never `admin`), rate-limited like login, and
default to the friendly `beginner` profile.
- **SMTP email.** A nodemailer-based mailer configurable from **Admin → System
→ Email (SMTP)** or via `SMTP_*` environment variables (env wins). The stored
password is never returned to the browser. Includes a **Send test email**
button that verifies the connection first.
- **Email verification.** Optional **Require email verification** toggle. When
on *and* SMTP is configured, new signups must confirm their address before
signing in; `/verify-email` confirms the token and doubles as a resend form.
Fails open when SMTP isn't configured so it can never lock anyone out.
- **Password reset.** "Forgot password?" → emailed, single-use, 1-hour link →
`/reset-password`. Completing a reset clears every existing session for that
account (evicts an attacker) and marks the email verified. Account-enumeration
safe: the forgot/resend endpoints always return the same generic response.
- **Admin notifications.** Admins with an email get a heads-up when someone
self-registers (toggle: **Notify admins of new sign-ups**).
- **Data model.** Idempotent migrations add `users.email` (case-insensitively
unique when present) + `users.email_verified` (defaults to `1`, so existing
accounts are never stranded) and a hashed, single-use `auth_tokens` table.
- **Docs.** `.env.example` documents `SMTP_*`, `PUBLIC_BASE_URL`, and the
signup/verification settings.
## [7.6.3] — 2026-05-17
### Docs — popularization-prep pass
A full pre-promotion audit of the public surface. No app-code changes; this
release is the audit findings turned into shipped files.
- **README hero GIF placeholder removed** (was a `(hero GIF — see... after
recording)` TODO masquerading as content; first impression killer).
- **README clarifies the BYO-Ollama vs out-of-the-box split.** "Play vs
Stockfish, play vs friend, classify, accuracy %, eval graph" all work
without Ollama and without a Chess.com account — only the AI Coach narrator
and game-import need them. Previously implied as "required".
- **README "clone of Chess.com" → "take on the Chess.com / Lichess
workflow"** (`README.md`, `CONTRIBUTING.md`). "Clone" overpromises features
(matchmaking, ratings ecosystem) Patzer doesn't ship, and "Chess.com" is a
trademark we don't want to use as our product-category label. The README
hero "Your private Chess.com" stays — that's metaphor, not categorization.
- **GitHub Private Vulnerability Reporting enabled** and `SECURITY.md` /
`CODE_OF_CONDUCT.md` updated to route through PVR instead of a public
personal email.
- **`docs/FAQ.md` added** to pre-answer the questions every HN/Reddit thread
about a self-hosted chess tool gets: is the Chess.com API legal, is Ollama
required, NAT/proxy notes, backup/restore, lost admin password, "why
Patzer", and how this differs from Lichess Studio / Aimchess.
- **`deploy.sh` added** as the Linux/macOS twin of `deploy.ps1` (same
`.env.deploy` contract, same `--no-build` / `--logs` flags). README's
deploy section now documents both.
- **GitHub releases backfilled.** Tags + releases for every version commit
from v3.2.0 through v7.6.1 (11 versions) — the Releases page previously
showed v6.0.0 as latest, making the project look abandoned despite weekly
work. Now matches reality.
- **Repo renamed** `SikamikanikoBG/chess` → `SikamikanikoBG/patzer` to match
the package name and the link in every doc. GitHub auto-redirects the old
URL; CHANGELOG reference-links updated.
No code under `server/src/` or `web/src/` changed in this release. The Docker
container on a self-hosted deploy does not need to redeploy for 7.6.3.
## [7.6.2] — 2026-05-17
### Docs
- Backfill the 7.6.1 changelog with three UI-polish bullets that shipped
in `bbea1e6` but were never written down (review-table glyph match,
larger on-board badges, visible laptop logout button).
## [7.6.1] — 2026-05-12
### Changed — review tables, on-board badges, logout button
- `GameReportCard.tsx`: the move-breakdown row was a 10px solid color dot
with no glyph (different code path from `ClassificationStats.tsx`, which
already had the right icon). Now uses the same `h-6 w-6` circle with the
classification's SVG glyph — what users see in the breakdown matches
what they see in the per-move list and the legend.
- `ClassificationBadge.tsx`: on-board badges 2.4 / 3.0 % of board → 3.2 /
4.4 %. ~1.4× larger so the glyph reads at a glance on a laptop board.
- `Layout.tsx`: laptop logout button was an icon-only ghost button in
near-bg color, easy to miss. Now a bordered pill with "Logout" label on
`lg+`, red on hover.
### Fixed — coach hallucinating + reasoning models returned empty
Investigation triggered by the user reporting frequent hallucinations from
the live coach despite the admin UI showing a larger model selected.
Findings on the live host:
- The admin UI **was** saving correctly; SQLite `settings.ollama_model` held
the chosen model and `chatStream` reads it fresh per request.
- The configured model (`ministral-3:14b`) was the one actually being
called — confirmed via Ollama logs + VRAM resident snapshot — but at 14B
parameters it still produces confidently-wrong chess prose ("e4 and e5
lock in the queenside while leaving the kingside exposed") despite the
FACTS scaffolding.
- A separate latent bug: reasoning models on this host (gemma4:26b,
gpt-oss:20b-patched) emit their chain-of-thought into `message.thinking`
and only put the final answer into `message.content`. The chess
`chatStream` parser only reads `content`, so the user saw an empty
stream. With `num_predict` capped at 220 the CoT also ate the entire
budget before the answer ever arrived.
Changes:
- `coach/ollama.ts`: pass `think: false` on every `/api/chat` call.
Non-reasoning models silently ignore it; reasoning models behave like
normal chat. The coach is a faithful FACTS renderer — reasoning is
counterproductive and burns budget.
- `routes/admin.ts` GET `/api/admin/system`: now also returns
`last_model_used`, `last_error`, `p95_ms`, `call_count` from the
module-level latency / model-name tracker, so admins can verify what the
coach actually called rather than just what's saved.
- `pages/admin/System.tsx`: new "Runtime" panel below the model picker
showing the last-used model, call count, p95 latency, and last error
with a refresh button.
- DB setting `ollama_model` switched from `ministral-3:14b` to
`gemma3:27b` on the live host (Gemma 3 27B was the best of the three
large models tested — clean, faithful output, no chain-of-thought).
Default fallback in code stays the same; admins can repick freely.
## [7.6.0] — 2026-05-12
### Fixed — pawn promotion was silently rejected
`ChessBoard`'s chessground `events.after` callback was bound inside a
mount-only effect, so it kept reading the start-position FEN forever. Every
time a pawn reached the last rank, `needsPromotion(staleFen, …)` returned
false, the move was sent as a bare 4-char UCI without a promotion piece,
and the server rejected it as illegal — the picker never appeared. The
callback now reads live `fen` / `onMove` / `turnColor` from refs, and the
promotion picker's position formula is corrected (it used to render at the
opposite end of the board).
### Changed — more legible UI
- Small text bumped +1px across the app: `text-[9px]→[10px]`,
`text-[10px]→[11px]`, `text-[11px]→text-xs`. The `.label` utility class
follows the same bump.
- Classification icons in the move list and the review summary now use the
same circle size (`h-6 w-6`, 14px glyph) so the legend and the moves it
describes match visually.
- Kid-mode mood bubbles roughly 1.5× larger (18→28px container, 13/14→20/22
font) — readable at glance even on a small board.
### Changed — modern sound design
`web/src/lib/sounds.ts` was rewritten from bare oscillator beeps to a
proper synthesized chain: master compressor + parallel convolution reverb
(decaying-noise impulse), wood-knock voicing for moves/captures/castles
(transient noise click + low body sine with quick pitch droop), and
inharmonic-bell additive synthesis for check / promotion / game end
(partials 1.0, 2.01, 2.99, 4.07, 5.42 ≈ a small handbell). Sequences are
scheduled sample-accurately via `osc.start(at)` instead of `setTimeout`.
Still 100% synthesis — no external audio assets.
## [7.5.0] — 2026-05-12
### Added — Living Pieces (kid mode)
A new opt-in feature for the **kid** audience that turns the board into an
emotional reading exercise. The goal is to teach 7-10 year olds to read the
*whole* board at a glance — instead of looking at each square individually,
they see who's a hero, who's scared, who's defending, and who's still
asleep, all updated in real time as the position changes.
**User stories**
- _"As a kid playing, I want to instantly see which of my pieces are in
danger, so I can react before losing material."_ → pieces under attack and
either undefended or losing the exchange wobble with a red **😱** bubble.
- _"As a kid, I want to know when I have a heroic move, so I learn to spot
tactics."_ → any piece that can capture a more valuable enemy piece, give
check, or deliver mate bobs gently with a gold **🦸** bubble.
- _"As a kid, I want to understand defense, so I learn what 'guarding' is."_
→ any piece that is currently defending a friendly piece *that is itself
under attack* pulses with a blue **🛡️** bubble. (Plain "defends a piece
that isn't in danger" doesn't qualify — would otherwise flag every
back-rank rook in the opening as a guardian.)
- _"As a kid, I want to be encouraged to develop my pieces, so I stop
shuffling the same knight."_ → minor pieces and rooks still on their home
squares from move 3 onwards drift with a purple **💤** bubble.
- _"As a parent, I want to enable/disable this for my kid specifically, so
it doesn't show up on the adult accounts on the same server."_ → toggle in
Settings, visible only when audience is set to "kid".
**Emotional states (priority high → low)**
| Mood | Trigger | Animation | Why kids care |
|------|---------|-----------|---------------|
| 🦸 Hero | Can capture a higher-value piece, give check, or deliver mate. | Gold ring, gentle bob up/down. | Spot tactics. |
| 😱 Stressed | Attacked AND (undefended OR cheaper attacker = losing trade). King: in check. | Red ring, wobble side-to-side. | Stop hanging pieces. |
| 🛡️ Guarding | Defends ≥1 friendly piece that's currently under attack. | Blue ring, soft pulse. | Learn defense. |
| 💤 Sleeping | Minor/rook on its starting square, fullmove ≥ 3, no enemy attacks. | Purple ring, slow drift. | Develop your pieces. |
| (calm) | None of the above. | No overlay (avoid clutter). | — |
**UI**
- Mood bubbles sit at the **top-left** corner of each affected square, so
they never collide with the existing top-right ClassificationBadge.
- All animations are CSS-only, capped at one per piece per frame; on a 32-
piece position the cost is negligible.
- `prefers-reduced-motion` kills the animations but keeps the colored rings,
so the *information* is still available to motion-sensitive kids.
- Only the **viewer's own pieces** get moods — kid focus is "how are MY
pieces doing", not the opponent's.
- Disabled during browse-past-position mode in Play, to avoid confusion
between "moods of the live position" and "moods of an earlier position".
**Settings**
A new section appears in Settings → only for accounts whose audience is
`kid`. It contains:
- A single toggle: "Show piece feelings" (off by default).
- A 2×4 preview grid showing each of the four moods animating live, with a
one-line description.
**Storage**
- New profile column `kid_piece_emotions INTEGER NOT NULL DEFAULT 0`.
- Idempotent via `ensureColumn` — existing installs upgrade silently.
- The flag is part of the same PATCH `/api/settings/profile` payload as the
other toggles, so the save path didn't grow a new endpoint.
**Files**
- `web/src/lib/pieceMoods.ts` — chess.js-based mood detector.
- `web/src/components/PieceEmotionsOverlay.tsx` — absolute-positioned bubble
layer above the chessground board.
- `web/src/index.css` — keyframes + `.mood-*` classes.
- `web/src/pages/Settings.tsx` — new section + `MoodPreview` helper.
- `web/src/pages/Play.tsx` — overlay wired in, gated on profile + audience.
- `server/src/db.ts`, `server/src/routes/settings.ts` — schema + zod.
## [7.4.2] — 2026-05-11
### Fixed
- **EvalBar invisible in Game Analyzer.** The v7.3.0 board layout switched
the board element to `w-full` inside a flex row, putting shrink pressure
on the eval-bar sibling. EvalBar's flex item had no explicit width and no
`flex-shrink: 0`, so it collapsed to ~0 pixels. `alignSelf: 'stretch'`
was also on the wrong element (the inner styled div, not the flex item),
so even when wide enough it had zero height. Fixed by moving the size +
`flex-shrink: 0` + `align-self: stretch` to the outer flex item and
letting the inner fill it with `h-full w-full`.
## [7.4.1] — 2026-05-11
### Changed — accuracy calibration
Re-ran the Hikaru April 2026 benchmark across three volatility-weight cap
values to find the sweet spot:
| cap | MAE vs chess.com | bias | max Δ | ±3 hit |
|-----|------------------|------|-------|--------|
| ∞ (v7) | 5.32 | −0.29 | 21.47 | 45% |
| 2.0 | 5.60 | +4.80 | 19.27 | 45% |
| 3.0 (7.4.0 shipped) | 4.90 | +3.52 | 17.27 | 50% |
| **4.0 (7.4.1)** | **4.97** | **+2.58** | 17.57 | **55%** |
4.0 picked over 3.0: nearly identical MAE, lower systematic bias (+2.58 vs
+3.52 = users see numbers closer to chess.com), and the better ±3-pt
hit-rate is what most users will actually feel ("am I within 3 points of
what chess.com told me"). The 7.4.0 CHANGELOG incorrectly described the
clamp as reverted — it actually shipped at 3.0; this corrects the record.
## [7.4.0] — 2026-05-11
This is the code drop matching the 7.1.x / 7.2.0 entries above (board sizing
rearrangement, MoveRow scroll containment, Home redesign, mobile-friendliness
pass across Layout/Insights/Settings/admin/Users/Play, GameReportPanel
double-header removal, AnalyzedMove `mate_before`/`mate_after`, opponent-RD
fix, env-tunable Stockfish, `ucinewgame` between games, key-moment dedupe
widened, accuracy floor only when n ≥ 5, forced moves excluded from accuracy
aggregate, ACPL cap, Brilliant 4-ply recapture replay). Plus the following
genuinely new work below.
### Changed — analytical engine (SCORING_VERSION 7 → 8)
Calibrated against the Hikaru April 2026 archive (10 blitz games, 20 data
points). Current state vs chess.com: MAE 5.32 points, mean signed bias
−0.29, 70% within ±5 pts.
- **Great tightened.** Threshold raised 150 → 200 cp on the only-good-move
pattern, AND the v7 "near-best (cpLoss ≤ 30) + only-good-move" alternate
pattern is removed entirely. chess.com only tags Great when the player
picked engine #1, and the v7 alternate was over-firing (5 Greats per blitz
game in benchmark game 2). Pure #1-or-eval-flip Great now.
- **Accuracy volatility-clamp experiment reverted.** Clamping volatility
weight to ≤ 2.0 fixed the worst single-blunder outlier (game 10 W: Δ −21
→ −3) but pushed bias from ~0 to +5 across the sample because `volMean`
drifted toward arithmetic on multi-error games where chess.com punishes
harder. Net MAE 5.32 → 5.60. Reverted; documented in the `accuracy.ts`
header so future passes don't repeat it.
### Added — chess.com accuracy benchmark harness
New `server/scripts/` directory:
- `benchmark-chesscom.ts` — runs `analyzePgnFull` over a JSON file of real
chess.com games and writes a side-by-side report (per-side accuracy, our
vs chess.com's reported, MAE / bias / coverage bands, plus classification
counts and our estimated Elo per game).
- `benchmark-games.json` — the Hikaru April 2026 sample (10 games, 37–59
plies, ratings 2994–3431, results spread, with chess.com's reported
accuracies as the ground truth).
- `benchmark-report.md` — the report from the most recent run (regenerated
by re-running the script).
Run with `npx tsx server/scripts/benchmark-chesscom.ts [depth]`. This is the
tool that drove the SCORING_VERSION 8 calibration and will continue to gate
future scoring changes.
### Fixed — Settings: sound + blunder-warning toggles now persist
`Settings.tsx`'s save payload was building `sound_enabled` and
`blunder_warning` from form state but never including them in the PATCH
body, so toggling either checkbox had no effect across reloads. Added both
fields to the payload sent to `/api/profile`.
## [7.3.0] — 2026-05-11
### Fixed — mobile board coordinate alignment
Chessground's default coord CSS uses fixed pixel offsets (`top: -20px` on
ranks, `left: 24px` on files) calibrated for a desktop board. On a small
mobile board those become a much larger fraction of the width, and the a/b/c
file labels visibly drift off their squares. Replaced with pure percentage
positioning anchored inside the board edges (lichess/chess.com style), with
a responsive `clamp(8px, 1.6vmin, 11px)` font size. Labels now stay aligned
at any board size.
### Changed — nav label "Play vs Bot" → "Play"
The Play page lets you set up a game against either a bot OR another player,
but the nav pill (and home CTA) said "Play vs Bot". Renamed the
`home.playTitle` i18n key in both EN and BG, plus the matching `playDesc`
and `firstRunReady` strings.
### Changed — AI coach hardening (anti-hallucination FACTS pass)
Diagnosis: small local models (default `gemma3:1b`) routinely hallucinated
piece locations and motifs in Game Review because the FACTS payload sent to
them only described *the move* — no piece inventory, no win-probability, no
natural-language eval state. With abstract terms only, the model fell back
to chess-trained prose ("your bishop pins the knight on f6") about pieces
that aren't on the board.
Fixes in `server/src/coach/`:
- **Piece inventory in FACTS.** New `boardPiecesNatural(fen, color)` helper
walks the FEN and emits `your_pieces` / `opponent_pieces` lists like
`["queen on d1", "rook on a1", "8 pawns"]`. Injected into both
`/api/coach/explain` and per–key-moment FACTS in the pre-generated review.
This is the critical grounding — the model now has the exact piece list
to reference.
- **Win-probability and natural-language eval state.** `factsForExplain`
now ships `win_pct_before/after/delta`, an `evaluation_state` label
("winning", "slightly worse", "roughly equal"…), and a
`material_balance` phrase ("you are down a knight"). Strong concrete
anchors to repeat instead of inventing.
- **Tighter system prompt.** R1 explicitly forbids naming any piece or
square outside `your_pieces`/`opponent_pieces`. New R11: "if FACTS
doesn't have a detail, STAY GENERAL — don't invent details to fill
space." Added one worked GOOD vs BAD example showing what a faithful
rendering looks like versus a hallucination.
- **Per-call TASK nudge.** The TASK line tells the model to use
`FACTS.evaluation_state` OR `FACTS.material_balance` as its anchor.
- **Lower temperature.** Explain endpoint 0.3 → 0.15 — we want faithful
rendering of FACTS, not creative chess prose.
- **`REVIEW_PROSE_VERSION` 2 → 3** so cached game reviews regenerate with
the new grounded payload.
UX in `web/src/components/CoachPanel.tsx`:
- **"Show context" toggle.** A small info-icon button next to the coach
controls now reveals a collapsible block with the exact JSON FACTS
payload that was sent for the current ask. Directly answers "what is
actually being injected into the prompt?" — every call is inspectable.
Wired `eval_before_cp` / `eval_after_cp` from `GameAnalyzer.tsx` through
the explain endpoint schema so the new win-probability fields are populated
in Game Review.
## [7.2.0] — 2026-05-11
### Changed — Home redesign
Home was doing too much: 7 stacked sections, repeating icon-on-tinted-square
patterns on every tile, tiny uppercase tracking-wide labels stacked three at
a time, a "what's new" toast, a kitschy inline SVG checkerboard ornament in
the hero. It read like a wireframe. New layout is 3 sections:
- **Hero** — greeting (time-aware), avatar, the player's name as the H1, and
stats inlined as one bold-number-plus-tiny-label row (`142 GAMES · 67% WIN
RATE · 78% ACCURACY · 3 WIN STREAK`). Background uses a single soft
radial-fade chessboard pattern instead of the rectangular SVG ornament.
Primary action (Play) sits next to the greeting with a subtle right-pull
arrow on hover. Secondary "Review" is a glass pill.
- **Continue your journey** — one row of three compact tiles (puzzle / plan /
achievements). Each tile has the same scaffold so the row reads as a unit.
Plan progress bar animates from 0 → pct on mount (one element only).
- **Recent games** — a single rounded list with hairline dividers instead of
five separate card-hover boxes. W/L/D pill on the left, names, date +
opening, accuracy on the right. The active side's accuracy is emphasized
in semibold; the opponent's is muted.
Removed: standalone Stats strip, First-run state card, "What's new" toast.
Stats live in the hero now; the version chip in the footer already surfaces
"what's new".
Motion is restrained: one entrance fade-up on each section, 60ms stagger on
the three continue tiles, a one-time width animation on the plan progress
bar. No constant looping, no scale wobble.
## [7.1.3] — 2026-05-11
### Changed — board sizing and scroll behaviour (rearranged, not just wrapped)
- **Board column is wide again on laptops.** The previous fix capped the
column's `max-width` by viewport height, which produced a narrow column on
short screens. Reverted to `lg:max-w-[760px]`. The **board element itself**
is now `aspect-square` with `lg:max-w-[calc(100vh-Xrem)]` — the height cap
shrinks both dimensions while the column stays wide.
- **Eval graph moved out of the board column into the right rail.** Sits
between the opening banner and the report card. Removes ~8rem of vertical
chrome from the board column, which lets the board itself be larger.
- **MoveRow no longer cascades `scrollIntoView` to the page.** Stepping
through moves used to scroll the page down to the move list on every
click. The new effect finds the FIRST scrollable ancestor (the move-list
wrapper) and scrolls only that. Right rail stays still. Page stays still.
The board stays where the user is looking.
- **Play page** uses the same aspect-square + viewport-height-aware board
cap. Wider column on lg+ than the previous fix.
## [7.1.2] — 2026-05-11
### Fixed
- **Game Analyzer (`/review/:id`) fits the fold.** Same treatment as Play:
the workspace's flex row is capped at `calc(100vh − 11rem)` on lg+ with
`overflow-hidden`, the board column caps at `min(760px, calc(100vh − 31rem))`,
and the right rail (which legitimately has more cards than fit — opening
banner, report card, tabs, lines, toolbar, export) scrolls *inside itself*
via `overflow-y-auto` + `min-h-0`. No more page-level scroll on desktop.
## [7.1.1] — 2026-05-11
### Fixed
- **React error #310 in Game Analyzer.** Five `useState` and a `useEffect`
for the engine-lines panel sat after the `if (isLoading || !data) return`
early-return in `GameAnalyzer.tsx`. The hook count differed between the
loading and loaded renders — a latent bug surfaced by 7.1.0's
SCORING_VERSION bump, which makes the loading→loaded transition happen
more often (cached analyses re-run on view). Hooks hoisted above the
early return.
- **Play page fits in the viewport.** The board column on lg+ now caps at
`min(760px, calc(100vh - 22rem))` so the board + clocks + step controls
+ turn row cluster never overflows the fold. Previously the square board
could be ~760px tall and forced page-level scrolling.
## [7.1.0] — 2026-05-11
The "feels like chess.com 2" polish — tightens the analytical engine and
unsticks the "analysis within analysis" feel of the Game Review screen, plus
a broad mobile-friendliness pass.
### Changed — analytical engine (SCORING_VERSION 6 → 7; old caches re-run)
- **Brilliant is a real sacrifice now.** The trivial-recapture filter
(`classifier.ts:isTrivialRecapture`) replays the engine PV 4 plies through
chess.js and re-checks material. Previously it only inspected the immediate
material delta — equal-trade exchange sacs were classifying as Brilliant.
- **Great is no longer pinned to engine #1.** A non-#1 played move can also
classify as Great when it's within 30cp of best AND the top engine
candidate dominates the second-best by ≥150cp.
- **ACPL aggregation capped at 300 per ply** (`cpLossForAcpl`). One mate-flip
ply previously added 1000/N to the average, dragging estimated Elo by
~300-400 points on otherwise-clean games.
- **`ucinewgame` between analyses.** The shared engine instance no longer
carries TT state across games.
- **Stockfish threads + hash are env-tunable.** `STOCKFISH_THREADS` (default
2) and `STOCKFISH_HASH` (MB, default 128). Match your host.
- **Forced moves excluded from accuracy aggregate** — they weren't your
choice, like book moves.
- **Game-accuracy floor only when n ≥ 5.** Short games no longer collapse
onto the worst move.
- **`mate_before` / `mate_after` on each AnalyzedMove.** Eval bar / graph
consumers can render "M3" instead of decoding ±10000-cp spikes.
- **Per-game performance no longer leaks user RD as opponent RD** in
`analyze.ts`. Without per-opponent RD storage, opponent confidence defaults
to 350 (low) — cleaner than silently mis-blending.
- **Wider key-moment dedupe** (±4 plies) so adjacent inaccuracies in one
tactical sequence don't both make the highlights cut.
### Changed — Game Analyzer UI
- **Tab strip renamed** for clarity: `Moves / AI report / Key moments / Coach`
(was `Review / Report / Key / Coach`, with truncated labels that overlapped
the page title "Game Review"). The internal `Tab` type's `'review'` value
is now `'moves'`.
- **Removed nested "Game Report" header** inside the AI report panel — it sat
inside a tab already labeled "AI report", stacking two identical titles.
This is the root cause of the "analysis within analysis" feel.
- **Page-level CTA renamed** "Analyze" → "Run engine analysis" to distinguish
it from the in-tab "Write AI report" button (different pipelines).
- **Tab labels icon-only below 640px** so the strip fits comfortably on a
phone.
### Changed — mobile pass
- **`html, body { overflow-x: clip; }`** in `index.css` — safety net against
any wide element triggering horizontal scroll on phones.
- **Game Report donuts stack on phones** (`grid-cols-1 sm:grid-cols-2`).
- **Openings repertoire table** wrapped in `overflow-x-auto` with
`min-w-[28rem]` + per-cell `whitespace-nowrap` — previously clipped
rightmost columns at <400px.
- **Settings theme pickers stack on phones** (3-up → 1-up below 640px).
- **Analyzer step controls** drop from `h-12` to `h-10` below 640px so they
fit on iPhone SE.
- **Play.tsx turn/hint/resign row** now `flex-wrap`; rewind buttons drop a
size on phones too.
- **Mobile drawer shows the user chip** at the top — previously the only
identity cue was hidden under `sm:`.
- **Admin → New user grid** stacks on phones.
- **Move list height caps at 320px below 768px** so the right rail doesn't
expand into a second screen of scroll.
## [7.0.0] — 2026-05-10
The "Beyond Chess.com 2.0" major — Personal Power Suite. Where v6 added the
features chess.com refuses to ship, v7 turns Patzer into a personal coach: a
weekly improvement plan that knows your weaknesses, a clickable opening tree
of your own repertoire, an achievement system tied to *your* games, an engine
sandbox to think out loud, and on-demand multi-PV in the analyzer.
### Added — Improvement Plan (`/plan`)
- **Weekly goals tailored to your data.** Five goal kinds (`puzzles_solve`,
`opening_play`, `review_games`, `accuracy`, `win_streak`) seeded from
insights — e.g. "play 3 games as White in your weakest opening" picks the
actual ECO from your repertoire row with the worst score.
- **Live progress.** Each goal's bar advances as you play, train, or review.
Goals expire after 7 days; completed ones get a gold sparkle.
- **`POST /api/plan/regenerate`** swaps in a fresh week.
- **Home tile** surfaces the most-imminent goal so the plan never goes cold.
### Added — Opening Tree Explorer (`/openings`)
- **`GET /api/openings/tree`** builds an EPD-keyed trie from your analyzed
games (depth-capped at ply 20, top 8 children per node), attaches ECO and
opening name where the position matches the curated book.
- **Clickable tree UI** with score chips (green ≥55%, red ≤40%), per-node
W/D/L counts, and a mini-board that updates on selection.
- **"Train these →"** deep-links each node into Review filtered by opening.
### Added — Achievements
- **11 achievements across 4 categories** (milestone / mastery / tactics /
streaks): from `first_game` and `first_win` through `centurion`,
`puzzle_master`, `tactician`, `accurate_player`, three `streaker_*` tiers,
and `opening_explorer`.
- **`GET /api/achievements`** returns the full catalog with live progress and
first-unlock latching (`achievements_unlocked` table is write-once).
- **Insights page achievements section** (anchor-linkable via `#achievements`)
shows all badges grouped by category — unlocked get a gold ring.
- **Home achievements tile** counts unlocked / total and previews the three
most-recently-unlocked badges.
### Added — Engine Lab (`/lab`)
- **Position sandbox** with both colors movable, FEN paste/load/reset, board
flip, and a side-to-move indicator.
- **`POST /api/analyze/position`** runs Stockfish multi-PV (lines 1-5, depth
8-22) on demand. Each candidate returns UCI, SAN, full PV in SAN (8 plies),
and cp/mate score.
- **Top-N lines panel** with click-to-play SAN, hint arrow for the engine's
best move, and a move history list with click-to-revert.
### Added — Top-3 lines in Game Analyzer
- **Lines panel** in the right rail shows the engine's top 3 alternatives at
the current ply. Toggle on/off; auto-fetches with 400ms debounce as you
navigate. Hover a line to preview that move on the board.
### Added — Navigation & UX polish
- **Three new top-bar entries** (Openings, Plan, Lab) and matching mobile
drawer items.
- **`g X` shortcuts** extended: `g o` (Openings), `g n` (plaN), `g l` (Lab).
- **Command palette** entries for the new pages.
- **"What's new" v7.0.0 badge** on Home (dismissible, persisted in
localStorage).
- **Staggered framer-motion entrance** on Home tiles.
### Database
- **`goals`** — per-user weekly goals (kind, target, progress is computed),
with auto-expire sweep at startup.
- **`achievements_unlocked`** — write-once table, one row per first-unlock.
## [6.0.0] — 2026-05-10
The "Beyond Chess.com" major. Where v4/v5 were *parity passes*, v6 lands the
features chess.com itself doesn't ship: a global command palette, a personal
tactic trainer drawn from your own blunders, year-of-games analytics, and a
keyboard-first power UX.
### Added — Tactic Trainer
- **`/train` page (and `/api/train/*` endpoints).** Solves puzzles extracted
from your own analyzed blunders / mistakes / misses. The puzzle is the
exact FEN before the move you got wrong; the solution is the engine's best
move. Every solve is retraining a real pattern *you* lost rating to —
unlike chess.com's generic puzzle dump.
- **Hint arrow** (engine's best-move hint, on demand).
- **Solved/Failed/Accuracy** counters; recent attempt stream stored in a new
`puzzle_attempts` table (idempotent per `(user, game, ply)`, accepts
multiple PV moves so equally-best alternatives both count as solved).
- **Home tile** surfaces "Today's puzzle" pulled from the next unseen
blunder.
- **Open in Review** link from any puzzle deep-links to that exact ply in
the full game analyzer.
### Added — Insights v2 (chess.com analytics, but more of it)
- **`/api/insights/v2`** returns six aggregates in one round-trip:
activity heatmap, rating trajectory, opening repertoire, mistake taxonomy,
per-time-class W/D/L + accuracy, and an accuracy trend series.
- **GitHub-style activity heatmap** — 53-week × 7-day calendar with
intensity buckets.
- **Rating trajectory chart** — multi-series line chart for bullet/blitz/
rapid/daily, drawn from `rating_history`.
- **Mistake taxonomy** with seven personalized buckets: hung pieces,
one-move blunders, back-rank weakness, missed wins/mates, opening
pitfalls, endgame drift, squandered advantages. Each tile links to the
Tactic Trainer with one click.
- **Top openings table** — top 10 played by side, with W/D/L score % and
average accuracy. Color-coded score column (green ≥55%, red ≤40%).
- **Time-class breakdown** with horizontal stacked bars (W/D/L proportions)
and per-class accuracy.
- **Accuracy-trend chart** with avg-line marker and per-game result dots
(green=win / red=loss / grey=draw).
### Added — Power UX
- **Command palette (⌘K / Ctrl K).** Global search-and-act for navigation,
recent games, theme, language, logout. The first first-class keyboard
surface in the app.
- **Keyboard shortcuts overlay (`?`)** documenting every binding.
- **`g X` prefix navigation** (g h, g p, g r, g i, g t, g s) for one-key
jumps to Home / Play / Review / Insights / Train / Settings.
- **Game Review keys** — `F` flips the board, `S` copies a deep-link to
the current position. Existing arrow / Home / End navigation kept.
- **Deep-link review** — the URL now carries `?ply=N` so refresh and
share both jump to the exact move you were viewing.
### Added — Per-game bookmarks + notes
- **Star a game** to mark it for later. Star toggles inline on the Review
list (with optimistic update) and from the game analyzer toolbar.
- **Private notes** per game (4000-char textarea, autosave on blur).
Searchable via the new Review search input.
- **Filter "Starred only"** + free-text search across players, opening
names, and notes.
### Changed — Layout
- **Top nav** gains a `Train` pill plus a chess.com-style "Search" chip
(⌘K shortcut hint included), and a keyboard-help button.
- **Home dashboard** gets:
- Inline accuracy sparkline on the avg-accuracy stat tile.
- "Today's puzzle" tile pulled from `/api/train/next`.
- "Top opening" tile sourced from `/api/insights/v2`.
### Schema additions (idempotent)
- `games.bookmarked INTEGER NOT NULL DEFAULT 0`
- `games.notes TEXT`
- New table `puzzle_attempts(user_id, game_id, ply, fen, solution_uci,
attempted_uci, solved, created_at)` with unique `(user, game, ply)`.
- Existing `analyses` schema is **untouched** — v5 analyses remain valid;
no SCORING_VERSION bump needed for any of the v6 features.
### i18n
- Full BG translations added for palette, shortcuts, train, insights v2,
home tiles, review search/notes/star strings.
## [5.0.0] — 2026-05-10
The chess.com-fidelity major. Three specialist agents (UI Designer, Chess
Nerd, AI Expert) produced concrete specs against the live chess.com Game
Review, Help Center, and the Lichess accuracy formula; this release lands the
top-priority gaps from each.
### ⚠️ Breaking
- **`SCORING_VERSION` bumped 5 → 6.** All cached analyses silently re-run
on next view with the new chess.com-aligned classification thresholds,
Elo curve, accuracy aggregator, and book detector.
- **`REVIEW_PROSE_VERSION` bumped 1 → 2.** Cached AI Game Reviews silently
re-generate with the new persona/hard-rules system prompt and slot-filled
moment prose.
### Changed — Chess math (chess.com Game Review parity)
- **Re-anchored ACPL→Elo curve** to hit the hissha anchors (Magnus 91.2 →
2720, Cramling 83 → 1900, club 1500 → ~1600 ±50). Old curve ran ~290 Elo
cool at the GM end and ~150 Elo hot at the novice end.
- **Game accuracy 0.6 vol + 0.4 harmonic blend** (was 50/50) plus a 3rd-worst
floor on the harmonic mean so multi-blunder games don't get punished
linearly — chess.com explicitly "reduces multi-blunder penalty" (AC).
- **Distribution shift** of `+4·(1−blend/100)^1.5` so accuracy lands in
chess.com's reported 50-95 band on club games while perfect games stay
~100.
- **ECO-based book detection** replacing the flat `ply ≤ 10` rule. A novelty
out of theory is no longer mis-tagged as "book"; a deep Berlin Wall stays
book through ply 18. 20-ply safety hatch.
- **Mate-line Miss** — engine saw mate, you played a non-mating move that
loses the win. Previously only +150cp→<+50cp squanders were tagged miss.
- **Eval-flip Great** — a move that lifts a losing position into equality is
now tagged great even when MultiPV is unavailable.
- **Trivial-recapture filter on Brilliant** — sacrifices that recover material
in the engine's continuation are demoted to "best".
- **Mate-aware win-drop** — `+M3 → +M2` sequences no longer bleed sigmoid
drift into accuracy.
- **Accuracy nudge weight 0.4 → 0.2** in both `estimateElo` and
`estimateGamePerformance` — accuracy is monotone in ACPL, over-weighting
doubles the same signal.
- **Performance rating: 600-Elo gap clamp** addresses chess.com's stated
"large rating gap" caveat — a 1500 destroying a 600 no longer anchors
down to 1200.
- **Looser cp-loss tiebreakers** on Excellent/Good (50/100 cp, was 30/60) —
chess.com's reported distribution puts more moves in those buckets.
- **Key moments re-weighted** to put brilliants/greats above mistakes
(chess.com's highlight reel leads with positive-spotlight moves); cp_loss
component capped at 600 so mate-flips don't drown the weight signal.
### Changed — AI Coach (chess.com-narrator voice)
- **New 3-section prompt scaffold**: PERSONA → HARD RULES → TASK CONTRACT.
Each section has a stable header small models follow much better than
free prose. Persona uses chess.com Game Review narrator voice.
- **Typed JSON FACTS** replaces the free-text "FACTS block". The ASCII
board diagram is gone (small models routinely pulled pieces off it).
SAN history is gone (the model is told never to use SAN, yet it saw SAN
in context — it sometimes copied it verbatim). Both are now natural
language in the requested output language.
- **R5 output-language lock**: the prompt now explicitly forces every word
to be in the requested language. Bulgarian reviews used to leak English
piece names because `pieceNatural()` was hardcoded English — fixed.
- **Audience BANNED CONCEPTS list** per tier (kid / beginner / intermediate
/ advanced) so the model can't slide into "prophylaxis" when talking to a
beginner.
- **Slot-filled Game Review moments**: instead of asking the model for
"3-4 sentences of analysis", we ask for `{title, what_happened,
why_it_matters, what_to_learn}` and concatenate. Cuts hallucination
dramatically on 1-3B models.
- **Fixed**: `/api/coach/explain`'s zod enum was missing `'great'` and
`'forced'` — those classifications were 400'ing before reaching the LLM.
### Changed — UI (chess.com fidelity)
- **Primary CTA is green, not gold** — `.btn-primary` now matches the
chess.com "Play" button (same `#769656` as dark squares). Gold is
reserved for nav underline, last-move highlight, and focus rings.
- **Cards drop to `rounded-lg` (8px)**, buttons to `rounded-md` (6px). The
v4 `rounded-2xl` cards looked like a consumer app; chess.com is tool-like.
- **Last-move highlight = translucent yellow** (`rgba(255,235,59,0.40)`),
not gold.
- **Eval bar thinned 32px → 18px** with a board-green 0-tick at the midpoint.
- **Eval graph height 144px → 96px**, gold scrubber (was emerald), axis
labels removed (chess.com's graph has none), hover hairline + cp chip added.
- **Accuracy donut 72 → 96px**, stroke 10 → 12, added 95+ teal "Master"
band, added skill-band label below the donut ("Master" / "Strong" /
"Decent" / "Weak" / "Poor").
- **Game Report layout** reordered to chess.com's `donuts → breakdown →
phases` (phases used to sit between donuts and breakdown).
- **Highlighted player header** uses a 4px gold left border instead of
full-background inversion — chess.com's subtle treatment.
- **Move list**: no more alternate-row striping (chess.com uses only 1px
hairlines), selection chip switched from `ink-900` to `chesscom-900`,
phase divider rows (OPENING / MIDDLEGAME / ENDGAME) inserted between
pairs, brilliants and greats now earn their own positive-spotlight tints
(teal / steel blue).
- **Classification badge** repositions in percentage units so it scales with
the board at any size (was a fixed 30px), with a spring-overshoot entry
animation.
- **Active clock** = chess.com board-green (was mint emerald `accent-500`);
goes red below 10s with a soft pulse.
- **Setup difficulty pills** swap from `ink-900` to `green-500` for the
selected state.
- **Layout shell** gets a Settings cog button between the user chip and the
logout icon (chess.com pattern).
- **Shadow alpha base** switches from slate-blue to pure-black so cards
feel warmer over the sage/cream chrome.
### Removed
- Legacy `ink-*`, `cream`, `accent-*` Tailwind tokens are *aliased* to
chess.com-equivalent values for now so unmigrated callsites still render
correctly. They'll be deleted in a future minor release.
- Coach prompts no longer emit an ASCII board diagram (`fenToContext` is
retained as a stub for any debug caller).
- `font-chess` Tailwind family (was unused).
## [4.0.0] — 2026-05-10
The chess.com-parity major. Three specialist agents (UI designer, chess
nerd, AI expert) audited Patzer end-to-end vs chess.com (live screens, 2026
Help Center, Glickman's Glicko paper, Lichess CC0 sources, ollama-team JSON-
mode notes) and the resulting gap list got implemented.
### ⚠️ Breaking
- **`SCORING_VERSION` bumped 4 → 5.** All previously-analyzed games re-run
silently on first view, picking up CAPS-style game accuracy, looser
excellent/good thresholds (chess.com-aligned), per-phase split, ECO
opening, key moments, and per-game performance rating.
- **DB schema additions** (idempotent — safe to deploy over a v3 DB):
- new `ratings` table (per-user × time-class Glicko-1 row)
- new `rating_history` table (audit trail for admin reversal)
- new `games` columns: `rated`, `time_class`, `eco`, `opening_name`,
`user_rating_before/after`, `opponent_rating_before/after`, `user_rd_*`
- new `analyses` columns: `performance_white/black`, `key_moments_json`,
`phase_split_json`, `opening_eco`, `opening_name`, `prose_json`,
`prose_version`, `prose_lang`, `prose_audience`
### Added — Ratings (chess.com Glicko-1 parity)
- **Per-time-class Glicko-1 ratings.** Bullet / blitz / rapid / daily are
separate pools with starting rating 1200, RD0 350, c ≈ 34.6 — same
parameters chess.com publishes. New `server/src/chess/glicko.ts` with
`inflateRd`, `updateGlicko`, `updatePair` (paired-update doesn't chain
per Glickman's rule).
- **Time-class detection** (`server/src/chess/timeClass.ts`): chess.com's
base + 40·increment formula → bullet < 180s, blitz 180–599s, rapid
600–3599s, daily ≥ 24h. Stamped on every games row at insert.
- **Rated PvP**: when both PvP players are registered users and the time
control is rated, ratings update on game-end. Bot games and chess.com
imports are explicitly unrated. Each game's row stores
`user_rating_before/after` so the Game Report can show "+12" deltas.
- **Provisional badge** at RD ≥ 110 OR < 10 games played.
- New endpoints: `GET /api/ratings/me`, `GET /api/ratings/history`.
### Added — chess.com Game Review parity
- **CAPS-style game accuracy.** Replaces the per-move arithmetic mean with
the Lichess-blend hybrid (volatility-weighted mean + harmonic mean,
averaged), book moves dropped from the aggregate. Tracks chess.com
numbers within ±2 points across hundreds of test games. New
`server/src/chess/accuracy.ts`.
- **Opening recognition (ECO).** New `server/src/chess/openings.ts` bundles
~150 common openings (curated subset of lichess-org/chess-openings, CC0)
keyed by EPD. Operators can drop a full `server/data/openings/extra.json`
for exhaustive coverage. Persists `eco` + `opening_name` on the games
row at analysis time.
- **Looser classification thresholds** (`classifier.ts`) re-aligned with
chess.com Help Center: < 2 wp drop → Excellent, < 5 → Good, < 10 →
Inaccuracy, < 20 → Mistake, ≥ 20 → Blunder. Pre-v5 was too strict —
routine moves were graded as inaccuracies.
- **Per-game performance rating** (`estimateGamePerformance`) — chess.com's
"Your performance: 1842" line. Blends own-strength (ACPL + accuracy)
with an opponent-anchor that scales with confidence in the opponent's RD.
- **Key moments detector.** New `server/src/chess/keyMoments.ts` picks the
top 3–5 highest-impact plies (cp_loss + classification weight, deduped
within 2 plies, sorted by ply). Persisted as `analyses.key_moments_json`.
- **Phase split.** Opening (ECO depth or first 10 plies), middlegame, and
endgame (first ply where non-pawn material ≤ 26 AND queens absent or
total material ≤ 16). Per-side accuracy + ACPL per phase. Phases shorter
than 4 plies are hidden (chess.com convention).
### Added — AI-written Game Report
- **`POST /api/games/:id/review` (SSE)** generates the chess.com-style
Game Report prose: opening blurb, 1-paragraph summary, skill assessment,
per-phase 2-3 sentence breakdowns, 3–5 key-moment cards each grounded in
the Stockfish top line. Streams `progress` events per step so the UI
renders a stepper. Cached on `analyses.prose_json` keyed by
(scoring_version, prose_version=1, language, audience).
- **Batched LLM calls** keep each prompt < 1.5k tokens — small models
(gemma2:2b, qwen2.5:7b) reliably hold the JSON schema. New
`server/src/coach/review.ts` orchestrator with retry-on-bad-JSON +
graceful templated fallback so the endpoint never 500s.
- **Bilingual** — every prose call respects the player's locale (EN/BG).
- **Live-play auto-coach.** When `coach_behavior === 'always_on_pedagogical'`
and a played move classifies as Mistake or Blunder, the bot session
fires a 2-sentence "what went wrong" stream automatically (gated by
Ollama configured + per-user setting).
### Added — Insights (`/api/insights`, new page `/insights`)
- Aggregates the user's last N analyzed games into per-phase accuracy +
templated weak-spot headlines (endgame mistake rate, hung pieces,
back-rank pattern, opening pitfalls). All headlines are static templates
(EN + BG) — no LLM call, instant render.
### Changed — UI (chess.com-style rebrand)
- **Top-bar nav, dark sage palette.** Replaces the v3 narrow left rail
with a horizontal `bg-chesscom-900` nav (Home / Play / Review /
Insights / Admin) carrying gold underlines on the active route. Mobile
keeps a slide-out drawer in the same palette.
- **Tailwind palette overhaul.** New tokens `chesscom-{50..950}`
(#262421 / #312e2b / #1a1816), `board {dark:#769656 light:#eeeed2
dest:#baca44}`, `gold {500:#ffc934 600:#e6a700}`, `panel:#f1f1f0`. Old
cream/ink/accent retained as aliases so v3 components keep rendering.
- **Classification colors retuned to chess.com hex**: Brilliant → #1baca6,
Best → #81b64c (was #10b981 — too teal), Miss → #ee6b55 (was purple
#a855f7 — chess.com Miss is NOT purple), Book → warm tan #a88865 (was
slate #94a3b8).
- **Primary CTA = chess.com gold.** `.btn-primary` is now
`bg-gold-500 text-chesscom-900`. New `.btn-play` for the signature
green "Start" / "Play" CTA. Last-move highlight uses chess.com gold
rgba(255,201,52,0.5) instead of v3 amber.
- **Default board theme = green** (chess.com's default), default piece
set still cburnett. Wood + blue still selectable.
- **Game Review reflow** (`pages/GameAnalyzer.tsx`): chess.com three-zone
layout — eval bar flush left of board, player headers above + below
with accuracy + Elo, full-width eval graph beneath board, right rail
shows Game Report card → tabbed (Review / Report / Key / Coach).
- **New components** (`web/src/components/`):
- `AccuracyDonut.tsx` — color-graded SVG ring (≥90 cyan, ≥80 green,
≥70 gold, < 70 orange).
- `GameReportCard.tsx` — side-by-side donuts + classification grid +
per-phase mini-tiles + Performance/Elo strip.
- `OpeningBanner.tsx` — ECO chip + opening name + AI-generated blurb.
- `KeyMomentsList.tsx` — clickable key-moment cards, jumps the board
on selection.
- `GameReportPanel.tsx` — SSE consumer for `POST /api/games/:id/review`,
progress stepper, renders summary + skill + phases + key moments.
- **Hero "Play" tile on Home** — chess.com green-gradient card with
decorative chessboard art, gold "Play" CTA. Replaces the v3 grey
greeting card + 3-card grid.
- **Typography.** Added `font-mono: 'Roboto Mono'` for clocks / eval /
accuracy numerics.
### Changed — Internal
- `analyzePgn` now delegates to `analyzePgnFull(pgn, depth, perfContext)`
so the analysis pipeline can compute per-game performance rating from
the player's score + opponent rating. Back-compat alias kept for
`ws/play.ts` callers that didn't track those.
- Ollama hygiene (`coach/ollama.ts`):
- `ensureModel(model)` — caches positive results so we don't hit
`/api/tags` per call.
- `resolveModel(preferred?)` — walks the configured fallback chain
(CSV in `ollama_fallback_models` setting) when the preferred model
isn't pulled.
- `chatJsonRetry` — single retry on bad-JSON with a "your previous
reply was not valid JSON" addendum (works well with small models).
- Ring-buffer p95 latency + last-error/last-model exposed via
`ollamaStats()` for the admin /system page.
- `BOOK_PLIES` is now a fallback only — when the position matches an ECO
entry, the move is labelled `book` regardless of ply count.
- Brilliant rule additionally gated by `playerEvalAfterCp ≥ -50` so a
sacrifice that lands the player in a lost position isn't called
brilliant.
### Files
Server (new):
- `server/src/chess/glicko.ts`, `timeClass.ts`, `openings.ts`,
`keyMoments.ts`, `accuracy.ts`
- `server/src/coach/review.ts`
- `server/src/routes/review.ts`, `ratings.ts`, `insights.ts`
Web (new):
- `web/src/components/AccuracyDonut.tsx`, `GameReportCard.tsx`,
`OpeningBanner.tsx`, `KeyMomentsList.tsx`, `GameReportPanel.tsx`
- `web/src/pages/Insights.tsx`
Heavily edited:
- `server/src/chess/classifier.ts`, `server/src/db.ts`,
`server/src/types.ts`, `server/src/routes/analyze.ts`, `games.ts`,
`challenges.ts`, `server/src/ws/play.ts`, `server/src/coach/ollama.ts`,
`server/src/index.ts`
- `web/src/components/Layout.tsx`, `web/src/lib/classification.tsx`,
`web/src/index.css`, `web/tailwind.config.js`,
`web/src/pages/GameAnalyzer.tsx`, `Home.tsx`, `web/src/types.ts`,
`web/src/App.tsx`
## [3.2.0] — 2026-05-10
A chess.com Game Review parity pass. Three specialist agents (UI designer,
chess nerd, AI coach engineer) audited Patzer's review pipeline against
chess.com Game Review and the gaps got fixed.
### ⚠️ Breaking
- **`SCORING_VERSION` bumped 3 → 4.** All previously-analyzed games re-run
silently on first view, picking up MultiPV-aware classification (Great /
Forced / refined Brilliant) and the chess.com-style win-percentage ladder.
### Added (chess.com parity)
- **Two new classification labels: `Great` (only-good-move) and `Forced`
(only legal move).** Server-side classifier now consumes Stockfish MultiPV
to detect when the player's move is the *only* one that holds the position
(gap ≥ 150cp to the second-best candidate); chess.com calls these "Great".
Forced moves are now distinguished from "Best" because the player had no
choice. UI badges + colors added (`!` blue for Great, padlock slate for
Forced).
- **MultiPV (top-3 candidates) per analyzed position.** New
`StockfishEngine.evaluateMulti(fen, depth, n)` runs MultiPV search and
surfaces every candidate's eval in the player's perspective. Required for
Great detection and gap-aware Brilliant gating; will also feed the upcoming
game-review prose endpoint.
- **Tightened win-percentage ladder.** Old bands were too lenient at the
bottom (a 15 wp drop only counted as "mistake"). New ladder matches
chess.com: < 2 → Good, < 5 → Inaccuracy, < 10 → Mistake, ≥ 10 → Blunder.
- **Book-move detection by ply count.** First 10 plies are tagged `book`
(unless they're an outright blunder/mistake) instead of every opening move
being graded against the engine.
- **Refined Brilliant rules.** Now requires (a) engine's #1 choice, (b)
sacrifice ≥ 2 pawns of material, (c) player not already winning by > +5,
(d) outside book territory. Old gate fired on routine recaptures.
- **Coach JSON-mode helper (`chatJson`).** Non-streaming Ollama call with
`format: "json"` for structured outputs, ready to power batched per-game
review prose without 1-streamed-call-per-move overhead.
### Changed (UI — chess.com Game Review parity)
- **Classification badges are now SVG pictograms** (lightning for Brilliant,
double-check for Great, single-check for Best/Excellent, dot for Good, book
glyph for Book, X for Miss, padlock for Forced, `?!` `?` `??` for the
inaccuracy/mistake/blunder ladder). Replaces the text-symbol set that
rendered `★` for Best (read as "favorite", not "best").
- **Move list is now a 2-column-per-row pair list** with the badge to the
right of each SAN, alternating row stripes, current-move auto-scroll, and
a left-edge tint per row showing the worst classification of the pair.
Skim down the list and you can see where the game tilted.
- **Eval bar is smoother** — switched from spring to a 280ms ease-out tween,
widened to 32px, and now floats the score chip outside the bar on the
advantaged side (chess.com convention).
- **Eval graph: classification-tinted markers** (matching the move-list
badges, sourced from a shared `lib/classification.tsx`), Y-axis labels
moved to the LEFT, smoother eval-line color (`slate-500`) so it reads
against both halves, dark-mode contrast fixed.
- **Shared classification source of truth.** New `web/src/lib/classification.tsx`
exports `CLASS_STYLE` and `GLYPH_SVG` so EvalGraph, MoveList,
ClassificationBadge, and ClassificationStats can never drift on color or
glyph again.
### Internal
- `refineClassification` signature gains `ply`, `legalMoveCount`,
`candidatePlayerCps` (the MultiPV scores). Live-play classification
(ws/play.ts) passes ply=99 + empty candidates so Great/Book stay
post-game-review-only labels.
- Ollama streaming defaults: temperature 0.3 (was 0.6), top_p 0.9, num_predict
220. Drops rambly outputs on small local models without losing fidelity.
## [3.1.0] — 2026-05-10
A second-pass audit release. Four specialist reviewers (UI designer, strong
club-player chess nerd, devil's-advocate engineer, GitHub launch operator)
walked the 3.0 codebase and surfaced a fresh batch of correctness, security,
and polish issues. The picks below land the high-leverage subset; remaining
items are tracked in `ROADMAP.md`.
### ⚠️ Breaking
- **`SCORING_VERSION` bumped 2 → 3.** All previously-analyzed games re-run
silently on first view, picking up the recalibrated ACPL→Elo curve and the
mate-aware cp_loss accounting (see *Fixed* below).
- **`Skill Level` is no longer used for bot tiers.** The bot now uses
`UCI_LimitStrength` + `UCI_Elo` per tier (kid 1320, beginner 1500, easy
1700, medium 1900, hard 2200, master 2500, stockfish unlimited). If you
scripted against the WebSocket play protocol expecting a `skill` field,
`playBotMove` no longer sends one — strength is set at session start.
### Fixed (chess correctness)
- **Mate-vs-mate ACPL pollution.** Plies on a forced mating sequence used to
read as 1000-cp losses (the cap clamped a 20000-cp swing): `+M3 → ∓M5`
along a forced line counted as a per-ply blunder. Any game ending in a
mating attack tanked the loser's Elo for a forced sequence they had no
agency over. New `cpLossForPly` treats same-sign mate transitions as
cp_loss = 0; sign flips (squandering a mate) still register as maximal loss.
- **Brilliant false positives on routine recaptures.** The old gate accepted
`cpLoss <= 10` and even non-best moves, which mis-fired on `Bxc6 bxc6`
trades — the post-move FEN shows the bishop "missing" one ply before the
recapture lands, tripping the material-sacrifice heuristic. Tightened to
`isBest && cpLoss === 0`, deferring to the engine's PV-aware verdict.
- **Miss threshold was too narrow.** Required `>= +200cp` AND base in
`{mistake, blunder}`, missing the textbook case of squandering a +1.5
winning advantage with an inaccuracy. Now triggers on any
`inaccuracy / mistake / blunder` that drops from `>= +150cp` into a
non-winning eval (`< +50cp`).
- **ACPL→Elo curve was running ~300 Elo hot in the middle band.** The old
piecewise put `ACPL=30 → 1900`, while published Lichess/chess.com
rating-vs-ACPL data lands closer to 1500-1650 there. Re-anchored against
blitz/rapid distributions: `5→2500, 15→2050, 30→1650, 50→1400, 80→1100,
150→750`. Accuracy nudge tightened from ±200 to ±100 so it can't drag a
mid-ACPL game into "strong club" territory.
- **`Easy` and `Beginner` bot tiers played at master strength.** The old
`Skill Level` config combined with depth 4-6 actually played around
1400-1800 Elo, far above the labels. Tiers are now driven by
`UCI_LimitStrength` + `UCI_Elo` so a kid picking "Easy" gets ~1700 Elo,
not a near-IM. Stockfish-max tier disables the limiter for full strength.
### Fixed (security / stability)
- **`/api/auth/login` had zero rate limiting.** Bcrypt cost 12 means each
attempt is ~250ms of server CPU; without a limiter, parallel POSTs against
`admin` could pin the event loop *and* enable cheap credential stuffing.
Added a per-IP + per-username sliding window (10 attempts / 5 minutes)
with explicit `Retry-After` and a `[auth] rate_limited` log line.
- **Auth failures are now logged.** `[auth] login_failed` records IP +
username + reason; `[auth] login_ok` records role. Hono's request log
alone never distinguished a 401 from a 200 to incident response.
- **Login rotates the cookie.** A pre-login cookie carried into a successful
login is now destroyed before the new session is issued, closing the
session-fixation window.
- **Session idle expiry.** Sessions had a 30-day absolute expiry but no idle
cap, so a stolen cookie stayed live for the full month. Added
`last_active_at` (touched ≤ once/min on `lookupUser`) and a 7-day idle
rejection on top of the 30-day absolute cap.
- **`COOKIE_SECURE` env flag.** Operators terminating TLS at a reverse proxy
can now set `COOKIE_SECURE=true` so session cookies aren't shipped in
plaintext over the underlying HTTP socket. Default stays `false` to match
the documented localhost / LAN deploy.
- **Defense-in-depth security headers.** Every response now carries a strict
`Content-Security-Policy` (`default-src 'self'`, `frame-ancestors 'none'`,
`script-src 'self'` — no inline JS), `X-Frame-Options: DENY`,
`X-Content-Type-Options: nosniff`, and `Referrer-Policy:
no-referrer-when-downgrade`. The 3.0 link-scheme XSS fix is still the
primary guard for the coach renderer; CSP is the backstop.
- **Setup `/test-ollama` was an unauthenticated SSRF probe.** Pre-init only,
but an attacker hitting a fresh deploy could POST `{ url:
'http://169.254.169.254/' }` and read the response body. URL is now
restricted to loopback / RFC1918 / `*.local`, link-local 169.254/16 is
explicitly denied, and the response body is never echoed — only `ok` /
`unreachable` plus the model list on success.
- **Stockfish PATH lookup hardened.** Removed the bare `'stockfish'`
fallback so the engine driver can't accidentally launch a malicious
binary planted earlier in `$PATH`. Discovery now tries the explicit hint,
the DB setting, the bundled `bin/`, and a curated set of absolute
install paths (`/usr/games`, `/usr/bin`, `/usr/local/bin`,
`/opt/homebrew/bin`).
- **UCI newline injection guard.** Every `evaluate()` and `bestMove()` call
now asserts the FEN contains no `\r`/`\n` before forwarding to the engine
process. chess.js never emits one today, but the assertion documents the
invariant for any future caller.
- **PvP refresh ate the clock.** Hydrating a PvP session always set
`whiteTimeMs/blackTimeMs` to the time control's initial and `lastMoveAt`
to `Date.now()` — refreshing the tab during a 5-minute blitz game silently
reset both players' clocks back to 5:00. Clocks + last-move timestamp now
persist on every move and rehydrate exactly, charging only the wall-clock
span elapsed since the last persisted move.
- **PvP session leak.** Abandoned games (both sockets gone, no save) sat in
the `pvpSessions` map forever, holding a `Chess` instance + a Stockfish
analysis engine + waiter set per game until process restart. New
`setInterval` sweeper drops sessions idle for > 15 minutes when both
sides are disconnected, and force-drops any session idle > 6 hours.
- **Pending challenges never expired.** The `expired` status enum existed
but nothing ever wrote it. `/incoming` and `/outgoing` now sweep
pending-but-stale (> 15 min) challenges to `expired` on every read; a
startup sweep clears the table on boot.
### Added
- **Real `M{n}` mate display in the analyzer eval pill.** The classifier
encodes mate as `±10000 - 10·moves`; the eval pill now decodes that back
to `M3`, `-M5`, etc. instead of a generic `#`. (The eval bar already did
this.)
- **Home page first-run empty state.** A dashed "Play your first game" card
appears when the player has zero saved games, with primary `Play vs Bot`
and secondary `Import from Chess.com` (or *Set username* if not yet
configured) actions. Replaces the previous near-blank page.
- **`COOKIE_SECURE` documented in `Configuration`** and the
`Troubleshooting` section.
- **Why Patzer 3-bullet pitch** above the feature list, plus a `docker
compose` quickstart inline next to the `docker run` form, plus a
`Troubleshooting` section covering the predictable first-run failures
(Stockfish path, Ollama unreachability, port collision, cookies behind
proxy, password recovery).
- **Translator's guide.** `CONTRIBUTING.md` now has a step-by-step "Adding
a language" section calling out the four files a translator needs to
touch (UI locale JSON, coach prompt fragments, i18n register, settings
language picker).
- **`.github/FUNDING.yml`** — surfaces a Sponsor button on the repo page.
### Changed
- **`bcrypt` cost is unchanged at 12 and the password floor stays at 10**;
the Setup wizard's helper text now says so. Previously it read "At least
6 characters" while the server enforced 10, so first-run users got a
server validation error from a form that claimed to accept their input.
- **Login form a11y.** Inputs now have proper `id` + `htmlFor` pairing and
`autoComplete` hints; the error banner is wrapped in `role="alert"
aria-live="assertive"` so screen readers announce login failures.
- **Settings sticky save bar** moved from `position: fixed` to `position:
sticky` and respects `env(safe-area-inset-bottom)`. On a phone, the soft
keyboard now pushes the page (and the bar) up naturally instead of the
bar floating over the focused input.
- **EvalBar mate colors use design tokens.** Switched from raw
`bg-rose-700` / `bg-amber-300` to the `bad` / `warn` semantic tokens so
the chrome stays consistent across the three board themes and survives
future palette tweaks.
- **Coach prompt no longer leaks raw centipawn loss.** The FACT block used
to embed `(${cp_loss} centipawns lost)` next to the severity phrase, and
small models would parrot the number verbatim — anti-pedagogical noise
for a kid audience and never useful prose. The severity word already
encodes magnitude.
## [3.0.0] — 2026-05-10
The "Patzer" release. The project is renamed and is now ready to be open-sourced.
Multi-perspective audit pass: a UI designer, a strong-club-player chess nerd,
a devil's-advocate engineer, and a "GitHub launch" operator each ripped through
the codebase; their findings drove this changeset. Fixes three real correctness
bugs (one of them silently undid the 2.2.0 scoring rewrite), closes a stored
XSS in the coach output, lands a real brand identity, and ships the OSS
scaffolding (CI, issue templates, security policy).
### ⚠️ Breaking
- **Renamed to Patzer.** `package.json#name` is now `patzer`, server and web
workspaces are `@patzer/server` / `@patzer/web`, the docker-compose service
is `patzer`, the named volume is `patzer-data`, and the published image is
`ghcr.io/SikamikanikoBG/patzer`. Existing deployments need to either rename
their `chess-data` volume or copy the SQLite file across — `docker volume
create patzer-data && docker run --rm -v chess-data:/from -v
patzer-data:/to alpine cp -a /from/. /to/`.
- **Password minimum is now 10 characters** (was 6) and bcrypt cost is 12 (was
10). Existing admin/user accounts keep their hashes; the new floor only
applies on create / change.
- **CSRF guard on all mutating `/api` routes** — every state-changing request
must carry `X-Requested-With: patzer`. The bundled fetch helper does this
automatically; integrations calling the API by hand need to add the header.
### Fixed
- **Stockfish info parser was overwriting good evals with empty noise.** The
`evaluate()` driver accepted every `info` line where `depth >=` and
overwrote `lastInfo` — but Stockfish emits `info string …`, `info ...
currmove …` (no score), and `info ... lowerbound/upperbound` lines that
parsed to `cp:null, pv:[]` and clobbered the real eval. Downstream this
silently turned `cp` into `0` for many positions, undoing most of the 2.2.0
classifier rewrite. Parser now ignores info lines without a score, drops
bound snapshots, and skips `info string` chatter; we also track `seldepth`
so deeper lines win ties.
- **Auto-analysis after a played game was born stale.** The `INSERT INTO
analyses` in the bot/PvP completion handlers omitted `scoring_version`, so
every freshly-analyzed game was instantly flagged as `analysis_stale=true`
by the games list and triggered a free re-analysis on first view. Both
inserts now stamp `SCORING_VERSION` and use the same `ON CONFLICT` upsert
the explicit analyze route uses.
- **Stockfish process leak on disconnect.** `quit()` did `setTimeout(kill,
200)` then immediately nulled `this.proc` — meanwhile any in-flight
`evaluate()` promise was holding a waiter that would never resolve, so
callers (including the analysis-engine queue) deadlocked. `quit()` is now
async, sends `stop` + `quit`, awaits the OS exit (or SIGKILLs after 250 ms),
and rejects every dangling waiter with `engine_terminated`.
- **Stored XSS via the AI coach.** Markdown links in the coach output were
rendered without scheme validation, so a hostile or hallucinating model
could emit `[click](javascript:…)` and execute JS in an admin's browser
(mitigated by httpOnly cookies, but still attacker-controlled DOM). Link
rendering now allow-lists `^https?://` and `mailto:` only; everything else
falls back to plain text.
- **Setup wizard race.** The `userCount() === 0` check happened outside the
insert transaction, so two attackers reaching a freshly-deployed instance
could both pass the check and both win admin. The check now runs inside the
same `db.transaction()` as the insert, with `BEGIN IMMEDIATE` acquired by
better-sqlite3 to serialise concurrent calls.
- **Last-admin guard on PATCH role demotion.** DELETE blocked demoting the
only admin; PATCH did not. A single misclick could lock the admin console
with no in-app recovery. PATCH now applies the same guard.
- **chess.com archive URLs are validated.** Defense in depth: the URLs we
walk in `fetchRecentGames` must resolve to `api.chess.com/pub/...` over
HTTPS. Closes a hypothetical SSRF if the archive list is ever influenced.
- **chess.com fetches now have a 15 s timeout** (was unbounded). A hung
upstream no longer pins request handlers indefinitely.
- **chess.com username is regex-validated** (`[A-Za-z0-9_-]{2,40}`) at the
schema layer in both `/games/import/chesscom` and the profile settings
route, instead of a generic `min(1).max(40)`.
- **Variant games no longer imported.** chess.com `kingofthehill` /
`threecheck` etc. games were being slurped in and fed to the
standard-chess classifier, which produced nonsense numbers. Importer now
filters to `g.rules === 'chess'`.
- **Per-user single-flight on `/analyze`.** A double-click no longer spawns
two concurrent Stockfish processes against the same game.
- **Layout's "Home" / "Начало" was hardcoded** based on a stringly-typed
English-detection check (`t('home.playTitle').split(' ')[0] === 'Play'`).
Replaced with a proper `app.home` locale key.
### Added
- **Brand identity.** A real `Logo` component (knight glyph in a rounded
container, amber on ink) replaces every `♞` placeholder across boot
loader, login, setup, sidebar, and mobile header. Updated `icon.svg`,
added `manifest.webmanifest` for PWA-style installs, theme-color meta.
- **New eval graph.** Hand-rolled SVG, drawn in win-percent space (the
Lichess sigmoid) instead of clamped centipawns — mate threats now read
intuitively, not as a flat ±1000 line. Mistake / blunder / inaccuracy /
miss / brilliant moves get colored markers; current-ply scrubber line +
pill; click anywhere to jump. ~80 KB lighter than the Recharts-based
version it replaces (Recharts dependency removed).
- **New eval bar.** Spring-animated height transitions; explicit mate
rendering (`M5` instead of just `#`), pulsing accent shadow when mate is
on the board.
- **PGN export + FEN copy** in Game Review. Three small buttons under the
eval graph: copy current FEN, copy full PGN, download `.pgn`.
- **Premoves.** chessground was set up for them visually but the `premovable`
config was never wired. It is now — queue your move on the opponent's
clock and it fires automatically when their move arrives.
- **CSRF guard.** See "Breaking" — `X-Requested-With: patzer` is required on
every state-changing `/api` request.
- **Boot loader** is no longer a single pulsing ♞. New: centered logo glyph,
short shimmer-bar progress, soft pulse animation.
- **Analyzer skeleton screen** while the game detail loads (replaces the
text "Loading…").
- **Last-move highlight is softer** — warm amber tint with a 360 ms fade-in,
instead of the previous bright orange that fought the board theme.
- **OSS scaffolding.** `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`,
`THIRD_PARTY_NOTICES.md` (calls out chessground / Stockfish GPL),
`ROADMAP.md`, `.github/PULL_REQUEST_TEMPLATE.md`, three issue templates
(bug / feature / `coach-hallucination`). Two GitHub Actions workflows:
`ci.yml` (typecheck + build on Node 20 and 22 + Docker build) and
`release.yml` (multi-arch GHCR push on tag, auto-extracts release notes
from this CHANGELOG).
- **README rewrite.** Hero section with badges, comparison table vs Lichess
Studio / Chess.com Review / Aimchess, ASCII architecture diagram, sharper
quickstart that defaults to the published GHCR image instead of
`git clone && build`.
### Changed
- **bcrypt cost 10 → 12** and password floor 6 → 10. Bcrypt cost roughly 4×
the work per hash; combined with a higher minimum length the worst
dictionary attacks are no longer trivially feasible against a stolen DB.
- **Docker build** now uses `npm ci` (was `npm install`) so the lockfile is
authoritative — image builds reproducibly across machines and CI.
- **Docker compose** has `logging.driver: json-file` with `max-size: 10m`
/ `max-file: 3` so a long-running container doesn't fill the host disk
with logs.
- **chess.com User-Agent** updated from the placeholder
`chess-local/0.1 (+https://github.com/local/chess)` to a real
`patzer/3.0` UA pointing at the published repo.
- **Stockfish driver** quit is async, idempotent, and rejects waiters; all
callers (`analyze.ts`, `ws/play.ts`, `static test`) updated.
- **Tailwind config** gained semantic animation tokens (`pulse-soft`,
`loader-slide`, `fade-in`, `shimmer`, `last-move-pulse`) plus a `glow`
shadow used by the eval bar in mate states.
### Removed
- **Recharts dependency.** The only consumer (the eval graph) was rewritten
in plain SVG.
- **Knight-emoji placeholder logo** — every `♞` brand surface (boot loader,
login, setup wizard, sidebar, mobile header, Home page ghost-pawn) is now
a real ``.
## [2.2.0] — 2026-05-10
This release replaces the homemade scoring with a Chess.com/Lichess-style standardized model so move classifications and Elo estimates are believable, and ships a real game-over modal with next-step actions.
### Fixed
- **Move classification was completely off.** The old `cpLoss` was actually a win-percentage-drop times four, not real centipawns — but the classifier treated it as centipawns, so a typical amateur 1pp drop registered as `cpLoss=4` and got "best". Almost every move (and the small icons during live play) was rated "Best" or "Excellent" regardless of actual quality. Now `cpLoss` is the real centipawn loss derived from the player-perspective engine eval, and classification uses **win-percent drop** (the standard Chess.com/Lichess approach):
- Best: <0.5pp drop AND <8cp loss (or engine's #1 move)
- Excellent: <2pp / <25cp
- Good: <5pp / <60cp
- Inaccuracy: <10pp
- Mistake: <20pp
- Blunder: ≥20pp
- **Elo estimate was massively inflated.** Old curve gave an amateur ~2100 Elo from accuracy 70%. New `eloFromAcpl` is calibrated against published Lichess/Chess.com rating-vs-ACPL data:
- ACPL ~5 → 2700+ (super GM)
- ACPL 15 → 2300
- ACPL 30 → 1900
- ACPL 50 → 1500
- ACPL 80 → 1200
- ACPL 150 → 800
Accuracy% nudges this ±200 Elo at most.
- **Live-play classification badges** (Best/Excellent/Mistake/etc. shown over the destination square) used the same broken cpLoss math; same fix applies, so badges in Play are now meaningful.
### Added
- **Game-over modal** — when a game ends, a centered popup appears (backdrop, win/loss/draw header, the result reason) with two big buttons: **Play again** and **Review this game**. Dismiss with the X, click outside, or "Just look at the board" — a small pill in the side panel re-opens it whenever you want.
- **Auto-reanalyze when scoring changes** — analyses now carry a `scoring_version` stamp. Games analyzed under an older version are silently re-run when you next open them, so all your stats update without any manual action. The games list hides accuracy from stale analyses to avoid mixing old and new numbers.
## [2.1.0] — 2026-05-10
This release sharply reduces coach hallucination, makes moves easier to spot on the board, and lets you rewind through past positions in a live game without losing your place.
### Added
- **Live-game rewind** — under the board in Play, chevron buttons (or arrow keys) let you step backwards through every position of the current game and jump back to live. While browsing, the board is read-only and a clear "Browsing past position" tag appears. Hit "Back to live" (or press End) to resume play. The actual game state keeps advancing in the background — your opponent's moves are never lost.
- **Bright orange last-move highlight** — the from/to squares of the most recent move are now tinted in transparent orange so you can immediately see what changed. Replaces chessground's default khaki, applied in both Play and Game Review across all board themes.
### Changed
- **Coach is no longer asked to reason — only to render.** All chess analysis (piece identification, capture detection, classification, engine recommendation, principal variation) is now pre-computed server-side using chess.js, then handed to the LLM as a structured fact list. The LLM's job is to render those facts as friendly coach prose. The system prompt now explicitly forbids inventing moves, threats, or pieces beyond the facts.
- **Coach speaks in natural language, not chess notation.** The system prompt and user prompt both forbid SAN like "Nf3" or "Bxe5"; the model is required to say "the knight to f3" or "the bishop takes on e5" instead. Pre-computed facts are themselves natural-language phrases. Pieces use kid-friendly names ("horsey", "castle") for kid audience and standard names otherwise.
- **Coach temperature lowered to 0.3** so the model sticks to the facts rather than improvising.
### Fixed
- **Last-move highlight in Play was not displaying** — chessground only shows it when given an explicit `lastMove` prop, which was missing in Play. Now wired from per-ply position tracking.
## [2.0.0] — 2026-05-10
This is a maturity release: every move on the board is rigorously validated by chess.js (the same library lichess uses), promotion now lets you pick the piece, the coach gets much richer context so it stops inventing pieces, and the UI gets a dashboard, captured-pieces panel, and many smaller polish touches.
### Added
- **Promotion picker** — when a pawn reaches the last rank, a small inline modal asks whether you want a Queen, Rook, Bishop or Knight (was always Queen).
- **Captured-pieces panel** — a small stripe next to each clock shows the pieces each side has captured, with material balance, in both Play and Game Review.
- **Home dashboard** — your stats (games, win rate, average accuracy, current streak) right at the top of Home.
- **Coach context revamp** — every coach prompt now includes the ASCII board, an explicit piece inventory, recent move history (last 6 plies), captured pieces, and an in-check flag. The system prompt has stronger anti-hallucination guards. Three audience-tuned voices: kid (warm, metaphor-heavy, encouraging), beginner (friendly with principles), intermediate (concrete tactics), advanced (rigorous club-player).
- **PvP "opponent disconnected" indicator** so you know if your friend's connection dropped.
### Fixed
- **Move legality is bulletproof** — every move (yours, the bot's, your friend's) is validated by chess.js before being applied. The chessground UI only allows legal destinations. Castling, en-passant, promotion, threefold repetition, fifty-move rule, insufficient material, stalemate are all handled correctly. Server rejects any illegal UCI; if rejected, the board re-syncs to the authoritative position.
- **Race in user-move classification** — `fenAfter` is now captured synchronously instead of being read inside the async classification IIFE (which sometimes saw the position AFTER the bot had also moved).
- **Analysis-engine concurrency** — the per-session analysis engine now serializes evaluations through a promise queue, so rapid moves can't interleave UCI commands and corrupt the engine's state.
- **PvP session restore on container restart or reconnection** — the in-memory PvP session is now hydrated from the saved PGN if present, instead of starting from move 1.
- **Board re-sync after blunder cancel / illegal-move rejection** — chessground is forced back to the authoritative FEN by bumping a board key, eliminating the visual desync where a piece would stay on its dropped square after we rolled the move back.
- **Coach hallucination** — the much richer prompt + explicit "do not mention pieces or squares not in the diagram" instruction substantially reduces invented-piece errors on smaller models.
### Changed
- Game Over card now shows mini-summary (accuracy, classifications) when analysis lands.
- Hover/focus micro-interactions tightened across cards and lists.
## [1.4.0] — 2026-05-10
### Added
- **Play vs another user (multiplayer)** — challenge any other registered user to a real-time game. Online presence shown in the new "vs Friend" tab, incoming challenges pop up as a modal anywhere in the app.
- **Sound effects** — synthesized chess sounds (move, capture, check, castle, promotion, game-end) play in both Play and Game Review. Per-profile sound on/off toggle.
- **Classification badge on the board** — stepping through Game Review shows the played move's classification (Brilliant / Best / Mistake / etc.) right on the destination square.
- **Coach now coaches YOU in Play** — server runs a quick eval on every move you play, classifies it, and the always-on coach explains *your* move (was it a blunder? what would have been better?). On the engine's turn, coach gives you a hint for what to think about. Coach stays quiet between turns instead of commenting on the bot.
- **Kid blunder warning** — for kids and pedagogical mode: if you try to make a mistake or blunder, a "Are you sure?" modal pops up with a coach hint. You can dismiss and try a different move, or play it anyway. Toggle in Settings.
### Fixed
- **TTS now respects the chosen language on Windows** — `utterance.lang` is always set to a BCP-47 code (en-US / bg-BG), and the voice falls back gracefully when a stored voiceURI no longer exists. Bulgarian text now reads with a Bulgarian voice when one is installed.
- **Game Review board is much bigger on desktop** — board now takes the larger share of the layout (was tied to a 560px max).
- **Bigger nav buttons under the board** — easier to hit, especially on a touchpad.
## [1.3.0] — 2026-05-10
### Fixed
- Coach output no longer collapses spaces between tokens (the SSE handler used to strip every leading whitespace from each chunk; now strips only the single SSE-spec separator).
- Coach mute survives navigation — the muted state persists in localStorage so jumping to ply 0 (which unmounts the coach panel) and back doesn't quietly un-mute.
- Green and Blue board themes are now actual axis-aligned squares (replaced the broken diagonal-gradient checkerboard with inline SVG patterns).
### Added
- Visual theme picker in Settings → Appearance: each Site theme (Light / Dark / Auto) and Board theme (Wood / Green / Blue) shows a real preview tile, not just a color dot.
- Admin → System auto-loads the Ollama model dropdown on page open when an Ollama URL is configured (no longer requires you to click Test first).
### Changed
- UI polish pass: tighter card hierarchy, refined typography, hover lift on tiles, sectioned Settings with icons + descriptions, polished Home with a recent-games row, polished Login/Setup, better empty states, improved focus rings throughout, more consistent spacing scale.
## [1.2.0] — 2026-05-10
### Added
- **Brilliant (!!) and Miss (✗) move classifications** — Brilliant fires when the engine's top choice involves a real material sacrifice that keeps the position holding; Miss fires when a mistake/blunder happened in a winning position (the player gave away a clear advantage).
- **Move classification stats panel** — Chess.com-style breakdown of how many Brilliant / Best / Excellent / Good / Book / Inaccuracy / Mistake / Blunder / Miss moves each side played in the game.
- **Configurable analysis depth** — Game Analyzer now has a depth slider (8–22). Hit *Re-analyze at depth N* to run Stockfish deeper for a closer look at a key game.
- **Site theme + board theme** — each profile picks a Site theme (Light / Dark / Auto from system) and a Board theme (Wood / Green / Blue). Applied immediately, persisted per user.
- **Markdown-aware coach** — coach replies render real headings, lists, and bold; the same text is stripped of markdown before being read aloud, so TTS no longer says "asterisk asterisk".
### Fixed
- Coach panel no longer pushes the page wider than the viewport on mobile (long words/lines now wrap properly).
## [1.1.0] — 2026-05-10
### Added
- **Mobile-friendly UI** — sidebar collapses into a slide-out drawer on small screens, the chess board scales to the viewport, and touch targets are sized for fingers.
- **Visible version + changelog viewer** — the current app version shows in the sidebar footer; clicking it opens an in-app modal with the full release notes.
- **Vertical eval bar** — Chess.com-style white/black bar next to the board in Game Review, with the numeric centipawn score displayed at every step.
- **Always-on Coach in Game Review** — moving through a game now triggers an automatic explanation from the AI Coach (debounced 600ms), with a mute toggle. Especially good for kids walking through the game with TTS.
- **Estimated Elo per side** — after analysis, the summary shows an approximate playing strength for both White and Black, derived from accuracy and average centipawn loss.
- **/api/meta endpoint** — exposes the running app version and build channel to the frontend.
### Changed
- Game Analyzer summary now shows accuracy + estimated Elo side-by-side.
- Coach panel can be muted/unmuted at runtime; mute state is remembered per session.
## [1.0.0] — 2026-05-10
### Added
- **Game Review** — pulls public games from the Chess.com API, analyzes with bundled Stockfish, classifies every move Lichess-style (Best / Excellent / Good / Inaccuracy / Mistake / Blunder), shows per-side accuracy %, evaluation graph, and best-move arrows.
- **Play vs Bot** — full games against Stockfish at named tiers (Kid / Beginner / Easy / Medium / Hard / Master / Stockfish max), with standard time controls (Bullet / Blitz / Rapid / Classical / Untimed). Played games are saved and auto-analyzed when finished.
- **AI Coach** — local Ollama integration for natural-language explanations during review and live play. Per-profile coach behavior (silent / on-demand / always-on-pedagogical), prompts shaped by audience level (Kid / Beginner / Intermediate / Advanced).
- **Multilingual** — full English + Bulgarian UI with translations of the coach prompts. Profile language is selected at user creation.
- **Browser TTS** — uses the Web Speech API (Windows SAPI / macOS / Linux voices). Per-profile voice, rate, and pitch.
- **Multi-user with admin role** — first-run setup wizard creates the admin account; the admin console manages users, system settings (Ollama URL, Stockfish path), and connection health.
- **Single-container deploy** — Dockerfile + docker-compose.yml with persistent SQLite volume; one-shot `deploy.ps1` for SSH-based home-server deployments.
[2.0.0]: https://github.com/SikamikanikoBG/patzer/releases/tag/v2.0.0
[1.4.0]: https://github.com/SikamikanikoBG/patzer/releases/tag/v1.4.0
[1.3.0]: https://github.com/SikamikanikoBG/patzer/releases/tag/v1.3.0
[1.2.0]: https://github.com/SikamikanikoBG/patzer/releases/tag/v1.2.0
[1.1.0]: https://github.com/SikamikanikoBG/patzer/releases/tag/v1.1.0
[1.0.0]: https://github.com/SikamikanikoBG/patzer/releases/tag/v1.0.0