Files
technitium-console/src/screens/apps/applyRequest.tsx
T
DenozordecandClaude Opus 5.5 57e492cceb 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 <[email protected]>
2026-10-31 19:57:33 +07:00

91 lines
4.4 KiB
TypeScript

import { applyConfig, getHistoryDiff, previewConfig, rollbackHistory, type HistoryEntry } from '../../api/apps'
import { Alert } from '../../ui/Alert'
import { Tag } from '../../ui/Tag'
import { DiffView } from './ConfigDiffView'
import type { PickerServer, RunRequest } from './RunFlow'
/*
The two changes to a server's config that go through the picker, the review and the results: bring
it to the baseline (the sync), and put an earlier version back (a rollback). The Config dialog and
the drift list both start them.
*/
const sessionEnded = (kind: string, message?: string): string => (kind === 'error' ? (message ?? 'Failed.') : 'The session ended.')
/* The baseline plus each server's local values, to the chosen servers, after a preview. */
export function applyRequest(token: string | null, name: string, servers: PickerServer[], preselected: string[]): RunRequest {
return {
title: `Apply the baseline of ${name}`,
intro: 'Each server gets the baseline plus its own local values. You see what changes before anything is written.',
confirmLabel: 'Preview',
servers,
preselected,
review: async (targets) => {
const outcome = await previewConfig(token, name, targets)
if (outcome.kind !== 'ok') return { kind: 'error', message: sessionEnded(outcome.kind, outcome.kind === 'error' ? outcome.message : undefined) }
const previews = outcome.data.response.targets
return {
kind: 'ok',
confirmLabel: 'Apply',
context: Object.fromEntries(previews.filter((p) => p.ok).map((p) => [p.server, p.currentHash])),
body: (
<div className="flex flex-col gap-6">
{previews.map((p) => (
<section key={p.server} aria-label={`Preview ${p.server}`} className="flex flex-col gap-3">
<h3 className="m-0 flex items-center gap-2 text-sm font-semibold">
{p.server}
{!p.ok ? <Tag tone="dan">Cannot apply</Tag> : p.unchanged ? <Tag tone="ok">Already in sync</Tag> : <Tag tone="warn">Will change</Tag>}
</h3>
{p.error !== undefined && <Alert type="danger" title="Error">{p.error}</Alert>}
{p.warnings.map((w) => (
<Alert key={w} type="warning" title="Warning">
{w}
</Alert>
))}
{p.ok && !p.unchanged && <DiffView diff={p.changes} from="Now" to="After apply" />}
</section>
))}
</div>
),
}
},
run: (targets, context) => applyConfig(token, name, targets, (context ?? {}) as Record<string, string>),
}
}
/* One saved version of a server's config, put back on the chosen servers after seeing what changes. */
export function rollbackRequest(token: string | null, name: string, entry: HistoryEntry, servers: PickerServer[], preselected: string[]): RunRequest {
return {
title: `Roll back ${name}`,
intro: `Put the version saved at ${new Date(entry.at).toLocaleString()} back on the servers you choose. Their current configs are saved to the history first.`,
confirmLabel: 'Preview',
servers,
preselected,
review: async (targets) => {
const diffs = await Promise.all(targets.map((server) => getHistoryDiff(token, name, entry.id, server)))
const failed = diffs.find((d) => d.kind !== 'ok')
if (failed !== undefined) return { kind: 'error', message: sessionEnded(failed.kind, failed.kind === 'error' ? failed.message : undefined) }
const ok = diffs.flatMap((d) => (d.kind === 'ok' ? [d.data.response] : []))
return {
kind: 'ok',
confirmLabel: 'Roll back',
context: Object.fromEntries(ok.map((d) => [d.server, d.currentHash])),
body: (
<div className="flex flex-col gap-6">
{ok.map((d) => (
<section key={d.server} aria-label={`Preview ${d.server}`} className="flex flex-col gap-3">
<h3 className="m-0 flex items-center gap-2 text-sm font-semibold">
{d.server}
{d.unchanged ? <Tag tone="ok">Already this version</Tag> : <Tag tone="warn">Will change</Tag>}
</h3>
{!d.unchanged && <DiffView diff={d.changes} from="Now" to="After rollback" />}
</section>
))}
</div>
),
}
},
run: (targets, context) => rollbackHistory(token, name, entry.id, targets, (context ?? {}) as Record<string, string>),
}
}