v0.16.2 PGN site, web client panel update
This commit is contained in:
1 parent
0461020c46
commit
a012d07cd8
12 files changed
+84
-45
No files matched your search
@@ -215,7 +215,7 @@ func runPGN(args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load game %s: %w", id, err)
|
return fmt.Errorf("load game %s: %w", id, err)
|
||||||
}
|
}
|
||||||
pgn, err := replay.BuildPGN(record, moves, *ply)
|
pgn, err := replay.BuildPGN(record, moves, *ply, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -62,14 +63,15 @@ func main() {
|
|||||||
|
|
||||||
// Password recovery by email: a transport (SMTP relay, or a directory
|
// Password recovery by email: a transport (SMTP relay, or a directory
|
||||||
// of .eml files for development) and the web client's address, which
|
// of .eml files for development) and the web client's address, which
|
||||||
// the emailed link opens. Without a transport, recovery is off.
|
// the emailed link opens. Without a transport, recovery is off. The
|
||||||
|
// address's host also names this server in exported PGN (Site).
|
||||||
smtpHost = flag.String("smtp-host", "", "SMTP relay for password-reset email (recovery is off when empty)")
|
smtpHost = flag.String("smtp-host", "", "SMTP relay for password-reset email (recovery is off when empty)")
|
||||||
smtpPort = flag.Int("smtp-port", 25, "SMTP relay port")
|
smtpPort = flag.Int("smtp-port", 25, "SMTP relay port")
|
||||||
smtpUser = flag.String("smtp-user", "", "SMTP user name (optional; the password comes from -smtp-password-file)")
|
smtpUser = flag.String("smtp-user", "", "SMTP user name (optional; the password comes from -smtp-password-file)")
|
||||||
smtpPassFile = flag.String("smtp-password-file", "", "File holding the SMTP password, mode 0600")
|
smtpPassFile = flag.String("smtp-password-file", "", "File holding the SMTP password, mode 0600")
|
||||||
mailFrom = flag.String("mail-from", "", "Sender address of server email, e.g. \"Chess <chess@example.com>\"")
|
mailFrom = flag.String("mail-from", "", "Sender address of server email, e.g. \"Chess <chess@example.com>\"")
|
||||||
mailDir = flag.String("mail-dir", "", "Write email to .eml files in this directory instead of sending (development)")
|
mailDir = flag.String("mail-dir", "", "Write email to .eml files in this directory instead of sending (development)")
|
||||||
publicURL = flag.String("public-url", "", "Web client address that emailed links open, e.g. https://example.com/chess/web-client/")
|
publicURL = flag.String("public-url", "", "Web client address: emailed links open it, and its host is the Site of exported games, e.g. https://example.com/chess/web-client/")
|
||||||
|
|
||||||
// Web UI server flags
|
// Web UI server flags
|
||||||
serve = flag.Bool("serve", false, "Enable web UI server")
|
serve = flag.Bool("serve", false, "Enable web UI server")
|
||||||
@@ -178,6 +180,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
svc.SetIntegrityMode(integrity)
|
svc.SetIntegrityMode(integrity)
|
||||||
|
|
||||||
|
if *publicURL != "" {
|
||||||
|
u, err := url.Parse(*publicURL)
|
||||||
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||||
|
log.Fatalf("Invalid -public-url %q: must be absolute (https://host/chess/web-client/)", *publicURL)
|
||||||
|
}
|
||||||
|
svc.SetSite(u.Hostname())
|
||||||
|
}
|
||||||
|
|
||||||
// Password recovery needs mail and the client's public address.
|
// Password recovery needs mail and the client's public address.
|
||||||
sender, err := mail.Config{
|
sender, err := mail.Config{
|
||||||
SMTPHost: *smtpHost, SMTPPort: *smtpPort, SMTPUser: *smtpUser, SMTPPasswordFile: *smtpPassFile,
|
SMTPHost: *smtpHost, SMTPPort: *smtpPort, SMTPUser: *smtpUser, SMTPPasswordFile: *smtpPassFile,
|
||||||
|
|||||||
@@ -18,8 +18,9 @@
|
|||||||
# Default: ${chessd_home}/jwt.key
|
# Default: ${chessd_home}/jwt.key
|
||||||
# chessd_trusted_proxies: Reverse-proxy address(es) whose X-Real-IP header is
|
# chessd_trusted_proxies: Reverse-proxy address(es) whose X-Real-IP header is
|
||||||
# trusted as the client address. Default: none
|
# trusted as the client address. Default: none
|
||||||
# chessd_public_url: The web client's public address, which emailed links
|
# chessd_public_url: The web client's public address: emailed links open
|
||||||
# open. Default: none
|
# it, and its host is the Site of exported PGN.
|
||||||
|
# Default: none
|
||||||
# chessd_smtp_host: SMTP relay for password-reset email (the mail jail).
|
# chessd_smtp_host: SMTP relay for password-reset email (the mail jail).
|
||||||
# Unset leaves password recovery off. Default: none
|
# Unset leaves password recovery off. Default: none
|
||||||
# chessd_smtp_port: Relay port. Default: 25
|
# chessd_smtp_port: Relay port. Default: 25
|
||||||
@@ -33,8 +34,8 @@
|
|||||||
# Add the following to /etc/rc.conf to enable:
|
# Add the following to /etc/rc.conf to enable:
|
||||||
# chessd_enable="YES"
|
# chessd_enable="YES"
|
||||||
# chessd_trusted_proxies="<address nginx connects from>"
|
# chessd_trusted_proxies="<address nginx connects from>"
|
||||||
# and, for password recovery by email, once the mail jail relays for this one:
|
|
||||||
# chessd_public_url="https://<site>/chess/web-client/"
|
# chessd_public_url="https://<site>/chess/web-client/"
|
||||||
|
# and, for password recovery by email, once the mail jail relays for this one:
|
||||||
# chessd_smtp_host="<mail jail address>"
|
# chessd_smtp_host="<mail jail address>"
|
||||||
# chessd_mail_from="Chess <chess@<site>>"
|
# chessd_mail_from="Chess <chess@<site>>"
|
||||||
|
|
||||||
@@ -105,11 +106,14 @@ chessd_start()
|
|||||||
if [ -n "${chessd_trusted_proxies}" ]; then
|
if [ -n "${chessd_trusted_proxies}" ]; then
|
||||||
set -- "$@" -trusted-proxies "${chessd_trusted_proxies}"
|
set -- "$@" -trusted-proxies "${chessd_trusted_proxies}"
|
||||||
fi
|
fi
|
||||||
# Password recovery by email: relay, sender, and the client address the
|
if [ -n "${chessd_public_url}" ]; then
|
||||||
# link opens. The server refuses a relay without the other two.
|
set -- "$@" -public-url "${chessd_public_url}"
|
||||||
|
fi
|
||||||
|
# Password recovery by email: relay and sender; the link opens the public
|
||||||
|
# URL. The server refuses a relay without both and the URL.
|
||||||
if [ -n "${chessd_smtp_host}" ]; then
|
if [ -n "${chessd_smtp_host}" ]; then
|
||||||
set -- "$@" -smtp-host "${chessd_smtp_host}" -smtp-port "${chessd_smtp_port}" \
|
set -- "$@" -smtp-host "${chessd_smtp_host}" -smtp-port "${chessd_smtp_port}" \
|
||||||
-mail-from "${chessd_mail_from}" -public-url "${chessd_public_url}"
|
-mail-from "${chessd_mail_from}"
|
||||||
if [ -n "${chessd_smtp_user}" ]; then
|
if [ -n "${chessd_smtp_user}" ]; then
|
||||||
set -- "$@" -smtp-user "${chessd_smtp_user}"
|
set -- "$@" -smtp-user "${chessd_smtp_user}"
|
||||||
secret="${chessd_smtp_password_file:-${chessd_home}/smtp.secret}"
|
secret="${chessd_smtp_password_file:-${chessd_home}/smtp.secret}"
|
||||||
|
|||||||
+3
-2
@@ -343,11 +343,12 @@ Same visibility, `ETag`, and error responses as the history. `ply` (optional,
|
|||||||
0 up to the number of stored plies) exports only the first N plies; the
|
0 up to the number of stored plies) exports only the first N plies; the
|
||||||
result is then `*`, the `Termination` tag is left out, and the filename ends
|
result is then `*`, the `Termination` tag is left out, and the filename ends
|
||||||
`-ply<N>.pgn`. A `fetch` of the URL returns the text, so clients can copy it
|
`-ply<N>.pgn`. A `fetch` of the URL returns the text, so clients can copy it
|
||||||
as well as download it.
|
as well as download it. `Site` is the host of the server's `-public-url` (`?`
|
||||||
|
without one); an imported game keeps its own.
|
||||||
|
|
||||||
```
|
```
|
||||||
[Event "Casual game"]
|
[Event "Casual game"]
|
||||||
[Site "?"]
|
[Site "example.com"]
|
||||||
[Date "2026.09.27"]
|
[Date "2026.09.27"]
|
||||||
[Round "-"]
|
[Round "-"]
|
||||||
[White "alice"]
|
[White "alice"]
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ file is kept next to it with a timestamp).
|
|||||||
| `CHESSD_DSN` | `"host=<socket dir> dbname=chess"` | Database over the socket as the OS account |
|
| `CHESSD_DSN` | `"host=<socket dir> dbname=chess"` | Database over the socket as the OS account |
|
||||||
| `CHESSD_JWT_KEY` | `/var/lib/chessd/jwt.key` | Stable signing key |
|
| `CHESSD_JWT_KEY` | `/var/lib/chessd/jwt.key` | Stable signing key |
|
||||||
| `CHESSD_TRUSTED_PROXIES` | `TRUSTED_PROXIES` | Comma-separated proxy addresses |
|
| `CHESSD_TRUSTED_PROXIES` | `TRUSTED_PROXIES` | Comma-separated proxy addresses |
|
||||||
| `CHESSD_FLAGS` | empty | Other server flags, e.g. `-max-users 500 -db-cleanup report`, or password recovery by email: `-smtp-host <relay> -mail-from "Chess <chess@example.com>" -public-url https://example.com/chess/web-client/` (see [deployment.md](deployment.md#password-recovery-by-email); the sandboxed unit can read a password file only under `/var/lib/chessd`) |
|
| `CHESSD_FLAGS` | empty | Other server flags, e.g. `-max-users 500 -db-cleanup report -public-url https://example.com/chess/web-client/` (the URL's host is the `Site` of exported PGN), or password recovery by email, which also needs `-public-url`: `-smtp-host <relay> -mail-from "Chess <chess@example.com>"` (see [deployment.md](deployment.md#password-recovery-by-email); the sandboxed unit can read a password file only under `/var/lib/chessd`) |
|
||||||
|
|
||||||
Server flags worth knowing: `-max-users` (registration cap, default 100),
|
Server flags worth knowing: `-max-users` (registration cap, default 100),
|
||||||
`-anonymous-game-ttl` (default `24h`), `-finished-game-ttl` (default `1h`),
|
`-anonymous-game-ttl` (default `24h`), `-finished-game-ttl` (default `1h`),
|
||||||
|
|||||||
+2
-2
@@ -154,7 +154,7 @@ as `postgres`, list them with `\dt chess.*` and name them as `chess.games`.
|
|||||||
| `chessd_dsn` | `postgres:///chess?host=/tmp` | Database `chess` over the socket as the OS user |
|
| `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_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_trusted_proxies` | empty | Proxy address(es) whose `X-Real-IP` is trusted |
|
||||||
| `chessd_public_url` | empty | The web client's public address, which emailed reset links open (`https://<site>/chess/web-client/`) |
|
| `chessd_public_url` | empty | The web client's public address (`https://<site>/chess/web-client/`): emailed reset links open it, and its host is the `Site` of exported PGN |
|
||||||
| `chessd_smtp_host`, `chessd_smtp_port` | empty, `25` | SMTP relay for password-reset email; unset leaves recovery off |
|
| `chessd_smtp_host`, `chessd_smtp_port` | empty, `25` | SMTP relay for password-reset email; unset leaves recovery off |
|
||||||
| `chessd_smtp_user`, `chessd_smtp_password_file` | empty, `${chessd_home}/smtp.secret` if present | Relay credentials, when the relay authenticates this host; the file is mode 0600 |
|
| `chessd_smtp_user`, `chessd_smtp_password_file` | empty, `${chessd_home}/smtp.secret` if present | Relay credentials, when the relay authenticates this host; the file is mode 0600 |
|
||||||
| `chessd_mail_from` | empty | Sender address, e.g. `Chess <chess@<site>>` |
|
| `chessd_mail_from` | empty | Sender address, e.g. `Chess <chess@<site>>` |
|
||||||
@@ -182,7 +182,7 @@ Server flags:
|
|||||||
| `-smtp-user`, `-smtp-password-file` | | Relay credentials, optional; the file is mode 0600 and only sent over TLS |
|
| `-smtp-user`, `-smtp-password-file` | | Relay credentials, optional; the file is mode 0600 and only sent over TLS |
|
||||||
| `-mail-from` | | Sender address of server email |
|
| `-mail-from` | | Sender address of server email |
|
||||||
| `-mail-dir` | | Development: write email to `.eml` files here instead of a relay |
|
| `-mail-dir` | | Development: write email to `.eml` files here instead of a relay |
|
||||||
| `-public-url` | | The web client's address that emailed links open; required with a transport |
|
| `-public-url` | | The web client's address: emailed links open it, and its host is the `Site` of exported PGN; required with a transport |
|
||||||
|
|
||||||
Administrative CLI commands run as `chess` so peer authentication selects the
|
Administrative CLI commands run as `chess` so peer authentication selects the
|
||||||
right role; root may use `su -m` although the account has no shell:
|
right role; root may use `su -m` although the account has no shell:
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ go build ./cmd/chess-client-cli
|
|||||||
- `-web-api-url`: Browser-visible API origin for the embedded web client, set on its script element as `data-api`; useful when its public origin differs from the listen address
|
- `-web-api-url`: Browser-visible API origin for the embedded web client, set on its script element as `data-api`; useful when its public origin differs from the listen address
|
||||||
- `-smtp-host`, `-smtp-port`, `-smtp-user`, `-smtp-password-file`, `-mail-from`: SMTP relay for password-reset email; without a transport, recovery is off
|
- `-smtp-host`, `-smtp-port`, `-smtp-user`, `-smtp-password-file`, `-mail-from`: SMTP relay for password-reset email; without a transport, recovery is off
|
||||||
- `-mail-dir`: Development: write email to `.eml` files in this directory instead of sending
|
- `-mail-dir`: Development: write email to `.eml` files in this directory instead of sending
|
||||||
- `-public-url`: The web client's address that emailed links open (required with a transport)
|
- `-public-url`: The web client's address: emailed links open it, and its host is the `Site` of exported PGN (required with a transport)
|
||||||
|
|
||||||
### Modes
|
### Modes
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ type PGN struct {
|
|||||||
|
|
||||||
// BuildPGN exports a stored game. ply < 0 exports every move; otherwise the
|
// BuildPGN exports a stored game. ply < 0 exports every move; otherwise the
|
||||||
// first ply moves, with result "*" and no termination when that stops short
|
// first ply moves, with result "*" and no termination when that stops short
|
||||||
// of the end of the line.
|
// of the end of the line. site is the Site tag of a game played on this
|
||||||
func BuildPGN(record *storage.GameRecord, moves []storage.MoveRecord, ply int) (*PGN, error) {
|
// server (PGN's "?" when empty); an imported game keeps its own.
|
||||||
|
func BuildPGN(record *storage.GameRecord, moves []storage.MoveRecord, ply int, site string) (*PGN, error) {
|
||||||
if ply > len(moves) {
|
if ply > len(moves) {
|
||||||
return nil, fmt.Errorf("%w: game has %d plies", ErrPlyOutOfRange, len(moves))
|
return nil, fmt.Errorf("%w: game has %d plies", ErrPlyOutOfRange, len(moves))
|
||||||
}
|
}
|
||||||
@@ -75,7 +76,7 @@ func BuildPGN(record *storage.GameRecord, moves []storage.MoveRecord, ply int) (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
start := record.StartTimeUTC.UTC()
|
start := record.StartTimeUTC.UTC()
|
||||||
event, site, round := "Casual game", "", ""
|
event, round := "Casual game", ""
|
||||||
if record.ImportedBy != "" {
|
if record.ImportedBy != "" {
|
||||||
event, site, round = record.Event, record.Site, record.Round
|
event, site, round = record.Event, record.Site, record.Round
|
||||||
if event == "" {
|
if event == "" {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func TestBuildPGN(t *testing.T) {
|
|||||||
rec := record(chess.StartFEN, "white_wins", "resignation")
|
rec := record(chess.StartFEN, "white_wins", "resignation")
|
||||||
rec.BlackType, rec.BlackLevel, rec.WhiteType, rec.WhiteName = 2, 4, 1, "alice"
|
rec.BlackType, rec.BlackLevel, rec.WhiteType, rec.WhiteName = 2, 4, 1, "alice"
|
||||||
|
|
||||||
pgn, err := BuildPGN(rec, moves, -1)
|
pgn, err := BuildPGN(rec, moves, -1, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -143,25 +143,45 @@ func TestBuildPGN(t *testing.T) {
|
|||||||
t.Errorf("filename %q", pgn.Filename)
|
t.Errorf("filename %q", pgn.Filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
partial, err := BuildPGN(rec, moves, 1)
|
partial, err := BuildPGN(rec, moves, 1, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(partial.Text, "\n\n1. e4 *\n") || partial.Filename != "chess-20260928-68007fd1-ply1.pgn" {
|
if !strings.HasSuffix(partial.Text, "\n\n1. e4 *\n") || partial.Filename != "chess-20260928-68007fd1-ply1.pgn" {
|
||||||
t.Errorf("partial %q:\n%s", partial.Filename, partial.Text)
|
t.Errorf("partial %q:\n%s", partial.Filename, partial.Text)
|
||||||
}
|
}
|
||||||
if _, err := BuildPGN(rec, moves, 3); err == nil {
|
if _, err := BuildPGN(rec, moves, 3, ""); err == nil {
|
||||||
t.Error("ply beyond the line accepted")
|
t.Error("ply beyond the line accepted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A game played here names the server's site; an imported game keeps its own.
|
||||||
|
func TestBuildPGNSite(t *testing.T) {
|
||||||
|
moves := stored(t, chess.StartFEN, "e2e4")
|
||||||
|
rec := record(chess.StartFEN, "", "")
|
||||||
|
for _, tc := range []struct{ importedBy, recordSite, site, want string }{
|
||||||
|
{"", "", "lixen.com", "lixen.com"},
|
||||||
|
{"", "", "", "?"},
|
||||||
|
{"alice", "Linares ESP", "lixen.com", "Linares ESP"},
|
||||||
|
} {
|
||||||
|
rec.ImportedBy, rec.Site = tc.importedBy, tc.recordSite
|
||||||
|
pgn, err := BuildPGN(rec, moves, -1, tc.site)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if want := "[Site \"" + tc.want + "\"]\n"; !strings.Contains(pgn.Text, want) {
|
||||||
|
t.Errorf("site %q, imported by %q: PGN lacks %q:\n%s", tc.site, tc.importedBy, want, pgn.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A resignation taken back by an undo is noted before the live result.
|
// A resignation taken back by an undo is noted before the live result.
|
||||||
func TestBuildPGNNotesContinuedConcession(t *testing.T) {
|
func TestBuildPGNNotesContinuedConcession(t *testing.T) {
|
||||||
moves := stored(t, chess.StartFEN, "e2e4", "e7e5", "d2d4")
|
moves := stored(t, chess.StartFEN, "e2e4", "e7e5", "d2d4")
|
||||||
rec := record(chess.StartFEN, "", "")
|
rec := record(chess.StartFEN, "", "")
|
||||||
rec.ConcessionResult, rec.ConcessionTermination, rec.ConcessionPly = "black_wins", "resignation", 2
|
rec.ConcessionResult, rec.ConcessionTermination, rec.ConcessionPly = "black_wins", "resignation", 2
|
||||||
|
|
||||||
pgn, err := BuildPGN(rec, moves, -1)
|
pgn, err := BuildPGN(rec, moves, -1, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -171,7 +191,7 @@ func TestBuildPGNNotesContinuedConcession(t *testing.T) {
|
|||||||
|
|
||||||
// The concession that is still the result needs no note.
|
// The concession that is still the result needs no note.
|
||||||
rec.Result, rec.Termination, rec.ConcessionPly = "black_wins", "resignation", 3
|
rec.Result, rec.Termination, rec.ConcessionPly = "black_wins", "resignation", 3
|
||||||
if pgn, _ = BuildPGN(rec, moves, -1); strings.Contains(pgn.Text, "continued") {
|
if pgn, _ = BuildPGN(rec, moves, -1, ""); strings.Contains(pgn.Text, "continued") {
|
||||||
t.Errorf("standing resignation noted as continued:\n%s", pgn.Text)
|
t.Errorf("standing resignation noted as continued:\n%s", pgn.Text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ func (s *Service) GetGamePGN(gameID string, ply int) (*replay.PGN, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pgn, err := replay.BuildPGN(record, moves, ply)
|
pgn, err := replay.BuildPGN(record, moves, ply, s.site)
|
||||||
if errors.Is(err, replay.ErrNotation) {
|
if errors.Is(err, replay.ErrNotation) {
|
||||||
slog.Warn("stored game cannot be exported as PGN", "game_id", gameID, "error", err)
|
slog.Warn("stored game cannot be exported as PGN", "game_id", gameID, "error", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type Service struct {
|
|||||||
integrityCursor string // last game ID the integrity sweep verified; cleanup goroutine only
|
integrityCursor string // last game ID the integrity sweep verified; cleanup goroutine only
|
||||||
|
|
||||||
mail *mailer // password recovery; nil when no transport is configured
|
mail *mailer // password recovery; nil when no transport is configured
|
||||||
|
site string // Site tag of exported games played here; empty is PGN's "?"
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a service with optional storage. jwtSecret signs session tokens
|
// New creates a service with optional storage. jwtSecret signs session tokens
|
||||||
@@ -93,6 +94,12 @@ func (s *Service) SetAnonymousGameTTL(ttl time.Duration) {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSite names this server in the Site tag of the games it exports, from
|
||||||
|
// the web client's public address; call it before serving.
|
||||||
|
func (s *Service) SetSite(site string) {
|
||||||
|
s.site = site
|
||||||
|
}
|
||||||
|
|
||||||
// SetMaxUsers caps the number of accounts public registration may create;
|
// SetMaxUsers caps the number of accounts public registration may create;
|
||||||
// zero removes the cap. Accounts created through the CLI are not limited.
|
// zero removes the cap. Accounts created through the CLI are not limited.
|
||||||
func (s *Service) SetMaxUsers(limit int) {
|
func (s *Service) SetMaxUsers(limit int) {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
/* Chess web client. Dark theme matching lixen.com. The board colours and
|
/* Chess web client. Dark theme matching lixen.com. The board colours and
|
||||||
piece glyph treatment are fixed (see pieceMap in app.js); everything else is
|
piece glyph treatment are fixed (see pieceMap in app.js); everything else is
|
||||||
open. Styles live here: the host's CSP has style-src 'unsafe-inline', but
|
open. State is classes and data attributes, never inline style. Two layouts
|
||||||
state is expressed through classes and data attributes, never inline style.
|
share the markup: board beside the panel from 700px up, board over the
|
||||||
|
panel below; the host's embed frame switches its height at that width. */
|
||||||
Two layouts share the markup: board beside the panel from 979px up, and
|
|
||||||
board over the panel below (the host's embed frame switches its height at
|
|
||||||
the same width). Both must fit the host's frame: 30rem tall side by side,
|
|
||||||
46rem (capped at 80svh) stacked. */
|
|
||||||
|
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
|
||||||
@@ -45,16 +41,14 @@
|
|||||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
||||||
|
|
||||||
/* Side-by-side sizes: the board edge includes its coordinate margin. The
|
/* Side by side, the board (its edge holds the coordinates) takes the
|
||||||
panel takes a quarter of the width between 260px and 420px; the board
|
height under the top bar, or the width the panel's minimum leaves, and
|
||||||
takes everything else the window leaves beside it and under the top
|
the panel takes the rest of the row. 3.5rem and 2rem are the app's
|
||||||
bar, so the margins give way first, the board second, and the panel
|
padding and gaps (8px each side, a gap of up to 2rem) plus 8px of slack;
|
||||||
is never pushed out of view. 3.5rem and 2rem are the app's padding and
|
the host's embed sizes its frame to this formula. */
|
||||||
gaps (8px each side, a gap of up to 2rem beside the board) plus 8px of
|
|
||||||
slack; the host's embed sizes its frame to this formula. */
|
|
||||||
--topbar-height: 2.5rem;
|
--topbar-height: 2.5rem;
|
||||||
--panel-width: clamp(260px, 24vw, 420px);
|
--panel-min: 260px;
|
||||||
--board-edge: clamp(280px, min(100vw - var(--panel-width) - 3.5rem, 100vh - var(--topbar-height) - 2rem), 840px);
|
--board-edge: clamp(280px, min(100vw - var(--panel-min) - 3.5rem, 100vh - var(--topbar-height) - 2rem), 840px);
|
||||||
--board-pad: clamp(14px, 1.04vw, 20px);
|
--board-pad: clamp(14px, 1.04vw, 20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,9 +393,12 @@ body[data-mode="replay"] .square, .square.inert { cursor: default; }
|
|||||||
|
|
||||||
/* --- Panel ----------------------------------------------------------- */
|
/* --- Panel ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Up to 48rem: a full window on a large screen centres the pair rather than
|
||||||
|
stretching the controls across it. */
|
||||||
.panel {
|
.panel {
|
||||||
flex: none;
|
flex: 1 1 0;
|
||||||
width: var(--panel-width);
|
min-width: var(--panel-min);
|
||||||
|
max-width: 48rem;
|
||||||
height: var(--board-edge);
|
height: var(--board-edge);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -1104,9 +1101,6 @@ input[type="range"]:focus-visible { outline: 2px solid var(--periwinkle); outlin
|
|||||||
than the controls dropping below the fold. */
|
than the controls dropping below the fold. */
|
||||||
--board-edge: max(240px, min(100vw, calc(100svh - 15.75rem)));
|
--board-edge: max(240px, min(100vw, calc(100svh - 15.75rem)));
|
||||||
--board-pad: 14px;
|
--board-pad: 14px;
|
||||||
--panel-width: 100%;
|
|
||||||
--panel-height: auto;
|
|
||||||
--topbar-height: 2.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body { overflow-y: auto; overflow-x: hidden; }
|
body { overflow-y: auto; overflow-x: hidden; }
|
||||||
@@ -1154,6 +1148,8 @@ input[type="range"]:focus-visible { outline: 2px solid var(--periwinkle); outlin
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|||||||
Reference in new issue
Block a user