Hub stage 8: settings drift, webhook notices, profiles
CI / check (push) Successful in 4m27s
Hub / check (push) Successful in 1m17s
CI / image (push) Successful in 1m54s
Hub / image (push) Successful in 1m56s

The drift check now also covers the settings baseline and the Blocked
and Allowed lists baseline; the drift dialog offers Open compare,
Accept as baseline and Revert to baseline for them. Old drift.json
files still load.

HUB_WEBHOOK_URL (and an optional HUB_WEBHOOK_SECRET for an HMAC
signature) receives drift.found, drift.cleared and write.failed
notices, only on change, without values, never blocking a request and
never logging the address beyond its host. Admins see the webhook
status and can send a test from About and from the drift dialog.

Profiles bundle the hub's app, settings and lists baselines. Applying
one to a server runs install, app configs, settings and lists in order
after a preview; a failed step stops that server's later steps and
Retry failed resumes from it. Profiles export and import as a file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-01 02:11:19 +07:00
1 parent 392b644faa
commit 9742f0fe69
32 files changed
+3123 -106

No files matched your search

+17
View File
@@ -32,6 +32,10 @@ export interface HubConfig {
/** How often the drift check runs (HUB_DRIFT_MINUTES); 0 turns it off. */
driftMinutes: number
timeoutMs: number
/** Where drift and failed-write notices are POSTed (HUB_WEBHOOK_URL); null when not set. */
webhookUrl: string | null
/** Signs the notices with HMAC-SHA256 (HUB_WEBHOOK_SECRET); null when not set. Never logged. */
webhookSecret: string | null
/** Shown as `dnsServerDomain` (and `clusterDomain`) in the session. */
domain: string
/** The console's static build. */
@@ -85,6 +89,17 @@ export function parseConfig(env: Env): HubConfig {
throw new Error('HUB_SERVERS is required, e.g. dns=https://dns.example.net,ndns=https://ndns.example.net.')
}
const webhookUrl = (env.HUB_WEBHOOK_URL ?? '').trim()
if (webhookUrl !== '') {
let ok = /^https?:\/\/[^/\s]/i.test(webhookUrl)
try {
new URL(webhookUrl)
} catch {
ok = false
}
if (!ok) throw new Error('HUB_WEBHOOK_URL must be an http or https address.')
}
return {
servers,
bootstrap: user === '' ? null : { user, password },
@@ -94,6 +109,8 @@ export function parseConfig(env: Env): HubConfig {
cacheSeconds: number(env, 'HUB_CACHE_SECONDS', 15, 0),
driftMinutes: number(env, 'HUB_DRIFT_MINUTES', 15, 0),
timeoutMs: number(env, 'HUB_TIMEOUT_MS', 8000, 1),
webhookUrl: webhookUrl === '' ? null : webhookUrl,
webhookSecret: (env.HUB_WEBHOOK_SECRET ?? '') === '' ? null : (env.HUB_WEBHOOK_SECRET as string),
domain: env.HUB_DOMAIN?.trim() || 'hub',
staticDir: env.HUB_STATIC?.trim() || './dist',
}