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

+5 -4
View File
@@ -286,9 +286,9 @@ export class AppsConfig {
return importLocalValues(Object.fromEntries([...live].map(([id, l]) => [id, l.installed && l.ok ? l.text : null])), paths)
}
async preview(name: string, ids: string[]): Promise<{ targets: TargetPreview[] }> {
async preview(name: string, ids: string[], overrideText?: string): Promise<{ targets: TargetPreview[] }> {
const targets = this.#targets(ids)
const baseline = this.getBaseline(name)
const baseline = overrideText === undefined ? this.getBaseline(name) : ({ app: name, text: overrideText, updatedAt: '', updatedBy: '' } satisfies Baseline)
if (baseline === null) throw new UserError('This app has no baseline yet. Import one from a server first.')
const overrides = this.getOverrides(name)
const live = await this.#liveAll(name, true)
@@ -368,8 +368,9 @@ export class AppsConfig {
return { results: done.map((d) => d.result), details }
}
async apply(name: string, ids: string[], expect: Record<string, string>): Promise<{ results: TargetResult[]; details: Record<string, string> }> {
const baseline = this.getBaseline(name)
/* `overrideText` is a config that is not the hub's baseline (a profile's). The server's local values still apply. */
async apply(name: string, ids: string[], expect: Record<string, string>, overrideText?: string): Promise<{ results: TargetResult[]; details: Record<string, string> }> {
const baseline = overrideText === undefined ? this.getBaseline(name) : ({ app: name, text: overrideText, updatedAt: '', updatedBy: '' } satisfies Baseline)
if (baseline === null) throw new UserError('This app has no baseline yet. Import one from a server first.')
const overrides = this.getOverrides(name)
return this.#applyMany(name, ids, (s) => () => effectiveConfig(baseline.text, overrides, s.id).text, expect)