Instanzmanager Stufe 1: Protokolle und Sicherungen je Instanz (#528)
Mehrere Instanzen sollen unabhaengig voneinander laufen. Heute tun sie das nicht: Sie teilen sich Protokollverzeichnis, Sicherungsordner und Sicherungszeitplan. In einer gemeinsamen Startup.log standen abwechselnd Zeilen zweier Instanzen, ohne Kennzeichnung, welche gerade spricht -- im Mehrinstanzbetrieb ist damit jede Fehlersuche Ratearbeit. Genau in dieser Lage ist der Aufhaenger aus #529 entstanden. Fuenf Schluessel wandern aus AppSettings nach InstanceSettings.json: logDirectory, backupDirectory, backupKeepCount, autoBackupEnabled, autoBackupTime. Leer heisst jetzt <Instanz>/Logs beziehungsweise <Instanz>/Backups; ein relativer Wert zaehlt weiterhin gegen das Instanzverzeichnis, nicht gegen das Arbeitsverzeichnis des Prozesses (#375 bleibt gewahrt). autoBackupEnabled und autoBackupTime standen in der Planung nicht, sind aber mitgegangen: Ein Schalter, der die taegliche Sicherung fuer eine Instanz einschaltet und sie damit fuer alle einschaltet, ist derselbe Fehler wie ein Ordner fuer alle -- und zwei von vier Sicherungsfeldern zu verschieben hinterliesse eine Einstellungsseite, auf der die Haelfte anwendungsweit gilt. Keine Uebernahme der Altwerte, aber eine Ansage: SettingsManager. AbandonedKeys meldet die aufgegebenen Schluessel, solange sie noch in Settings.json stehen, und der Start protokolliert einmal, was nicht mehr gilt und wohin diese Instanz stattdessen schreibt. Ein anwendungsweiter Pfad, in alle Instanzen kopiert, waere wieder ein Ordner fuer alle. webServerPort bleibt vorerst stehen. Es entfaellt mit Stufe 2, sobald Kestrel auf dem Socket hoert; vorher wegzunehmen hiesse, die Weboberflaeche ohne Ersatz abzuschalten. Korrektur an der eigenen Befundlage: "Instanz A wirft die Sicherungen von Instanz B weg" war zu breit. Beide Rotationen filtern bereits nach dem Praefix backup_<Instanzname>_. Der reale Defekt ist enger, und ein Praefix ist keine Grenze: backup_Test_2_*.zip faengt mit backup_Test_ an, also zaehlt Test im gemeinsamen Ordner die Archive von Test_2 mit. Belegt in BackupSchedulerTests, das den alten Zustand eigens wiederherstellt. Nachgeprueft an der laufenden Installation, nicht nur im Test: Nach dem Umbau schreibt Instance-Test nach Instances/Instance-Test/Logs, das alte gemeinsame Verzeichnis bekommt keine Zeile mehr, und die Warnung zu den aufgegebenen Schluesseln erscheint genau einmal. Enthaelt ausserdem, was seit der Weboberflaeche liegengeblieben war: - #526: Instanzgeheimnisse wurden beim Speichern verschluesselt und beim Laden nie entschluesselt. LoadInstanceConfig hatte kein ConfigSecrets.Unprotect; ConfigLoader.LoadAsync macht es richtig, wird zur Laufzeit aber gar nicht aufgerufen. Der OpenRouter-Schluessel ging so verschluesselt an die API. - Ein offener Ereignisstrom hielt das Beenden fuenf Sekunden auf. Die Stroeme hingen nur an RequestAborted, das beim geordneten Herunterfahren nicht feuert; jetzt zusaetzlich an ApplicationStopping. Der Fang in WebUiHost gilt dazu ohne Bedingung auf RequestAborted -- mit ihr waere jeder offene Strom beim Beenden als Fehler im Bugtracker gelandet. - Nach der Anmeldung blieb die Weboberflaeche leer: #anmeldung { display:flex } schlaegt als ID-Selektor das hidden-Attribut. - docs/Instanzmanager-Konzept.md: Befund, die Entscheidungen E-M1 bis E-M6, der Schnitt zwischen Manager und Instanz und die sechs Stufen. Build ohne Warnungen, 709 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Threading;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.App.Settings;
|
||||
using ClawdDotNet.App.Services;
|
||||
using ClawdDotNet.Core.Backup;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -29,7 +30,14 @@ public sealed partial class BackupPageViewModel : PageViewModel
|
||||
{
|
||||
private readonly BackupService _service = new();
|
||||
private readonly AppHost? _host;
|
||||
private readonly SettingsManager? _settingsManager;
|
||||
|
||||
/// <summary>
|
||||
/// Schalter, Uhrzeit, Zielordner und Aufbewahrung stehen seit #528 (Stufe 1) hier
|
||||
/// und nicht mehr in den Anwendungseinstellungen. Dort galten sie fuer jede Instanz:
|
||||
/// Wer die taegliche Sicherung fuer eine einschaltete, schaltete sie fuer alle ein.
|
||||
/// </summary>
|
||||
private readonly InstanceConfig? _instance;
|
||||
|
||||
private readonly ILogger? _logger;
|
||||
private readonly string _instanceDir;
|
||||
private readonly string _instanceName;
|
||||
@@ -88,7 +96,7 @@ public sealed partial class BackupPageViewModel : PageViewModel
|
||||
{
|
||||
_instanceDir = host.InstancePath;
|
||||
_instanceName = host.Instance.InstanceName;
|
||||
_settingsManager = host.Settings;
|
||||
_instance = host.Instance;
|
||||
_logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Backup");
|
||||
|
||||
LoadSettings();
|
||||
@@ -108,17 +116,19 @@ public sealed partial class BackupPageViewModel : PageViewModel
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
if (_settingsManager is null) return;
|
||||
if (_host is null || _instance is null) return;
|
||||
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var settings = _settingsManager.AppSettings;
|
||||
TargetFolder = Path.GetFullPath(settings.BackupDirectory);
|
||||
AutoBackupEnabled = settings.AutoBackupEnabled;
|
||||
KeepCount = Math.Clamp(settings.BackupKeepCount, 0, 999);
|
||||
// Aufgeloest und nicht roh angezeigt: In der Datei steht in aller Regel
|
||||
// nichts, und ein leeres Feld beantwortet die Frage nicht, wohin gesichert
|
||||
// wird.
|
||||
TargetFolder = AppHost.ResolveBackupDirectory(_instance, _instanceDir);
|
||||
AutoBackupEnabled = _instance.AutoBackupEnabled;
|
||||
KeepCount = Math.Clamp(_instance.BackupKeepCount, 0, 999);
|
||||
|
||||
AutoBackupTime = TimeSpan.TryParse(settings.AutoBackupTime, out var time)
|
||||
AutoBackupTime = TimeSpan.TryParse(_instance.AutoBackupTime, out var time)
|
||||
? time.ToString(@"hh\:mm")
|
||||
: "03:00";
|
||||
}
|
||||
@@ -130,15 +140,23 @@ public sealed partial class BackupPageViewModel : PageViewModel
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
if (_loading || _settingsManager is null) return;
|
||||
if (_loading || _host is null || _instance is null) return;
|
||||
|
||||
var settings = _settingsManager.AppSettings;
|
||||
settings.BackupDirectory = TargetFolder.Trim();
|
||||
settings.AutoBackupEnabled = AutoBackupEnabled;
|
||||
settings.AutoBackupTime = AutoBackupTime.Trim();
|
||||
settings.BackupKeepCount = KeepCount;
|
||||
var folder = TargetFolder.Trim();
|
||||
|
||||
_settingsManager.Save();
|
||||
// Der Vorgabeort bleibt als Leereintrag stehen. Sonst schriebe die Ansicht den
|
||||
// aufgeloesten Pfad fest, und eine umgezogene Instanz sicherte weiter dorthin,
|
||||
// wo sie einmal lag.
|
||||
var vorgabe = AppHost.ResolveBackupDirectory(new InstanceConfig(), _instanceDir);
|
||||
|
||||
_instance.BackupDirectory =
|
||||
string.Equals(folder, vorgabe, PathBoundary.Comparison) ? "" : folder;
|
||||
|
||||
_instance.AutoBackupEnabled = AutoBackupEnabled;
|
||||
_instance.AutoBackupTime = AutoBackupTime.Trim();
|
||||
_instance.BackupKeepCount = KeepCount;
|
||||
|
||||
_host.SaveInstance(new InstanceChange(InstanceChangeKind.Settings));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.App.Settings;
|
||||
using ClawdDotNet.App.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -14,10 +14,10 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
private readonly AppHost? _host;
|
||||
private readonly ILogger? _logger;
|
||||
|
||||
// ─── Allgemein & Pfade ───
|
||||
[ObservableProperty]
|
||||
private string _logDirectory = "";
|
||||
/// <summary>Womit die Seite geladen wurde — nur, um eine Änderung zu erkennen.</summary>
|
||||
private string _logDirectoryAtLoad = "";
|
||||
|
||||
// ─── Allgemein & Pfade ───
|
||||
[ObservableProperty]
|
||||
private string _instancesDirectory = "";
|
||||
|
||||
@@ -70,6 +70,20 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
[ObservableProperty]
|
||||
private string _instanceName = "";
|
||||
|
||||
/// <summary>
|
||||
/// Wohin diese Instanz protokolliert. Steht seit #528 (Stufe 1) hier und nicht mehr
|
||||
/// bei den Anwendungseinstellungen: Ein Verzeichnis für alle hiess, dass in
|
||||
/// Startup.log abwechselnd zwei Instanzen schrieben, ohne Kennzeichnung, welche
|
||||
/// gerade spricht.
|
||||
///
|
||||
/// <para>Leer angezeigt heisst leer gemeint — die Vorgabe steht als Hinweis unter
|
||||
/// dem Feld. Anders als bei der Sicherungsseite wird hier bewusst nicht aufgeloest:
|
||||
/// Der geltende Ort steht auf der Info-Seite, und ein aufgeloester Pfad im Feld
|
||||
/// wuerde beim naechsten Speichern festgeschrieben.</para>
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _logDirectory = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _openRouterApiKey = "";
|
||||
|
||||
@@ -109,7 +123,6 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
if (_host is null) return;
|
||||
|
||||
var s = _host.Settings.AppSettings;
|
||||
LogDirectory = s.LogDirectory;
|
||||
InstancesDirectory = s.InstancesDirectory;
|
||||
MinimumLogLevel = s.MinimumLogLevel;
|
||||
MaxLogLinesInUi = s.MaxLogLinesInUi;
|
||||
@@ -126,6 +139,8 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
|
||||
var inst = _host.Instance;
|
||||
InstanceName = inst.InstanceName;
|
||||
LogDirectory = inst.LogDirectory;
|
||||
_logDirectoryAtLoad = inst.LogDirectory;
|
||||
OpenRouterApiKey = inst.OpenRouterApiKey;
|
||||
InstanceDailyCostUsd = inst.Budget.DailyCostUsd;
|
||||
InstanceDailyTokens = inst.Budget.DailyTokens;
|
||||
@@ -137,7 +152,6 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
if (_host is null) return;
|
||||
|
||||
var s = _host.Settings.AppSettings;
|
||||
s.LogDirectory = LogDirectory.Trim();
|
||||
s.InstancesDirectory = InstancesDirectory.Trim();
|
||||
s.MinimumLogLevel = MinimumLogLevel;
|
||||
s.MaxLogLinesInUi = MaxLogLinesInUi;
|
||||
@@ -156,13 +170,21 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
|
||||
var inst = _host.Instance;
|
||||
inst.InstanceName = InstanceName.Trim();
|
||||
inst.LogDirectory = LogDirectory.Trim();
|
||||
inst.OpenRouterApiKey = OpenRouterApiKey.Trim();
|
||||
inst.Budget.DailyCostUsd = InstanceDailyCostUsd;
|
||||
inst.Budget.DailyTokens = InstanceDailyTokens;
|
||||
|
||||
_host.Directories.SaveInstanceConfig(_host.InstancePath, inst);
|
||||
_host.SaveInstance(new InstanceChange(InstanceChangeKind.Settings));
|
||||
|
||||
_logger?.LogInformation("Einstellungen und Instanz-Konfiguration gespeichert.");
|
||||
StatusText = "Einstellungen erfolgreich gespeichert.";
|
||||
|
||||
// Das Protokoll-Verzeichnis greift erst beim naechsten Start: Die
|
||||
// Protokollierung steht seit dem Aufbau des AppHost und schreibt bereits.
|
||||
StatusText = string.Equals(inst.LogDirectory, _logDirectoryAtLoad, StringComparison.Ordinal)
|
||||
? "Einstellungen erfolgreich gespeichert."
|
||||
: "Gespeichert. Das Protokoll-Verzeichnis gilt ab dem nächsten Start.";
|
||||
|
||||
_logDirectoryAtLoad = inst.LogDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user