UI-Entwurf umgesetzt, Rocket.Chat-Tool, Deploymentcenter 2.4

Sicherungspunkt vor dem Aufraeumen. Buendelt die Arbeit, die seit dem
Abschluss der Avalonia-Portierung im Arbeitsverzeichnis lag.

Oberflaeche
- Entwurf aus Mockup/ umgesetzt: Theme.axaml (Farben je Thema, Barlow als
  mitgelieferte Schrift), Icons.axaml (Symbolgeometrien), Shell.axaml
  (eigene ControlThemes statt Fluent umzufaerben).
- Neue Steuerelemente StrokeIcon und BlueprintFrame, Seiten fuer
  Token-Verbrauch und Agenten-Chats, Werkzeug-Einstellungen als Seite
  statt eigenem Fenster, Texteditor-Fenster.
- ThemeManager mit hellem und dunklem Thema; die beiden Pinsel-Konverter
  entfallen, weil ein fester Farbwert den Themenwechsel nicht ueberlebt.

Rocket.Chat
- Neues Tool-Projekt (Client, Konfiguration, Workspace-Dateien) nach der
  Bauform des Telegram-Tools: rocketchat_poll als Tool-Job, geweckt wird
  nur, wenn wirklich etwas anliegt.
- send_file ist freigabepflichtig, send_message bewusst nicht: Der Raum
  ist Arbeitsraum, der Schutz sitzt an der Raum-Allowlist.
- Konzept-Doc um die Messung gegen die echte Instanz 8.7 ergaenzt; drei
  Annahmen waren falsch und sind korrigiert.

Deploymentcenter
- DC6 (Update anwenden) und DC7 (Erstinstallation ueber setup.json)
  erledigt, DC3 fuer win-x64/dev; deploy/publish.py als Release-Strecke.
- AppHost.DisposeAsync gegen doppeltes Herunterfahren gesperrt - sonst
  ueberschreibt eine zweite Abmeldung den Wartungszustand am Watchdog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 12:16:05 +02:00
co-authored by Claude Opus 5
parent b5bf97ae74
commit 33d95a6c3f
88 changed files with 11665 additions and 1029 deletions
@@ -0,0 +1,24 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
namespace ClawdDotNet.Desktop.Controls;
/// <summary>
/// Der Rahmen mit den vier Eckmarken — das wiederkehrende Motiv des Entwurfs.
///
/// <para>Eigenes Steuerelement statt vier von Hand gesetzter Striche je Stelle: Die Marken
/// liegen bewusst <em>halb außerhalb</em> des Rahmens, und das ist genau die Sorte Detail,
/// die beim Nachbauen an der fünften Stelle nicht mehr stimmt.</para>
///
/// <para>Weil die Marken über den Rand hinausragen, darf kein Elternteil zuschneiden.
/// In der Praxis heißt das: nicht direkt an den Fensterrand setzen, sondern mit etwas
/// Abstand — die Vorlage im Entwurf tut dasselbe.</para>
///
/// <para>Sparsam verwenden. Im Entwurf tragen nur die Info-Karte und die beiden Dialoge
/// die Marken; alle übrigen Karten sind schlichte Haarlinien-Kästen (Klasse
/// <c>card</c> in <c>Styles/Shell.axaml</c>).</para>
/// </summary>
public sealed class BlueprintFrame : ContentControl
{
protected override Type StyleKeyOverride => typeof(BlueprintFrame);
}
@@ -0,0 +1,113 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media;
namespace ClawdDotNet.Desktop.Controls;
/// <summary>
/// Ein Strichsymbol aus dem Entwurf — 24×24-Geometrie, gezeichnet mit der geerbten
/// Vordergrundfarbe.
///
/// <para>Warum nicht <c>PathIcon</c>: Das füllt seine Geometrie. Die Symbole des Entwurfs
/// sind Umrisse (Lucide, Strichstärke 1.5); gefüllt werden daraus schwarze Klumpen.</para>
///
/// <para>Warum nicht <c>Path</c> mit <c>Stroke</c>: Dann müsste an jeder einzelnen Stelle
/// die Farbe mitgegeben werden — und zwar die <em>richtige</em>, je nachdem ob das Symbol
/// in einer Schaltfläche, einer ausgewählten Navigationszeile oder der Statusleiste sitzt.
/// <see cref="Foreground"/> ist hier von <see cref="TextElement"/> übernommen und erbt
/// daher wie eine Textfarbe: Wer den Text einfärbt, färbt das Symbol mit.</para>
///
/// <para>Die Strichstärke wird beim Skalieren mitskaliert. Das ist keine Nachlässigkeit,
/// sondern das Verhalten von SVG: <c>stroke-width</c> zählt in Einheiten des viewBox.
/// Ein Symbol mit 1.5 in 24×24, dargestellt auf 16 px, hat genau 1 px starke Linien.</para>
/// </summary>
public sealed class StrokeIcon : Control
{
/// <summary>Kantenlänge des Koordinatenraums aller Symbole (SVG-viewBox 0 0 24 24).</summary>
private const double ViewBox = 24d;
public static readonly StyledProperty<Geometry?> DataProperty =
AvaloniaProperty.Register<StrokeIcon, Geometry?>(nameof(Data));
/// <summary>Geerbt wie eine Textfarbe — siehe Klassenbeschreibung.</summary>
public static readonly StyledProperty<IBrush?> ForegroundProperty =
TextElement.ForegroundProperty.AddOwner<StrokeIcon>();
public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<StrokeIcon, double>(nameof(StrokeThickness), 1.5d);
/// <summary>Für die wenigen massiven Symbole des Entwurfs (Wiedergabe, Halt).</summary>
public static readonly StyledProperty<bool> IsFilledProperty =
AvaloniaProperty.Register<StrokeIcon, bool>(nameof(IsFilled));
static StrokeIcon()
{
AffectsRender<StrokeIcon>(
DataProperty, ForegroundProperty, StrokeThicknessProperty, IsFilledProperty);
AffectsMeasure<StrokeIcon>(DataProperty);
}
public Geometry? Data
{
get => GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
public IBrush? Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public double StrokeThickness
{
get => GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
public bool IsFilled
{
get => GetValue(IsFilledProperty);
set => SetValue(IsFilledProperty, value);
}
/// <summary>
/// Ohne gesetzte Größe 16×16 — die Größe, in der die meisten Symbole im Entwurf stehen.
/// Ein unbegrenzter Vorschlag würde das Symbol den ganzen Platz nehmen lassen.
/// </summary>
protected override Size MeasureOverride(Size availableSize)
{
var width = double.IsInfinity(availableSize.Width) ? 16d : Math.Min(16d, availableSize.Width);
var height = double.IsInfinity(availableSize.Height) ? 16d : Math.Min(16d, availableSize.Height);
return new Size(width, height);
}
public override void Render(DrawingContext context)
{
if (Data is not { } geometry || Foreground is not { } brush) return;
var scale = Math.Min(Bounds.Width / ViewBox, Bounds.Height / ViewBox);
if (scale <= 0) return;
// Mittig setzen, falls das Steuerelement nicht quadratisch ist.
var offsetX = (Bounds.Width - ViewBox * scale) / 2;
var offsetY = (Bounds.Height - ViewBox * scale) / 2;
using var _ = context.PushTransform(
Matrix.CreateScale(scale, scale) * Matrix.CreateTranslation(offsetX, offsetY));
if (IsFilled)
{
context.DrawGeometry(brush, null, geometry);
return;
}
// Runde Enden und Verbindungen: Ohne sie verschwinden die Punkt-Symbole
// (Pfadstücke der Länge 0.01, etwa in „Agenten-Chats") vollständig.
var pen = new Pen(brush, StrokeThickness,
lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round);
context.DrawGeometry(null, pen, geometry);
}
}