Phase 4: backups + import/export config
Release / build-installer (push) Has been cancelled
CI / build (pull_request) Has been cancelled

ConfigStore.SaveAsync now snapshots the previous config to
%ProgramData%\WebhookServer\backups\config-<timestamp>.json before
overwriting, retaining the last 30. Failures are silent so a
backup-write hiccup never blocks an actual save.

Three new admin pipe ops:
- list-backups: returns newest 50 entries with timestamps and sizes
- restore-backup: takes a fileName, refuses path-traversal chars,
  loads the named backup over the live config (which itself triggers
  a fresh backup of the current state via the SaveAsync hook)
- import-config: replaces the current config with a GUI-supplied
  ServerConfig, merging encrypted secrets where the GUI didn't supply
  new plaintext

GUI File menu items are wired:
- Import config: file picker -> ImportConfigAsync
- Export config: SaveFileDialog writes the current config as JSON
- Backups: dynamic submenu auto-refreshed when opened, listing
  backups with timestamp + size; click to confirm-and-restore

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 09:55:03 -04:00
parent 9e6abeef74
commit 93a9c327e0
7 changed files with 226 additions and 3 deletions
@@ -86,4 +86,18 @@ public sealed class AdminPipeClient
var lst = resp.Data.Value.GetProperty("lines").Deserialize<List<LogLine>>(AdminProtocol.JsonOptions);
return lst ?? new List<LogLine>();
}
public async Task<List<BackupEntry>> ListBackupsAsync(CancellationToken ct = default)
{
var resp = await InvokeAsync(AdminOps.ListBackups, null, ct).ConfigureAwait(false);
if (!resp.Ok || resp.Data is null) return new List<BackupEntry>();
var lst = resp.Data.Value.GetProperty("backups").Deserialize<List<BackupEntry>>(AdminProtocol.JsonOptions);
return lst ?? new List<BackupEntry>();
}
public Task<AdminResponse> RestoreBackupAsync(string fileName, CancellationToken ct = default) =>
InvokeAsync(AdminOps.RestoreBackup, new RestoreBackupArgs { FileName = fileName }, ct);
public Task<AdminResponse> ImportConfigAsync(ServerConfig config, CancellationToken ct = default) =>
InvokeAsync(AdminOps.ImportConfig, config, ct);
}