v0.16.0 import PGN, delete games, two-layout web client
This commit is contained in:
1 parent
e1fa5242e3
commit
664cf9654e
34 files changed
+2007
-150
No files matched your search
+57
-1
@@ -290,6 +290,7 @@ carries its SAN, derived on read from the stored position before it.
|
||||
"termination": "checkmate",
|
||||
"startTimeUtc": "2026-09-07T12:00:00Z",
|
||||
"endTimeUtc": "2026-09-07T12:15:00Z",
|
||||
"origin": "played",
|
||||
"players": {
|
||||
"white": {"id": "user-id", "color": 1, "type": 1, "claimedBy": "user-id", "name": "alice"},
|
||||
"black": {"id": "player-id", "color": 2, "type": 1}
|
||||
@@ -312,6 +313,8 @@ resignation or agreed draw, in stored-result form (`"result": "black_wins"`);
|
||||
see [Continuing after a resignation or agreed
|
||||
draw](#continuing-after-a-resignation-or-agreed-draw).
|
||||
|
||||
`origin` is `played` or `imported`; an imported game also carries the
|
||||
`event`, `site` and `round` its PGN had.
|
||||
`result` is omitted while a game is ongoing. Persisted terminal values are
|
||||
`white_wins`, `black_wins`, `draw`, and `stalemate`. `pgnResult` is the PGN
|
||||
token for the same outcome (`1-0`, `0-1`, `1/2-1/2`, or `*` while ongoing),
|
||||
@@ -389,7 +392,8 @@ later first-move slot claim, newest first. Requires
|
||||
Each item contains game ID, initial FEN, final FEN (after the last stored move,
|
||||
or the initial FEN when there is none), result, its `pgnResult` token and
|
||||
`termination` (as in the history; both omitted while ongoing), timestamps,
|
||||
players, and move count. `nextCursor` is present only when another
|
||||
players, move count, `origin` (`played` or `imported`) and, for an imported
|
||||
game, its `event`. `nextCursor` is present only when another
|
||||
page exists; `nextOffset` also, on requests without a cursor. A game belongs
|
||||
to a user who created it while authenticated or claimed a slot with a first
|
||||
move; the association survives later player reconfiguration.
|
||||
@@ -410,6 +414,7 @@ Returns 400 `INVALID_REQUEST` for a malformed cursor or filter.
|
||||
"startTimeUtc": "2026-09-07T12:00:00Z",
|
||||
"endTimeUtc": "2026-09-07T12:15:00Z",
|
||||
"moveCount": 7,
|
||||
"origin": "played",
|
||||
"players": {
|
||||
"white": {"id": "user-id", "color": 1, "type": 1, "claimedBy": "user-id", "name": "alice"},
|
||||
"black": {"id": "player-id", "color": 2, "type": 1}
|
||||
@@ -422,6 +427,56 @@ Returns 400 `INVALID_REQUEST` for a malformed cursor or filter.
|
||||
}
|
||||
```
|
||||
|
||||
### Import a Game from PGN
|
||||
`POST /games/import`
|
||||
|
||||
Stores one PGN game under the signed-in account, for replay and export.
|
||||
Requires `Authorization: Bearer <token>` and persistent storage; 5 requests a
|
||||
minute per IP. The PGN is read strictly: well-formed tag pairs, printable
|
||||
text, balanced comments and variations (which are dropped, with annotations
|
||||
and numeric glyphs), one game, at most 64 KiB and 1000 plies. Every move is
|
||||
replayed by the rules core; an illegal move is refused with its ply. The
|
||||
result comes from the PGN's result token and must agree with the final
|
||||
position (a `1-0` after Black mates is refused); the stored termination is
|
||||
checkmate or stalemate when the position says so, a draw rule when one
|
||||
applies, otherwise resignation or agreement. `White`, `Black`, `Event`,
|
||||
`Site`, `Round` and `Date`/`UTCDate`/`UTCTime` tags are kept; `SetUp`/`FEN`
|
||||
set the starting position.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{"pgn": "[White \"Spassky, Boris\"]\n[Black \"Fischer, Robert\"]\n[Result \"1-0\"]\n\n1. d4 Nf6 2. c4 e6 3. Nf3 d5 1-0\n"}
|
||||
```
|
||||
|
||||
**Response (201):**
|
||||
```json
|
||||
{"gameId": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "moveCount": 6, "result": "white_wins", "pgnResult": "1-0"}
|
||||
```
|
||||
|
||||
Errors: 400 `INVALID_REQUEST` with `details` saying what is wrong with the
|
||||
PGN, 401 without a token, 429 past the limit, 503 `STORAGE_UNAVAILABLE`.
|
||||
|
||||
An imported game has no claimed side: it is listed for its importer with
|
||||
`"origin": "imported"` and its `event`, is excluded from the `color` filter,
|
||||
and is never purged as anonymous. Its history and PGN carry the tags.
|
||||
|
||||
### Remove a Stored Game
|
||||
`DELETE /users/me/games/{gameId}`
|
||||
|
||||
Takes a stored game off the signed-in user's list. A game the user alone
|
||||
holds (the only claimant, or its importer) is deleted with its moves, and
|
||||
unloaded if it was still in memory; a game another user also claims is kept
|
||||
for them, and only the caller's claim and name are cleared from it.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{"removed": "game"}
|
||||
```
|
||||
|
||||
`removed` is `game` or `claim`. Errors: 400 for a malformed ID, 404
|
||||
`GAME_NOT_FOUND` when the user holds nothing in that game, 503 when storage
|
||||
is unavailable.
|
||||
|
||||
### Make Move
|
||||
`POST /games/{gameId}/moves`
|
||||
|
||||
@@ -603,6 +658,7 @@ Error codes:
|
||||
- Login: 10 requests/minute/IP
|
||||
- Account deletion: 5 requests/minute/IP
|
||||
- Password recovery: 3 requests/minute/IP; reset: 10 requests/minute/IP
|
||||
- PGN import: 5 requests/minute/IP
|
||||
|
||||
Exceeding limit returns 429 status. Behind a reverse proxy the client IP is the
|
||||
proxy's `X-Real-IP` header, trusted only from addresses listed in
|
||||
|
||||
+16
-4
@@ -182,11 +182,23 @@ repetition.` or `Black wins by resignation.`; a pending offer is shown as
|
||||
|
||||
#### `games` / `g`
|
||||
List your stored games, newest first, 20 per page (requires login), numbered
|
||||
for `replay <#>`. The result column is the PGN token (`1-0`, `0-1`,
|
||||
`1/2-1/2`, `*`).
|
||||
for `replay <#>` and `games delete <#>`. The result column is the PGN token
|
||||
(`1-0`, `0-1`, `1/2-1/2`, `*`). Deleting a game you alone hold removes it
|
||||
with its moves; a game another player also claims is only released from
|
||||
your list.
|
||||
```
|
||||
chess > games # Newest games
|
||||
chess > games more # Next page
|
||||
chess > games # Newest games
|
||||
chess > games more # Next page
|
||||
chess > games delete 3 # Remove the third game of the last listing (or an ID)
|
||||
```
|
||||
|
||||
#### `import`
|
||||
Store a PGN file (one game) under your account, for replay with `replay`.
|
||||
The server checks every move; tags such as `White`, `Black`, `Event` and
|
||||
`Date` are kept. Not available in the browser build, which has no files;
|
||||
the web board imports a pasted PGN instead.
|
||||
```
|
||||
chess > import games/fischer-spassky-1972-1.pgn
|
||||
```
|
||||
|
||||
#### `pgn`
|
||||
|
||||
@@ -137,6 +137,14 @@ one-time password-reset link (valid an hour) to hand to someone who cannot
|
||||
receive the recovery email. CLI-created accounts are identical to site
|
||||
registrations, and the CLI ignores the registration cap.
|
||||
|
||||
A game imported from PGN (`POST /api/games/import`, `db game import`) has
|
||||
`imported_by` set to its owner, no claims, the PGN's `event`, `site` and
|
||||
`round`, and both sides human with the tag names as snapshots. It is listed
|
||||
for its owner, never purged as anonymous, and deleted with its moves when the
|
||||
owner removes it (`DELETE /api/users/me/games/{id}`) or by `db game delete
|
||||
-gameId <id|prefix> -confirm`. A player removes a game they share with
|
||||
another registered player by releasing their own claim instead.
|
||||
|
||||
Pending password resets live in `password_resets`: the SHA-256 of the token
|
||||
in the emailed link, one per account, deleted when used, expired, or by the
|
||||
hourly cleanup. The token itself is never stored.
|
||||
|
||||
+3
-2
@@ -194,8 +194,9 @@ jail# su -m chess -c '/home/chess/bin/chess-server db user list -dsn "postgres:/
|
||||
`db` subcommands: `init`, `delete -confirm` (drops the tables), `query`
|
||||
(games, with 8-digit ID prefixes), `pgn -gameId <id|prefix> [-ply N]`,
|
||||
`verify [-gameId <id|prefix>]` (replays stored games against the rules and
|
||||
exits non-zero on a mismatch; run it after an upgrade), and `user add|delete|
|
||||
set-password|set-hash|set-email|set-username|reset-link|list`.
|
||||
exits non-zero on a mismatch; run it after an upgrade), `game delete -gameId
|
||||
<id|prefix> -confirm`, `game import -file <pgn> -username <owner>`, and `user
|
||||
add|delete|set-password|set-hash|set-email|set-username|reset-link|list`.
|
||||
|
||||
A forgotten password is reset from the jail without mail:
|
||||
|
||||
|
||||
+29
-12
@@ -141,9 +141,11 @@ Constraints from the host's security headers:
|
||||
- `connect-src 'self'`: all requests go to the same origin under `/chess/`
|
||||
(the board's default API base, or the host's `data-api`); no CDN or
|
||||
cross-origin API.
|
||||
- `frame-ancestors 'self'` and `X-Frame-Options: SAMEORIGIN`: the iframe host
|
||||
page must stay on the same origin. Downloads from `blob:` URLs and
|
||||
`navigator.clipboard` work in a same-origin frame.
|
||||
- `frame-ancestors 'self'`: the iframe host page must stay on the same
|
||||
origin. No `X-Frame-Options` beside it: browsers that read
|
||||
`frame-ancestors` ignore the header and warn in the console on every page
|
||||
load. Downloads from `blob:` URLs and `navigator.clipboard` work in a
|
||||
same-origin frame.
|
||||
|
||||
Test gate:
|
||||
|
||||
@@ -152,7 +154,10 @@ Test gate:
|
||||
step backward and forward with buttons and keys, autoplay to the end,
|
||||
compare the exported FEN and PGN with the API, download, copy a link,
|
||||
browser back through replay and list; a deep link to a checkmate; the
|
||||
device list for an anonymous visitor.
|
||||
device list for an anonymous visitor; a PGN import replayed and deleted
|
||||
from its row; a phone viewport that holds the board and the controls,
|
||||
with a long replay scrolling inside its move list; a password reset
|
||||
through the emailed link.
|
||||
- [x] Keyboard-only navigation and labels: every control is a button with a
|
||||
name, the move list is a list of buttons, the current ply has
|
||||
`aria-current` and is announced; `prefers-reduced-motion` disables the
|
||||
@@ -204,14 +209,25 @@ Later for the replay clients:
|
||||
- [ ] Email verification at registration and on `set-email`; today an
|
||||
address is taken as typed, so a typo sends nobody a link.
|
||||
|
||||
## Phase R5 — Curated Archive (later)
|
||||
## Phase R5 — Imported and Curated Games
|
||||
|
||||
- [ ] Schema: game origin (`player`, `curated`), archive metadata (event, site,
|
||||
date, round, names, Elo, ECO, source URL, license, attribution), publication
|
||||
state, featured order; no user ownership.
|
||||
- [ ] `chess-server db archive import <pgn|dir>` using the R1 SAN decoder:
|
||||
validate every move, import each game in one transaction, idempotent by
|
||||
content hash, dry-run and per-file summary.
|
||||
Done, per user: a signed-in user imports one PGN game (`POST
|
||||
/api/games/import`, **Import PGN** in My games, `import <file>` in the CLI,
|
||||
`db game import` for an administrator) into their own list. Schema v5 adds
|
||||
`imported_by`, `event`, `site`, `round`; the strict parser
|
||||
(`chess.ParsePGN`, fuzzed) refuses anything but one well-formed game, every
|
||||
move is replayed by the rules core, and the result must agree with the
|
||||
final position. Imported games are never purged as anonymous and are
|
||||
deleted by their owner (`DELETE /api/users/me/games/{id}`, the row's delete
|
||||
button, `games delete`), as are played games the user alone holds; a game
|
||||
shared with another player is released instead.
|
||||
|
||||
Later, a curated archive:
|
||||
|
||||
- [ ] No user ownership: publication state, featured order, archive metadata
|
||||
(Elo, ECO, source URL, license, attribution).
|
||||
- [ ] Bulk import of a PGN file with many games or a directory, idempotent
|
||||
by content hash, dry-run and per-file summary.
|
||||
- [ ] Public, bounded, cacheable list endpoint; replay through the same
|
||||
history/PGN endpoints.
|
||||
- [ ] Verify redistribution rights for every bundled collection.
|
||||
@@ -223,7 +239,8 @@ Later for the replay clients:
|
||||
- [ ] Degraded-mode recovery without a restart (replay missing writes from
|
||||
memory or fail gameplay closed); today recovery requires a restart.
|
||||
- [ ] Live-game restoration after restart (unfinished games are replay-only).
|
||||
- [ ] Retention and user-initiated deletion or anonymization of games.
|
||||
- [x] User-initiated deletion of games (R5); retention of played games is
|
||||
the anonymous purge only.
|
||||
- [ ] Thread request contexts from HTTP handlers into storage calls; today each
|
||||
call carries its own deadline.
|
||||
- [x] Draw and resign flows (D5) for live play.
|
||||
|
||||
Reference in new issue
Block a user