- Java 50.3%
- Vue 33.8%
- TypeScript 13.8%
- JavaScript 0.9%
- CSS 0.7%
- Other 0.4%
The build cache had grown to about 10 GB on the server. update.sh now runs docker builder prune -f after each deploy, which only removes cache that nothing uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| .trail | ||
| backend | ||
| deploy | ||
| docs | ||
| frontend | ||
| images | ||
| .dockerignore | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| docker-compose.prod.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| README.md | ||
Alp Chess
Self-hosted chess opening trainer: import PGN files, get a navigable repertoire tree, and (from phase 3 on) train it with spaced repetition that is biased towards mainlines.
Design: docs/architecture-review.md and docs/adr.
Status: import, tree view, training by lines, statistics, Lichess login
What works:
-
Login with Lichess (ADR 0009): repertoires and training progress belong to the Lichess account. Lichess OAuth needs no app registration. The app asks for one permission,
study:read(read your studies, private ones too, for study imports); it reads your username and, when you import, your studies. The token stays on the server and is revoked at Lichess when you log out. The first account that logs in takes over the repertoires created before login existed (or the account named inLOCAL_DATA_OWNER).ALLOWED_USERSlimits who may log in. You stay logged in for 30 days; sessions and the Lichess token are stored in the database (Spring Session JDBC), so restarts and updates keep you logged in. -
Lichess studies (ADR 0011): create a repertoire from a study ("New repertoire", start "From a Lichess study") or import a study into an existing one (Import, "Lichess study"). Pick one of your studies or paste any study link (a link to a chapter works too), then choose the chapters. Name and side are suggested from the study (the side most chapters are shown from) and can be changed. Chapters that start from a custom position, use a variant or have no moves are shown but cannot be selected. Public studies of other people can be imported; private ones only by their members.
-
Chapters: every study chapter becomes a chapter of the repertoire (PGN files exported from a Lichess study get them too). The Moves tab shows the chapters as chips above the moves; choosing one shows only its moves, and moves you add there belong to it only. Playing a move there that so far belongs to another chapter adds it to this one, so a shared start (1.c4 in "King's English" and "Anglo-Indian") can be continued separately in each. With all chapters shown, a new move joins the chapters of the move before it; a note above the moves says where new moves go. "Train this chapter" (or the chapter choice in the training session) trains only that chapter's lines; Statistics can show the lines of one chapter. Importing the study again adds new moves and new chapters; a chapter renamed on Lichess is renamed here. Comment commands such as arrows (
[%cal ...]) are removed from comments. -
Chapters by hand: for a repertoire built on the board, the Moves tab makes chapters too ("New chapter", optionally starting with the line up to the selected move; a first chapter can be named in the New repertoire form). A chapter can be renamed and deleted (its moves stay); with all chapters shown, the line up to the selected move (with everything after it) can be added to a chapter, and in a chapter a move and what follows can be taken out of it. A chapter made by hand is listed even while it has no moves yet.
-
Shared repertoires: "Share with everyone" on a repertoire's Moves tab puts it into a pool that every logged-in user can browse (name, side, author's Lichess name, moves, chapters). Anyone can take a copy: "New repertoire", start "From a shared repertoire" (the side is the shared one's), or Import, "Shared", into a repertoire of the same side. The copy is the taker's own, with moves, comments, NAGs and chapters (the server writes the shared tree as PGN, trained moves as main continuations so roles survive, and imports it like a study); taking it again later merges in the moves its owner added. Stopping sharing or deleting the repertoire takes it out of the pool; copies stay with their owners. The repertoire list marks shared repertoires.
-
Atomic chess: a repertoire is for standard chess or for Atomic chess (Lichess rules), chosen under "Rules" in "New repertoire" and fixed afterwards (
repertoire.variant, V12). The server's own move generator (chess.PositionwithVariant.ATOMIC) explodes the capturing piece and every non-pawn piece around the capture square, keeps kings from capturing, allows no check between touching kings, forbids exploding the own king and ends the game when a king explodes (SAN#); perft against the python-chess / Fairy-Stockfish reference counts proves it. The Moves page uses chessops' Atomic rules for drafts; the server has the final say. PGN files with[Variant "Atomic"]and Atomic study chapters go only into Atomic repertoires (games without the tag are read with the repertoire's rules); other variants and mixing are rejected with a clear error, and the study picker only offers chapters of the repertoire's variant. A new repertoire from a study takes the study's variant. Shared repertoires keep their variant. The Games tab fetches Lichess Atomic games (one rating for all time controls). Stockfish does not play Atomic, so the engine bar and panel are hidden for Atomic repertoires. Puzzles stay standard chess. -
Interface and navigation (ADR 0010): a calm, flat look in the spirit of Lichess, light and dark (follows the system; the menu under your name switches it). The top bar has the repertoire list and a switcher that jumps to another repertoire on the same tab. Each repertoire has four tabs: Moves, Train (with the number of lines due), Statistics and Games. The home page is a table of your repertoires with lines, due, new and last trained, and the one action that makes sense now (Review, Learn or Practice). The repertoire view is laid out like the Lichess analysis board: position and training summary on the left, board in the middle, engine and a two-column move list on the right with ⏮ ◀ ▶ ⏭ (keys ← → ↑ ↓, Home, End). In training, a row of markers shows each of your moves in the line: answered correctly, learned, missed, still to come.
-
On a phone: the top bar shows the repertoire, the tabs sit right above the board, and the move list with its comments follows directly under the board (navigation first), then the training panel with a chapter dropdown.
-
Installable as an app ("Install app" in Chrome, also on Android): web app manifest and icons; no service worker (an earlier one is removed automatically). Needs HTTPS (or localhost).
-
Create repertoires (White or Black).
-
Upload a PGN file: multiple games, nested variations, comments, NAGs, Latin-1 or UTF-8. A
FENtag with the standard start position counts as a normal game. Strict import: one transaction; any illegal move rejects the whole file with game, line:column, ply, move and the path leading to it. -
Tree merge: shared move sequences are reused, transpositions are recognized via
PositionKey, re-imports create no duplicates, conflicts are reported as warnings. -
Move list: the mainline in two columns, alternatives and comments as indented blocks after the move they branch from, analysis moves in italics, with a playable board. Editing (ADR 0008): play a move on the board to go to it, or, if it is not in the tree, to add it after the selected move (a first continuation extends the line, otherwise it becomes a variation). Every edit is saved right away ("Saved" next to the buttons); Undo takes back the moves added on the page one by one. Right-click a move (or use the button under the board) for Delete from here; deleting more than one saved move asks first. ← → walk through the moves. An own move where the position already has a repertoire move is added as analysis (not trained); Train this move makes it the trained move instead (the previous one becomes analysis). Comment (or Edit comment) writes, changes or removes the comment after the selected move, the one training shows; a comment before the move (from an import) can be changed there too. The same box sets the move's NAGs: one for the move (!! ! !? ?! ? ??) and one for the position (+− ± ⩲ = ∞ ⩱ ∓ −+); they replace the imported ones and are shown in the move list, in training and in a shared copy. Promote makes a variation the first continuation of the move before it: below a mainline move it becomes the mainline with its first continuations (training: main line), and the line that was first becomes a side line with everything after it; below a side line only the order changes. It does not change which own move is trained (that is Train this move). On a phone, navigation, the edit buttons and the chapters come right under the board. A line whose moves you change becomes a new card; untouched lines keep their progress.
-
Resizable board (training and repertoire view): drag the handle in the bottom-right corner of the board, like on Lichess; double-click resets. The size is remembered per browser.
-
Repertoires can be renamed (Rename in the list, or click the name on the repertoire's pages; Enter saves, Esc cancels) and deleted (list and repertoire page), with all moves, imports and training progress.
-
Training by lines (ADR 0006):
- A line is a variation from the first move to its end: your moves follow your repertoire move, every opponent reply in the file opens its own line. Each line has its own spaced repetition schedule. Lines are derived from the tree automatically; later imports add lines without touching progress.
- You always play a whole line forward; opponent moves are played automatically.
- New lines, by the repertoire's training order (New repertoire form, changeable on the Moves tab): Mainlines first (default) introduces the mainline first, then side lines by branch point (early deviations first), about 30 % side lines per day. Breadth first orders new lines only by branch point, mainline or not: after the first line, every line that leaves it early comes before deeper ones, so all the opponent's early tries are covered before the deep mainline (preparation against one opponent). Moves you already know from other lines are played automatically up to two of your moves before the first new move; new moves are shown and replayed. Right after, the line is quizzed without hints. Daily limit 5 new lines; "Learn another new line" raises it by one for the rest of the training day (kept on the server, so a reloaded page and every answer show the same limit).
- A line passes only without mistakes. A mistake sends it back (learning or relearning) and it is repeated. After a wrong move the correct move is shown and you play it yourself before the line continues. The button after a line says what comes next: "Play from memory" (the quiz of a line you just learned), "Try again" (the same line after a mistake) or "Next line". "Try again" is also offered right after a mistake: the line counts as not passed (the mistake stays in the statistics) and starts over without playing the rest of it.
- "Skip line" leaves the current line for this session and goes on with the next one; its schedule stays as it is and it comes back in the next session (or with "Train the skipped lines"). A line skipped after a mistake ends as not passed first, so the mistake counts.
- Premoves: while the opponent's reply is coming, your next move can be made in advance; it is played as soon as it is your turn (also in tactics). On every board, right-click circles a square and right-drag draws an arrow, as on Lichess (Shift, Alt or both for red, blue, yellow); a left click clears them.
- Intervals: mainline target recall 97 %, at most 21 days; side lines 85 %, at most 180 days (ADR 0004). Shared mainline moves are replayed in every side line as well. The training day starts at 04:00 Europe/Berlin.
- Practice anyway (or the Practice mode): your weakest known lines, even if nothing is due. A line without mistakes keeps its schedule; a mistake counts as a lapse.
- A move from your file that is not the repertoire move (analysis) is not penalized; you try again. PGN comments of the move are shown after you play it.
-
Statistics per repertoire: line counts, accuracy, lines trained per day (last 30 days), accuracy per day, due forecast (next 30 days, mainline and side lines), the moves you miss most often (with the position), and every line with state, next review, predicted recall, mistakes.
-
Games: your latest 20 or 50 finished Lichess games with the repertoire's side, followed move by move through the repertoire (transpositions included, also when an opponent's move without an answer leads back into it later, as 1.d4 d5 2.Nf3 Nf6 3.c4 c6 into a Slav; a move order of your own that leaves it still counts as leaving). Each game shows where it left the repertoire: a move of yours other than the repertoire move, an opponent move without a prepared answer, or the end of the prepared line, with links to that position in the repertoire and in the game on Lichess. Filter by time control (bullet, blitz, rapid, classical, correspondence; several at once). The server keeps the fetched games in memory for an hour (not in the database), so the tab opens at once and Lichess is asked again only for another count or time control, or with "Check again"; the check against the repertoire always uses its current moves.
-
Sparring (ADR 0014): the Spar tab plays a game out of the repertoire against Maia 3, a bot that plays like a person of a chosen rating and runs entirely in the browser (standard chess only).
- The bot follows a random line of a chapter, or a specific line ("Spar from this move" on the Moves page), then plays freely; "Allow surprises" lets it leave the repertoire early. There is no clock, or 3+2, 5+3, 10+0 or 15+10; the bot's thinking time is simulated and it never flags.
- Before a move that leaves the repertoire, a warning asks "Play it anyway?" without naming the repertoire move, so that you still have to remember it; the clock keeps running. It can be turned off in the setup.
- A first game downloads the model once: 149 MB, or 10 MB for the small model, preselected on devices with little memory and after the full one crashed a page. An unfinished game can be resumed.
- Finished games are saved with the repertoire (the latest 200). The review shows where the game left the repertoire, runs Stockfish, and adds a missing answer to the repertoire so that it comes up in training. "Copy PGN" gives the game with its tags and clocks, e.g. for Lichess.
-
Tactics (ADR 0012): a second tab next to Repertoires with sets of puzzles, trained the Woodpecker way and with spaced repetition.
- Puzzles come from a PGN file (the
FENtag is the position, the game's moves are the solution; games that cannot be used are skipped and listed; optionally the first move is the opponent's, as in the Lichess puzzle database) or from the position builder: drag pieces from a palette (or pick one and click squares), choose side to move and castling, or paste a FEN, then play the solution on the board, the opponent's replies included. - Solving: only your moves are asked, the opponent replies by itself. A wrong move is taken back and counts as a mistake; another move that mates is accepted. "Hint" first circles the piece to move, then shows the move; "Show solution" plays it out. A puzzle counts as solved only without mistakes and hints. Moves are judged in the browser, so there is no waiting; the result is saved when the puzzle is done. A solved puzzle moves on by itself (if that option is on); otherwise "Next puzzle" waits for you. Afterwards ← → go through the solution with the engine (hidden while solving).
- Woodpecker cycles: the whole set in order, one counted attempt per puzzle and cycle; then start the next cycle. From cycle 2 on, the goal is half the time per puzzle of the cycle before; after a cycle of an hour or more, its summary also gives the calendar time and half of it as the next goal. Statistics compare the cycles (accuracy, time per puzzle, how much faster).
- Spaced repetition: the line scheduler with one card per puzzle (85 % target recall, intervals up to 180 days); due puzzles first, then up to 10 new ones a day (more on request). Cycles feed it: a failed puzzle comes back sooner, a solved one keeps its schedule unless due.
- Puzzles can be reordered, edited (a changed position or solution starts the schedule anew) and deleted; the statistics list the puzzles missed most often.
- Puzzles come from a PGN file (the
-
Sounds: moves, captures, mistakes and a finished line without mistakes have short sounds (made in the browser, no sound files); the menu under your name turns them off.
-
Engine (optional): Stockfish 19 in the browser (WebAssembly, ADR 0007). Toggle "Engine" above the move list in the repertoire view and below the line in training: an evaluation bar beside the board, the evaluation, depth and the best line. In training the best line stays hidden until the line is finished, so it cannot give away your moves. "Full" is the strongest network (about 94 MB, downloaded once and cached); "Lite" is a small network (2 MB) and the default on phones and tablets. A load that takes long offers "Use Lite"; if the engine does not get ready with several threads within 10 seconds, it starts again on one thread. The search stops at depth 24; with "Infinite" it runs until the position changes. A hidden tab pauses the search. The settings are remembered per browser.
-
Opening explorer ("Masters"): below the repertoire's moves on the Moves page (with a switch to turn it off, remembered per browser) it shows what over-the-board masters played in the position on the board: about a million games from Lumbra's Gigabase where both players are rated 2400+ (CC BY-NC 4.0, credited in the explorer and on the Open source page). Per move: share of the games, count, average rating and white/draw/black results; transpositions are counted together, as in the repertoire. Moves of your repertoire carry a green mark; a reply played in at least 5% of the games that your repertoire does not answer yet is flagged. Hovering a move draws it on the board, clicking plays it (as on the board: it goes to the move or adds it). Below: the 20 top rated and the 20 most recent games (players without titles), which open in a viewer at this position (← → through the game). The first 20 moves of each game are indexed. Not for Atomic repertoires. The games are imported on the server with one command (deploy/README.md).
-
Positions (ADR 0015): a third tab next to Repertoires and Tactics, a personal database of positions.
- A position comes from the board editor of the tactics tab or a pasted FEN (it keeps its move number), with an optional title, free tags and notes. "Save and add another" keeps the tags, for entering many at once. A position you have already is pointed out.
- The solution is played on the board and saved as you go: a move played from an earlier one is a variation, with comments, the glyphs of the Moves page, "Make main line" and delete. Opening a position hides its solution and the engine until you ask, and the choice is remembered per browser.
- Browsing: small boards, search (title, notes, tags, author, or a pasted FEN), tags with counts (several narrow it down), side to move, with or without solution, and four orders. Arrows on a position go through the search it was opened from.
- Sharing: "Share with everyone" on one position, or "Share n" for all positions of a search. The Shared view browses everyone's shared positions; "Add to my positions" (or "Add n to mine") takes a copy with tags and solution, skipping positions you have already. Copies are independent.
-
Home and tiles (ADR 0017):
/shows the four sections with a real position, where you stand and what to do next. Repertoires are tiles with a position you choose, as starting moves, a FEN or on a board, when creating one or later. -
Endgames (ADR 0016): a fourth tab, with lessons in the style of a Lichess interactive lesson, for understanding rather than memorising. No schedule and no score, only a tick on lessons you completed and a filter for those you have not.
- A lesson is a position with a goal (win or draw) and usually a solution. You find the moves, the comments explain them, and a wrong move can be tried again; a lesson cannot be failed. Hint shows the author's hint or else the piece to move, then "Show the move" plays it.
- Any move gets an answer: a variation marked ? or ?? is a wrong move with its comment; outside the solution the 5-piece tablebase judges it, or Stockfish 19 above five pieces. A move that holds gets "That works too". "Show why" plays out what a wrong move runs into.
- "Play it out after the solution" goes on against the toughest defence, always the same one, and is always on without a solution. It stops when a pawn queens, when the other side has nothing left, or after ten moves; "Finish here" ends it earlier. Only the basic mates go on to the mate.
- Premoves work. The move list and arrow keys go through the lesson with its comments and what you tried at each move.
- Lessons are made like positions (board, tags, solution with comments), plus goal, side and hints per move; the edit page shows the tablebase for every position. They can be shared.
- Twenty-six starter lessons in four categories, Basic, Advanced, Practical and Studies, come with
one click, and "Update starter lessons" brings new ones in later.
Chapters of a Lichess study that start from their own position, or games with a
FENtag in a PGN file, become lessons with their variations and comments. Interactive chapters keep their wrong moves.
Upgrading from a version before V4 resets training progress once (per-position progress cannot be mapped onto lines); the repertoire itself is untouched.
Not yet: reordering chapters, arrows and highlighted squares from study comments on the board, chapters from a custom position, import preview, editing tiers, undoing a misclick in training.
Promotions open a chooser (queen, knight, rook, bishop) on the board, in training and when editing; a wrong piece counts as a mistake. Moves are not animated.
Verification status
| Part | Verified how |
|---|---|
Scheduler (training.scheduler) |
Unit tests plus a one-year simulation with a synthetic learner (SchedulerSimulationTest) |
chess, pgn, repertoire.tree packages |
88 JUnit tests passing, including perft against reference counts and two real study chapters: ruy-lopez-cozio.pgn (White, 104 moves) and pure-slav-nf3-g3.pgn (Black, 452 moves, one transposition conflict) |
Flyway migrations V1–V4 |
Applied to PostgreSQL 16 on a fresh database, and V4 on a database the previous version had created and trained on; path-uniqueness and single-primary constraints checked by hand |
| Frontend | vue-tsc type check, Vitest (34 tests) and production build pass; UI checked in a headless browser against a mock API serving a real tree |
| Spring layer (entities, services, controllers) | mvn verify: 156 tests passing, including the 31 integration tests (import, Lichess studies, training, tree editing, headers, login) against PostgreSQL 16. Those ran with a local database instead of Testcontainers (no Docker in the build environment), so the Testcontainers wiring itself is only exercised when you run mvnw verify with Docker |
| Engine | Unit tests for UCI parsing, bar mapping and SAN lines; in a headless browser against the packaged jar: page cross-origin isolated, full multi-threaded build first evaluation after about 2 s, depth 24, switching to Lite while loading, best line hidden in training, toggle off, infinite analysis past depth 24 (also after a position change), pause while the tab is hidden, back to depth 24; the single-threaded fallback checked on a page without isolation |
| Tree editing, board size | Unit tests for legal moves, draft rules and change sets; in a headless browser against the packaged jar: resize by dragging (640 → 791 px, kept after reload, reset by double-click), adding a variation and a mainline extension, playing an existing move navigates, ← →, right-click "Delete from here", discard, save (roles and mainline on the server, lines become new cards), leave warning, deleting a repertoire from the page and from the list, no sideways scrolling at phone width. Renaming a repertoire (new ETag of the tree, blank or too long → 400, another user's → 404) in an integration test and in the browser from the header, the list and the Moves page at phone width. Comments (add, change, remove, shown in training) and promoting a variation (order, mainline flags, line tiers, progress kept) in integration tests and in the browser at desktop and phone width |
| Interface | Screenshots of every page in light, dark and at phone width (390 px, no sideways scrolling) in a headless browser against the packaged jar; the training, tree-editing, login and engine flows above rerun against the new interface |
| Lichess studies, chapters | 10 integration tests against a stand-in for the Lichess API: preview (chapters, moves, what cannot be imported, suggested side), creating a repertoire from a study (chapters in study order, shared moves in several chapters, comment commands removed), a failed import leaves no repertoire, chosen chapters, a renamed chapter on re-import, private studies only with the token, 429/404/bad links, the user's studies newest first, chapters from an uploaded study PGN, chapter-scoped training and statistics, edits joining the right chapter, parallel requests not creating lines twice, token revoked on logout. In a headless browser against the packaged jar and the fake Lichess (which checks the requested scope and the token): create from a private study with two of three chapters, chapter view and editing in it, "Train this chapter", the statistics chapter filter, importing a public study from a chapter link, logout revoking the token, no sideways scrolling at phone width. Not tested against lichess.org itself |
| Positions | 4 integration tests (saving with tags and an annotated solution, what is refused, positions private until shared, copies keeping the solution and the taker's tag spelling, nothing taken twice) and unit tests for the solution tree, search and tags. In the browser at desktop and phone width: FEN, title and tags, a solution with a variation, comment and glyph, reload, tag filter, FEN search, sharing one and several, changing the board with a solution. Another user's read-only view was checked with a changed API response (the local server has one user); taking a copy only in the integration test. Not tried on a real phone |
| Home and tiles | Unit tests for moves and FENs into a position (cover.spec.ts), and integration tests against PostgreSQL for setting, changing, removing and refusing a position, the owner check and shared copies. In the browser against the real backend at 360 and 375 px and at desktop width: the home page, creating a repertoire with moves, adding a position on a tile, the board setup, the menu, a Lucena played with real clicks. Nothing on a real phone |
| Endgames | Unit tests for judging moves, the opponent and the end of a lesson (lesson.spec.ts), the tablebase client against a local HTTP server, reading lessons from PGN (also the export of a real public interactive study: 6 of 6 chapters with a position, wrong moves kept). Six integration tests against PostgreSQL (migration V17, lessons, ticks, sharing, starter lessons, PGN import). The tablebase service against the public one on 8 positions in the tables, with 13 tables of the 145: same result, DTZ and move verdicts. All starter lessons checked against the public tablebase by check-starter-lessons.mjs. In the browser at 375 px against a stand-in API with the real lessons and the public tablebase, with real clicks and Stockfish in the page: a whole Lucena with hint, wrong move, "Show why", "That works too" and back to the lesson's line, Philidor as Black, a draw held for 15 moves, the fifty-move rule with "Go back a move", a 6-piece lesson with Stockfish, the tablebase switched off. The edit page at desktop width. The container built and run as its real user with those tables mounted read-only: health check, a bad FEN, a position with six pieces. Against the real backend with real clicks in the browser: premoves, a wrong move with "Show why", a whole lesson, going through it with the arrow keys, the list and the lesson at 375 px. The import page itself was not tried against lichess.org, and nothing was tried on a real phone |
| Login | 7 tests: 401 without login, PKCE redirect parameters, CSRF, logout, account mapping (first login takes over the local data, case-insensitive ids, new accounts) and isolation between accounts (list, read, tree, training, edit, delete). In a headless browser against the packaged jar and a stand-in for Lichess that checks the S256 PKCE verifier, single-use codes, redirect URI, client id and that no secret is sent: deep link → login → back to the page, earlier data taken over, create and delete with the CSRF cookie (a POST without it gets 403), logout, a second account sees nothing of the first, failed login message. Not tested against lichess.org itself (not reachable from the build environment) |
| Full app | Packaged jar with bundled frontend started against PostgreSQL; create → import → tree → rejected import → deep-link reload, and training by lines checked in a headless browser on the Slav chapter (30 lines): mainline introduced (15 moves), quiz with a deliberate miss and replay of the correct move, repeat and pass, four side lines, "Learn another new line", Practice; statistics page in light, dark and at phone width |
Requirements
- Java 21, Docker (for PostgreSQL and the integration tests), Node 24.12+ (chessground requires Node 24).
Run locally (development)
docker compose up -d postgres
cd backend
./mvnw spring-boot:run # Windows: mvnw.cmd spring-boot:run -> http://localhost:8080
cd frontend
npm ci
npm run dev # -> http://localhost:5173 (proxies /api to :8080)
npm run fetch-maia # once, only to try sparring: the bot's models (about 160 MB)
To try the app without a Lichess login, start the backend from the test sources instead: it runs
a throwaway PostgreSQL in Docker and logs every browser in as the local user (LocalDevServer,
never part of the jar):
cd backend
./mvnw spring-boot:test-run -Dspring-boot.test-run.main-class=de.alparslanyalcin.openingtrainer.LocalDevServer
Tests
cd backend && ./mvnw verify # unit + integration tests (Testcontainers starts PostgreSQL 17)
cd frontend && npx vitest run && npm run build
On a server
Production setup behind Caddy (HTTPS, own PostgreSQL container, updates, backups):
deploy/README.md, using docker-compose.prod.yml.
Full stack in Docker
cp .env.example .env # set a real password
docker compose up -d --build # -> http://localhost:8080
The production image builds the frontend and serves it from the Spring Boot jar.
Login: open the app under the address you use in the browser (e.g. http://localhost:8080); the
Lichess callback goes to <that address>/login/oauth2/code/lichess. Behind a reverse proxy with
another host name or HTTPS, the proxy must pass X-Forwarded-* headers and the app needs
SERVER_FORWARD_HEADERS_STRATEGY=framework, otherwise the callback address is wrong. In
development (npm run dev), Vite forwards the login paths to the backend, so log in on
http://localhost:5173.
Layout
backend/ Spring Boot 4.1, Java 21, PostgreSQL, Flyway
chess/ rules core: Position, Move, San, PositionKey (no dependencies)
pgn/ PGN lexer/parser, encoding detection (no dependencies)
repertoire/tree/ tree model and import merge planner (no dependencies)
repertoire/... persistence, services, REST
frontend/ Vue 3, TypeScript, Vite, @lichess-org/chessground
docs/ architecture review, ADRs
API (slice 1)
| Method | Path | Notes |
|---|---|---|
POST |
/api/repertoires |
{ "name": "...", "color": "WHITE" | "BLACK", "trainingOrder"?: "MAINLINES_FIRST" | "BREADTH_FIRST", "chapterName"?: "...", "variant"?: "STANDARD" | "ATOMIC" } → 201 (with chapterName, a first chapter is made; variant cannot change later) |
GET |
/api/me |
the logged-in Lichess user, or 401 |
POST |
/api/logout |
ends the session → 204 |
GET |
/api/repertoires |
list |
GET |
/api/overview |
every repertoire with its line counts and when it was last trained (home page, switcher) |
GET |
/api/repertoires/{id} |
summary |
PATCH |
/api/repertoires/{id} |
{ "name"?, "trainingOrder"?: "MAINLINES_FIRST" | "BREADTH_FIRST" } (fields left out stay; name not blank, ≤ 200) → summary |
DELETE |
/api/repertoires/{id} |
deletes the repertoire with everything in it → 204 |
POST |
/api/repertoires/{id}/chapters |
{ "name" } → 201 { chapter, tree }; a chapter made by hand |
PATCH |
/api/repertoires/{id}/chapters/{chapterId} |
{ "name" } → { chapter, tree } |
DELETE |
/api/repertoires/{id}/chapters/{chapterId} |
deletes the chapter, its moves stay → tree |
PUT |
/api/repertoires/{id}/chapters/{chapterId}/moves/{nodeId} |
adds the move, the moves before it and everything after it to the chapter → tree |
DELETE |
/api/repertoires/{id}/chapters/{chapterId}/moves/{nodeId} |
takes the move and everything after it out of the chapter → tree |
GET |
/api/repertoires/{id}/tree |
flat node list in pre-order; ETag of the repertoire's version, 304 for If-None-Match while unchanged |
POST |
/api/repertoires/{id}/tree/changes |
{ "deletions": [nodeId], "additions": [{ "ref": -1, "parent": nodeId | -ref | null, "uci": "g1f3" }], "chapterId": 7, "links": [nodeId], "primary": nodeId, "promote": nodeId, "comments": [{ "node": nodeId, "placement": "AFTER" | "BEFORE", "text": "…" }], "nags": [{ "node": nodeId, "nags": [1, 14] }] } (links: existing moves that join chapterId; primary: own move that becomes the trained one; promote: move that becomes the first continuation, mainline flags following; comments: replace a move's comments at that place, blank text removes them; nags: replace a move's NAGs, an empty list removes them) → tree and new ids; all or nothing, 422 with a reason |
POST |
/api/repertoires/{id}/imports |
multipart file → 201, or 422 with errors[] |
POST |
/api/repertoires/{id}/imports/lichess-study |
{ "study": "link or id", "chapterIds": ["..."] } (none = all importable) → 201, or 422 |
POST |
/api/repertoires/from-lichess-study |
{ "name", "color", "study", "chapterIds", "variant"? } → 201 (only chapters of that variant are imported) with the repertoire and the import result; all or nothing |
GET |
/api/lichess/studies |
your Lichess studies, newest first |
PUT |
/api/repertoires/{id}/sharing |
{ "shared": true } → { "sharedAt" } (null when not shared); only the owner |
GET |
/api/shared-repertoires |
the pool: every shared repertoire with author, moves, chapters, mine |
POST |
/api/repertoires/{id}/imports/shared |
{ "sharedId" } → 201 with the import result; 422 for another side, another variant or the repertoire itself |
POST |
/api/repertoires/from-shared |
{ "sharedId", "name" } → 201 with the repertoire (side of the shared one) and the import result |
GET |
/api/positions |
the user's positions, newest first |
POST |
/api/positions |
{ "fen", "title", "note", "tags", "solution" } → 201 with the position; 422 if the position, a tag or a move is not valid |
GET |
/api/positions/{id} |
one position with its solution: the user's own or a shared one |
PUT |
/api/positions/{id} |
replaces a position (same body as POST); only the owner |
DELETE |
/api/positions/{id} |
204; only the owner |
PUT |
/api/positions/sharing |
{ "ids", "shared" } → 204; other users' positions are left alone |
GET |
/api/shared-positions |
every shared position with author and mine |
POST |
/api/positions/take |
{ "ids" } → 201 with the ids of the copies and how many were skipped |
GET |
/api/lichess/study-preview?study=... |
the study's chapters (with their variant) and which can be imported; suggestedColor, suggestedVariant |
GET |
/api/repertoires/{id}/training/chapters |
chapters with line counts (new, learning, due, known) |
POST |
/api/repertoires/{id}/training/attempts |
{ "mode": "SCHEDULED" | "PRACTICE", "extraNew": 1, "exclude": [lineIds], "chapterId": 7, "lineId": 42 } (extraNew: raise today's new-line limit by that many, kept for the training day; exclude: lines skipped in this session, plus lines already practiced in PRACTICE mode; lineId: play that line now) → next line as an attempt (or none) plus stats, canLearnMore, canPractice |
POST |
/api/repertoires/{id}/training/attempts/{attemptId}/give-up |
ends an attempt that has a mistake without playing the rest ("Try again") → { lineResult, stats }, graded as not passed; 409 without a mistake |
POST |
/api/repertoires/{id}/training/attempts/{attemptId}/answers |
{ "ply": 2, "uci": "d7d5", "responseMs": 1200 }; answers in line order (409 otherwise); the last answer carries lineResult |
GET |
/api/repertoires/{id}/training/stats |
counts of lines: new, learning, due, known |
GET |
/api/repertoires/{id}/training/statistics |
line table, activity, forecast, problem moves |
GET |
/api/explorer/otb?fen=... |
master games in a position (standard chess): source (data set, games, indexed half-moves; null before the import), totals, moves by popularity (SAN, counts, average rating, last year), topGames and recentGames |
GET |
/api/explorer/otb/games/{id} |
one master game with its tags and SAN moves |
GET |
/api/repertoires/{id}/game-check?max=20&speeds=blitz,rapid&refresh=false |
your latest Lichess games with the side, checked against the repertoire; games kept for an hour unless refresh; Atomic repertoires get Atomic games and ignore speeds |
All /api/** endpoints need a login (401 otherwise); requests that change data need the CSRF token from the XSRF-TOKEN cookie in the X-XSRF-TOKEN header. Errors use RFC 9457 problem details.
License
Alp Chess is licensed under the GNU General Public License v3.0 or later (LICENSE). The frontend uses chessground (GPL-3.0-or-later), chessops (GPL-3.0-or-later) and Stockfish (stockfish.js, GPL-3.0), so it has to be GPL as well; see ADR 0002 and ADR 0007.
The GPL asks that everyone who gets the frontend code can get its source. The repository is
public at https://git.alparslanyalcin.de/alpa/openingtrainer. The app also offers the source of
exactly the version it runs: the Docker build packs the build context (see .dockerignore) into
/source/alp-chess-source.tar.gz. Both are linked from the Open source page (/open-source,
footer). That page also credits the
libraries and assets and links their license texts: /licenses/third-party.md (generated by
Vite's build.license from all bundled libraries), /engine/COPYING.txt (Stockfish, copied by
scripts/copy-engine.mjs), /licenses/tabler-icons.txt and /licenses/gpl-3.0.txt.
The master games of the opening explorer come from Lumbra's Gigabase under CC BY-NC 4.0: free to use with credit, not commercially. They are not part of the repository or the image; they are imported into the database on the server. The explorer and the Open source page credit them and say how they were changed. Commercial use would need one of Lumbra's commercial licenses.
The app icon (images/app-icon.svg, and the PNG icons in frontend/public made from it) uses
the chess knight of Tabler Icons (icons/filled/chess-knight.svg,
version 3.49.0), MIT License, Copyright (c) 2020-2026 Paweł Kuna: permission is hereby granted,
free of charge, to deal in it without restriction; it is provided "as is", without warranty of
any kind. The knight is drawn white on a rounded square in the app's green (#4e8417).