feat(admin): online dictionary update — upload archive, preview word diff, install & activate
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 23s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s

Replace the dictionary hot-reload with an online update flow in the GM console
(/_gm/dictionary): the operator uploads scrabble-dawg-vX.Y.Z.tar.gz, previews the
per-variant words added/removed against the active dictionary, and confirms to
install + activate it. Versions are immutable; in-progress games keep their pinned
version while new games use the new one.

- engine: DiffWords (enumerate both DAWGs, decode only the differences), OpenFinder,
  Registry.Finder, DictFiles; OpenWithVersions skips the .staging area.
- dictadmin: hardened release-archive validation + extraction (path-traversal,
  symlink, oversize, entry-count rejection) and staging -> install (atomic rename).
- game: active dictionary version persisted in the dictionary_state singleton
  (single source of truth, restored on boot), concurrency-safe accessor.
- storage: BACKEND_DICT_DIR is a named volume seeded from the image (nonroot-owned),
  so uploaded versions persist across redeploys; the build's DICT_VERSION labels the
  seed and equals the resident tag (BACKEND_DICT_VERSION).
- docs: ARCHITECTURE §5, FUNCTIONAL (+ru), backend README, TESTING, PRERELEASE.

Tests: engine + dictadmin unit; integration upload->preview->install->activate->
restart->pin->immutability->CSRF.
This commit is contained in:
Ilia Denisov
2026-06-13 23:29:06 +02:00
parent 5ff07da025
commit 0f3671f42d
31 changed files with 1590 additions and 78 deletions
+6 -1
View File
@@ -10,7 +10,12 @@ POSTGRES_USER=scrabble
POSTGRES_PASSWORD=change-me # required
# --- Dictionary -------------------------------------------------------------
DICT_VERSION=v1.0.0 # scrabble-dictionary release tag (image build-arg)
# scrabble-dictionary release tag baked into the image as the SEED dictionary
# (image build-arg; also labels the resident seed version). After first boot the
# dawg-data volume preserves versions uploaded through the admin console, and the
# active version lives in the DB — so bumping this on a later deploy does NOT change
# a running contour; update the dictionary through /_gm/dictionary instead.
DICT_VERSION=v1.0.0
# --- Logging ----------------------------------------------------------------
LOG_LEVEL=info
+10
View File
@@ -90,6 +90,15 @@ services:
# GOMAXPROCS matches the CPU limit below so the Go scheduler aligns with the
# cgroup quota (the runtime otherwise sees all of the host's cores).
GOMAXPROCS: "2"
# The dictionary lives on a named volume seeded from the image on first boot
# (the image's /opt/dawg is owned by the nonroot UID, which the fresh volume
# inherits). The admin console writes new version subdirectories here, and the
# volume preserves them — and the versions in-progress games pin — across
# redeploys. Once seeded the volume is not re-seeded, so bumping DICT_VERSION on a
# later deploy has no effect on an existing contour; update via the console
# instead (docs/ARCHITECTURE.md §5).
volumes:
- dawg-data:/opt/dawg
# No container healthcheck: the distroless image has no shell/wget. Readiness
# is covered by the CI post-deploy probe (GET / through caddy).
# R7 starting limits (generous over the R2 ~1-core / <=100 MiB peak); tightened to
@@ -366,6 +375,7 @@ networks:
volumes:
postgres-data:
dawg-data:
caddy-data:
prometheus-data:
tempo-data: