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
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:
+27
-12
@@ -233,17 +233,32 @@ Key points:
|
||||
The registry holds dictionaries in memory addressed by `(variant,
|
||||
dict_version)`, tracking the latest version per variant, and answers the
|
||||
word-check tool through `Registry.Lookup`.
|
||||
- **Dictionary versioning — pin per game.** A game records the `dict_version` it
|
||||
started on and finishes on that version; new games use the latest. Multiple
|
||||
versions may be resident at once. The boot version loads from the flat
|
||||
`BACKEND_DICT_DIR`; the admin console **hot-reloads** a new version from a
|
||||
per-version subdirectory `BACKEND_DICT_DIR/<version>/` through
|
||||
`Registry.LoadAvailable` (only the variants whose DAWG is present there), and a
|
||||
restart re-loads every resident version via `engine.OpenWithVersions` (the flat
|
||||
boot version plus each subdirectory). In-flight games keep their pinned version;
|
||||
new games use the latest. (The solver is published as a versioned module and the
|
||||
dictionaries ship as a separate versioned **release artifact** from the
|
||||
`scrabble-dictionary` repo; the runtime contract above is unchanged.)
|
||||
- **Dictionary versioning — pin per game, update through the console.** A game
|
||||
records the `dict_version` it started on and finishes on it; new games pin the
|
||||
**active version**, the single source of truth persisted in the
|
||||
`dictionary_state` singleton and restored on boot, so an operator's choice
|
||||
survives a restart. Multiple versions are resident at once. `BACKEND_DICT_DIR` is
|
||||
a writable named volume seeded from the image on first boot (its nonroot
|
||||
ownership is inherited from the image, so the runtime can write it); the flat
|
||||
directory is the seed version, labelled `BACKEND_DICT_VERSION` — set from the
|
||||
build's `DICT_VERSION`, so the resident label equals the release tag — and each
|
||||
uploaded version lives in a `BACKEND_DICT_DIR/<version>/` subdirectory. The admin
|
||||
console **updates** a dictionary online (`internal/dictadmin`): an operator
|
||||
uploads the `scrabble-dawg-vX.Y.Z.tar.gz` release archive, previews the
|
||||
per-variant words added/removed against the active dictionary (`engine.DiffWords`
|
||||
enumerates both DAWGs and decodes only the differences), and confirms — the
|
||||
backend extracts the archive into the version subdirectory (hardened against
|
||||
path-traversal, symlink and decompression-bomb attacks), loads it via
|
||||
`Registry.LoadAvailable`, and makes it the active version. Versions are immutable
|
||||
(re-uploading a resident tag is rejected), so in-flight games keep their pinned
|
||||
version while new games use the new one. A restart re-loads every resident
|
||||
version via `engine.OpenWithVersions` (the flat seed plus each subdirectory,
|
||||
skipping the `.staging/` upload area) and restores the active pointer from
|
||||
`dictionary_state`. The volume preserves uploaded versions across redeploys;
|
||||
once seeded it is not re-seeded, so after bootstrap dictionary changes go through
|
||||
the console rather than a rebuild. (The dictionaries ship as a versioned
|
||||
**release artifact** from the `scrabble-dictionary` repo; the build's
|
||||
`DICT_VERSION` selects only the seed.)
|
||||
- Move generation/validation/scoring use `Solver.GenerateMoves` (ranked),
|
||||
`Solver.ValidatePlay` and `Solver.ScorePlay`; board mutation uses
|
||||
`scrabble.Apply`. The engine adds its own deterministic, seeded tile **bag**
|
||||
@@ -330,7 +345,7 @@ Key points:
|
||||
review queue. An operator resolves it (`open → resolved`) with a **disposition** —
|
||||
reject, accept-add or accept-remove; the accepted ones form a derived
|
||||
**pending-changes** list that feeds the offline dictionary rebuild and is marked
|
||||
applied once the rebuilt version is hot-reloaded (§5, §12).
|
||||
applied once the rebuilt version is installed through the console (§5, §12).
|
||||
|
||||
## 7. Robot opponent
|
||||
|
||||
|
||||
+7
-4
@@ -183,10 +183,13 @@ renders it; the gateway gates it with HTTP Basic Auth on its public listener and
|
||||
proxies it verbatim. The console lists and inspects **users** (profile, statistics,
|
||||
identities, their games) and **games** (summary + seats), works the **word-complaint
|
||||
review queue** — resolving each as reject / accept-add / accept-remove — and exposes
|
||||
the **dictionary**: the resident versions per variant, a **hot-reload** of a new
|
||||
version from `BACKEND_DICT_DIR/<version>/`, and the **pending wordlist changes**
|
||||
derived from accepted complaints (which feed the offline rebuild and are marked
|
||||
applied after a reload). When a Telegram connector is configured an operator can also
|
||||
the **dictionary**: the active version new games pin, the resident versions per
|
||||
variant, an online **dictionary update** (upload the `scrabble-dawg-vX.Y.Z.tar.gz`
|
||||
release archive, preview the words added and removed per variant against the active
|
||||
dictionary, then install — which writes the version, loads it and makes it active;
|
||||
versions are immutable and games in progress keep their own), and the **pending
|
||||
wordlist changes** derived from accepted complaints (which feed the offline rebuild
|
||||
and are marked applied after an update). When a Telegram connector is configured an operator can also
|
||||
**message a user** (by their Telegram identity) or **post to the game channel**.
|
||||
State-changing actions are protected by a same-origin check; the console tracks no
|
||||
operator identity.
|
||||
|
||||
@@ -188,9 +188,13 @@ backend; gateway закрывает её HTTP Basic Auth на публичном
|
||||
один-в-один. В консоли можно смотреть **пользователей** (профиль, статистика,
|
||||
identity, их игры) и **игры** (сводка + места), разбирать **очередь жалоб на слова** —
|
||||
закрывая каждую как reject / accept-add / accept-remove — и управлять **словарём**:
|
||||
резидентные версии по вариантам, **горячая перезагрузка** новой версии из
|
||||
`BACKEND_DICT_DIR/<version>/` и **список ожидающих правок**, выведенный из принятых
|
||||
жалоб (он питает офлайн-пересборку и отмечается применённым после перезагрузки). Если
|
||||
активная версия, на которую опираются новые партии, резидентные версии по вариантам,
|
||||
онлайн-**обновление словаря** (загрузить релизный архив `scrabble-dawg-vX.Y.Z.tar.gz`,
|
||||
посмотреть предпросмотр добавленных и удалённых слов по каждому варианту относительно
|
||||
активного словаря, затем установить — версия записывается, загружается и становится
|
||||
активной; версии неизменяемы, а идущие партии остаются на своей) и **список ожидающих
|
||||
правок**, выведенный из принятых жалоб (он питает офлайн-пересборку и отмечается
|
||||
применённым после обновления). Если
|
||||
подключён Telegram-коннектор, оператор также может **написать пользователю** (по его
|
||||
Telegram-identity) или **отправить пост в игровой канал**. Изменяющие действия
|
||||
защищены проверкой same-origin; личность оператора не отслеживается.
|
||||
|
||||
+10
-5
@@ -91,15 +91,20 @@ tests or touching CI.
|
||||
the profile-update away round-trip) and a `notify`-event constructor round-trip.
|
||||
- **Admin & dictionary ops** — `backend/internal/adminconsole` unit-tests
|
||||
the template renderer over every page plus the embedded asset; `backend/internal/engine`
|
||||
adds the **dictionary hot-reload** cases (`LoadAvailable` loads only the present
|
||||
variants, `OpenWithVersions` scans version subdirectories, a reload registers a new
|
||||
version and moves "latest"); `backend/internal/server` unit-tests the console's
|
||||
adds the **dictionary** cases (`LoadAvailable` loads only the present variants,
|
||||
`OpenWithVersions` scans version subdirectories and skips the staging area, `DiffWords`
|
||||
decodes the words added/removed between two versions); `backend/internal/dictadmin`
|
||||
unit-tests the release-archive validation and hardened extraction (path-traversal,
|
||||
symlink, oversize and entry-count rejection, immutable versions); `backend/internal/server` unit-tests the console's
|
||||
**same-origin** CSRF guard; the gateway adds the **verbatim `/_gm` Basic-Auth proxy**
|
||||
(401 / forward, path preserved) and the h2c **console mount** (routed when configured,
|
||||
404 when not). Postgres-backed `inttest` drives the **complaint resolution →
|
||||
dictionary-change pipeline** (file → resolve with a disposition → pending change → mark
|
||||
applied), the admin **list/count** read queries, and the **/_gm console over HTTP**
|
||||
(pages render; a resolve POST needs a same-origin header). `ratewatch` has
|
||||
applied), the **dictionary update flow** (upload a release archive over HTTP → preview →
|
||||
install → the new version becomes active and resident, persists across a restart, pins new
|
||||
games while in-progress games keep theirs, and is immutable), the admin **list/count** read
|
||||
queries, and the **/_gm console over HTTP** (pages render; a resolve POST needs a
|
||||
same-origin header). `ratewatch` has
|
||||
unit tests (window accumulation, the auto-flag threshold + expiry, the bounded
|
||||
episode map), the account-store **high-rate flag round-trip** (set-once / clear /
|
||||
re-flag) and a console flow in `inttest`: a gateway report auto-flags the account,
|
||||
|
||||
Reference in New Issue
Block a user