Eine Roadmap statt neun verteilter Listen
Der Stand lag ueber eine Bestandsaufnahme, drei Konzeptpapiere, vier Umsetzungsplaene und zwei Deploymentcenter-Dokumente verteilt - jedes mit eigener Reihenfolge, teils widersprueglich. Alle offenen Punkte daraus sind in docs/Roadmap.md zusammengefuehrt. Aufbau der neuen Roadmap - Statusvokabular: erledigt / beschlossen-offen / Entscheidung noetig / zurueckgestellt / Idee. Damit steht das Geparkte sichtbar drin, statt in einem Konzeptpapier zu verschwinden. - Herkunft-Spalte traegt das alte Kuerzel (S4, K2, B9, T4, F-A1, C1, DC1), damit die archivierten Papiere auffindbar bleiben, ohne sie zu lesen. - Abschnitt 1 Reihenfolge, 2 offene Entscheidungen, 3 die Vorhaben nach Bereich, 4 Ideenspeicher, 5 Chronik des Erledigten, 6 Modell-Einstufung, 7 Herkunftskarte. Was dabei sichtbar wurde - Neun Entscheidungen blockieren Arbeit, ohne dass sie Aufwand kosten - allen voran Matrix oder Rocket.Chat. Sie stehen jetzt gesammelt in Abschnitt 2 statt verstreut in den Diskussionsteilen der Konzepte. - B8 (keine Tiefenbegrenzung bei AgentComm) ist unveraendert offen. Das ist keine Theorie: A haelt sein Gate, waehrend es auf B wartet - ruft B nun A, warten beide bis zum Timeout. Der Testfall A13 dafuer fehlt bis heute. - Die vier Umsetzungsplaene vom 2026-08-05 sind alle unumgesetzt und waren in keiner Roadmap verzeichnet. Archiv Sechs Dokumente ziehen nach docs/archiv/: Bestandsaufnahme, Konzepte Backup/Finanz/Analyse, Linux-Portierung-Analyse, Lizenz-HardwareId-v2 (gegenstandslos - LicenseLabrador ist ersetzt), Deploymentcenter-Review und der 2.4-Integrationsplan. Sie bleiben als Begruendung lesbar, werden aber nicht mehr fortgeschrieben; das README ordnet jedes einzeln ein und warnt, dass ihre Quelltext-Verweise ins Leere gehen koennen. Bauplan bleibt Bauplan Taskboard, Audit, Staging, Memory, Agentenkommunikation, RocketChat und Deploymentcenter-Integration bleiben in docs/ - sie sind die Detailvorgabe fuer die Umsetzung, nicht Vorhabenlisten. Jedes bekommt oben eine Zeile, die seine Rolle und den Umsetzungsstand nennt und auf die Roadmap zeigt. Dasselbe fuer die vier Umsetzungsplaene: die Reihenfolge gilt in der Roadmap, nicht im Plan. Nebenbei repariert: Taskboard-Konzept und Teststrategie verwiesen auf AgentScheduler und ToolJobScheduler, die seit der Scanner-Konsolidierung geloescht sind. Alle Dokument-Verweise in docs/ sind geprueft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
# Linux-Portierung — Analyse
|
||||
|
||||
Stand: 2026-08-06. Reine Bestandsaufnahme und Aufwandsschätzung, **kein** Umbau.
|
||||
|
||||
> **Nachtrag 2026-08-23:** Der teure Teil dieser Analyse hat sich erledigt. Die rund
|
||||
> 8.900 Zeilen Windows-Forms-Oberfläche samt WebView2 und den vier `PropertyGrid`-
|
||||
> Instanzen gibt es nicht mehr — sie sind durch `src/ClawdDotNet.Desktop` (Avalonia,
|
||||
> `net10.0`) ersetzt und beim Frühjahrsputz entfernt worden. Damit fällt der zweite
|
||||
> der beiden unten vorgeschlagenen Schnitte weg: Es gibt keine Windows-gebundene
|
||||
> Oberfläche mehr, die noch umzuziehen wäre. Offen bleiben die drei Kernstellen
|
||||
> (DPAPI, Pfadvergleiche, Zeitzonen-IDs) und ein `linux-x64`-Release (Roadmap DC3).
|
||||
|
||||
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](../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