v0.12.2 drop version from api path, deploy script update

This commit is contained in:
lixen committed 2026-09-27 02:30:00 -04:00
1 parent 1e7d759a19
commit 52ac0fe31a
24 files changed
+658 -650

No files matched your search

+3 -1
View File
@@ -1,6 +1,8 @@
# API Reference
Base URL: `http://localhost:8080/api/v1`
Base URL: `http://localhost:8080/api`
The API is unversioned; `/health` is served at the root, outside `/api`.
Content-Type: `application/json` (required for POST/PUT)
+5 -3
View File
@@ -111,7 +111,7 @@ schema is current, which lets a DML-only runtime role start the server.
8. Game deletion notifies and removes all waiters
### Durable Replay Read
1. Client requests `GET /api/v1/games/{id}/history`
1. Client requests `GET /api/games/{id}/history`
2. Storage queues a barrier after all previously accepted gameplay writes
3. The writer reaches the barrier only after those transactions finish
4. Storage reads the game row and ordered moves in one REPEATABLE READ transaction
@@ -263,8 +263,10 @@ outlives deleted accounts, and the name snapshot keeps it readable.
database 24 hours after their last activity (`-anonymous-game-ttl`). The
hourly cleanup evicts idle games first, then queues the delete through the
ordered writer while excluding every game still loaded, so no live game can
lose its row. See [`deploy/postgresql`](../deploy/postgresql) for
provisioning and the one-time SQLite import.
lose its row.
See [`deploy/postgresql`](../deploy/postgresql) for provisioning and
[deployment.md](./deployment.md) for the jail service.
## Security Architecture
+2 -2
View File
@@ -166,8 +166,8 @@ chess > url http://localhost:9090 # Change server URL
#### `raw` / `:`
Send raw API request.
```
chess > raw GET /api/v1/games/<id>
chess > raw POST /api/v1/games '{"white":{"type":1},"black":{"type":2}}'
chess > raw GET /api/games/<id>
chess > raw POST /api/games '{"white":{"type":1},"black":{"type":2}}'
```
#### `clear` / `-`
+166 -177
View File
@@ -1,96 +1,107 @@
# Deployment: FreeBSD Jail with PostgreSQL 18
Production layout: host nginx terminates TLS and reverse-proxies `/chess/` to
`chess-server` in a Bastille jail. PostgreSQL 18 runs in the same jail and
listens **only on its Unix socket**. The `chess` OS account has no shell or
password; its database role reaches only the `chess` database through peer
authentication, so no database password exists.
Layout: host nginx terminates TLS and proxies `/chess/api/` and `/chess/health`
to `chess-server` in a Bastille jail. The rc.d service is `chessd`, running as
the unprivileged `chess` account (no shell, no password). PostgreSQL 18 runs in
the same jail and listens **only on its Unix socket**. The `chess` role reaches
only the `chess` database through peer authentication, so no database password
exists.
```
internet ──TLS──▶ host nginx ──HTTP──▶ jail: chess-server (API port)
│ Unix socket, peer auth
▼
jail: PostgreSQL 18 (no TCP listener)
browser ──TLS──▶ host nginx ──HTTP──▶ jail: chessd (chess-server, API port)
│ Unix socket, peer auth
▼
jail: PostgreSQL 18 (no TCP listener)
```
`host#` runs on the Bastille host as root; `jail#` runs inside the jail as root
(`bastille console <jail>`).
(`bastille console <jail>`). Addresses in this document are placeholders.
## Scripted Deployment
## Scripted Installation and Upgrade
The scripts in [`deploy/`](../deploy) perform every step below and are safe to
rerun for upgrades. Their header comments list all inputs.
The scripts in [`deploy/`](../deploy) are idempotent; rerun them to upgrade.
Their header comments list every input.
1. **Host:** give the jail its own System V shared-memory namespace, which
PostgreSQL needs, and restart it:
1. **Host, once:** give the jail its own System V shared-memory namespace,
which PostgreSQL needs, and restart it:
```sh
host# sh deploy/freebsd/host.sh chess
host# sh deploy/freebsd/host.sh <jail>
```
2. **Build** the static server on any machine with Go 1.26+, then copy the
2. **Jail, once:** install the packages:
```sh
jail# pkg install postgresql18-server postgresql18-client stockfish
```
3. **Build** the release (FreeBSD `make` or GNU make) and copy the server
binary and a checkout of this repository into the jail:
```sh
make server-freebsd # bin/chess-server-freebsd-amd64
```
3. **Jail:** install and configure everything. `OLD_SERVICE` names the rc.d
service of the SQLite deployment; `SQLITE_DB` imports its data once.
4. **Jail:** install or upgrade the service. `TRUSTED_PROXIES` is the address
the host nginx connects from, as seen inside the jail.
```sh
jail# CHESS_BINARY=/tmp/chess-server-freebsd-amd64 \
API_HOST=10.17.89.10 TRUSTED_PROXIES=10.17.89.1 \
OLD_SERVICE=chess SQLITE_DB=/usr/local/chess/db/chess.db \
TRUSTED_PROXIES=<nginx-address> \
ENABLE_LOG_ROTATION=yes ENABLE_BACKUP=yes \
sh deploy/freebsd/setup-jail.sh
```
It ends with a health check that requires `"storage":"ok"`.
The script stops `chessd`, installs everything, starts it again, and fails
unless `/health` reports `"storage":"ok"`.
4. **Host:** add the location block from
[`deploy/freebsd/nginx-chess.conf`](../deploy/freebsd/nginx-chess.conf) to
the TLS server block, then `nginx -t && service nginx reload`.
5. **Verify** with the checklist at the end of this document. Keep the old
SQLite file until you are satisfied; the import does not modify it.
5. **Host:** point nginx at the jail
([`deploy/freebsd/nginx-chess.conf`](../deploy/freebsd/nginx-chess.conf))
and publish the web client (below).
`setup-jail.sh` inputs:
| Variable | Default | Meaning |
|---|---|---|
| `CHESS_BINARY` | required | New `chess-server` binary |
| `API_HOST` | required | Listen address (the jail IP) |
| `API_PORT` | `8080` | Listen port |
| `TRUSTED_PROXIES` | empty (warns) | nginx address(es) as seen from the jail |
| `CHESS_HOME` | account home, else `/usr/local/chess` | Binary and JWT key location |
| `LOG_DIR` | `/var/log/chess` | Server log directory |
| `SQLITE_DB` | none | v0.11 SQLite database to import into an empty schema |
| `OLD_SERVICE` | none | Previous rc.d service to stop and disable |
| `CHESS_BINARY` | required | `chess-server` binary to install |
| `TRUSTED_PROXIES` | unset (warns) | Stored as `chessd_trusted_proxies` in `rc.conf` |
| `SPLIT_PRIVILEGES` | `no` | `yes` for a DML-only runtime role (below) |
| `EXTRA_FLAGS` | none | More server flags, e.g. `-max-users 500` |
| `PGDATA` | `/var/db/postgres/data18` | PostgreSQL data directory |
| `ENABLE_LOG_ROTATION` | `no` | newsyslog entry for the service log |
| `ENABLE_BACKUP` | `no` | Nightly `pg_dump` via cron |
The service layout (account, home, binary, log, listen address, DSN, key)
comes from `rc.conf` `chessd_*` settings, with the defaults of
[`rc.d/chessd`](../deploy/freebsd/rc.d/chessd): account `chess`, home
`/home/chess`, binary `~/bin/chess-server`, log `/var/log/chessd.log`,
`0.0.0.0:8080`, DSN `postgres:///chess?host=/tmp`, key `~/jwt.key`.
What the script changes, in order:
| Step | Result |
|---|---|
| Packages | `postgresql18-server`, `postgresql18-client`, `stockfish` (and `sqlite3` for an import) |
| PostgreSQL | `initdb` with UTF-8, builtin `C.UTF-8` locale, peer local auth, host auth rejected; data checksums are on by default in 18 |
| `pg_hba.conf` | Only `postgres` (all databases) and `chess` (database `chess`), both `local` + `peer`; original kept as `pg_hba.conf.orig` |
| `postgresql.auto.conf` | `listen_addresses = ''` (no TCP), socket in `/tmp`, `scram-sha-256` |
| Provisioning | [`deploy/postgresql/setup.sql`](../deploy/postgresql/setup.sql) on first run only |
| Account | `chess` created if missing (`nologin`, no password); a warning if an existing account has a login shell |
| Files | Binary `root:wheel 0555` (previous copy kept as `.prev`); `jwt.key` generated once, `chess:chess 0600`; log directory `chess:chess 0750` |
| Preflight | Requires root, PostgreSQL 17+ installed, `chessd_user=chess`; refuses an `rc.conf` `chessd_flags` that still contains `-storage-path`; warns if `stockfish` is missing |
| Cluster | `initdb` only when none exists: UTF-8, builtin `C.UTF-8` locale, peer local auth, host auth rejected |
| `pg_hba.conf` | Only `postgres` (all databases) and `chess` (database `chess`), both `local` + `peer`; original kept as `pg_hba.conf.orig`. This replaces the trust-everything default of FreeBSD's standard `initdb` flags |
| Listener | `ALTER SYSTEM SET listen_addresses = ''` (no TCP) and a restart, only when TCP is still enabled |
| Provisioning | [`setup.sql`](../deploy/postgresql/setup.sql) when neither role nor database `chess` exists |
| Service stop | `service chessd onestop` through the installed rc.d script |
| Binary | `root:wheel 0555`; the previous file kept as `chess-server.prev` |
| rc.d | [`rc.d/chessd`](../deploy/freebsd/rc.d/chessd) installed; a differing previous copy saved as `/var/backups/chessd.rc.<time>` (not inside `rc.d`, where rc(8) would run it) |
| JWT key | `~/jwt.key`, `chess:chess 0600`, generated once |
| Schema | `chess-server db init` (as `chess`, or as `chess_owner` in split mode) |
| Import | [`migrate-sqlite.sh`](../deploy/postgresql/migrate-sqlite.sh) when `SQLITE_DB` is set and the schema is empty |
| Service | [`rc.d/chess_server`](../deploy/freebsd/rc.d/chess_server) and `rc.conf` via `sysrc` |
| Log rotation | `/usr/local/etc/newsyslog.conf.d/chess_server.conf`: daily or 1 MB, 7 kept |
| Backups | `/usr/local/sbin/chess-backup` via `/usr/local/etc/cron.d/chess-backup`: `pg_dump` at 03:30 as `postgres`, 14 days kept |
| `rc.conf` | `chessd_enable=YES`, `chessd_trusted_proxies` when given; removes the unused `chessd_storage_path` and `chessd_dir` |
| Log rotation (opt-in) | `/usr/local/etc/newsyslog.conf.d/chessd.conf`: daily or 1 MB, 7 kept, SIGHUP to `daemon(8)` |
| Backups (opt-in) | `/usr/local/sbin/chess-backup` via `/usr/local/etc/cron.d/chess-backup`: `pg_dump` at 03:30 as `postgres`, 14 days kept in `/var/db/postgres/backups` |
| Start | `service chessd start`, then `/health` on the configured port |
The script was exercised on Linux with FreeBSD-only commands (`pkg`, `sysrc`,
`service`, `pw`, `fetch`) stubbed and everything else real: PostgreSQL 18.6,
`su`, provisioning in both privilege modes, schema migration, the SQLite
import, and the health check of the running server.
Files of an earlier SQLite release are not touched; delete them once the new
release is verified.
The script and rc.d script were exercised on Linux with the FreeBSD-only
commands (`rc.subr`, `daemon`, `service`, `sysrc`, `pw`, `fetch`) stubbed and
everything else real (PostgreSQL 18.6, `su`, both server generations). The
tested runs were an upgrade from a running v0.11 SQLite `chessd`, an idempotent
rerun, and a fresh split-privilege install.
## Accounts and Data Retention
@@ -102,42 +113,93 @@ import, and the health check of the running server.
claimed a slot with their first move. Such games are kept indefinitely and
record the player's username at claim time.
- Games with no registered player are **deleted 24 hours after their last
activity** (creation, move, undo, reconfiguration, or result), from memory and
from the database, by the hourly cleanup. `-anonymous-game-ttl` changes the
window; `0` keeps them.
activity**, from memory and from the database, by the hourly cleanup.
`-anonymous-game-ttl` changes the window; `0` keeps them.
- Deleting a user removes their sessions; their games keep the claim and name.
## Service (`rc.d/chessd`)
`rc.conf` settings, with defaults:
| Setting | Default | Purpose |
|---|---|---|
| `chessd_enable` | `NO` | Start at boot |
| `chessd_user`, `chessd_group` | `chess` | Service account; must stay `chess` for peer authentication |
| `chessd_home` | `/home/chess` | Account home |
| `chessd_bin` | `${chessd_home}/bin/chess-server` | Server binary |
| `chessd_logs` | `/var/log/chessd.log` | Log file |
| `chessd_host`, `chessd_port` | `0.0.0.0`, `8080` | Listen address |
| `chessd_dsn` | `postgres:///chess?host=/tmp` | Database `chess` over the socket as the OS user |
| `chessd_jwt_key` | `${chessd_home}/jwt.key` | JWT signing key; created (0600) on first start if missing |
| `chessd_trusted_proxies` | empty | Proxy address(es) whose `X-Real-IP` is trusted |
| `chessd_flags` | empty | Extra flags, e.g. `-max-users 500` |
The script runs `daemon(8)` as root, which writes the supervisor pidfile
(`/var/run/chessd.pid`), drops to `chess`, restarts the server 5 seconds after
an unexpected exit, and reopens the log on SIGHUP. It declares `REQUIRE:
postgresql`, so the database starts first at boot. The environment passed to
the server is `HOME` and a `PATH` that includes `/usr/local/bin` for Stockfish.
Server flags:
| Flag | Environment default | Purpose |
|---|---|---|
| `-dsn` | `CHESS_DSN` | PostgreSQL connection string; empty disables persistence |
| `-jwt-secret-file` | `CHESS_JWT_SECRET_FILE` | Stable JWT signing key |
| `-trusted-proxies` | | Proxy address(es) trusted for the client-IP header |
| `-proxy-header` | | Client-IP header set by the proxy (default `X-Real-IP`) |
| `-max-users` | | Registration cap (default 100, `0` = none) |
| `-anonymous-game-ttl` | | Retention of games without a registered player (default `24h`) |
| `-finished-game-ttl` | | Memory retention of finished games (default `1h`) |
Administrative CLI commands run as `chess` so peer authentication selects the
right role; root may use `su -m` although the account has no shell:
```sh
jail# su -m chess -c '/home/chess/bin/chess-server db user list -dsn "postgres:///chess?host=/tmp"'
```
## Host nginx and Web Clients
The API has no version segment: routes are `/api/...` and `/health`. With the
layout in [`nginx-chess.conf`](../deploy/freebsd/nginx-chess.conf), browsers
call `<origin>/chess/api/...` and nginx maps `/chess/api/` to `/api/`.
- `X-Real-IP` must carry the real client address (`$remote_addr`, after
`real_ip` processing when PROXY protocol is in front of nginx). The server
honors it only on connections from `chessd_trusted_proxies`.
- `proxy_set_header Connection "";` lets nginx reuse the upstream `keepalive`
connections; without it, every request opens a new connection.
- The existing security headers are unaffected: the API is same-origin under
`/chess/`.
Browser clients are static files published by the host, and must match the
server's API paths:
- **Web client:** copy `internal/server/webserver/chess-client-web/`
(`index.html`, `app.js`, `style.css`) to the site directory. Without the
embedded `/config` endpoint, it uses the `/chess` API prefix.
- **WASM terminal client (if published):** `make wasm` (xterm.js is committed
under `lib/`), then copy `web/chess-client-wasm/`. It derives its API base
as `<origin>/chess`.
Publish the clients in the same change as the server: a client built for
`/api/v1` gets 404 from this release.
## Reference
The sections below describe what the scripts configure, for review or manual
installation.
### PostgreSQL configuration
### Jail prerequisites
PostgreSQL allocates a small System V shared-memory segment even though its
main buffers use `mmap`. `host.sh` runs:
What `setup-jail.sh` applies, for review or manual installation:
```sh
host# bastille config chess set sysvshm new
host# bastille restart chess
```
`sysvshm=new` gives the jail an isolated namespace, unlike the older shared
`allow.sysvipc`. Without it, `initdb` fails with `could not create shared
memory segment`.
### PostgreSQL
```sh
jail# pkg install postgresql18-server postgresql18-client stockfish
jail# sysrc postgresql_enable=YES
jail# sysrc postgresql_initdb_flags="--encoding=UTF8 --locale-provider=builtin --builtin-locale=C.UTF-8 --auth-local=peer --auth-host=reject"
jail# service postgresql initdb
jail# service postgresql initdb # only when no cluster exists
jail# service postgresql start
jail# su -m postgres -c "psql -X -d postgres" <<'SQL'
ALTER SYSTEM SET listen_addresses = '';
ALTER SYSTEM SET unix_socket_directories = '/tmp';
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SQL
jail# service postgresql restart
```
@@ -150,6 +212,9 @@ local all postgres peer
local chess chess peer
```
Database administration uses the `postgres` account over the socket:
`su -m postgres -c psql`.
### Provisioning and privilege models
[`setup.sql`](../deploy/postgresql/setup.sql) creates the login role `chess`
@@ -159,128 +224,52 @@ connections), the `chess` database (`template0`, builtin `C.UTF-8`), and the
`public`, and sets role defaults for `search_path`, `statement_timeout`,
`lock_timeout`, and `idle_in_transaction_session_timeout`.
```sh
jail# su -m postgres -c 'psql -X -d postgres -f -' < deploy/postgresql/setup.sql
```
| Mode | Schema owner | `chess` role can | Schema migrations |
|---|---|---|---|
| Owner (default) | `chess` | DML and DDL in schema `chess` only | Automatic at server start |
| Split (`-v split=true`, `SPLIT_PRIVILEGES=yes`) | `chess_owner` (NOLOGIN) | `SELECT/INSERT/UPDATE/DELETE` only | `setup-jail.sh`, or manually as `postgres` (below) |
```sh
jail# su -m postgres -c "/usr/local/chess/chess-server db init \
-dsn \"dbname=chess user=postgres options='-c role=chess_owner -c search_path=chess'\""
```
| Split (`SPLIT_PRIVILEGES=yes`) | `chess_owner` (NOLOGIN) | `SELECT/INSERT/UPDATE/DELETE` only | `setup-jail.sh`, or manually as `postgres` |
In split mode the server refuses to start while a migration is pending.
### The `chess` account
| Resource | Requirement |
|---|---|
| Shell/password | `/usr/sbin/nologin`, password disabled |
| Home | Holds `chess-server` (`root:wheel 0555`) and `jwt.key` |
| JWT key | `chess:chess 0600`, at least 32 bytes; the server refuses group- or world-readable keys |
| Log directory | `chess:chess 0750` |
| Database | Role `chess` over `/tmp/.s.PGSQL.5432`, database `chess` only |
| Network | Listens on the API port (unprivileged, above 1024) |
| Engine | `stockfish` in `PATH` (`/usr/local/bin`; set through `chess_server_env`) |
Administrative CLI commands run as `chess` so peer authentication selects the
right role; root may use `su -m` although the account has no shell:
```sh
jail# su -m chess -c '/usr/local/chess/chess-server db user list -dsn "postgres:///chess?host=/tmp"'
```
### JWT signing key
`setup-jail.sh` generates the key once with `openssl rand -base64 48`. Without
a key file, production mode generates a new key at every start and every
browser session ends on restart. To rotate, replace the file and restart the
service; users then sign in again.
The key is 48 random bytes, base64-encoded, readable only by `chess`. Without a
key file, production mode generates a new key at every start and every session
ends on restart. To rotate, replace the file and restart `chessd`; users sign
in again.
### Service
### Rollback
`rc.conf` as written by `setup-jail.sh`:
Until the old release's files are deleted, a failed upgrade can be reverted:
```sh
chess_server_enable="YES"
chess_server_account="chess"
chess_server_binary="/usr/local/chess/chess-server"
chess_server_logfile="/var/log/chess/chess-server.log"
chess_server_env="PATH=/usr/local/bin:/usr/bin:/bin"
chess_server_flags="-api-host 10.17.89.10 -api-port 8080 -dsn postgres:///chess?host=/tmp -jwt-secret-file /usr/local/chess/jwt.key -log-level info -trusted-proxies 10.17.89.1"
jail# service chessd onestop
jail# cp /var/backups/chessd.rc.<time> /usr/local/etc/rc.d/chessd
jail# cp /home/chess/bin/chess-server.prev /home/chess/bin/chess-server
jail# service chessd start
```
The rc.d script starts `daemon(8)` as root, which writes the supervisor
pidfile, drops to `chess`, restarts the server if it exits, and reopens the log
on SIGHUP for newsyslog. It declares `REQUIRE: postgresql`. The SQLite-era
`-storage-path` flag no longer exists; a server started with it exits with
`flag provided but not defined`.
Restore the previous web client files on the host as well.
| Flag | Environment default | Purpose |
|---|---|---|
| `-dsn` | `CHESS_DSN` | PostgreSQL connection string; empty disables persistence |
| `-jwt-secret-file` | `CHESS_JWT_SECRET_FILE` | Stable JWT signing key |
| `-trusted-proxies` | | nginx address(es) as seen from the jail |
| `-proxy-header` | | Client-IP header set by nginx (default `X-Real-IP`) |
| `-max-users` | | Registration cap (default 100, `0` = none) |
| `-anonymous-game-ttl` | | Retention of games without a registered player (default `24h`) |
| `-finished-game-ttl` | | Memory retention of finished games (default `1h`) |
### Backups and restores
### Host nginx
[`nginx-chess.conf`](../deploy/freebsd/nginx-chess.conf) passes the client
address in `X-Real-IP` (which nginx overwrites), allows 30-second long-polls,
and bounds request bodies. The server keys rate limits on `X-Real-IP` only for
connections from `-trusted-proxies`; any other peer is keyed by its own
address. Without `-trusted-proxies`, every proxied request shares one bucket.
The existing security headers (HSTS, `X-Frame-Options`, CSP with
`connect-src 'self'`) are unaffected: the API is same-origin under `/chess/`,
and this release does not change the web client.
### SQLite import
[`migrate-sqlite.sh`](../deploy/postgresql/migrate-sqlite.sh) reads a
schema-version-2 SQLite database (v0.11) and imports, in one transaction, all
users as regular accounts with their Argon2id hashes, unexpired sessions,
games with claims and player names, results, and moves. It refuses a schema
that already has data. Imported games without a registered player follow the
24-hour retention. Sessions carry over, but tokens signed with the old
per-restart key do not, so users sign in once. Manual invocation:
```sh
jail# su -m postgres -c 'sh /path/to/migrate-sqlite.sh /tmp/legacy.db dbname=chess'
```
### Upgrades, backups, and restores
- **Upgrade:** build the new binary and rerun `setup-jail.sh` with the same
inputs (omit `SQLITE_DB`). It keeps the previous binary as
`chess-server.prev`, migrates the schema, and restarts the service. To roll
back a release without a schema change, copy `.prev` back and restart.
- **Migrations** are transactional and serialized by an advisory lock. A
server refuses to start against a schema newer than it supports.
- **Backups:** `chess-backup` writes `pg_dump --format=custom` files to
`/var/db/postgres/backups` nightly and removes those older than 14 days. Copy
them off the jail.
- **Restore** into a freshly provisioned database:
- `chess-backup` writes `pg_dump --format=custom` files nightly and removes
those older than 14 days. Copy them off the jail.
- Restore into a freshly provisioned database:
`su -m postgres -c 'pg_restore --no-owner --role=chess -d chess <dump>'`
(`--role=chess_owner` in split mode).
- **PostgreSQL restarts:** the server retries in-flight writes across a brief
- PostgreSQL restarts: the server retries in-flight writes across a brief
outage. A write that still fails marks storage `degraded` in `/health`; live
games continue in memory, but durable history stops until the server is
restarted.
games continue in memory, but durable history stops until `chessd` restarts.
### Verification checklist
```sh
jail# su -m chess -c 'psql -X -d chess -Atc "SHOW search_path"' # chess
jail# su -m chess -c 'psql -X -d postgres -c "select 1"' # rejected by pg_hba
jail# sockstat -4 -6 -l | grep postgres # no TCP listener
jail# service chess_server status
jail# fetch -qo - http://10.17.89.10:8080/health # "storage":"ok"
host# curl -s https://lixen.com/chess/health # through nginx
jail# service chessd status
jail# fetch -qo - http://127.0.0.1:8080/health # "storage":"ok"
jail# sockstat -4 -6 -l | grep postgres # no TCP listener
jail# sockstat -4 -c | grep ':8080' # peer = nginx address
jail# su -m chess -c 'psql -X -d chess -Atc "SHOW search_path"' # chess
jail# su -m chess -c 'psql -X -d postgres -c "select 1"' # rejected by pg_hba
host# curl -s https://<site>/chess/health # through nginx
```
+3 -3
View File
@@ -15,9 +15,9 @@ into phases that each ship and test on their own.
times, and slot claims; transactional migrations (`schema_version`).
- [x] Each accepted move, first-move claim, and move-caused result commits in
one transaction; undo removes moves and clears the result atomically.
- [x] `GET /api/v1/games/{id}/history`: initial FEN plus UCI and resulting FEN
- [x] `GET /api/games/{id}/history`: initial FEN plus UCI and resulting FEN
for every ply, read in one REPEATABLE READ snapshot after pending writes.
- [x] `GET /api/v1/users/me/games`: claimed games, newest first, with move
- [x] `GET /api/users/me/games`: claimed games, newest first, with move
count and final FEN (thumbnail) from a primary-key probe per game.
- [x] Claims survive player reconfiguration and terminal-game eviction; the
listing indexes are ordered for keyset pagination.
@@ -74,7 +74,7 @@ Test gate:
- [ ] Implement D7 (keyset cursor) once decided.
- [ ] Additive history fields: `san` per move, `outcome`/`termination`, and
`pgnResult`; keep existing fields unchanged.
- [ ] `GET /api/v1/games/{id}/pgn?ply=N`: `application/x-chess-pgn`,
- [ ] `GET /api/games/{id}/pgn?ply=N`: `application/x-chess-pgn`,
`Content-Disposition: attachment; filename="chess-<date>-<id8>.pgn"`,
moves 1..N (default all). The FEN at any ply is already in the history.
- [ ] `ETag` and `Cache-Control: private, max-age` for terminal games;