Backup-Oberflaeche im Settings-Tab

Der BackupService bekommt eine Bedienoberflaeche im neuen Unterreiter "Backup".

Aufbau als eigenes UserControl (UI/BackupPanel) statt direkt in frm_main.Designer.cs.
Diese Datei ist ueber 1250 Zeilen gewachsene Handarbeit; ein Fehler beim Bearbeiten
waere teuer. Eingehaengt wird das Panel mit vier Zeilen.

Drei Bereiche: Sicherung erstellen (Zielordner, Umgang mit Zugangsdaten, Umfang),
automatische Sicherung (Uhrzeit, Aufbewahrungszahl) und die Liste vorhandener
Sicherungen mit Wiederherstellen, Anzeigen und Loeschen. Die Liste liest das
Manifest jeder Datei und zeigt Instanz und ob Zugangsdaten enthalten sind;
unlesbare oder fremde Archive werden ausgegraut mitgezeigt, statt sie zu
verschweigen.

Zwei Entscheidungen praegen das Verhalten.

Die Passphrase wird nirgends gespeichert. Laege sie neben den Sicherungen, waere
die Verschluesselung wirkungslos. Sie muss deshalb bei jeder geschuetzten
Sicherung neu eingegeben und wiederholt werden, und vor dem Erstellen wird
ausdruecklich bestaetigt, dass sie notiert ist — ohne sie sind die Zugangsdaten
unwiederbringlich. Die Automatik sichert aus demselben Grund ohne Zugangsdaten.

Wiederhergestellt wird standardmaessig in einen NEUEN Ordner neben der Instanz,
nicht ueber die laufende. Diese haelt Chatverlaeufe im Speicher und die Datenbank
geoeffnet — ein Ueberschreiben im Betrieb wuerde teils sofort wieder ueberschrieben
und teils scheitern. Der Dialog benennt das.

Vor dem Schreiben laeuft immer ein Probelauf: Er prueft die Pruefsummen und zaehlt
die Konflikte, sodass ein beschaedigtes Archiv auffaellt und Ueberschreiben
bestaetigt wird, bevor etwas passiert.

BackupScheduler sichert taeglich zur eingestellten Uhrzeit und wendet die Rotation
an. Der Zeitpunkt wird bei jedem Durchlauf neu aus den Einstellungen gelesen,
damit eine Aenderung ohne Neustart greift.

Die Oberflaeche ist noch nicht optisch geprueft — die Dev-Instanz haelt einen
echten API-Key und wuerde beim Start Agenten ausfuehren.

385 Tests gruen (237 Core, 148 Tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-30 10:37:16 +02:00
co-authored by Claude Opus 4.8
parent b55512e734
commit b51cc29667
7 changed files with 1399 additions and 365 deletions
+179
View File
@@ -0,0 +1,179 @@
using ClawdDotNet.Core.Backup;
namespace ClawdDotNet;
/// <summary>
/// Fragt ab, wohin wiederhergestellt wird und — falls nötig — die Passphrase.
///
/// Bewusst ein eigener Schritt statt stiller Vorbelegung: Eine Wiederherstellung
/// überschreibt Arbeitsstände, das soll niemand versehentlich auslösen.
/// </summary>
public sealed class frm_RestoreBackup : Form
{
private readonly TextBox _txtTarget = new();
private readonly TextBox _txtPassphrase = new() { UseSystemPasswordChar = true };
private readonly CheckBox _chkOverwrite = new();
public string TargetDirectory => _txtTarget.Text.Trim();
public string? Passphrase => _txtPassphrase.Text.Length > 0 ? _txtPassphrase.Text : null;
public bool Overwrite => _chkOverwrite.Checked;
public frm_RestoreBackup(BackupManifest manifest, string suggestedTarget)
{
Text = "Sicherung wiederherstellen";
FormBorderStyle = FormBorderStyle.FixedDialog;
StartPosition = FormStartPosition.CenterParent;
MinimizeBox = false;
MaximizeBox = false;
ClientSize = new Size(680, 380);
var layout = new TableLayoutPanel
{
ColumnCount = 3,
Dock = DockStyle.Fill,
Padding = new Padding(14),
RowCount = 7
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
for (var i = 0; i < 7; i++)
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
// ─── Beschreibung der Sicherung ───
var info = new Label
{
AutoSize = true,
Margin = new Padding(0, 0, 0, 12),
Text = $"Instanz: {manifest.InstanceName}\n"
+ $"Erstellt: {manifest.CreatedAt:g}\n"
+ $"Dateien: {manifest.Files.Count}\n"
+ $"Zugangsdaten: {DescribeSecrets(manifest)}"
};
layout.Controls.Add(info, 0, 0);
layout.SetColumnSpan(info, 3);
// ─── Zielordner ───
layout.Controls.Add(new Label { Anchor = AnchorStyles.Left, AutoSize = true, Text = "Zielordner:" }, 0, 1);
_txtTarget.Anchor = AnchorStyles.Left | AnchorStyles.Right;
_txtTarget.Text = suggestedTarget;
layout.Controls.Add(_txtTarget, 1, 1);
var browse = new Button { AutoSize = true, Text = "…" };
browse.Click += OnBrowse;
layout.Controls.Add(browse, 2, 1);
var targetHint = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 2, 0, 10),
MaximumSize = new Size(620, 0),
Text = "Vorgeschlagen ist ein neuer Ordner. Die laufende Instanz zu überschreiben "
+ "wird nicht empfohlen: Sie hält Dateien geöffnet und schreibt Teile davon "
+ "sofort wieder zurück."
};
layout.Controls.Add(targetHint, 1, 2);
layout.SetColumnSpan(targetHint, 2);
// ─── Passphrase ───
var needsPassphrase = manifest.HasSecrets;
var lblPass = new Label
{
Anchor = AnchorStyles.Left,
AutoSize = true,
Enabled = needsPassphrase,
Text = "Passphrase:"
};
layout.Controls.Add(lblPass, 0, 3);
_txtPassphrase.Anchor = AnchorStyles.Left | AnchorStyles.Right;
_txtPassphrase.Enabled = needsPassphrase;
layout.Controls.Add(_txtPassphrase, 1, 3);
var passHint = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 2, 0, 10),
MaximumSize = new Size(620, 0),
Text = needsPassphrase
? "Diese Sicherung enthält geschützte Zugangsdaten."
: "Diese Sicherung enthält keine Zugangsdaten — API-Schlüssel und Passwörter "
+ "müssen nach dem Wiederherstellen neu eingetragen werden."
};
layout.Controls.Add(passHint, 1, 4);
layout.SetColumnSpan(passHint, 2);
// ─── Überschreiben ───
_chkOverwrite.AutoSize = true;
_chkOverwrite.Margin = new Padding(0, 0, 0, 12);
_chkOverwrite.Text = "Vorhandene Dateien im Zielordner überschreiben";
layout.Controls.Add(_chkOverwrite, 1, 5);
layout.SetColumnSpan(_chkOverwrite, 2);
// ─── Schaltflächen ───
var buttons = new FlowLayoutPanel
{
AutoSize = true,
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft
};
var cancel = new Button { AutoSize = true, DialogResult = DialogResult.Cancel, Text = "Abbrechen" };
var ok = new Button { AutoSize = true, Text = "Wiederherstellen" };
ok.Click += OnConfirm;
buttons.Controls.Add(cancel);
buttons.Controls.Add(ok);
layout.Controls.Add(buttons, 1, 6);
layout.SetColumnSpan(buttons, 2);
Controls.Add(layout);
AcceptButton = ok;
CancelButton = cancel;
}
private static string DescribeSecrets(BackupManifest manifest) => manifest.HasSecrets
? $"{manifest.SecretCount}, mit Passphrase geschützt"
: manifest.SecretCount > 0
? $"{manifest.SecretCount} entfernt"
: "keine";
private void OnBrowse(object? sender, EventArgs e)
{
using var dialog = new FolderBrowserDialog
{
Description = "Zielordner für die Wiederherstellung",
UseDescriptionForTitle = true
};
if (dialog.ShowDialog(this) == DialogResult.OK)
_txtTarget.Text = dialog.SelectedPath;
}
private void OnConfirm(object? sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(TargetDirectory))
{
MessageBox.Show(this, "Bitte einen Zielordner angeben.",
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (_txtPassphrase.Enabled && string.IsNullOrEmpty(_txtPassphrase.Text))
{
MessageBox.Show(this,
"Diese Sicherung enthält geschützte Zugangsdaten. Ohne Passphrase lässt sie " +
"sich nicht wiederherstellen.",
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Information);
_txtPassphrase.Focus();
return;
}
DialogResult = DialogResult.OK;
Close();
}
}