Weboberflaeche (#531): X-Instanz an der einen fetch-Stelle, Instanzauswahl in der Kopfzeile, Verwaltungsansicht mit Anlegen/Starten/Anhalten/Fenster, Herkunft der Ereignisse, Sicherungs- und Einstellungsansicht. 402/409/502 werden eigen behandelt. Sicherungen im Manager (#530): Auflisten, Anlegen, Wiederherstellen, Loeschen. Das Zurueckspielen haelt die Instanz vorher an und startet sie danach wieder. Geordnetes Ende (Konzept 9.8). Zwei Befunde waren der Anlass: Der Manager konnte eine Instanz MIT FENSTER nie geordnet beenden. Control.ShutdownRequested war nur in HeadlessRun verdrahtet, nicht in der Avalonia-Anwendung - und ein Fenster ist die Vorgabe. Die Bitte wurde mit 202 beantwortet, niemand hoerte zu, und der Manager schoss den Prozess nach Ablauf seiner Frist jedes Mal ab. InstanceSupervisor.StopAsync hielt Socket-Schweigen fuer einen Todesnachweis und meldete "beendet", ohne den Prozess anzufassen. Bei einer verstummten Instanz ist der Socket von vornherein still - der Neustart haette einen zweiten Prozess auf dieselbe state.db gesetzt. Genau der Schaden, gegen den #528 gebaut ist. - ShutdownCoordinator: erst die Tuer schliessen, dann auf offene Arbeiten warten, Frist zwei Minuten. Vorher brach TaskScanner.DisposeAsync mitten in ProcessAsync ab, sodass die Aufgabe bis zum Lease-Ablauf auf "running" stehenblieb. - GET /api/instanz/puls hinter dem Socket-Gate: Phase und offene Arbeiten. Weiter gefragt statt gemeldet - ein Puls, den die Instanz von sich aus schickt, fehlt genau dann, wenn sie haengt. - Eskalationsleiter: drei Durchgaenge ohne Antwort, einmal um ein Ende bitten, nach 30s beenden und den Vollzug abwarten, gemeldet als Absturz. - EnsureDeadAsync vor jedem Start. Weiteres aus dem Review: - .gitignore verankert: "Instances/" passte auch auf src/ClawdDotNet.Core/Instances/. InstanceEndpoint.cs und InstanceLayout.cs waren dadurch nie eingecheckt. - Das WebUI-Token stand im Protokoll des Managers. Konsole ja, Datei nein. - Ein Klick auf "Instanz anlegen" loeste requestSubmit() zweimal aus. - [Obsolete] auf WebServerPort wieder entfernt: sechs Warnungen, die niemand abstellen konnte, solange E-M5 nicht fertig ist. - Die Wiederherstellung bricht nicht mehr ab, wenn der Browser weggeht. - Entwicklungspfade zum Instanzmanager aus Segmenten statt mit Rueckstrichen, und der Port kommt aus manager.json statt fest aus der Zeile. Offen und gemeldet: --managed hebelt die Lizenzpruefung aus (#535). 806 Tests gruen, 0 Warnungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,48 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private bool _confirmedClose;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Closing += OnClosing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schließt ohne Rückfrage — für eine Beendigung, die nicht von diesem Fenster ausgeht.
|
||||
///
|
||||
/// <para><b>Warum das nötig ist.</b> Der Bestätigungsdialog schützt vor dem
|
||||
/// versehentlichen Klick eines Menschen. Bittet dagegen der Instanzmanager über den
|
||||
/// Socket, steht niemand davor: Der Dialog bliebe offen, die zehn Sekunden liefen ab,
|
||||
/// und niemand klickte. Der Manager wartete seine Frist ab und schösse den Prozess
|
||||
/// dann ab — der Dialog, der ein hartes Ende verhindern soll, hätte es garantiert.</para>
|
||||
/// </summary>
|
||||
public void BeendenOhneRueckfrage()
|
||||
{
|
||||
_confirmedClose = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private async void OnClosing(object? sender, WindowClosingEventArgs e)
|
||||
{
|
||||
if (_confirmedClose) return;
|
||||
|
||||
// Schließen zunächst abbrechen, um Bestätigungsdialog anzuzeigen
|
||||
e.Cancel = true;
|
||||
|
||||
var confirmed = await ShowExitConfirmationAsync(this);
|
||||
if (confirmed)
|
||||
{
|
||||
_confirmedClose = true;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
@@ -21,4 +56,113 @@ public partial class MainWindow : Window
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
vm.RequestExit += Close;
|
||||
}
|
||||
|
||||
internal static async Task<bool> ShowExitConfirmationAsync(Window? owner)
|
||||
{
|
||||
var completion = new TaskCompletionSource<bool>();
|
||||
var remainingSeconds = 10;
|
||||
|
||||
var cancelBtn = new Button
|
||||
{
|
||||
Content = "Abbrechen (Instanz läuft weiter)",
|
||||
IsCancel = true
|
||||
};
|
||||
|
||||
var exitBtn = new Button
|
||||
{
|
||||
Content = $"Instanz beenden ({remainingSeconds}s)",
|
||||
IsEnabled = false
|
||||
};
|
||||
exitBtn.Classes.Add("danger");
|
||||
|
||||
var countdownText = new TextBlock
|
||||
{
|
||||
Text = $"Bestätigung gesperrt: Noch {remainingSeconds} Sekunden…",
|
||||
Classes = { "caption" }
|
||||
};
|
||||
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
timer.Tick += (_, _) =>
|
||||
{
|
||||
remainingSeconds--;
|
||||
if (remainingSeconds > 0)
|
||||
{
|
||||
countdownText.Text = $"Bestätigung gesperrt: Noch {remainingSeconds} Sekunden…";
|
||||
exitBtn.Content = $"Instanz beenden ({remainingSeconds}s)";
|
||||
}
|
||||
else
|
||||
{
|
||||
timer.Stop();
|
||||
exitBtn.IsEnabled = true;
|
||||
exitBtn.Content = "Instanz jetzt endgültig beenden";
|
||||
countdownText.Text = "Sperre aufgehoben — Bestätigung jetzt möglich.";
|
||||
}
|
||||
};
|
||||
|
||||
var window = new Window
|
||||
{
|
||||
Title = "ClawdDotNet – Instanz beenden",
|
||||
Width = 500,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
CanResize = false,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Icon = owner?.Icon,
|
||||
Content = new StackPanel
|
||||
{
|
||||
Margin = new Thickness(20),
|
||||
Spacing = 16,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = "Achtung: Produktivinstanz beenden",
|
||||
Classes = { "h1" }
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = "ClawdDotNet-Instanzen sind auf dauerhaften Dauerbetrieb ausgelegt. "
|
||||
+ "Das Beenden stoppt alle aktiven Agenten, Automationen, Web-Zugriffe und Hintergrundaufgaben dieser Instanz geordnet.",
|
||||
TextWrapping = Avalonia.Media.TextWrapping.Wrap
|
||||
},
|
||||
countdownText,
|
||||
new StackPanel
|
||||
{
|
||||
Orientation = Avalonia.Layout.Orientation.Horizontal,
|
||||
Spacing = 10,
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||
Children = { cancelBtn, exitBtn }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cancelBtn.Click += (_, _) =>
|
||||
{
|
||||
timer.Stop();
|
||||
completion.TrySetResult(false);
|
||||
window.Close();
|
||||
};
|
||||
|
||||
exitBtn.Click += (_, _) =>
|
||||
{
|
||||
timer.Stop();
|
||||
completion.TrySetResult(true);
|
||||
window.Close();
|
||||
};
|
||||
|
||||
window.Closed += (_, _) =>
|
||||
{
|
||||
timer.Stop();
|
||||
completion.TrySetResult(false);
|
||||
};
|
||||
|
||||
timer.Start();
|
||||
|
||||
if (owner is not null)
|
||||
await window.ShowDialog(owner);
|
||||
else
|
||||
window.Show();
|
||||
|
||||
return await completion.Task;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user