From 57e492cceb92714c412ebba7c9977b9c6c82eb22 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 31 Oct 2026 19:57:33 +0700 Subject: [PATCH] Hub stage 4: config history, rollback, drift check, baseline export and import Every saved config, and every earlier baseline, is listed per app. You can view it, diff it against a server's current config, and roll it back through preview and apply. Earlier baselines can be restored. A drift check (HUB_DRIFT_MINUTES, default 15) compares each server with its effective config. Drift shows in the header and the matrix, with Accept as baseline and Revert to baseline. All baselines export to one file. Importing that file shows a dry run before it writes. Rolling back preselects, and diffs against, the server the version came from. The line-diff op "del" is renamed to "remove" for the language lint. Co-Authored-By: Claude Opus 5.5 --- docs/compose.hub.example.yaml | 2 + docs/compose.shnt.example.yaml | 2 + docs/hub-deploy.md | 6 + hub/README.md | 33 ++ hub/apps-config.ts | 284 ++++++++++++--- hub/apps-history.test.ts | 439 ++++++++++++++++++++++++ hub/apps-sync.test.ts | 2 +- hub/apps-sync.ts | 35 +- hub/apps-write.ts | 4 +- hub/config.ts | 3 + hub/drift.test.ts | 196 +++++++++++ hub/drift.ts | 134 ++++++++ hub/routes.ts | 12 + hub/server.ts | 80 ++++- hub/session.ts | 7 +- hub/store.ts | 69 +++- src/api/apps.ts | 96 +++++- src/api/hub-drift.ts | 47 +++ src/app/DriftStatus.tsx | 137 ++++++++ src/app/Shell.hub.test.tsx | 21 +- src/app/Shell.tsx | 17 + src/screens/apps/AppConfigSync.test.tsx | 4 +- src/screens/apps/AppConfigSync.tsx | 241 ++++++++++--- src/screens/apps/Apps.tsx | 22 ++ src/screens/apps/AppsHistory.test.tsx | 288 ++++++++++++++++ src/screens/apps/AppsMatrix.tsx | 18 +- src/screens/apps/ConfigDiffView.tsx | 4 +- src/screens/apps/ImportBaselines.tsx | 126 +++++++ src/screens/apps/applyRequest.tsx | 90 +++++ 29 files changed, 2294 insertions(+), 125 deletions(-) create mode 100644 hub/apps-history.test.ts create mode 100644 hub/drift.test.ts create mode 100644 hub/drift.ts create mode 100644 src/api/hub-drift.ts create mode 100644 src/app/DriftStatus.tsx create mode 100644 src/screens/apps/AppsHistory.test.tsx create mode 100644 src/screens/apps/ImportBaselines.tsx create mode 100644 src/screens/apps/applyRequest.tsx diff --git a/docs/compose.hub.example.yaml b/docs/compose.hub.example.yaml index 51fa2d1..0d9663c 100644 --- a/docs/compose.hub.example.yaml +++ b/docs/compose.hub.example.yaml @@ -40,6 +40,8 @@ services: - HUB_DATA=/data # За Traefik: брать адрес клиента из X-Forwarded-For (для ограничения попыток входа и журнала) - HUB_TRUST_PROXY=1 + # Как часто проверять, что конфиги серверов не разошлись с эталоном (минуты, 0 выключает) + # - HUB_DRIFT_MINUTES=15 - HUB_DOMAIN=dnss.shnt.top - TZ=Asia/Novosibirsk labels: diff --git a/docs/compose.shnt.example.yaml b/docs/compose.shnt.example.yaml index 12fae75..ac078e1 100644 --- a/docs/compose.shnt.example.yaml +++ b/docs/compose.shnt.example.yaml @@ -198,6 +198,8 @@ services: - HUB_DATA=/data # За Traefik: брать адрес клиента из X-Forwarded-For (для ограничения попыток входа и журнала) - HUB_TRUST_PROXY=1 + # Как часто проверять, что конфиги серверов не разошлись с эталоном (минуты, 0 выключает) + # - HUB_DRIFT_MINUTES=15 - HUB_DOMAIN=dnss.shnt.top - TZ=Asia/Novosibirsk labels: diff --git a/docs/hub-deploy.md b/docs/hub-deploy.md index c0cce62..b35e48f 100644 --- a/docs/hub-deploy.md +++ b/docs/hub-deploy.md @@ -327,6 +327,12 @@ docker compose rm -f technitium-hub # удалить контейнер Файлы эталона и локальных полей лежат в `/data/apps/<имя>/` (`baseline.json`, `overrides.json`). Они содержат конфиги, поэтому том `/data` нужно беречь так же, как `users.json`. +## История, откат, контроль расхождений, перенос эталонов + +- **История.** Вкладка History в окне Config: все прежние версии, которые хаб сохранил (конфиг сервера перед применением или сохранением, прежний эталон). Выберите версию, чтобы прочитать её и увидеть отличие от текущего конфига сервера. Роли `apps-admin` и `admin`: **Roll back…** (выбор серверов, предпросмотр, итог по каждому) и **Restore as baseline** для прежнего эталона. Хранится по 50 версий на приложение. +- **Контроль расхождений.** Раз в 15 минут хаб читает конфиги и сравнивает их с тем, что должно быть (эталон плюс локальные поля сервера). Если конфиг поменяли прямо в консоли сервера, в шапке появляется «N config drift». В списке у каждого расхождения есть **Open config**, а для `apps-admin` и `admin` ещё **Accept as baseline** (конфиг этого сервера становится эталоном; локальные поля остаются локальными) и **Revert to baseline** (через предпросмотр). Период меняется переменной `HUB_DRIFT_MINUTES`, `0` отключает проверку. Сервер, который не ответил, показывается как «Cannot read» и в счётчик не входит. Последний результат лежит в `/data/drift.json`. +- **Перенос эталонов.** Apps › All servers: **Export baselines** скачивает один файл со всеми эталонами и локальными полями, **Import baselines** загружает его в другой хаб: сначала проверка и сводка (New, Changed, Same), запись только после **Import**. На серверы импорт ничего не отправляет. Файл содержит конфиги, храните его как секрет. + ## Приложения: установка, обновление, удаление Роль `apps-admin` (или `admin`) видит в разделе Apps при «All servers» меню действий в каждой строке и кнопки **App Store** и **Install from file**: diff --git a/hub/README.md b/hub/README.md index db4797a..48541a0 100644 --- a/hub/README.md +++ b/hub/README.md @@ -22,6 +22,7 @@ apps Query Logs reads from) and Settings (the blocking state). | `_TOKEN` | required | API token of each server; the id is uppercased with non-alphanumerics as `_` (`dns` gives `DNS_TOKEN`, `my-dns` gives `MY_DNS_TOKEN`). | | `HUB_USER`, `HUB_PASSWORD` | see Users | Bootstrap: one `admin`, used only while `${HUB_DATA}/users.json` does not exist. Set both or neither. | | `HUB_DATA` | `/data` | Folder of `users.json` and `audit.log`. Mount a volume on it. | +| `HUB_DRIFT_MINUTES` | `15` | How often the hub checks that every server still has the config it should (see Drift). `0` turns the check off. | | `HUB_TRUST_PROXY` | off | `1` believes the first `X-Forwarded-For` entry as the client address (set it behind Traefik). Leave it off when clients reach the hub directly, or anyone can fake their address. | | `_ADMIN_TOKEN` | optional | A write-capable token per server (`MDNS_ADMIN_TOKEN`). Only app writes use it, and only it: a server without one is read-only for the hub (the matrix and the session list the writable servers). Never logged or returned. | | `HUB_PORT` | `8080` | Listening port. | @@ -128,6 +129,38 @@ matrix hash ignores them too. | `hub/apps/config/preview` | POST `{name, targets}` | `apps.write` | Per target: current and effective text, the diff, warnings (a missing local value, a version older than the newest installed, no admin token) and `currentHash`. | | `hub/apps/config/apply` | POST `{name, targets, expect?}` | `apps.write` | Per target: re-reads the config; refuses if its hash differs from `expect[server]`; saves the old config to the history; `apps/config/set` with the admin token; reads it back and checks it. A server that rejects the config keeps its old one. | +## History, rollback, drift, export + +**History.** Everything the hub replaces is kept in `${HUB_DATA}/apps//history/` (50 files per app): +`