# 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