diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs
index 462d4fb..4917a71 100644
--- a/Models/AppSettings.cs
+++ b/Models/AppSettings.cs
@@ -54,5 +54,29 @@ public sealed class AppSettings
[JsonPropertyName("openRouterBaseUrl")]
public string OpenRouterBaseUrl { get; set; } = "https://openrouter.ai/api/v1/";
+ [Category("Backup")]
+ [DisplayName("Backup-Verzeichnis")]
+ [Description("Ordner, in dem Sicherungen abgelegt werden.")]
+ [JsonPropertyName("backupDirectory")]
+ public string BackupDirectory { get; set; } = "./Backups";
+
+ [Category("Backup")]
+ [DisplayName("Automatisch sichern")]
+ [Description("Erstellt täglich zur angegebenen Uhrzeit eine Sicherung der laufenden Instanz.")]
+ [JsonPropertyName("autoBackupEnabled")]
+ public bool AutoBackupEnabled { get; set; }
+
+ [Category("Backup")]
+ [DisplayName("Uhrzeit der automatischen Sicherung")]
+ [Description("Tageszeit im Format HH:mm.")]
+ [JsonPropertyName("autoBackupTime")]
+ public string AutoBackupTime { get; set; } = "03:00";
+
+ [Category("Backup")]
+ [DisplayName("Aufbewahrte Sicherungen")]
+ [Description("Wie viele Sicherungen je Instanz behalten werden. Ältere werden entfernt. 0 = alle behalten.")]
+ [JsonPropertyName("backupKeepCount")]
+ public int BackupKeepCount { get; set; } = 14;
+
public override string ToString() => "Anwendungseinstellungen";
}
diff --git a/Services/BackupScheduler.cs b/Services/BackupScheduler.cs
new file mode 100644
index 0000000..958c359
--- /dev/null
+++ b/Services/BackupScheduler.cs
@@ -0,0 +1,147 @@
+using ClawdDotNet.Core.Backup;
+using Microsoft.Extensions.Logging;
+
+namespace ClawdDotNet.Services;
+
+///
+/// Erstellt einmal täglich zur eingestellten Uhrzeit eine Sicherung.
+///
+/// Bewusst ohne Zugangsdaten: Die Passphrase müsste dafür gespeichert werden, und
+/// neben den Sicherungen abgelegt wäre sie wirkungslos. Wer die Zugangsdaten
+/// mitsichern will, macht das von Hand.
+///
+/// Der Zeitpunkt wird bei jedem Durchlauf neu gegen die Einstellungen geprüft, damit
+/// eine Änderung ohne Neustart greift.
+///
+public sealed class BackupScheduler : IDisposable
+{
+ private readonly string _instanceDir;
+ private readonly string _instanceName;
+ private readonly SettingsManager _settings;
+ private readonly ILogger _logger;
+ private readonly BackupService _service = new();
+ private readonly System.Windows.Forms.Timer _timer;
+
+ /// Verhindert mehrere Sicherungen innerhalb derselben Minute.
+ private DateTime? _lastRun;
+
+ private bool _running;
+
+ public event Action? OnBackupCreated;
+
+ public BackupScheduler(string instanceDir, string instanceName,
+ SettingsManager settings, ILogger logger)
+ {
+ _instanceDir = instanceDir;
+ _instanceName = instanceName;
+ _settings = settings;
+ _logger = logger;
+
+ // Minütlich prüfen reicht — die Uhrzeit ist auf Minuten genau eingestellt.
+ _timer = new System.Windows.Forms.Timer { Interval = 60_000 };
+ _timer.Tick += async (_, _) => await TickAsync();
+ }
+
+ public void Start() => _timer.Start();
+
+ public void Stop() => _timer.Stop();
+
+ private async Task TickAsync()
+ {
+ if (_running)
+ return;
+
+ var settings = _settings.AppSettings;
+ if (!settings.AutoBackupEnabled)
+ return;
+
+ if (!TimeSpan.TryParse(settings.AutoBackupTime, out var scheduled))
+ return;
+
+ var now = DateTime.Now;
+
+ // Fällig, sobald die Uhrzeit erreicht ist und heute noch nichts lief.
+ if (now.TimeOfDay < scheduled)
+ return;
+
+ if (_lastRun?.Date == now.Date)
+ return;
+
+ _running = true;
+ try
+ {
+ await RunAsync(settings.BackupDirectory, settings.BackupKeepCount);
+ _lastRun = now;
+ }
+ catch (Exception ex)
+ {
+ // Ein Fehlschlag darf die Anwendung nicht stören; er wird protokolliert
+ // und morgen erneut versucht.
+ _logger.LogError(ex, "Automatische Sicherung fehlgeschlagen");
+ _lastRun = now;
+ }
+ finally
+ {
+ _running = false;
+ }
+ }
+
+ private async Task RunAsync(string folder, int keepCount)
+ {
+ var safeName = string.Concat(
+ _instanceName.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
+
+ var file = Path.Combine(
+ Path.GetFullPath(folder),
+ $"backup_{safeName}_{DateTime.Now:yyyy-MM-dd_HHmm}.zip");
+
+ var result = await _service.CreateAsync(_instanceDir, file, new BackupOptions
+ {
+ Secrets = SecretMode.Exclude,
+ IncludeChatHistory = true,
+ IncludeLogs = false
+ });
+
+ _logger.LogInformation("Automatische Sicherung erstellt: {Path} ({Size} Bytes)",
+ result.ZipPath, result.SizeBytes);
+
+ ApplyRotation(Path.GetFullPath(folder), safeName, keepCount);
+
+ OnBackupCreated?.Invoke(result.ZipPath);
+ }
+
+ /// Behält die neuesten Sicherungen dieser Instanz und entfernt den Rest.
+ private void ApplyRotation(string folder, string safeName, int keepCount)
+ {
+ if (keepCount <= 0 || !Directory.Exists(folder))
+ return;
+
+ try
+ {
+ var prefix = $"backup_{safeName}_";
+
+ var obsolete = new DirectoryInfo(folder)
+ .GetFiles("*.zip")
+ .Where(f => f.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(f => f.LastWriteTime)
+ .Skip(keepCount)
+ .ToList();
+
+ foreach (var file in obsolete)
+ {
+ file.Delete();
+ _logger.LogInformation("Alte Sicherung entfernt: {Name}", file.Name);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Rotation der Sicherungen fehlgeschlagen");
+ }
+ }
+
+ public void Dispose()
+ {
+ _timer.Stop();
+ _timer.Dispose();
+ }
+}
diff --git a/UI/BackupPanel.Designer.cs b/UI/BackupPanel.Designer.cs
new file mode 100644
index 0000000..eacb255
--- /dev/null
+++ b/UI/BackupPanel.Designer.cs
@@ -0,0 +1,428 @@
+namespace ClawdDotNet.UI;
+
+partial class BackupPanel
+{
+ private System.ComponentModel.IContainer components = null;
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && components is not null)
+ components.Dispose();
+
+ base.Dispose(disposing);
+ }
+
+ private void InitializeComponent()
+ {
+ layoutRoot = new TableLayoutPanel();
+ grp_create = new GroupBox();
+ layoutCreate = new TableLayoutPanel();
+ label_target = new Label();
+ txt_targetFolder = new TextBox();
+ btn_browseFolder = new Button();
+ label_secrets = new Label();
+ panel_secretMode = new FlowLayoutPanel();
+ rb_secretsExclude = new RadioButton();
+ rb_secretsPassphrase = new RadioButton();
+ label_passphrase = new Label();
+ txt_passphrase = new TextBox();
+ label_passphraseRepeat = new Label();
+ txt_passphraseRepeat = new TextBox();
+ label_scope = new Label();
+ panel_scope = new FlowLayoutPanel();
+ chk_includeChatHistory = new CheckBox();
+ chk_includeLogs = new CheckBox();
+ panel_createActions = new FlowLayoutPanel();
+ btn_createBackup = new Button();
+ label_createStatus = new Label();
+
+ grp_auto = new GroupBox();
+ layoutAuto = new TableLayoutPanel();
+ chk_autoEnabled = new CheckBox();
+ dtp_autoTime = new DateTimePicker();
+ label_keepCount = new Label();
+ num_keepCount = new NumericUpDown();
+ label_autoHint = new Label();
+
+ grp_existing = new GroupBox();
+ layoutExisting = new TableLayoutPanel();
+ lv_backups = new ListView();
+ col_file = new ColumnHeader();
+ col_created = new ColumnHeader();
+ col_instance = new ColumnHeader();
+ col_size = new ColumnHeader();
+ col_secrets = new ColumnHeader();
+ panel_existingActions = new FlowLayoutPanel();
+ btn_refresh = new Button();
+ btn_restore = new Button();
+ btn_showInFolder = new Button();
+ btn_delete = new Button();
+
+ layoutRoot.SuspendLayout();
+ grp_create.SuspendLayout();
+ layoutCreate.SuspendLayout();
+ panel_secretMode.SuspendLayout();
+ panel_scope.SuspendLayout();
+ panel_createActions.SuspendLayout();
+ grp_auto.SuspendLayout();
+ layoutAuto.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)num_keepCount).BeginInit();
+ grp_existing.SuspendLayout();
+ layoutExisting.SuspendLayout();
+ panel_existingActions.SuspendLayout();
+ SuspendLayout();
+
+ // ─── Wurzel ───
+ layoutRoot.ColumnCount = 1;
+ layoutRoot.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
+ layoutRoot.Dock = DockStyle.Fill;
+ layoutRoot.Name = "layoutRoot";
+ layoutRoot.Padding = new Padding(12);
+ layoutRoot.RowCount = 3;
+ layoutRoot.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ layoutRoot.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ layoutRoot.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
+ layoutRoot.Controls.Add(grp_create, 0, 0);
+ layoutRoot.Controls.Add(grp_auto, 0, 1);
+ layoutRoot.Controls.Add(grp_existing, 0, 2);
+
+ // ═══ Sicherung erstellen ═══
+ grp_create.AutoSize = true;
+ grp_create.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ grp_create.Controls.Add(layoutCreate);
+ grp_create.Dock = DockStyle.Fill;
+ grp_create.Name = "grp_create";
+ grp_create.Padding = new Padding(10);
+ grp_create.TabIndex = 0;
+ grp_create.TabStop = false;
+ grp_create.Text = "Sicherung erstellen";
+
+ layoutCreate.AutoSize = true;
+ layoutCreate.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ layoutCreate.ColumnCount = 3;
+ layoutCreate.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
+ layoutCreate.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
+ layoutCreate.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
+ layoutCreate.Dock = DockStyle.Fill;
+ layoutCreate.Name = "layoutCreate";
+ layoutCreate.RowCount = 6;
+ for (var i = 0; i < 6; i++)
+ layoutCreate.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+
+ label_target.Anchor = AnchorStyles.Left;
+ label_target.AutoSize = true;
+ label_target.Name = "label_target";
+ label_target.Text = "Zielordner:";
+
+ txt_targetFolder.Anchor = AnchorStyles.Left | AnchorStyles.Right;
+ txt_targetFolder.Name = "txt_targetFolder";
+
+ btn_browseFolder.AutoSize = true;
+ btn_browseFolder.Name = "btn_browseFolder";
+ btn_browseFolder.Text = "Durchsuchen…";
+ btn_browseFolder.UseVisualStyleBackColor = true;
+
+ label_secrets.Anchor = AnchorStyles.Left;
+ label_secrets.AutoSize = true;
+ label_secrets.Name = "label_secrets";
+ label_secrets.Text = "Zugangsdaten:";
+
+ panel_secretMode.AutoSize = true;
+ panel_secretMode.Controls.Add(rb_secretsExclude);
+ panel_secretMode.Controls.Add(rb_secretsPassphrase);
+ panel_secretMode.Margin = new Padding(0);
+ panel_secretMode.Name = "panel_secretMode";
+ panel_secretMode.WrapContents = false;
+
+ rb_secretsExclude.AutoSize = true;
+ rb_secretsExclude.Checked = true;
+ rb_secretsExclude.Name = "rb_secretsExclude";
+ rb_secretsExclude.TabStop = true;
+ rb_secretsExclude.Text = "nicht mitsichern";
+ rb_secretsExclude.UseVisualStyleBackColor = true;
+
+ rb_secretsPassphrase.AutoSize = true;
+ rb_secretsPassphrase.Name = "rb_secretsPassphrase";
+ rb_secretsPassphrase.Text = "mit Passphrase schützen";
+ rb_secretsPassphrase.UseVisualStyleBackColor = true;
+
+ label_passphrase.Anchor = AnchorStyles.Left;
+ label_passphrase.AutoSize = true;
+ label_passphrase.Name = "label_passphrase";
+ label_passphrase.Text = "Passphrase:";
+
+ txt_passphrase.Anchor = AnchorStyles.Left | AnchorStyles.Right;
+ txt_passphrase.Name = "txt_passphrase";
+ txt_passphrase.UseSystemPasswordChar = true;
+
+ label_passphraseRepeat.Anchor = AnchorStyles.Left;
+ label_passphraseRepeat.AutoSize = true;
+ label_passphraseRepeat.Name = "label_passphraseRepeat";
+ label_passphraseRepeat.Text = "Wiederholen:";
+
+ txt_passphraseRepeat.Anchor = AnchorStyles.Left | AnchorStyles.Right;
+ txt_passphraseRepeat.Name = "txt_passphraseRepeat";
+ txt_passphraseRepeat.UseSystemPasswordChar = true;
+
+ label_scope.Anchor = AnchorStyles.Left;
+ label_scope.AutoSize = true;
+ label_scope.Name = "label_scope";
+ label_scope.Text = "Umfang:";
+
+ panel_scope.AutoSize = true;
+ panel_scope.Controls.Add(chk_includeChatHistory);
+ panel_scope.Controls.Add(chk_includeLogs);
+ panel_scope.Margin = new Padding(0);
+ panel_scope.Name = "panel_scope";
+ panel_scope.WrapContents = false;
+
+ chk_includeChatHistory.AutoSize = true;
+ chk_includeChatHistory.Checked = true;
+ chk_includeChatHistory.CheckState = CheckState.Checked;
+ chk_includeChatHistory.Name = "chk_includeChatHistory";
+ chk_includeChatHistory.Text = "Chatverläufe";
+ chk_includeChatHistory.UseVisualStyleBackColor = true;
+
+ chk_includeLogs.AutoSize = true;
+ chk_includeLogs.Name = "chk_includeLogs";
+ chk_includeLogs.Text = "Protokolle";
+ chk_includeLogs.UseVisualStyleBackColor = true;
+
+ panel_createActions.AutoSize = true;
+ panel_createActions.Controls.Add(btn_createBackup);
+ panel_createActions.Controls.Add(label_createStatus);
+ panel_createActions.Margin = new Padding(0, 8, 0, 0);
+ panel_createActions.Name = "panel_createActions";
+ panel_createActions.WrapContents = false;
+
+ btn_createBackup.AutoSize = true;
+ btn_createBackup.Name = "btn_createBackup";
+ btn_createBackup.Padding = new Padding(8, 2, 8, 2);
+ btn_createBackup.Text = "Sicherung jetzt erstellen";
+ btn_createBackup.UseVisualStyleBackColor = true;
+
+ label_createStatus.Anchor = AnchorStyles.Left;
+ label_createStatus.AutoSize = true;
+ label_createStatus.Margin = new Padding(12, 8, 3, 0);
+ label_createStatus.Name = "label_createStatus";
+
+ layoutCreate.Controls.Add(label_target, 0, 0);
+ layoutCreate.Controls.Add(txt_targetFolder, 1, 0);
+ layoutCreate.Controls.Add(btn_browseFolder, 2, 0);
+ layoutCreate.Controls.Add(label_secrets, 0, 1);
+ layoutCreate.Controls.Add(panel_secretMode, 1, 1);
+ layoutCreate.Controls.Add(label_passphrase, 0, 2);
+ layoutCreate.Controls.Add(txt_passphrase, 1, 2);
+ layoutCreate.Controls.Add(label_passphraseRepeat, 0, 3);
+ layoutCreate.Controls.Add(txt_passphraseRepeat, 1, 3);
+ layoutCreate.Controls.Add(label_scope, 0, 4);
+ layoutCreate.Controls.Add(panel_scope, 1, 4);
+ layoutCreate.Controls.Add(panel_createActions, 1, 5);
+
+ // ═══ Automatik ═══
+ grp_auto.AutoSize = true;
+ grp_auto.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ grp_auto.Controls.Add(layoutAuto);
+ grp_auto.Dock = DockStyle.Fill;
+ grp_auto.Margin = new Padding(3, 10, 3, 3);
+ grp_auto.Name = "grp_auto";
+ grp_auto.Padding = new Padding(10);
+ grp_auto.TabIndex = 1;
+ grp_auto.TabStop = false;
+ grp_auto.Text = "Automatische Sicherung";
+
+ layoutAuto.AutoSize = true;
+ layoutAuto.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ layoutAuto.ColumnCount = 5;
+ for (var i = 0; i < 4; i++)
+ layoutAuto.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
+ layoutAuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
+ layoutAuto.Dock = DockStyle.Fill;
+ layoutAuto.Name = "layoutAuto";
+ layoutAuto.RowCount = 2;
+ layoutAuto.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ layoutAuto.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+
+ chk_autoEnabled.Anchor = AnchorStyles.Left;
+ chk_autoEnabled.AutoSize = true;
+ chk_autoEnabled.Name = "chk_autoEnabled";
+ chk_autoEnabled.Text = "täglich sichern um";
+ chk_autoEnabled.UseVisualStyleBackColor = true;
+
+ dtp_autoTime.Anchor = AnchorStyles.Left;
+ dtp_autoTime.Format = DateTimePickerFormat.Time;
+ dtp_autoTime.Name = "dtp_autoTime";
+ dtp_autoTime.ShowUpDown = true;
+ dtp_autoTime.Width = 90;
+
+ label_keepCount.Anchor = AnchorStyles.Left;
+ label_keepCount.AutoSize = true;
+ label_keepCount.Margin = new Padding(24, 6, 3, 0);
+ label_keepCount.Name = "label_keepCount";
+ label_keepCount.Text = "Aufbewahren:";
+
+ num_keepCount.Anchor = AnchorStyles.Left;
+ num_keepCount.Maximum = 999;
+ num_keepCount.Name = "num_keepCount";
+ num_keepCount.Width = 70;
+
+ label_autoHint.Anchor = AnchorStyles.Left;
+ label_autoHint.AutoSize = true;
+ label_autoHint.ForeColor = SystemColors.GrayText;
+ label_autoHint.Name = "label_autoHint";
+ label_autoHint.Text = "Die Automatik sichert ohne Zugangsdaten — eine gespeicherte Passphrase "
+ + "neben den Sicherungen wäre wirkungslos.";
+
+ layoutAuto.Controls.Add(chk_autoEnabled, 0, 0);
+ layoutAuto.Controls.Add(dtp_autoTime, 1, 0);
+ layoutAuto.Controls.Add(label_keepCount, 2, 0);
+ layoutAuto.Controls.Add(num_keepCount, 3, 0);
+ layoutAuto.Controls.Add(label_autoHint, 0, 1);
+ layoutAuto.SetColumnSpan(label_autoHint, 5);
+
+ // ═══ Vorhandene Sicherungen ═══
+ grp_existing.Controls.Add(layoutExisting);
+ grp_existing.Dock = DockStyle.Fill;
+ grp_existing.Margin = new Padding(3, 10, 3, 3);
+ grp_existing.Name = "grp_existing";
+ grp_existing.Padding = new Padding(10);
+ grp_existing.TabIndex = 2;
+ grp_existing.TabStop = false;
+ grp_existing.Text = "Vorhandene Sicherungen";
+
+ layoutExisting.ColumnCount = 1;
+ layoutExisting.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
+ layoutExisting.Dock = DockStyle.Fill;
+ layoutExisting.Name = "layoutExisting";
+ layoutExisting.RowCount = 2;
+ layoutExisting.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
+ layoutExisting.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ layoutExisting.Controls.Add(lv_backups, 0, 0);
+ layoutExisting.Controls.Add(panel_existingActions, 0, 1);
+
+ lv_backups.Columns.AddRange(new[] { col_file, col_created, col_instance, col_size, col_secrets });
+ lv_backups.Dock = DockStyle.Fill;
+ lv_backups.FullRowSelect = true;
+ lv_backups.MultiSelect = false;
+ lv_backups.Name = "lv_backups";
+ lv_backups.UseCompatibleStateImageBehavior = false;
+ lv_backups.View = View.Details;
+
+ col_file.Text = "Datei";
+ col_file.Width = 420;
+ col_created.Text = "Erstellt";
+ col_created.Width = 150;
+ col_instance.Text = "Instanz";
+ col_instance.Width = 180;
+ col_size.Text = "Größe";
+ col_size.Width = 100;
+ col_secrets.Text = "Zugangsdaten";
+ col_secrets.Width = 140;
+
+ panel_existingActions.AutoSize = true;
+ panel_existingActions.Controls.Add(btn_refresh);
+ panel_existingActions.Controls.Add(btn_restore);
+ panel_existingActions.Controls.Add(btn_showInFolder);
+ panel_existingActions.Controls.Add(btn_delete);
+ panel_existingActions.Margin = new Padding(0, 8, 0, 0);
+ panel_existingActions.Name = "panel_existingActions";
+ panel_existingActions.WrapContents = false;
+
+ btn_refresh.AutoSize = true;
+ btn_refresh.Name = "btn_refresh";
+ btn_refresh.Text = "Aktualisieren";
+ btn_refresh.UseVisualStyleBackColor = true;
+
+ btn_restore.AutoSize = true;
+ btn_restore.Name = "btn_restore";
+ btn_restore.Padding = new Padding(8, 2, 8, 2);
+ btn_restore.Text = "Wiederherstellen…";
+ btn_restore.UseVisualStyleBackColor = true;
+
+ btn_showInFolder.AutoSize = true;
+ btn_showInFolder.Name = "btn_showInFolder";
+ btn_showInFolder.Text = "Im Ordner zeigen";
+ btn_showInFolder.UseVisualStyleBackColor = true;
+
+ btn_delete.AutoSize = true;
+ btn_delete.Name = "btn_delete";
+ btn_delete.Text = "Löschen";
+ btn_delete.UseVisualStyleBackColor = true;
+
+ // ─── Steuerelement ───
+ AutoScaleDimensions = new SizeF(10F, 25F);
+ AutoScaleMode = AutoScaleMode.Font;
+ Controls.Add(layoutRoot);
+ Name = "BackupPanel";
+ Size = new Size(1200, 800);
+
+ layoutRoot.ResumeLayout(false);
+ layoutRoot.PerformLayout();
+ grp_create.ResumeLayout(false);
+ grp_create.PerformLayout();
+ layoutCreate.ResumeLayout(false);
+ layoutCreate.PerformLayout();
+ panel_secretMode.ResumeLayout(false);
+ panel_secretMode.PerformLayout();
+ panel_scope.ResumeLayout(false);
+ panel_scope.PerformLayout();
+ panel_createActions.ResumeLayout(false);
+ panel_createActions.PerformLayout();
+ grp_auto.ResumeLayout(false);
+ grp_auto.PerformLayout();
+ layoutAuto.ResumeLayout(false);
+ layoutAuto.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)num_keepCount).EndInit();
+ grp_existing.ResumeLayout(false);
+ layoutExisting.ResumeLayout(false);
+ layoutExisting.PerformLayout();
+ panel_existingActions.ResumeLayout(false);
+ panel_existingActions.PerformLayout();
+ ResumeLayout(false);
+ }
+
+ private TableLayoutPanel layoutRoot;
+
+ private GroupBox grp_create;
+ private TableLayoutPanel layoutCreate;
+ private Label label_target;
+ private TextBox txt_targetFolder;
+ private Button btn_browseFolder;
+ private Label label_secrets;
+ private FlowLayoutPanel panel_secretMode;
+ private RadioButton rb_secretsExclude;
+ private RadioButton rb_secretsPassphrase;
+ private Label label_passphrase;
+ private TextBox txt_passphrase;
+ private Label label_passphraseRepeat;
+ private TextBox txt_passphraseRepeat;
+ private Label label_scope;
+ private FlowLayoutPanel panel_scope;
+ private CheckBox chk_includeChatHistory;
+ private CheckBox chk_includeLogs;
+ private FlowLayoutPanel panel_createActions;
+ private Button btn_createBackup;
+ private Label label_createStatus;
+
+ private GroupBox grp_auto;
+ private TableLayoutPanel layoutAuto;
+ private CheckBox chk_autoEnabled;
+ private DateTimePicker dtp_autoTime;
+ private Label label_keepCount;
+ private NumericUpDown num_keepCount;
+ private Label label_autoHint;
+
+ private GroupBox grp_existing;
+ private TableLayoutPanel layoutExisting;
+ private ListView lv_backups;
+ private ColumnHeader col_file;
+ private ColumnHeader col_created;
+ private ColumnHeader col_instance;
+ private ColumnHeader col_size;
+ private ColumnHeader col_secrets;
+ private FlowLayoutPanel panel_existingActions;
+ private Button btn_refresh;
+ private Button btn_restore;
+ private Button btn_showInFolder;
+ private Button btn_delete;
+}
diff --git a/UI/BackupPanel.cs b/UI/BackupPanel.cs
new file mode 100644
index 0000000..5f57a12
--- /dev/null
+++ b/UI/BackupPanel.cs
@@ -0,0 +1,511 @@
+using ClawdDotNet.Core.Backup;
+using ClawdDotNet.Models;
+using ClawdDotNet.Services;
+using Microsoft.Extensions.Logging;
+
+namespace ClawdDotNet.UI;
+
+///
+/// Bedienoberfläche für Sicherung und Wiederherstellung einer Instanz.
+///
+/// Zwei Entscheidungen prägen das Verhalten:
+///
+/// Die Passphrase wird nirgends gespeichert. Läge sie neben den Sicherungen, wäre die
+/// Verschlüsselung wirkungslos. Deshalb muss sie bei jeder geschützten Sicherung neu
+/// eingegeben werden, und die Automatik sichert ohne Zugangsdaten.
+///
+/// Wiederhergestellt wird standardmäßig in einen NEUEN Ordner, nicht über die laufende
+/// Instanz. Diese hält Chatverläufe im Speicher und die Datenbank geöffnet — ein
+/// Überschreiben im Betrieb würde teils sofort wieder überschrieben und teils
+/// scheitern.
+///
+public sealed partial class BackupPanel : UserControl
+{
+ private readonly BackupService _service = new();
+
+ private SettingsManager? _settingsManager;
+ private ILogger? _logger;
+ private string _instanceDir = "";
+ private string _instanceName = "";
+ private bool _loading;
+
+ public BackupPanel()
+ {
+ InitializeComponent();
+ WireEvents();
+ }
+
+ ///
+ /// Aktualisiert die Liste von außen — etwa nachdem die Automatik gesichert hat.
+ ///
+ public void RefreshFromOutside()
+ {
+ if (!IsHandleCreated || IsDisposed)
+ return;
+
+ RefreshBackupList();
+ }
+
+ /// Wird vom Hauptfenster nach dem Laden der Instanz aufgerufen.
+ public void Initialize(string instanceDir, string instanceName,
+ SettingsManager settingsManager, ILogger logger)
+ {
+ _instanceDir = instanceDir;
+ _instanceName = instanceName;
+ _settingsManager = settingsManager;
+ _logger = logger;
+
+ LoadSettings();
+ UpdateEnabledState();
+ RefreshBackupList();
+ }
+
+ private void WireEvents()
+ {
+ btn_browseFolder.Click += OnBrowseFolder;
+ btn_createBackup.Click += async (_, _) => await CreateBackupAsync();
+ btn_refresh.Click += (_, _) => RefreshBackupList();
+ btn_restore.Click += async (_, _) => await RestoreSelectedAsync();
+ btn_showInFolder.Click += (_, _) => ShowSelectedInFolder();
+ btn_delete.Click += (_, _) => DeleteSelected();
+
+ rb_secretsExclude.CheckedChanged += (_, _) => UpdateEnabledState();
+ rb_secretsPassphrase.CheckedChanged += (_, _) => UpdateEnabledState();
+
+ chk_autoEnabled.CheckedChanged += (_, _) => { UpdateEnabledState(); SaveSettings(); };
+ dtp_autoTime.ValueChanged += (_, _) => SaveSettings();
+ num_keepCount.ValueChanged += (_, _) => SaveSettings();
+ txt_targetFolder.Leave += (_, _) => SaveSettings();
+
+ lv_backups.SelectedIndexChanged += (_, _) => UpdateEnabledState();
+ lv_backups.DoubleClick += async (_, _) => await RestoreSelectedAsync();
+ }
+
+ // ═══════════════════════════════════════════════════
+ // Einstellungen
+ // ═══════════════════════════════════════════════════
+
+ private void LoadSettings()
+ {
+ if (_settingsManager is null) return;
+
+ _loading = true;
+ try
+ {
+ var settings = _settingsManager.AppSettings;
+
+ txt_targetFolder.Text = Path.GetFullPath(settings.BackupDirectory);
+ chk_autoEnabled.Checked = settings.AutoBackupEnabled;
+ num_keepCount.Value = Math.Clamp(settings.BackupKeepCount, 0, 999);
+
+ dtp_autoTime.Value = TimeSpan.TryParse(settings.AutoBackupTime, out var time)
+ ? DateTime.Today.Add(time)
+ : DateTime.Today.AddHours(3);
+ }
+ finally
+ {
+ _loading = false;
+ }
+ }
+
+ private void SaveSettings()
+ {
+ if (_loading || _settingsManager is null) return;
+
+ var settings = _settingsManager.AppSettings;
+ settings.BackupDirectory = txt_targetFolder.Text.Trim();
+ settings.AutoBackupEnabled = chk_autoEnabled.Checked;
+ settings.AutoBackupTime = dtp_autoTime.Value.ToString("HH:mm");
+ settings.BackupKeepCount = (int)num_keepCount.Value;
+
+ _settingsManager.Save();
+ }
+
+ private void UpdateEnabledState()
+ {
+ var withPassphrase = rb_secretsPassphrase.Checked;
+ label_passphrase.Enabled = withPassphrase;
+ txt_passphrase.Enabled = withPassphrase;
+ label_passphraseRepeat.Enabled = withPassphrase;
+ txt_passphraseRepeat.Enabled = withPassphrase;
+
+ dtp_autoTime.Enabled = chk_autoEnabled.Checked;
+ num_keepCount.Enabled = chk_autoEnabled.Checked;
+
+ var hasSelection = lv_backups.SelectedItems.Count > 0;
+ btn_restore.Enabled = hasSelection;
+ btn_showInFolder.Enabled = hasSelection;
+ btn_delete.Enabled = hasSelection;
+ }
+
+ // ═══════════════════════════════════════════════════
+ // Sichern
+ // ═══════════════════════════════════════════════════
+
+ private void OnBrowseFolder(object? sender, EventArgs e)
+ {
+ using var dialog = new FolderBrowserDialog
+ {
+ Description = "Ordner für Sicherungen wählen",
+ UseDescriptionForTitle = true,
+ SelectedPath = Directory.Exists(txt_targetFolder.Text)
+ ? txt_targetFolder.Text
+ : Environment.CurrentDirectory
+ };
+
+ if (dialog.ShowDialog(this) == DialogResult.OK)
+ {
+ txt_targetFolder.Text = dialog.SelectedPath;
+ SaveSettings();
+ RefreshBackupList();
+ }
+ }
+
+ private async Task CreateBackupAsync()
+ {
+ if (string.IsNullOrWhiteSpace(_instanceDir))
+ return;
+
+ var options = BuildOptions();
+ if (options is null)
+ return; // Eingabefehler, bereits gemeldet
+
+ var folder = txt_targetFolder.Text.Trim();
+ var file = Path.Combine(folder, BuildFileName());
+
+ btn_createBackup.Enabled = false;
+ SetStatus("Sicherung läuft…", SystemColors.GrayText);
+
+ try
+ {
+ var result = await Task.Run(() => _service.CreateAsync(_instanceDir, file, options));
+
+ SetStatus($"Fertig: {Path.GetFileName(result.ZipPath)} ({FormatSize(result.SizeBytes)})",
+ Color.FromArgb(0, 128, 0));
+
+ _logger?.LogInformation("Sicherung erstellt: {Path} ({Size} Bytes)",
+ result.ZipPath, result.SizeBytes);
+
+ txt_passphrase.Clear();
+ txt_passphraseRepeat.Clear();
+
+ ApplyRotation(folder);
+ RefreshBackupList();
+ }
+ catch (Exception ex)
+ {
+ SetStatus("Fehlgeschlagen", Color.Firebrick);
+ _logger?.LogError(ex, "Sicherung fehlgeschlagen");
+ MessageBox.Show(this, $"Die Sicherung ist fehlgeschlagen:\n\n{ex.Message}",
+ "Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ finally
+ {
+ btn_createBackup.Enabled = true;
+ }
+ }
+
+ /// Liest die Eingaben. Gibt null zurück, wenn etwas fehlt.
+ private BackupOptions? BuildOptions()
+ {
+ if (string.IsNullOrWhiteSpace(txt_targetFolder.Text))
+ {
+ MessageBox.Show(this, "Bitte einen Zielordner angeben.",
+ "Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return null;
+ }
+
+ if (!rb_secretsPassphrase.Checked)
+ {
+ return new BackupOptions
+ {
+ Secrets = SecretMode.Exclude,
+ IncludeLogs = chk_includeLogs.Checked,
+ IncludeChatHistory = chk_includeChatHistory.Checked
+ };
+ }
+
+ var passphrase = txt_passphrase.Text;
+
+ if (string.IsNullOrEmpty(passphrase))
+ {
+ MessageBox.Show(this, "Bitte eine Passphrase eingeben.",
+ "Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ txt_passphrase.Focus();
+ return null;
+ }
+
+ if (passphrase != txt_passphraseRepeat.Text)
+ {
+ MessageBox.Show(this, "Die beiden Passphrasen stimmen nicht überein.",
+ "Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ txt_passphraseRepeat.Focus();
+ txt_passphraseRepeat.SelectAll();
+ return null;
+ }
+
+ // Eine vergessene Passphrase bedeutet unwiederbringlich verlorene Zugangsdaten.
+ var answer = MessageBox.Show(this,
+ "Ohne diese Passphrase lassen sich die Zugangsdaten in der Sicherung " +
+ "nicht wiederherstellen. Es gibt keinen Ersatzweg.\n\n" +
+ "Passphrase sicher notiert?",
+ "Passphrase bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
+
+ if (answer != DialogResult.Yes)
+ return null;
+
+ return new BackupOptions
+ {
+ Secrets = SecretMode.Passphrase,
+ Passphrase = passphrase,
+ IncludeLogs = chk_includeLogs.Checked,
+ IncludeChatHistory = chk_includeChatHistory.Checked
+ };
+ }
+
+ private string BuildFileName()
+ {
+ var safeName = string.Concat(
+ (_instanceName.Length > 0 ? _instanceName : "Instanz")
+ .Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
+
+ return $"backup_{safeName}_{DateTime.Now:yyyy-MM-dd_HHmm}.zip";
+ }
+
+ // ═══════════════════════════════════════════════════
+ // Liste
+ // ═══════════════════════════════════════════════════
+
+ private void RefreshBackupList()
+ {
+ lv_backups.BeginUpdate();
+ lv_backups.Items.Clear();
+
+ try
+ {
+ var folder = txt_targetFolder.Text.Trim();
+ if (!Directory.Exists(folder))
+ return;
+
+ var files = new DirectoryInfo(folder)
+ .GetFiles("*.zip")
+ .OrderByDescending(f => f.LastWriteTime);
+
+ foreach (var file in files)
+ {
+ var item = new ListViewItem(file.Name) { Tag = file.FullName };
+ item.SubItems.Add(file.LastWriteTime.ToString("g"));
+
+ // Das Manifest verrät Instanz und ob Zugangsdaten enthalten sind.
+ try
+ {
+ var manifest = _service.InspectAsync(file.FullName).GetAwaiter().GetResult();
+ item.SubItems.Add(manifest.InstanceName);
+ item.SubItems.Add(FormatSize(file.Length));
+ item.SubItems.Add(manifest.HasSecrets
+ ? $"ja ({manifest.SecretCount}, Passphrase)"
+ : "nein");
+ }
+ catch (Exception)
+ {
+ // Fremde oder beschädigte Archive trotzdem anzeigen — der Benutzer
+ // soll sehen, was im Ordner liegt.
+ item.SubItems.Add("—");
+ item.SubItems.Add(FormatSize(file.Length));
+ item.SubItems.Add("unlesbar");
+ item.ForeColor = SystemColors.GrayText;
+ }
+
+ lv_backups.Items.Add(item);
+ }
+ }
+ finally
+ {
+ lv_backups.EndUpdate();
+ UpdateEnabledState();
+ }
+ }
+
+ private string? SelectedBackupPath
+ => lv_backups.SelectedItems.Count > 0
+ ? lv_backups.SelectedItems[0].Tag as string
+ : null;
+
+ private void ShowSelectedInFolder()
+ {
+ if (SelectedBackupPath is not { } path) return;
+
+ System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
+ }
+
+ private void DeleteSelected()
+ {
+ if (SelectedBackupPath is not { } path) return;
+
+ var answer = MessageBox.Show(this,
+ $"Sicherung endgültig löschen?\n\n{Path.GetFileName(path)}",
+ "Löschen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
+
+ if (answer != DialogResult.Yes)
+ return;
+
+ try
+ {
+ File.Delete(path);
+ RefreshBackupList();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(this, $"Konnte nicht gelöscht werden:\n\n{ex.Message}",
+ "Löschen", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+
+ // ═══════════════════════════════════════════════════
+ // Wiederherstellen
+ // ═══════════════════════════════════════════════════
+
+ private async Task RestoreSelectedAsync()
+ {
+ if (SelectedBackupPath is not { } zipPath) return;
+
+ BackupManifest manifest;
+ try
+ {
+ manifest = await _service.InspectAsync(zipPath);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(this, $"Die Sicherung ist nicht lesbar:\n\n{ex.Message}",
+ "Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ return;
+ }
+
+ using var dialog = new frm_RestoreBackup(manifest, SuggestRestoreTarget(manifest));
+ if (dialog.ShowDialog(this) != DialogResult.OK)
+ return;
+
+ var options = new RestoreOptions
+ {
+ Passphrase = dialog.Passphrase,
+ Overwrite = dialog.Overwrite
+ };
+
+ try
+ {
+ // Erst ohne zu schreiben prüfen — Prüfsummen und Konflikte.
+ var preview = await Task.Run(() =>
+ _service.RestoreAsync(zipPath, dialog.TargetDirectory,
+ options with { DryRun = true, Overwrite = true }));
+
+ if (preview.WouldOverwrite.Count > 0 && !dialog.Overwrite)
+ {
+ var answer = MessageBox.Show(this,
+ $"Im Zielordner liegen bereits {preview.WouldOverwrite.Count} der enthaltenen " +
+ "Dateien.\n\nVorhandene Dateien überschreiben?",
+ "Wiederherstellen", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
+
+ if (answer == DialogResult.Cancel)
+ return;
+
+ options = options with { Overwrite = answer == DialogResult.Yes };
+ }
+
+ var result = await Task.Run(() =>
+ _service.RestoreAsync(zipPath, dialog.TargetDirectory, options));
+
+ _logger?.LogInformation("Wiederherstellung nach {Target}: {Written} Dateien, {Skipped} übersprungen",
+ dialog.TargetDirectory, result.Written.Count, result.Skipped.Count);
+
+ var summary = $"{result.Written.Count} Datei(en) wiederhergestellt nach:\n{dialog.TargetDirectory}";
+ if (result.Skipped.Count > 0)
+ summary += $"\n\n{result.Skipped.Count} vorhandene Datei(en) blieben unangetastet.";
+
+ if (!manifest.HasSecrets && manifest.SecretCount > 0)
+ summary += "\n\nHinweis: Diese Sicherung enthält keine Zugangsdaten. " +
+ "API-Schlüssel und Passwörter müssen neu eingetragen werden.";
+
+ summary += "\n\nDie wiederhergestellte Instanz wird erst nach einem Neustart " +
+ "der Anwendung sichtbar.";
+
+ MessageBox.Show(this, summary, "Wiederherstellen",
+ MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "Wiederherstellung fehlgeschlagen");
+ MessageBox.Show(this, $"Die Wiederherstellung ist fehlgeschlagen:\n\n{ex.Message}",
+ "Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+
+ ///
+ /// Schlägt einen neuen Ordner neben der Instanz vor, nicht die laufende Instanz
+ /// selbst — diese hält Dateien geöffnet und würde Teile sofort überschreiben.
+ ///
+ private string SuggestRestoreTarget(BackupManifest manifest)
+ {
+ var parent = Directory.GetParent(_instanceDir)?.FullName ?? _instanceDir;
+ var name = string.IsNullOrWhiteSpace(manifest.InstanceName)
+ ? "Instanz"
+ : manifest.InstanceName;
+
+ var safeName = string.Concat(
+ name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
+
+ return Path.Combine(parent, $"Instance-{safeName}_wiederhergestellt_{DateTime.Now:yyyyMMdd_HHmm}");
+ }
+
+ // ═══════════════════════════════════════════════════
+ // Rotation
+ // ═══════════════════════════════════════════════════
+
+ /// Entfernt die ältesten Sicherungen dieser Instanz über der Aufbewahrungszahl.
+ private void ApplyRotation(string folder)
+ {
+ var keep = (int)num_keepCount.Value;
+ if (keep <= 0 || !Directory.Exists(folder))
+ return;
+
+ try
+ {
+ var prefix = $"backup_{_instanceName}_";
+
+ var obsolete = new DirectoryInfo(folder)
+ .GetFiles("*.zip")
+ .Where(f => f.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(f => f.LastWriteTime)
+ .Skip(keep)
+ .ToList();
+
+ foreach (var file in obsolete)
+ {
+ file.Delete();
+ _logger?.LogInformation("Alte Sicherung entfernt: {Name}", file.Name);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Aufräumen darf eine erfolgreiche Sicherung nicht nachträglich entwerten.
+ _logger?.LogWarning(ex, "Rotation der Sicherungen fehlgeschlagen");
+ }
+ }
+
+ // ═══════════════════════════════════════════════════
+ // Kleinkram
+ // ═══════════════════════════════════════════════════
+
+ private void SetStatus(string text, Color color)
+ {
+ label_createStatus.Text = text;
+ label_createStatus.ForeColor = color;
+ }
+
+ private static string FormatSize(long bytes) => bytes switch
+ {
+ < 1024 => $"{bytes} B",
+ < 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
+ < 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:F1} MB",
+ _ => $"{bytes / 1024.0 / 1024 / 1024:F2} GB"
+ };
+}
diff --git a/frm_RestoreBackup.cs b/frm_RestoreBackup.cs
new file mode 100644
index 0000000..0e4ac5f
--- /dev/null
+++ b/frm_RestoreBackup.cs
@@ -0,0 +1,179 @@
+using ClawdDotNet.Core.Backup;
+
+namespace ClawdDotNet;
+
+///
+/// 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.
+///
+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();
+ }
+}
diff --git a/frm_main.Designer.cs b/frm_main.Designer.cs
index c7ec11b..fff2df8 100644
--- a/frm_main.Designer.cs
+++ b/frm_main.Designer.cs
@@ -52,6 +52,8 @@ namespace ClawdDotNet
label_openRouterStatus = new ToolStripStatusLabel();
toolStripStatusLabel1 = new ToolStripStatusLabel();
label_openRouterCredits = new ToolStripStatusLabel();
+ toolStripStatusLabel2 = new ToolStripStatusLabel();
+ label_openRouterCreditBalance = new ToolStripStatusLabel();
tabControl1 = new TabControl();
tabPage_Chat = new TabPage();
webView_chat = new Microsoft.Web.WebView2.WinForms.WebView2();
@@ -75,12 +77,6 @@ namespace ClawdDotNet
splitContainer2 = new SplitContainer();
splitContainer1 = new SplitContainer();
dgv_agentlist = new DataGridView();
- col_AgentDisplayName = new DataGridViewTextBoxColumn();
- col_AgentModel = new DataGridViewTextBoxColumn();
- col_AgentToolCount = new DataGridViewTextBoxColumn();
- col_AgentHasIdentity = new DataGridViewTextBoxColumn();
- col_AgentHasSoul = new DataGridViewTextBoxColumn();
- col_AgentStatus = new DataGridViewTextBoxColumn();
toolStrip_agentSettings = new ToolStrip();
btn_addAgent = new ToolStripButton();
btn_removeAgent = new ToolStripButton();
@@ -94,24 +90,11 @@ namespace ClawdDotNet
pg_agentsettings = new PropertyGrid();
splitContainer3 = new SplitContainer();
dgv_agentTools = new DataGridView();
- col_ToolAssigned2 = new DataGridViewCheckBoxColumn();
- col_ToolName2 = new DataGridViewTextBoxColumn();
- col_ToolDescription2 = new DataGridViewTextBoxColumn();
pg_agentToolSettings = new PropertyGrid();
tabPage_Worker = new TabPage();
tabControl3 = new TabControl();
tabPage_jobs = new TabPage();
dgv_Jobs = new DataGridView();
- col_JobType = new DataGridViewTextBoxColumn();
- col_JobAgentName = new DataGridViewTextBoxColumn();
- col_JobToolName = new DataGridViewTextBoxColumn();
- col_JobCron = new DataGridViewTextBoxColumn();
- col_JobTask = new DataGridViewTextBoxColumn();
- col_JobNextRun = new DataGridViewTextBoxColumn();
- col_JobLastRun = new DataGridViewTextBoxColumn();
- col_JobLastStatus = new DataGridViewTextBoxColumn();
- col_JobAutostart = new DataGridViewCheckBoxColumn();
- col_JobStatus = new DataGridViewTextBoxColumn();
toolStrip_jobs = new ToolStrip();
btn_addJob = new ToolStripButton();
btn_editJob = new ToolStripButton();
@@ -120,32 +103,19 @@ namespace ClawdDotNet
btn_refreshJobs = new ToolStripButton();
tabPage_services = new TabPage();
dgv_services = new DataGridView();
- col_SvcName = new DataGridViewTextBoxColumn();
- col_SvcType = new DataGridViewTextBoxColumn();
- col_SvcPort = new DataGridViewTextBoxColumn();
- col_SvcStatus = new DataGridViewTextBoxColumn();
- col_SvcStartedAt = new DataGridViewTextBoxColumn();
- col_SvcDescription = new DataGridViewTextBoxColumn();
- col_SvcBuiltIn = new DataGridViewCheckBoxColumn();
toolStrip_services = new ToolStrip();
btn_addService = new ToolStripButton();
btn_removeService = new ToolStripButton();
toolStripSeparator6 = new ToolStripSeparator();
btn_startService = new ToolStripButton();
btn_stopService = new ToolStripButton();
+ tabPage_jobHistory = new TabPage();
+ dgv_jobhistory = new DataGridView();
tabPage_Info = new TabPage();
label2 = new Label();
label_Softwareinfo = new Label();
- tabPage_jobHistory = new TabPage();
- dgv_jobhistory = new DataGridView();
- col_HistJobName = new DataGridViewTextBoxColumn();
- col_HistAgent = new DataGridViewTextBoxColumn();
- col_HistTime = new DataGridViewTextBoxColumn();
- col_HistDescription = new DataGridViewTextBoxColumn();
- col_HistInfo = new DataGridViewTextBoxColumn();
- col_HistStatus = new DataGridViewTextBoxColumn();
- toolStripStatusLabel2 = new ToolStripStatusLabel();
- label_openRouterCreditBalance = new ToolStripStatusLabel();
+ tabPage_backup = new TabPage();
+ backupPanel = new ClawdDotNet.UI.BackupPanel();
menuStrip1.SuspendLayout();
toolStrip1.SuspendLayout();
statusStrip1.SuspendLayout();
@@ -182,9 +152,9 @@ namespace ClawdDotNet
tabPage_services.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgv_services).BeginInit();
toolStrip_services.SuspendLayout();
- tabPage_Info.SuspendLayout();
tabPage_jobHistory.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgv_jobhistory).BeginInit();
+ tabPage_Info.SuspendLayout();
SuspendLayout();
//
// menuStrip1
@@ -283,6 +253,18 @@ namespace ClawdDotNet
label_openRouterCredits.Size = new Size(97, 25);
label_openRouterCredits.Text = "Credits: ---";
//
+ // toolStripStatusLabel2
+ //
+ toolStripStatusLabel2.Name = "toolStripStatusLabel2";
+ toolStripStatusLabel2.Size = new Size(16, 25);
+ toolStripStatusLabel2.Text = "|";
+ //
+ // label_openRouterCreditBalance
+ //
+ label_openRouterCreditBalance.Name = "label_openRouterCreditBalance";
+ label_openRouterCreditBalance.Size = new Size(179, 25);
+ label_openRouterCreditBalance.Text = "toolStripStatusLabel3";
+ //
// tabControl1
//
tabControl1.Controls.Add(tabPage_Chat);
@@ -405,6 +387,7 @@ namespace ClawdDotNet
//
tabControl2.Controls.Add(tabPage_appsettings);
tabControl2.Controls.Add(tabPage_instancesettings);
+ tabControl2.Controls.Add(tabPage_backup);
tabControl2.Dock = DockStyle.Fill;
tabControl2.Location = new Point(3, 3);
tabControl2.Name = "tabControl2";
@@ -507,7 +490,6 @@ namespace ClawdDotNet
//
dgv_agentlist.AllowUserToAddRows = false;
dgv_agentlist.AllowUserToDeleteRows = false;
- dgv_agentlist.AutoGenerateColumns = false;
dgv_agentlist.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
dgv_agentlist.BackgroundColor = Color.FromArgb(45, 45, 45);
dataGridViewCellStyle1.Alignment = DataGridViewContentAlignment.MiddleLeft;
@@ -519,7 +501,6 @@ namespace ClawdDotNet
dataGridViewCellStyle1.WrapMode = DataGridViewTriState.True;
dgv_agentlist.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1;
dgv_agentlist.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- dgv_agentlist.Columns.AddRange(new DataGridViewColumn[] { col_AgentDisplayName, col_AgentModel, col_AgentToolCount, col_AgentHasIdentity, col_AgentHasSoul, col_AgentStatus });
dataGridViewCellStyle2.Alignment = DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle2.BackColor = Color.FromArgb(55, 55, 55);
dataGridViewCellStyle2.Font = new Font("Segoe UI", 9F);
@@ -539,55 +520,6 @@ namespace ClawdDotNet
dgv_agentlist.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgv_agentlist.Size = new Size(1519, 505);
dgv_agentlist.TabIndex = 0;
- //
- // col_AgentDisplayName
- //
- col_AgentDisplayName.DataPropertyName = "DisplayName";
- col_AgentDisplayName.HeaderText = "Name";
- col_AgentDisplayName.MinimumWidth = 8;
- col_AgentDisplayName.Name = "col_AgentDisplayName";
- col_AgentDisplayName.ReadOnly = true;
- //
- // col_AgentModel
- //
- col_AgentModel.DataPropertyName = "Model";
- col_AgentModel.HeaderText = "Modell";
- col_AgentModel.MinimumWidth = 8;
- col_AgentModel.Name = "col_AgentModel";
- col_AgentModel.ReadOnly = true;
- //
- // col_AgentToolCount
- //
- col_AgentToolCount.DataPropertyName = "ToolCount";
- col_AgentToolCount.HeaderText = "Tools";
- col_AgentToolCount.MinimumWidth = 8;
- col_AgentToolCount.Name = "col_AgentToolCount";
- col_AgentToolCount.ReadOnly = true;
- //
- // col_AgentHasIdentity
- //
- col_AgentHasIdentity.DataPropertyName = "HasIdentity";
- col_AgentHasIdentity.HeaderText = "Identity";
- col_AgentHasIdentity.MinimumWidth = 8;
- col_AgentHasIdentity.Name = "col_AgentHasIdentity";
- col_AgentHasIdentity.ReadOnly = true;
- //
- // col_AgentHasSoul
- //
- col_AgentHasSoul.DataPropertyName = "HasSoul";
- col_AgentHasSoul.HeaderText = "Soul";
- col_AgentHasSoul.MinimumWidth = 8;
- col_AgentHasSoul.Name = "col_AgentHasSoul";
- col_AgentHasSoul.ReadOnly = true;
- //
- // col_AgentStatus
- //
- col_AgentStatus.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
- col_AgentStatus.DataPropertyName = "Status";
- col_AgentStatus.HeaderText = "Status";
- col_AgentStatus.MinimumWidth = 8;
- col_AgentStatus.Name = "col_AgentStatus";
- col_AgentStatus.ReadOnly = true;
//
// toolStrip_agentSettings
//
@@ -687,7 +619,6 @@ namespace ClawdDotNet
//
dgv_agentTools.AllowUserToAddRows = false;
dgv_agentTools.AllowUserToDeleteRows = false;
- dgv_agentTools.AutoGenerateColumns = false;
dgv_agentTools.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
dgv_agentTools.BackgroundColor = Color.FromArgb(45, 45, 45);
dataGridViewCellStyle3.Alignment = DataGridViewContentAlignment.MiddleLeft;
@@ -699,7 +630,6 @@ namespace ClawdDotNet
dataGridViewCellStyle3.WrapMode = DataGridViewTriState.True;
dgv_agentTools.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle3;
dgv_agentTools.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- dgv_agentTools.Columns.AddRange(new DataGridViewColumn[] { col_ToolAssigned2, col_ToolName2, col_ToolDescription2 });
dataGridViewCellStyle4.Alignment = DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle4.BackColor = Color.FromArgb(55, 55, 55);
dataGridViewCellStyle4.Font = new Font("Segoe UI", 9F);
@@ -718,31 +648,6 @@ namespace ClawdDotNet
dgv_agentTools.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgv_agentTools.Size = new Size(984, 334);
dgv_agentTools.TabIndex = 0;
- //
- // col_ToolAssigned2
- //
- col_ToolAssigned2.DataPropertyName = "Assigned";
- col_ToolAssigned2.HeaderText = "Aktiv";
- col_ToolAssigned2.MinimumWidth = 8;
- col_ToolAssigned2.Name = "col_ToolAssigned2";
- col_ToolAssigned2.Width = 60;
- //
- // col_ToolName2
- //
- col_ToolName2.DataPropertyName = "ToolName";
- col_ToolName2.HeaderText = "Tool";
- col_ToolName2.MinimumWidth = 8;
- col_ToolName2.Name = "col_ToolName2";
- col_ToolName2.ReadOnly = true;
- //
- // col_ToolDescription2
- //
- col_ToolDescription2.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
- col_ToolDescription2.DataPropertyName = "Description";
- col_ToolDescription2.HeaderText = "Beschreibung";
- col_ToolDescription2.MinimumWidth = 8;
- col_ToolDescription2.Name = "col_ToolDescription2";
- col_ToolDescription2.ReadOnly = true;
//
// pg_agentToolSettings
//
@@ -792,7 +697,6 @@ namespace ClawdDotNet
//
dgv_Jobs.AllowUserToAddRows = false;
dgv_Jobs.AllowUserToDeleteRows = false;
- dgv_Jobs.AutoGenerateColumns = false;
dgv_Jobs.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
dgv_Jobs.BackgroundColor = Color.FromArgb(45, 45, 45);
dataGridViewCellStyle5.Alignment = DataGridViewContentAlignment.MiddleLeft;
@@ -804,7 +708,6 @@ namespace ClawdDotNet
dataGridViewCellStyle5.WrapMode = DataGridViewTriState.True;
dgv_Jobs.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle5;
dgv_Jobs.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- dgv_Jobs.Columns.AddRange(new DataGridViewColumn[] { col_JobType, col_JobAgentName, col_JobToolName, col_JobCron, col_JobTask, col_JobNextRun, col_JobLastRun, col_JobLastStatus, col_JobAutostart, col_JobStatus });
dataGridViewCellStyle6.Alignment = DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle6.BackColor = Color.FromArgb(55, 55, 55);
dataGridViewCellStyle6.Font = new Font("Segoe UI", 9F);
@@ -825,87 +728,6 @@ namespace ClawdDotNet
dgv_Jobs.Size = new Size(1870, 803);
dgv_Jobs.TabIndex = 1;
dgv_Jobs.CellContentClick += dgv_Jobs_CellContentClick;
- //
- // col_JobType
- //
- col_JobType.DataPropertyName = "JobType";
- col_JobType.HeaderText = "Typ";
- col_JobType.MinimumWidth = 8;
- col_JobType.Name = "col_JobType";
- col_JobType.ReadOnly = true;
- //
- // col_JobAgentName
- //
- col_JobAgentName.DataPropertyName = "AgentName";
- col_JobAgentName.HeaderText = "Agent";
- col_JobAgentName.MinimumWidth = 8;
- col_JobAgentName.Name = "col_JobAgentName";
- col_JobAgentName.ReadOnly = true;
- //
- // col_JobToolName
- //
- col_JobToolName.DataPropertyName = "ToolName";
- col_JobToolName.HeaderText = "Tool";
- col_JobToolName.MinimumWidth = 8;
- col_JobToolName.Name = "col_JobToolName";
- col_JobToolName.ReadOnly = true;
- //
- // col_JobCron
- //
- col_JobCron.DataPropertyName = "CronExpression";
- col_JobCron.HeaderText = "Zeitplan";
- col_JobCron.MinimumWidth = 8;
- col_JobCron.Name = "col_JobCron";
- col_JobCron.ReadOnly = true;
- //
- // col_JobTask
- //
- col_JobTask.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
- col_JobTask.DataPropertyName = "TaskMessage";
- col_JobTask.HeaderText = "Aufgabe";
- col_JobTask.MinimumWidth = 8;
- col_JobTask.Name = "col_JobTask";
- col_JobTask.ReadOnly = true;
- //
- // col_JobNextRun
- //
- col_JobNextRun.DataPropertyName = "NextRun";
- col_JobNextRun.HeaderText = "Naechste Ausf.";
- col_JobNextRun.MinimumWidth = 8;
- col_JobNextRun.Name = "col_JobNextRun";
- col_JobNextRun.ReadOnly = true;
- //
- // col_JobLastRun
- //
- col_JobLastRun.DataPropertyName = "LastRun";
- col_JobLastRun.HeaderText = "Letzte Ausf.";
- col_JobLastRun.MinimumWidth = 8;
- col_JobLastRun.Name = "col_JobLastRun";
- col_JobLastRun.ReadOnly = true;
- //
- // col_JobLastStatus
- //
- col_JobLastStatus.DataPropertyName = "LastStatus";
- col_JobLastStatus.HeaderText = "Letzter Status";
- col_JobLastStatus.MinimumWidth = 8;
- col_JobLastStatus.Name = "col_JobLastStatus";
- col_JobLastStatus.ReadOnly = true;
- //
- // col_JobAutostart
- //
- col_JobAutostart.DataPropertyName = "RunOnStart";
- col_JobAutostart.HeaderText = "Autostart";
- col_JobAutostart.MinimumWidth = 8;
- col_JobAutostart.Name = "col_JobAutostart";
- col_JobAutostart.ReadOnly = true;
- //
- // col_JobStatus
- //
- col_JobStatus.DataPropertyName = "Status";
- col_JobStatus.HeaderText = "Status";
- col_JobStatus.MinimumWidth = 8;
- col_JobStatus.Name = "col_JobStatus";
- col_JobStatus.ReadOnly = true;
//
// toolStrip_jobs
//
@@ -967,7 +789,6 @@ namespace ClawdDotNet
//
dgv_services.AllowUserToAddRows = false;
dgv_services.AllowUserToDeleteRows = false;
- dgv_services.AutoGenerateColumns = false;
dgv_services.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
dgv_services.BackgroundColor = Color.FromArgb(45, 45, 45);
dataGridViewCellStyle7.Alignment = DataGridViewContentAlignment.MiddleLeft;
@@ -979,7 +800,6 @@ namespace ClawdDotNet
dataGridViewCellStyle7.WrapMode = DataGridViewTriState.True;
dgv_services.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle7;
dgv_services.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- dgv_services.Columns.AddRange(new DataGridViewColumn[] { col_SvcName, col_SvcType, col_SvcPort, col_SvcStatus, col_SvcStartedAt, col_SvcDescription, col_SvcBuiltIn });
dataGridViewCellStyle8.Alignment = DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle8.BackColor = Color.FromArgb(55, 55, 55);
dataGridViewCellStyle8.Font = new Font("Segoe UI", 9F);
@@ -999,63 +819,6 @@ namespace ClawdDotNet
dgv_services.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgv_services.Size = new Size(1870, 803);
dgv_services.TabIndex = 1;
- //
- // col_SvcName
- //
- col_SvcName.DataPropertyName = "Name";
- col_SvcName.HeaderText = "Name";
- col_SvcName.MinimumWidth = 8;
- col_SvcName.Name = "col_SvcName";
- col_SvcName.ReadOnly = true;
- //
- // col_SvcType
- //
- col_SvcType.DataPropertyName = "Type";
- col_SvcType.HeaderText = "Typ";
- col_SvcType.MinimumWidth = 8;
- col_SvcType.Name = "col_SvcType";
- col_SvcType.ReadOnly = true;
- //
- // col_SvcPort
- //
- col_SvcPort.DataPropertyName = "Port";
- col_SvcPort.HeaderText = "Port";
- col_SvcPort.MinimumWidth = 8;
- col_SvcPort.Name = "col_SvcPort";
- col_SvcPort.ReadOnly = true;
- //
- // col_SvcStatus
- //
- col_SvcStatus.DataPropertyName = "Status";
- col_SvcStatus.HeaderText = "Status";
- col_SvcStatus.MinimumWidth = 8;
- col_SvcStatus.Name = "col_SvcStatus";
- col_SvcStatus.ReadOnly = true;
- //
- // col_SvcStartedAt
- //
- col_SvcStartedAt.DataPropertyName = "StartedAt";
- col_SvcStartedAt.HeaderText = "Gestartet";
- col_SvcStartedAt.MinimumWidth = 8;
- col_SvcStartedAt.Name = "col_SvcStartedAt";
- col_SvcStartedAt.ReadOnly = true;
- //
- // col_SvcDescription
- //
- col_SvcDescription.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
- col_SvcDescription.DataPropertyName = "Description";
- col_SvcDescription.HeaderText = "Beschreibung";
- col_SvcDescription.MinimumWidth = 8;
- col_SvcDescription.Name = "col_SvcDescription";
- col_SvcDescription.ReadOnly = true;
- //
- // col_SvcBuiltIn
- //
- col_SvcBuiltIn.DataPropertyName = "BuiltIn";
- col_SvcBuiltIn.HeaderText = "Integriert";
- col_SvcBuiltIn.MinimumWidth = 8;
- col_SvcBuiltIn.Name = "col_SvcBuiltIn";
- col_SvcBuiltIn.ReadOnly = true;
//
// toolStrip_services
//
@@ -1099,6 +862,52 @@ namespace ClawdDotNet
btn_stopService.Size = new Size(83, 29);
btn_stopService.Text = "Stoppen";
//
+ // tabPage_jobHistory
+ //
+ tabPage_jobHistory.Controls.Add(dgv_jobhistory);
+ tabPage_jobHistory.Location = new Point(4, 34);
+ tabPage_jobHistory.Name = "tabPage_jobHistory";
+ tabPage_jobHistory.Padding = new Padding(3);
+ tabPage_jobHistory.Size = new Size(1876, 843);
+ tabPage_jobHistory.TabIndex = 2;
+ tabPage_jobHistory.Text = "Job History";
+ tabPage_jobHistory.UseVisualStyleBackColor = true;
+ //
+ // dgv_jobhistory
+ //
+ dgv_jobhistory.AllowUserToAddRows = false;
+ dgv_jobhistory.AllowUserToDeleteRows = false;
+ dgv_jobhistory.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
+ dgv_jobhistory.BackgroundColor = Color.FromArgb(45, 45, 45);
+ dataGridViewCellStyle9.Alignment = DataGridViewContentAlignment.MiddleLeft;
+ dataGridViewCellStyle9.BackColor = Color.FromArgb(35, 35, 35);
+ dataGridViewCellStyle9.Font = new Font("Segoe UI", 9F);
+ dataGridViewCellStyle9.ForeColor = Color.White;
+ dataGridViewCellStyle9.SelectionBackColor = SystemColors.Highlight;
+ dataGridViewCellStyle9.SelectionForeColor = SystemColors.HighlightText;
+ dataGridViewCellStyle9.WrapMode = DataGridViewTriState.True;
+ dgv_jobhistory.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle9;
+ dgv_jobhistory.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
+ dataGridViewCellStyle10.Alignment = DataGridViewContentAlignment.MiddleLeft;
+ dataGridViewCellStyle10.BackColor = Color.FromArgb(55, 55, 55);
+ dataGridViewCellStyle10.Font = new Font("Segoe UI", 9F);
+ dataGridViewCellStyle10.ForeColor = Color.White;
+ dataGridViewCellStyle10.SelectionBackColor = Color.FromArgb(80, 120, 200);
+ dataGridViewCellStyle10.SelectionForeColor = SystemColors.HighlightText;
+ dataGridViewCellStyle10.WrapMode = DataGridViewTriState.False;
+ dgv_jobhistory.DefaultCellStyle = dataGridViewCellStyle10;
+ dgv_jobhistory.Dock = DockStyle.Fill;
+ dgv_jobhistory.EnableHeadersVisualStyles = false;
+ dgv_jobhistory.Location = new Point(3, 3);
+ dgv_jobhistory.MultiSelect = false;
+ dgv_jobhistory.Name = "dgv_jobhistory";
+ dgv_jobhistory.ReadOnly = true;
+ dgv_jobhistory.RowHeadersVisible = false;
+ dgv_jobhistory.RowHeadersWidth = 62;
+ dgv_jobhistory.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
+ dgv_jobhistory.Size = new Size(1870, 837);
+ dgv_jobhistory.TabIndex = 0;
+ //
// tabPage_Info
//
tabPage_Info.Controls.Add(label2);
@@ -1129,115 +938,24 @@ namespace ClawdDotNet
label_Softwareinfo.TabIndex = 0;
label_Softwareinfo.Text = "label1";
//
- // tabPage_jobHistory
+ // tabPage_backup
//
- tabPage_jobHistory.Controls.Add(dgv_jobhistory);
- tabPage_jobHistory.Location = new Point(4, 34);
- tabPage_jobHistory.Name = "tabPage_jobHistory";
- tabPage_jobHistory.Padding = new Padding(3);
- tabPage_jobHistory.Size = new Size(1876, 843);
- tabPage_jobHistory.TabIndex = 2;
- tabPage_jobHistory.Text = "Job History";
- tabPage_jobHistory.UseVisualStyleBackColor = true;
- //
- // dgv_jobhistory
- //
- dgv_jobhistory.AllowUserToAddRows = false;
- dgv_jobhistory.AllowUserToDeleteRows = false;
- dgv_jobhistory.AutoGenerateColumns = false;
- dgv_jobhistory.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
- dgv_jobhistory.BackgroundColor = Color.FromArgb(45, 45, 45);
- dataGridViewCellStyle9.Alignment = DataGridViewContentAlignment.MiddleLeft;
- dataGridViewCellStyle9.BackColor = Color.FromArgb(35, 35, 35);
- dataGridViewCellStyle9.Font = new Font("Segoe UI", 9F);
- dataGridViewCellStyle9.ForeColor = Color.White;
- dataGridViewCellStyle9.SelectionBackColor = SystemColors.Highlight;
- dataGridViewCellStyle9.SelectionForeColor = SystemColors.HighlightText;
- dataGridViewCellStyle9.WrapMode = DataGridViewTriState.True;
- dgv_jobhistory.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle9;
- dgv_jobhistory.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- dgv_jobhistory.Columns.AddRange(new DataGridViewColumn[] { col_HistJobName, col_HistAgent, col_HistTime, col_HistDescription, col_HistInfo, col_HistStatus });
- dataGridViewCellStyle10.Alignment = DataGridViewContentAlignment.MiddleLeft;
- dataGridViewCellStyle10.BackColor = Color.FromArgb(55, 55, 55);
- dataGridViewCellStyle10.Font = new Font("Segoe UI", 9F);
- dataGridViewCellStyle10.ForeColor = Color.White;
- dataGridViewCellStyle10.SelectionBackColor = Color.FromArgb(80, 120, 200);
- dataGridViewCellStyle10.SelectionForeColor = SystemColors.HighlightText;
- dataGridViewCellStyle10.WrapMode = DataGridViewTriState.False;
- dgv_jobhistory.DefaultCellStyle = dataGridViewCellStyle10;
- dgv_jobhistory.Dock = DockStyle.Fill;
- dgv_jobhistory.EnableHeadersVisualStyles = false;
- dgv_jobhistory.Location = new Point(3, 3);
- dgv_jobhistory.MultiSelect = false;
- dgv_jobhistory.Name = "dgv_jobhistory";
- dgv_jobhistory.ReadOnly = true;
- dgv_jobhistory.RowHeadersVisible = false;
- dgv_jobhistory.RowHeadersWidth = 62;
- dgv_jobhistory.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
- dgv_jobhistory.Size = new Size(1870, 837);
- dgv_jobhistory.TabIndex = 0;
+ tabPage_backup.Controls.Add(backupPanel);
+ tabPage_backup.Location = new Point(4, 34);
+ tabPage_backup.Name = "tabPage_backup";
+ tabPage_backup.Padding = new Padding(3);
+ tabPage_backup.Size = new Size(1876, 843);
+ tabPage_backup.TabIndex = 2;
+ tabPage_backup.Text = "Backup";
+ tabPage_backup.UseVisualStyleBackColor = true;
//
- // col_HistJobName
+ // backupPanel
//
- col_HistJobName.DataPropertyName = "JobName";
- col_HistJobName.HeaderText = "Job";
- col_HistJobName.MinimumWidth = 8;
- col_HistJobName.Name = "col_HistJobName";
- col_HistJobName.ReadOnly = true;
- //
- // col_HistAgent
- //
- col_HistAgent.DataPropertyName = "Agent";
- col_HistAgent.HeaderText = "Agent";
- col_HistAgent.MinimumWidth = 8;
- col_HistAgent.Name = "col_HistAgent";
- col_HistAgent.ReadOnly = true;
- //
- // col_HistTime
- //
- col_HistTime.DataPropertyName = "Time";
- col_HistTime.HeaderText = "Zeit";
- col_HistTime.MinimumWidth = 8;
- col_HistTime.Name = "col_HistTime";
- col_HistTime.ReadOnly = true;
- col_HistTime.DefaultCellStyle = new DataGridViewCellStyle { Format = "yyyy-MM-dd HH:mm:ss", BackColor = Color.FromArgb(55, 55, 55), ForeColor = Color.White, SelectionBackColor = Color.FromArgb(80, 120, 200), SelectionForeColor = SystemColors.HighlightText };
- //
- // col_HistDescription
- //
- col_HistDescription.DataPropertyName = "JobDescription";
- col_HistDescription.HeaderText = "Beschreibung";
- col_HistDescription.MinimumWidth = 8;
- col_HistDescription.Name = "col_HistDescription";
- col_HistDescription.ReadOnly = true;
- //
- // col_HistInfo
- //
- col_HistInfo.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
- col_HistInfo.DataPropertyName = "Info";
- col_HistInfo.HeaderText = "Info";
- col_HistInfo.MinimumWidth = 8;
- col_HistInfo.Name = "col_HistInfo";
- col_HistInfo.ReadOnly = true;
- //
- // col_HistStatus
- //
- col_HistStatus.DataPropertyName = "Status";
- col_HistStatus.HeaderText = "Status";
- col_HistStatus.MinimumWidth = 8;
- col_HistStatus.Name = "col_HistStatus";
- col_HistStatus.ReadOnly = true;
- //
- // toolStripStatusLabel2
- //
- toolStripStatusLabel2.Name = "toolStripStatusLabel2";
- toolStripStatusLabel2.Size = new Size(16, 25);
- toolStripStatusLabel2.Text = "|";
- //
- // label_openRouterCreditBalance
- //
- label_openRouterCreditBalance.Name = "label_openRouterCreditBalance";
- label_openRouterCreditBalance.Size = new Size(179, 25);
- label_openRouterCreditBalance.Text = "toolStripStatusLabel3";
+ backupPanel.Dock = DockStyle.Fill;
+ backupPanel.Location = new Point(3, 3);
+ backupPanel.Name = "backupPanel";
+ backupPanel.Size = new Size(1870, 837);
+ backupPanel.TabIndex = 0;
//
// frm_main
//
@@ -1301,10 +1019,10 @@ namespace ClawdDotNet
((System.ComponentModel.ISupportInitialize)dgv_services).EndInit();
toolStrip_services.ResumeLayout(false);
toolStrip_services.PerformLayout();
- tabPage_Info.ResumeLayout(false);
- tabPage_Info.PerformLayout();
tabPage_jobHistory.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgv_jobhistory).EndInit();
+ tabPage_Info.ResumeLayout(false);
+ tabPage_Info.PerformLayout();
ResumeLayout(false);
PerformLayout();
}
@@ -1447,5 +1165,7 @@ namespace ClawdDotNet
private ToolStripStatusLabel toolStripStatusLabel2;
private ToolStripStatusLabel label_openRouterCreditBalance;
+ private TabPage tabPage_backup;
+ private ClawdDotNet.UI.BackupPanel backupPanel;
}
}
diff --git a/frm_main.cs b/frm_main.cs
index 274a023..20d5980 100644
--- a/frm_main.cs
+++ b/frm_main.cs
@@ -32,6 +32,7 @@ public partial class frm_main : Form
private LiveLogViewerService? _logViewer;
private OpenRouterStatusService? _statusService;
private JobHistoryService? _jobHistoryService;
+ private BackupScheduler? _backupScheduler;
// ─── Chat ───
private WebViewBridge? _bridge;
@@ -107,6 +108,7 @@ public partial class frm_main : Form
SetupSettingsTab();
SetupAgentSettingsTab();
SetupWorkerTab();
+ SetupBackupTab();
SetupStatusStrip();
SetupBuildInfo();
WireEvents();
@@ -121,6 +123,28 @@ public partial class frm_main : Form
Text = $"ClawdDotNet - {_instanceConfig.InstanceName}";
}
+ // ═══════════════════════════════════════════════════
+ // BACKUP TAB
+ // ═══════════════════════════════════════════════════
+
+ private void SetupBackupTab()
+ {
+ backupPanel.Initialize(
+ _instancePath,
+ _instanceConfig.InstanceName,
+ _settingsManager,
+ _loggerFactory.CreateLogger("ClawdDotNet.Backup"));
+
+ _backupScheduler = new BackupScheduler(
+ _instancePath,
+ _instanceConfig.InstanceName,
+ _settingsManager,
+ _loggerFactory.CreateLogger("ClawdDotNet.Backup.Scheduler"));
+
+ _backupScheduler.OnBackupCreated += _ => BeginInvoke(() => backupPanel.RefreshFromOutside());
+ _backupScheduler.Start();
+ }
+
///
/// Verdrahtet alle Designer-Controls mit ihren Event-Handlern.
/// Die Controls selbst werden im Designer erstellt.
@@ -1513,6 +1537,7 @@ public partial class frm_main : Form
_bridge?.Dispose();
_logViewer?.Dispose();
_statusService?.Dispose();
+ _backupScheduler?.Dispose();
_logger.LogInformation("MainForm wird geschlossen");
}