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"); }