feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
# Linux-Portierung — Analyse
|
||||
|
||||
Stand: 2026-08-06. Reine Bestandsaufnahme und Aufwandsschätzung, **kein** Umbau.
|
||||
|
||||
Frage: Was ist nötig, damit ClawdDotNet unter Linux läuft, und was kostet das?
|
||||
|
||||
---
|
||||
|
||||
## 0. Kurzfassung
|
||||
|
||||
Die gute Nachricht zuerst: **Der Kern ist bereits portabel.** Alle 16 Bibliotheks-
|
||||
und beide Testprojekte zielen auf `net10.0` (nicht `net10.0-windows`), es gibt im
|
||||
gesamten Repository **kein einziges `DllImport`, keinen Registry-Zugriff und keine
|
||||
`System.Drawing`-Nutzung** in `src/`. Windows steckt an genau drei Stellen im Kern:
|
||||
DPAPI-Verschlüsselung, Groß-/Kleinschreibung bei Pfadvergleichen und die
|
||||
Zeitzonen-IDs.
|
||||
|
||||
Die schlechte Nachricht: Die gesamte Bedienoberfläche — rund **8.900 Zeilen** in
|
||||
`frm_*.cs`, `UI/`, `Models/` und `Services/` — hängt an Windows Forms, an WebView2
|
||||
und, am unangenehmsten, an vier `PropertyGrid`-Instanzen, die praktisch die
|
||||
komplette Einstellungsverwaltung ausmachen. Dafür gibt es in Avalonia keine
|
||||
Eins-zu-eins-Entsprechung.
|
||||
|
||||
**Empfehlung: den Umzug in zwei Schnitte teilen.** Ein kopfloser Host (ohne GUI)
|
||||
auf Linux ist in etwa **12–18 Personentagen** erreichbar und liefert den
|
||||
eigentlichen Nutzen — Agenten laufen auf einem Server, nicht auf einem
|
||||
Windows-Desktop. Die Avalonia-Oberfläche ist ein davon unabhängiges Vorhaben
|
||||
von **32–52 Personentagen**, das man danach in Ruhe angehen kann.
|
||||
|
||||
Gesamt für „alles auf Linux, mit GUI": **50–80 Personentage.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Bestandsaufnahme
|
||||
|
||||
### 1.1 Was bereits portabel ist
|
||||
|
||||
| Bereich | Zeilen | Zielframework | Windows-Abhängigkeit |
|
||||
|---|---:|---|---|
|
||||
| `src/ClawdDotNet.Core` | 8.959 | `net10.0` | nur DPAPI (1 Datei) |
|
||||
| 15 Tool-Projekte | 6.415 | `net10.0` | nur `.exe`-Pfade im SocialMediaManager |
|
||||
| `tests/` (348 Tests, 39 Dateien) | 6.888 | `net10.0` | 3 Testfälle mit `C:\`-Pfaden |
|
||||
|
||||
Alle NuGet-Pakete laufen unter Linux: `Microsoft.Data.Sqlite` (bringt
|
||||
`e_sqlite3` nativ für linux-x64/arm64 mit), `MySqlConnector`, `Npgsql`,
|
||||
`Microsoft.Data.SqlClient`, `MongoDB.Driver`, `MailKit`, `FluentFTP`,
|
||||
`Telegram.Bot`, `WTelegramClient`, `SharpCompress`, `Snappier`,
|
||||
`Microsoft.Extensions.Logging`. Kein Paket muss ersetzt werden — mit zwei
|
||||
Ausnahmen (siehe 2.1 und 2.3).
|
||||
|
||||
Auch die Dinge, bei denen man Ärger erwarten würde, sind sauber gelöst:
|
||||
|
||||
- [AtomicFile.cs:167](src/ClawdDotNet.Core/Storage/AtomicFile.cs:167) — `Commit`
|
||||
prüft `File.Exists` und weicht auf `File.Move` aus. `File.Replace` verlangt
|
||||
unter Unix ebenfalls eine vorhandene Zieldatei; der Fall ist also schon
|
||||
abgedeckt. Die Wiederholschleife ist unter Linux überflüssig, aber harmlos.
|
||||
- [TaskFrontmatter.cs:27](src/ClawdDotNet.Core/Tasks/TaskFrontmatter.cs:27) —
|
||||
normalisiert `\r\n` und `\r` vor dem Zerlegen. Task-Dateien von einem
|
||||
Windows-Rechner werden unter Linux korrekt gelesen.
|
||||
- Textdateien werden durchgängig als **UTF-8 ohne BOM** geschrieben
|
||||
(`AtomicFile`, `FileLogWriter`, `AgentEditorTool`). Kein `Encoding.Default`,
|
||||
keine Codepage-Fallen.
|
||||
- Zeitstempel gehen als `DateTime.UtcNow` in die Datenbank und werden mit
|
||||
`DateTimeStyles.RoundtripKind` gelesen.
|
||||
|
||||
### 1.2 Was am Windows-Teil hängt
|
||||
|
||||
| Bereich | Zeilen | davon Designer |
|
||||
|---|---:|---:|
|
||||
| `frm_*.cs` (6 Formulare + Dialoge) | 4.946 | 1.865 |
|
||||
| `UI/` (BackupPanel, WebViewBridge, EmbeddedUiManager) | 1.087 | 428 |
|
||||
| `Models/` (PropertyGrid-ViewModels) | 1.257 | — |
|
||||
| `Services/` (4 Dienste, an WinForms-Timer gekoppelt) | 1.501 | — |
|
||||
| `Properties/` | 123 | — |
|
||||
| **Summe** | **8.914** | **2.293** |
|
||||
|
||||
Dazu drei `.resx`-Dateien à ~272 KB (eingebettete Symbole/Bilder) und eine
|
||||
`frm_main.en.resx` für die englische Lokalisierung über den
|
||||
WinForms-Resx-Mechanismus.
|
||||
|
||||
Steuerelement-Inventar aus den Designer-Dateien: 24 `Label`, 19
|
||||
`ToolStripButton`, 12 `TabPage`, 12 `Button`, 9 `TextBox`, 6 `DataGridView`, 6
|
||||
`ToolStrip`, 5 `ComboBox`, **4 `PropertyGrid`**, 4 `TableLayoutPanel`, 4
|
||||
`FlowLayoutPanel`, 3 `TabControl`, 3 `SplitContainer`, 1 `RichTextBox`, 1
|
||||
`ListView`, 1 `NotifyIcon`, 1 `DateTimePicker`, 1 `NumericUpDown`.
|
||||
|
||||
Tabs in `frm_main`: Chat, Logs, Settings (mit Unter-Tabs App-Settings,
|
||||
Instance-Settings), Agent Settings, Jobs/Services (mit Unter-Tabs Jobs,
|
||||
Services, Job History), Info, Backup.
|
||||
|
||||
---
|
||||
|
||||
## 2. Die harten Brocken
|
||||
|
||||
### 2.1 WebView2 → kein Linux (Chat- und Übersichts-Ansicht)
|
||||
|
||||
`Microsoft.Web.WebView2` ist die einzige Windows-only-Paketabhängigkeit des
|
||||
Hauptprojekts und trägt die zwei sichtbarsten Ansichten:
|
||||
[frm_main.cs:235](frm_main.cs:235) und [frm_chat.cs:44](frm_chat.cs:44) laden
|
||||
`overview.html` bzw. `chat.html` aus `EmbeddedUI/` über
|
||||
`SetVirtualHostNameToFolderMapping` unter `https://ui.clwd.internal/`. Die
|
||||
Kommunikation läuft über [WebViewBridge.cs](UI/WebViewBridge.cs) —
|
||||
`WebMessageReceived` in die eine, `ExecuteScriptAsync` in die andere Richtung.
|
||||
|
||||
Drei Wege, jeder mit einem eigenen Preis:
|
||||
|
||||
| Variante | Was passiert | Aufwand | Risiko |
|
||||
|---|---|---:|---|
|
||||
| **A — Avalonia.WebView** | HTML/JS bleiben. Unter Linux rendert WebKitGTK, unter Windows weiterhin WebView2. Die Bridge wird auf die Abstraktion der Bibliothek umgeschrieben. | 4–6 PT | Bibliothek ist deutlich weniger reif als WebView2; WebKitGTK-Abhängigkeit muss auf dem Zielserver vorhanden sein; Verhalten unterscheidet sich je Plattform. |
|
||||
| **B — nativ neu in Avalonia** | Chat als echte Avalonia-Ansicht mit `ItemsControl` und einem Markdown-Renderer. `EmbeddedUI/` entfällt. | 8–12 PT | Kein Fremdrisiko, aber Neuentwicklung. Am Ende deutlich wartbarer als HTML-in-Container. |
|
||||
| **C — lokaler HTTP-Server + Systembrowser** | Die App liefert `EmbeddedUI/` über `http://localhost:port` aus, der Nutzer öffnet den Browser. | 3–4 PT | Bricht die Ein-Fenster-Anmutung. Passt aber ausgezeichnet zum kopflosen Betrieb — dort **ist** der Browser die Oberfläche. |
|
||||
|
||||
**Empfehlung:** C für den kopflosen Host (fällt dort ohnehin an), B für die
|
||||
Desktop-Oberfläche. Variante A koppelt uns an eine Bibliothek, die weniger stabil
|
||||
ist als alles andere im Projekt.
|
||||
|
||||
### 2.2 PropertyGrid → es gibt keinen Ersatz von der Stange
|
||||
|
||||
Vier `PropertyGrid`-Instanzen in [frm_main.Designer.cs](frm_main.Designer.cs)
|
||||
bilden App-Settings, Instance-Settings, Agent-Settings und Tool-Settings ab. Sie
|
||||
werden vollständig durch Attribute gesteuert — **246 `[Category]`,
|
||||
`[DisplayName]`, `[Description]`-Angaben** verteilt auf vier Dateien:
|
||||
|
||||
- [Models/ToolSettingsViewModels.cs](Models/ToolSettingsViewModels.cs) — 108
|
||||
- [Models/AgentSettingsViewModel.cs](Models/AgentSettingsViewModel.cs) — 54
|
||||
- [Models/AppSettings.cs](Models/AppSettings.cs) — 51
|
||||
- [Models/InstanceSettingsViewModel.cs](Models/InstanceSettingsViewModel.cs) — 33
|
||||
|
||||
Dazu kommen `[TypeConverter(typeof(ExpandableObjectConverter))]` für
|
||||
verschachtelte Objekte, `[PasswordPropertyText(true)]` für Geheimnisse und ein
|
||||
eigener [ModelTypeConverter](Models/ModelTypeConverter.cs), der das
|
||||
Modell-Auswahlfeld dynamisch aus der OpenRouter-Modellliste füllt.
|
||||
|
||||
Avalonia hat kein `PropertyGrid`. Zwei Möglichkeiten:
|
||||
|
||||
1. **`Avalonia.PropertyGrid`** (Community, MIT). Versteht `Category`,
|
||||
`DisplayName`, `Description`, `Browsable`, `ReadOnly` und
|
||||
`ExpandableObjectConverter`. Die ViewModels und ihre Attribute könnten
|
||||
weitgehend unverändert bleiben — das spart am meisten. Zu prüfen ist, ob der
|
||||
dynamische `ModelTypeConverter` mit `GetStandardValues` unterstützt wird; das
|
||||
ist der Punkt, an dem so etwas erfahrungsgemäß hakt. **Aufwand 6–8 PT**, plus
|
||||
dauerhafte Abhängigkeit an ein Ein-Personen-Projekt.
|
||||
2. **Von Hand gebaute Einstellungsformulare.** Mehr Arbeit, aber wir bekommen
|
||||
eine Oberfläche, die man Nutzern zumuten kann — das `PropertyGrid` ist
|
||||
ehrlicherweise eine Entwickleransicht. Passwörter, Verzeichnisauswahl,
|
||||
Validierung und die Modell-Auswahl werden dabei richtig statt behelfsmäßig.
|
||||
**Aufwand 10–14 PT.**
|
||||
|
||||
Das ist der größte Einzelposten der GUI-Portierung. Die Entscheidung kann und
|
||||
sollte man verschieben, bis das Grundgerüst steht.
|
||||
|
||||
### 2.3 DPAPI → Geheimnisse liegen unter Linux im Klartext
|
||||
|
||||
[SecretProtector.cs:42](src/ClawdDotNet.Core/Security/SecretProtector.cs:42):
|
||||
|
||||
```csharp
|
||||
if (!OperatingSystem.IsWindows())
|
||||
return plainText;
|
||||
```
|
||||
|
||||
Unter Linux verschlüsselt `Protect` **stillschweigend nicht**. OpenRouter-Key,
|
||||
Datenbank-Verbindungszeichenfolgen mit Passwort, Mail-Zugangsdaten und das
|
||||
Telegram-2FA-Passwort lägen im Klartext in `InstanceConfig.json` und
|
||||
`AgentSettings.json` — genau der Zustand, den S7 behoben hat. Auf einem Server,
|
||||
der per SSH erreichbar ist und gesichert wird, ist das schlechter als auf einem
|
||||
Einzelplatz-Windows.
|
||||
|
||||
Dasselbe gilt für den Lizenz-Zustandsspeicher:
|
||||
`LicenseLabrador/client-dotnet/.../StateStore.cs:92` schützt seine Datei ebenfalls
|
||||
nur unter Windows per DPAPI.
|
||||
|
||||
Zu klären ist also ein plattformübergreifendes Verfahren. Realistisch:
|
||||
|
||||
- **AES-GCM mit Schlüssel aus einer Datei mit `0600`** neben der Konfiguration
|
||||
(Linux) bzw. weiterhin DPAPI (Windows). Einfach, wirkt gegen versehentliche
|
||||
Weitergabe und Backups, nicht gegen einen Angreifer mit demselben Benutzer —
|
||||
dieselbe Schutzstufe wie DPAPI heute.
|
||||
- Optional zusätzlich `libsecret`/Schlüsselbund, wenn eine Desktop-Sitzung da
|
||||
ist. Auf einem Server gibt es die nicht, also braucht es den Dateiweg ohnehin.
|
||||
|
||||
Nebenwirkung, die man einplanen muss: **Konfigurationen sind nicht mehr zwischen
|
||||
Betriebssystemen austauschbar.** Ein `enc:v1:`-Wert von Windows ist unter Linux
|
||||
nicht lesbar und umgekehrt. `Unprotect` wirft dann korrekterweise eine
|
||||
`SecretProtectionException` ([SecretProtector.cs:81](src/ClawdDotNet.Core/Security/SecretProtector.cs:81)) —
|
||||
für den Umzug einer Instanz braucht es einen Migrationsweg (Präfix `enc:v2:`,
|
||||
Werte neu eintragen oder ein Export/Import-Kommando).
|
||||
|
||||
**Aufwand 3–5 PT** inklusive Tests und Migration.
|
||||
|
||||
### 2.4 Zeitzonen → das ist die stillste Fehlerquelle
|
||||
|
||||
[TaskSchedule.cs:161](src/ClawdDotNet.Core/Tasks/TaskSchedule.cs:161):
|
||||
|
||||
```csharp
|
||||
try { return TimeZoneInfo.FindSystemTimeZoneById(id); }
|
||||
catch { return TimeZoneInfo.Utc; }
|
||||
```
|
||||
|
||||
Und [SchedulerTaskMigration.cs:32](src/ClawdDotNet.Core/Tasks/SchedulerTaskMigration.cs:32)
|
||||
schreibt `TimeZoneInfo.Local.Id` in die Task-Frontmatter. Auf dem
|
||||
Entwicklungsrechner ergibt das `"W. Europe Standard Time"`, unter Linux
|
||||
`"Europe/Berlin"`.
|
||||
|
||||
Task-Dateien sind Markdown im `SharedWorkspace` und wandern zwischen Rechnern.
|
||||
Trifft eine Windows-ID auf ein System ohne die Umsetzungsdaten, greift das
|
||||
`catch` — und der Task läuft ab sofort nach **UTC statt Ortszeit**, also im
|
||||
Sommer zwei Stunden zu früh. Ohne Fehlermeldung, ohne Logeintrag. Ein Task, der
|
||||
um 08:00 die Marktübersicht holen soll, läuft um 06:00.
|
||||
|
||||
.NET 6+ kann Windows-IDs unter Linux über ICU auflösen, aber nur wenn ICU
|
||||
vorhanden ist. In einem schlanken Container (Alpine ohne `icu-libs`, distroless)
|
||||
oder bei `InvariantGlobalization=true` ist es das nicht — dann schlägt jede
|
||||
Auflösung fehl und alles fällt auf UTC.
|
||||
|
||||
Was zu tun ist:
|
||||
|
||||
- Beim Schreiben auf **IANA normalisieren**
|
||||
(`TimeZoneInfo.TryConvertWindowsIdToIanaId`), beim Lesen beide Formen
|
||||
akzeptieren.
|
||||
- Das `catch` **nicht mehr still schlucken** — eine unbekannte Zeitzone muss
|
||||
protokolliert werden, besser noch den Task als fehlerhaft markieren.
|
||||
- Das Zielsystem muss `tzdata` haben. Für Container explizit installieren.
|
||||
|
||||
Verwandt: **82 Vorkommen von `DateTime.Now`/`UtcNow`**. Die meisten sind
|
||||
unkritisch, zwei fallen auf:
|
||||
[TaskboardService.cs:80](src/ClawdDotNet.Core/Tasks/TaskboardService.cs:80)
|
||||
schreibt `DateTime.Now`-Zeitstempel in Task-Dateien, und
|
||||
[LiveLogViewerService.cs:98](Services/LiveLogViewerService.cs:98) sucht die
|
||||
Logdatei des Tages über `DateTime.Now`. Server laufen üblicherweise mit `TZ=UTC`
|
||||
— dort wechselt die Logdatei dann um 02:00 Ortszeit statt um Mitternacht, und
|
||||
Task-Zeitstempel bekommen eine andere Bedeutung als bisher. Kein Fehler, aber
|
||||
eine Verhaltensänderung, die man kennen sollte.
|
||||
|
||||
**Aufwand 2–3 PT.**
|
||||
|
||||
### 2.5 Groß-/Kleinschreibung bei Pfaden → sicherheitsrelevant
|
||||
|
||||
Linux-Dateisysteme unterscheiden Groß- und Kleinschreibung, Windows nicht. An
|
||||
vier Stellen wird das Gegenteil angenommen — und drei davon bewachen eine
|
||||
Sandbox-Grenze:
|
||||
|
||||
- [WorkspacePath.cs:72](src/ClawdDotNet.Tools.FileRW/WorkspacePath.cs:72) —
|
||||
`normalizedCandidate.StartsWith(normalizedRoot, OrdinalIgnoreCase)`. Das ist
|
||||
die Prüfung, die Agenten daran hindert, aus ihrem Arbeitsverzeichnis
|
||||
auszubrechen.
|
||||
- [FileRWTool.cs:174](src/ClawdDotNet.Tools.FileRW/FileRWTool.cs:174) —
|
||||
Abgleich gegen die Liste geschützter Pfade.
|
||||
- [FtpTool.cs:155](src/ClawdDotNet.Tools.FTP/FtpTool.cs:155) — dieselbe
|
||||
Einschließungsprüfung.
|
||||
- [BackupService.cs:384](src/ClawdDotNet.Core/Backup/BackupService.cs:384).
|
||||
|
||||
Unter Linux sind `/home/x/Workspace` und `/home/x/workspace` **zwei
|
||||
verschiedene Verzeichnisse**. Der Vergleich mit `OrdinalIgnoreCase` würde einen
|
||||
Pfad im zweiten als „innerhalb" des ersten durchwinken. Genauso liefe die
|
||||
Sperrliste in `FileRWTool` ins Leere, sobald jemand die Schreibweise ändert.
|
||||
|
||||
Nötig ist ein Vergleichsverfahren, das die Plattform berücksichtigt — ein
|
||||
`PathComparer`, der unter Windows `OrdinalIgnoreCase` und unter Unix `Ordinal`
|
||||
verwendet, konsequent an allen vier Stellen.
|
||||
|
||||
Ebenfalls betroffen, aber harmlos:
|
||||
[AtomicFile.cs:35](src/ClawdDotNet.Core/Storage/AtomicFile.cs:35) schlüsselt
|
||||
seine Sperren mit `fullPath.ToLowerInvariant()`. Unter Linux teilen sich damit
|
||||
zwei verschiedene Dateien eine Sperre — das serialisiert zu viel, gefährdet aber
|
||||
nichts.
|
||||
|
||||
**Aufwand 2–3 PT**, davon der größere Teil Tests.
|
||||
|
||||
### 2.6 Prozessaufrufe und `.exe`-Annahmen
|
||||
|
||||
- **`Process.Start("explorer.exe", …)`** — 4 Stellen
|
||||
([frm_main.cs:1552](frm_main.cs:1552), [frm_main.cs:1557](frm_main.cs:1557),
|
||||
[frm_main.cs:1562](frm_main.cs:1562), [BackupPanel.cs:338](UI/BackupPanel.cs:338)).
|
||||
Ersatz: `Process.Start(new ProcessStartInfo(path) { UseShellExecute = true })`
|
||||
bzw. `xdg-open`. Die Variante `explorer.exe /select,"…"` hat unter Linux kein
|
||||
Gegenstück — dort öffnet man nur den Ordner.
|
||||
- **`Microsoft.VisualBasic.Interaction.InputBox`** — 3 Stellen
|
||||
([Program.cs:280](Program.cs:280), [Program.cs:291](Program.cs:291),
|
||||
[frm_main.cs:658](frm_main.cs:658)), zwei davon für den interaktiven
|
||||
Telegram-Login (Code und 2FA-Passwort). Braucht einen eigenen Dialog. Für den
|
||||
kopflosen Betrieb ohnehin problematisch: **ein Login, der ein Eingabefenster
|
||||
öffnet, blockiert einen Dienst.** Dort muss der Telegram-Login anders gelöst
|
||||
werden (vorab per CLI, oder über die Weboberfläche).
|
||||
- **`yt-dlp.exe` / `ffmpeg.exe`** —
|
||||
[SocialMediaManagerTool.cs:782](src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs:782)
|
||||
und `:825`. Die PATH-Suche davor funktioniert unter Linux bereits; nur die
|
||||
Ausweichpfade sind fest auf `.exe` verdrahtet und laufen dort ins Leere.
|
||||
Kleine Änderung, aber sie fällt sonst erst zur Laufzeit auf.
|
||||
|
||||
**Aufwand zusammen 1–2 PT.**
|
||||
|
||||
### 2.7 WinForms-Timer in der Dienstschicht
|
||||
|
||||
`Services/` ist logisch kein UI-Code, hängt aber an
|
||||
`System.Windows.Forms.Timer`:
|
||||
|
||||
- [BackupScheduler.cs:41](Services/BackupScheduler.cs:41)
|
||||
- [LiveLogViewerService.cs:38](Services/LiveLogViewerService.cs:38) — schreibt
|
||||
zusätzlich direkt in eine `RichTextBox`
|
||||
- [OpenRouterStatusService.cs:43](Services/OpenRouterStatusService.cs:43)
|
||||
- [frm_main.License.cs:41](frm_main.License.cs:41)
|
||||
|
||||
Der Backup-Zeitplan und die Lizenzprüfung gehören in den kopflosen Host und
|
||||
müssen dafür auf `System.Threading.PeriodicTimer` umgestellt werden. Der
|
||||
Log-Betrachter ist echte Oberfläche und wird ohnehin neu gebaut.
|
||||
|
||||
**Aufwand 2–3 PT.**
|
||||
|
||||
### 2.8 Lizenzierung — erledigt (Stand 2026-08-06)
|
||||
|
||||
> **Nachtrag.** LicenseLabrador und WatchDog sind im **Deploymentcenter**
|
||||
> zusammengefasst, Hardware-ID v2 ist dort umgesetzt. Der Client
|
||||
> (`Deploymentcenter.Client`, `netstandard2.0;net8.0`) läuft auf beiden
|
||||
> Plattformen, die HW-ID ist container- und umbenennungsfest, der Ablageort
|
||||
> löst sich auch ohne `HOME` auf, und der Zustandsspeicher ist mit AES-GCM
|
||||
> plattformübergreifend verschlüsselt.
|
||||
>
|
||||
> **Damit ist die einzige potenziell blockierende Frage dieser Analyse
|
||||
> beantwortet.** Details und offene Punkte der Anbindung:
|
||||
> [Deploymentcenter-Anbindung-Review.md](Deploymentcenter-Anbindung-Review.md).
|
||||
|
||||
Es bleibt reine Anschlussarbeit in ClawdDotNet: Projektverweis umhängen,
|
||||
[LicenseGate](Services/LicenseGate.cs) gegen die neue Ergebnisklasse schreiben
|
||||
(`LicenseState` ist entfallen, `Status` ist jetzt eine Zeichenkette), `MessageBox`
|
||||
durch das mitgelieferte `ILicensePrompt` ersetzen und die Lizenzprüfung aus
|
||||
[Program.cs:112](Program.cs:112) fensterfrei machen.
|
||||
|
||||
**Aufwand 2–3 PT** (vorher 3–5).
|
||||
|
||||
---
|
||||
|
||||
## 3. Kleinere Punkte, die trotzdem beißen
|
||||
|
||||
### 3.1 Kultur- und Zahlenformatierung
|
||||
|
||||
Nur 16 Stellen im gesamten Projekt nennen eine Kultur explizit. Das heißt
|
||||
umgekehrt: fast alles formatiert mit `CurrentCulture`. Auf dem
|
||||
Entwicklungsrechner ist das `de-DE`, auf einem Server mit unbesetztem `LANG`
|
||||
ist es `InvariantCulture`. Aus `1,25` wird `1.25`.
|
||||
|
||||
Wo das folgenlos bleibt:
|
||||
- **JSON** — `System.Text.Json` schreibt Zahlen immer invariant. Alle
|
||||
Konfigurationen, Zustandsdateien und API-Aufrufe sind sicher.
|
||||
- **SQLite** — Werte gehen typisiert über Parameter, nicht als Text.
|
||||
|
||||
Wo hinzuschauen ist:
|
||||
- Zeichenkettenverkettung in Logeinträgen und Prompts (`$"{cost:F4}"`). Wenn
|
||||
eine Zahl mit deutschem Dezimalkomma in einen Prompt gerät, muss das Modell
|
||||
raten.
|
||||
- Anzeigewerte in der Oberfläche — dort ist Ortsformat gewünscht, aber es sollte
|
||||
bewusst gesetzt sein, nicht zufällig.
|
||||
|
||||
**Empfehlung:** einmal alle Formatierungen durchgehen und trennen — invariant
|
||||
für alles Maschinenlesbare, `CurrentCulture` nur für die Anzeige. Am besten mit
|
||||
einem Analyzer (`CA1305`, `CA1304`, `CA1310`) als Warnung im Build, damit es so
|
||||
bleibt.
|
||||
|
||||
**Aufwand 2–3 PT.**
|
||||
|
||||
### 3.2 Globalisierungsmodus festlegen
|
||||
|
||||
`InvariantGlobalization=true` macht das Publikat kleiner und ICU überflüssig —
|
||||
kostet aber `TimeZoneInfo.FindSystemTimeZoneById` (siehe 2.4), kulturabhängige
|
||||
Vergleiche und korrektes `ToLower()` für Umlaute. Für dieses Projekt mit
|
||||
zeitzonenabhängiger Planung ist das **keine Option**; die Entscheidung sollte im
|
||||
Projekt dokumentiert und ICU/tzdata als Voraussetzung festgehalten werden.
|
||||
|
||||
Nebenbemerkung: `COLLATE NOCASE` in
|
||||
[SqliteMemoryRepository.cs:143](src/ClawdDotNet.Core/Memory/SqliteMemoryRepository.cs:143)
|
||||
und [SqliteTaskRepository.cs:105](src/ClawdDotNet.Core/Tasks/SqliteTaskRepository.cs:105)
|
||||
ist ASCII-beschränkt — `Ä` und `ä` gelten SQLite als verschieden. Das ist heute
|
||||
schon so und ändert sich beim Umzug nicht, ist also kein Portierungsthema,
|
||||
sondern eine bestehende Eigenheit.
|
||||
|
||||
### 3.3 Zeilenenden
|
||||
|
||||
1.230 Stellen verwenden `Environment.NewLine` oder `\r\n`. Für Logdateien ist
|
||||
das egal. Bei **Task-Dateien** und Agenten-erzeugten Dateien im geteilten
|
||||
Arbeitsverzeichnis führt es zu Rauschen: Eine Datei, die unter Windows
|
||||
geschrieben und unter Linux angefasst wird, ändert komplett ihre Zeilenenden.
|
||||
Wenn der Arbeitsbereich unter Git liegt oder synchronisiert wird, sieht jede
|
||||
Änderung wie eine Vollumschreibung aus. Der Parser kommt damit klar (siehe 1.1)
|
||||
— es ist eine Frage der Ordnung, kein Fehler. Empfehlung: für Task- und
|
||||
Konfigurationsdateien fest `\n` schreiben.
|
||||
|
||||
### 3.4 Dateinamen
|
||||
|
||||
`Path.GetInvalidFileNameChars()` liefert unter Windows 41 Zeichen, unter Linux
|
||||
genau zwei (`\0` und `/`). [FileLogWriter.cs:95](src/ClawdDotNet.Core/Logging/FileLogWriter.cs:95)
|
||||
säubert Modulnamen damit — unter Linux entstehen also Dateinamen, die auf
|
||||
Windows nicht mehr lesbar sind. Betrifft Sicherungen, die zwischen Systemen
|
||||
wandern. Ebenso die Windows-Sonderfälle `CON`, `PRN`, `AUX` und Namen mit
|
||||
abschließendem Punkt: unter Linux erlaubt, beim Rückspielen auf Windows nicht.
|
||||
Für den Sicherungs-/Wiederherstellungsweg über Systemgrenzen hinweg relevant.
|
||||
|
||||
### 3.5 Ablageorte
|
||||
|
||||
[SettingsManager.cs:24](Services/SettingsManager.cs:24) legt `AppSettings.json`
|
||||
neben die Programmdatei (`AppDomain.CurrentDomain.BaseDirectory`). Unter Windows
|
||||
in einem Benutzerverzeichnis geht das; unter Linux liegt die Anwendung typisch
|
||||
in `/opt/…` oder `/usr/lib/…` und ist für den Dienstbenutzer **nicht
|
||||
schreibbar**. Dasselbe gilt für die Zielordner `tools/`, `Logs/` und
|
||||
`Instances/`, die die Build-Ziele in `ClawdDotNet.csproj` neben der
|
||||
Programmdatei anlegen.
|
||||
|
||||
Nötig ist eine Trennung von Programm und Daten nach XDG-Konvention:
|
||||
`$XDG_CONFIG_HOME` bzw. `/etc/clawddotnet` für die Konfiguration,
|
||||
`$XDG_DATA_HOME` bzw. `/var/lib/clawddotnet` für Instanzen und Datenbanken,
|
||||
`/var/log/clawddotnet` für Logs. Dazu Dateirechte: Instanzverzeichnisse mit
|
||||
Geheimnissen gehören auf `0700`, Konfigurationsdateien auf `0600` — unter
|
||||
Windows regelt das die ACL des Benutzerprofils, unter Linux muss man es setzen.
|
||||
|
||||
**Aufwand 2–3 PT.**
|
||||
|
||||
### 3.6 Tests
|
||||
|
||||
Von 348 Tests sind fast alle portabel. Auffällig ist
|
||||
[WorkspacePathTests.cs:49](tests/ClawdDotNet.Tools.Tests/FileRW/WorkspacePathTests.cs:49):
|
||||
|
||||
```csharp
|
||||
[InlineData(@"C:\Windows\System32\config\SAM")]
|
||||
[InlineData(@"\\server\share\evil.txt")]
|
||||
[InlineData(@"C:\temp\datei.txt")]
|
||||
```
|
||||
|
||||
Unter Linux liefert `Path.IsPathRooted(@"C:\temp\datei.txt")` **`false`** — das
|
||||
ist ein gewöhnlicher relativer Dateiname mit Doppelpunkt und Backslashes darin.
|
||||
Der Test prüft dort also etwas anderes als beabsichtigt. Und da er die
|
||||
Sandbox-Grenze absichert, ist das keine Kleinigkeit: Er muss
|
||||
betriebssystemabhängig aufgeteilt werden, mit einer eigenen Linux-Fassung
|
||||
(`/etc/passwd`, `../../etc/passwd`, Symlinks). Symlinks sind überhaupt ein
|
||||
Prüfpunkt, den es unter Windows so nicht gab — `Path.GetFullPath` löst sie
|
||||
**nicht** auf, `File.ResolveLinkTarget` schon. Ein Agent könnte im
|
||||
Arbeitsverzeichnis einen Symlink nach `/etc` anlegen und die Prüfung ginge
|
||||
durch.
|
||||
|
||||
Ebenso in [YouTubeUrlTests.cs:118](tests/ClawdDotNet.Tools.Tests/SocialMedia/YouTubeUrlTests.cs:118)
|
||||
(harmlos, nur Beispieldaten).
|
||||
|
||||
**Aufwand 2–4 PT**, inklusive Symlink-Absicherung in `WorkspacePath` selbst.
|
||||
|
||||
### 3.7 Bau und Auslieferung
|
||||
|
||||
[Deploy-Build.ps1](Deploy-Build.ps1) setzt PowerShell 5.1 voraus, verwendet
|
||||
Backslash-Pfade und den festen Ausgabepfad `bin\Release\net10.0-windows`. Für
|
||||
Linux braucht es entweder eine `pwsh`-taugliche Fassung oder — besser — einen
|
||||
schlichten `dotnet publish -r linux-x64 --self-contained` mit einer
|
||||
systemd-Unit-Datei. Dazu:
|
||||
|
||||
- systemd-Unit mit eigenem Dienstbenutzer, `Restart=on-failure`
|
||||
- Prüfen, ob der bestehende Watchdog-Heartbeat
|
||||
([Program.cs:367](Program.cs:367)) mit `systemd-notify` zusammenspielen soll
|
||||
- optional `.deb` oder AppImage für den Desktop-Fall
|
||||
|
||||
**Aufwand 3–5 PT.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Der empfohlene Schnitt
|
||||
|
||||
Der entscheidende Befund dieser Analyse: **Die Oberfläche ist nicht der Grund,
|
||||
warum wir Linux wollen.** Der Grund ist, dass Agenten auf einem Server laufen
|
||||
sollen. [Program.cs](Program.cs) baut bereits alles — Speicher, Engine,
|
||||
Taskboard-Scanner, Watchdog, Lizenzprüfung — vollständig auf, **bevor**
|
||||
`frm_main` überhaupt entsteht (Zeilen 36–385 gegen 388–401). Diese Trennung
|
||||
existiert faktisch schon; sie muss nur formalisiert werden.
|
||||
|
||||
### Stufe 1 — Kern Linux-fest und kopfloser Host (12–18 PT)
|
||||
|
||||
| Schritt | PT |
|
||||
|---|---:|
|
||||
| Geheimnisse plattformübergreifend (2.3) | 3–5 |
|
||||
| Zeitzonen normalisieren, Fehler nicht mehr schlucken (2.4) | 2–3 |
|
||||
| Pfadvergleiche plattformabhängig + Symlink-Prüfung (2.5, 3.6) | 3–5 |
|
||||
| Prozessaufrufe, `.exe`-Pfade (2.6) | 1–2 |
|
||||
| Ablageorte und Dateirechte nach XDG (3.5) | 2–3 |
|
||||
| `ClawdDotNet.Host` — Startlogik aus `Program.cs` herauslösen, `PeriodicTimer` statt WinForms-Timer, Telegram-Login ohne Dialog | 4–6 |
|
||||
| Tests auf Linux grün, CI-Lauf für linux-x64 | 2–3 |
|
||||
|
||||
**Ergebnis:** Die Anwendung läuft als systemd-Dienst auf einem Linux-Server. Die
|
||||
Windows-GUI bleibt unverändert bestehen und wird weiter benutzt. Das ist der
|
||||
Punkt, an dem der Nutzen anfällt.
|
||||
|
||||
### Stufe 2 — Avalonia-Oberfläche (32–52 PT)
|
||||
|
||||
| Schritt | PT |
|
||||
|---|---:|
|
||||
| Grundgerüst: Avalonia-Projekt, DI, Dispatcher, Shell mit Tabs, MVVM-Schicht | 5–7 |
|
||||
| Logs-Tab (`RichTextBox` → `SelectingItemsControl` mit Filterung) | 2–3 |
|
||||
| Agent-Settings: Liste, Werkzeugauswahl, Aktionsschaltflächen | 5–8 |
|
||||
| Einstellungs-Tabs — PropertyGrid-Ersatz (2.2) | 6–10 |
|
||||
| Jobs / Services / Job History (4 `DataGridView`) | 4–6 |
|
||||
| Backup-Panel | 3–4 |
|
||||
| Instance-Manager und die fünf Dialoge | 4–6 |
|
||||
| Chat-Ansicht (Variante B, siehe 2.1) | 8–12 |
|
||||
| Info, Statusleiste, Werkzeugleisten, Menü, Lokalisierung de/en | 3–4 |
|
||||
|
||||
Die Spanne ist breit, weil zwei Entscheidungen noch offen sind (PropertyGrid-Ersatz
|
||||
und Chat-Variante). Sind die getroffen, lässt sich das auf etwa ±15 % genau
|
||||
angeben.
|
||||
|
||||
### Stufe 3 — Auslieferung und Härtung (6–10 PT)
|
||||
|
||||
Publish-Pipeline, systemd-Unit, Paketierung, Abnahme auf echter Hardware,
|
||||
Dokumentation, Umzugsweg für bestehende Instanzen.
|
||||
|
||||
### Gesamt
|
||||
|
||||
| | PT | bei Vollzeit |
|
||||
|---|---:|---|
|
||||
| Stufe 1 | 12–18 | 2,5–3,5 Wochen |
|
||||
| Stufe 2 | 32–52 | 6,5–10,5 Wochen |
|
||||
| Stufe 3 | 6–10 | 1,5–2 Wochen |
|
||||
| **Summe** | **50–80** | **10–16 Wochen** |
|
||||
|
||||
---
|
||||
|
||||
## 5. LiveCharts2
|
||||
|
||||
Zur Einordnung: **Das Projekt enthält heute keine einzige Diagrammdarstellung.**
|
||||
Die Suche nach `Chart`, `Series` oder `Plot` findet nur JSON-Feldnamen der
|
||||
Yahoo-Finance-Abfrage in
|
||||
[DirectAPITool.cs:126](src/ClawdDotNet.Tools.DirectAPI/DirectAPITool.cs:126).
|
||||
|
||||
LiveCharts2 ist damit **keine Portierung, sondern neue Funktionalität** — sie
|
||||
gehört zum Trading-Teil, nicht zum Linux-Umzug, und ist in den 50–80 PT oben
|
||||
nicht enthalten. Wenn die Kursansichten kommen, sind dafür grob 5–10 PT
|
||||
zusätzlich zu rechnen. Das passt zu dem, was in
|
||||
[docs/Roadmap.md](docs/Roadmap.md) und der Notiz „Basis vor Trading härten"
|
||||
festgehalten ist: Erst die Basis, dann die Handelsansichten.
|
||||
|
||||
Ein Punkt, der jetzt schon zählt: LiveCharts2 setzt auf SkiaSharp, genau wie
|
||||
Avalonia. Das spricht zusätzlich dafür, die Diagramme erst **nach** der
|
||||
Avalonia-Portierung zu bauen — sonst entstehen sie zweimal.
|
||||
|
||||
---
|
||||
|
||||
## 6. Was vor dem ersten Handgriff zu entscheiden ist
|
||||
|
||||
1. **Ist das Ziel Server oder Desktop?** Bei „Server" reicht Stufe 1, und Stufe 2
|
||||
kann entfallen oder durch eine Weboberfläche ersetzt werden. Das ändert die
|
||||
Schätzung um den Faktor drei.
|
||||
2. **Chat-Ansicht: HTML behalten oder nativ neu bauen?** (2.1)
|
||||
3. **PropertyGrid: Fremdbibliothek oder eigene Formulare?** (2.2)
|
||||
4. ~~**Erlaubt LicenseLabrador den Wechsel der Hardware-ID?**~~ — **geklärt**,
|
||||
siehe 2.8 und
|
||||
[Deploymentcenter-Anbindung-Review.md](Deploymentcenter-Anbindung-Review.md).
|
||||
5. **Bleibt Windows als Zielplattform bestehen?** Wenn ja, muss alles doppelt
|
||||
getestet werden, und die Geheimnis-Verschlüsselung braucht beide Wege plus
|
||||
Umzugspfad. Wenn nein, wird 2.3 deutlich einfacher.
|
||||
|
||||
Frage 1 und 5 beantworten sich vermutlich schnell; 2 und 3 kann man bis zum
|
||||
Beginn von Stufe 2 offenlassen, ohne Stufe 1 zu blockieren. Damit liegt nichts
|
||||
mehr außerhalb unserer Hand — **Stufe 1 kann beginnen.**
|
||||
|
||||
---
|
||||
|
||||
## Anhang — Vollständige Fundstellenliste
|
||||
|
||||
| Thema | Datei:Zeile |
|
||||
|---|---|
|
||||
| DPAPI | [SecretProtector.cs:42](src/ClawdDotNet.Core/Security/SecretProtector.cs:42), `:66`, `:90`, `:94` |
|
||||
| DPAPI (Lizenz) | `LicenseLabrador/client-dotnet/.../StateStore.cs:43`, `:92` |
|
||||
| Zeitzone | [TaskSchedule.cs:161](src/ClawdDotNet.Core/Tasks/TaskSchedule.cs:161), [SchedulerTaskMigration.cs:32](src/ClawdDotNet.Core/Tasks/SchedulerTaskMigration.cs:32) |
|
||||
| Pfad-Groß-/Kleinschreibung | [WorkspacePath.cs:72](src/ClawdDotNet.Tools.FileRW/WorkspacePath.cs:72), [FileRWTool.cs:174](src/ClawdDotNet.Tools.FileRW/FileRWTool.cs:174), [FtpTool.cs:155](src/ClawdDotNet.Tools.FTP/FtpTool.cs:155), [BackupService.cs:384](src/ClawdDotNet.Core/Backup/BackupService.cs:384), [AtomicFile.cs:35](src/ClawdDotNet.Core/Storage/AtomicFile.cs:35) |
|
||||
| `explorer.exe` | [frm_main.cs:1552](frm_main.cs:1552), `:1557`, `:1562`, [BackupPanel.cs:338](UI/BackupPanel.cs:338) |
|
||||
| `VisualBasic.InputBox` | [Program.cs:280](Program.cs:280), `:291`, [frm_main.cs:658](frm_main.cs:658) |
|
||||
| `.exe`-Werkzeugpfade | [SocialMediaManagerTool.cs:782](src/ClawdDotNet.Tools.SocialMediaManager/SocialMediaManagerTool.cs:782), `:825` |
|
||||
| WinForms-Timer | [BackupScheduler.cs:41](Services/BackupScheduler.cs:41), [LiveLogViewerService.cs:38](Services/LiveLogViewerService.cs:38), [OpenRouterStatusService.cs:43](Services/OpenRouterStatusService.cs:43), [frm_main.License.cs:41](frm_main.License.cs:41) |
|
||||
| WebView2 | [frm_main.cs:235](frm_main.cs:235), [frm_chat.cs:44](frm_chat.cs:44), [WebViewBridge.cs](UI/WebViewBridge.cs), [ClawdDotNet.csproj](ClawdDotNet.csproj) |
|
||||
| PropertyGrid | [frm_main.Designer.cs](frm_main.Designer.cs) (4×), [Models/](Models/) (246 Attribute) |
|
||||
| Datenablage | [SettingsManager.cs:24](Services/SettingsManager.cs:24), [ClawdDotNet.csproj](ClawdDotNet.csproj) (Build-Ziele) |
|
||||
| Testdaten mit Windows-Pfaden | [WorkspacePathTests.cs:49](tests/ClawdDotNet.Tools.Tests/FileRW/WorkspacePathTests.cs:49) |
|
||||
| Build-Skript | [Deploy-Build.ps1](Deploy-Build.ps1) |
|
||||
Reference in New Issue
Block a user