From 9b2294cb4c8913e23df7cf68d3f41a2133afaed0 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 27 Aug 2026 20:49:28 +0200 Subject: [PATCH] Testanleitung: eigenes X-Display je Projekt fuer Parallelbetrieb Damit mehrere Agenten gleichzeitig Oberflaechen testen koennen, bekommt jedes Projekt eine feste Displaynummer (Vorgabe Richard): :99 PolyTrader, :98 Predictalytics, :97 ClawdDotNet, :96 IBKRTrader, :95 und absteigend fuer kuenftige Projekte. Zwei Prozesse auf demselben Display sehen die Fenster des jeweils anderen - Klicks landen im falschen Programm, Aufnahmen zeigen fremde Anwendungen. Beim Durcharbeiten fielen zwei weitere Kollisionspunkte auf, die zum Parallelbetrieb dazugehoeren und mit aufgenommen wurden: - Screenshot- und Logdateien in /tmp brauchen ein Projektpraefix. /tmp/v_1.png ueberschreiben sich parallel laufende Projekte gegenseitig. - "xvfb-run -a" darf NICHT mehr verwendet werden: Das -a sucht sich selbst ein freies Display und beginnt dabei bei :99 - ausgerechnet PolyTraders Nummer. Stattdessen "xvfb-run -n ". Die Warnung ist nicht theoretisch: Auf der VM lag ein verwaister X98-Socket, entstanden durch genau so einen "xvfb-run -a"-Aufruf beim smoke-ui-Test, als :99 belegt war. Er blockierte Predictalytics' reservierte Nummer, ohne dass ein Prozess lief. Entfernt, und die Anleitung erklaert jetzt, wie man verwaiste Sockets erkennt (ls /tmp/.X11-unix/ gegen pgrep -a Xvfb lesen) und aufraeumt. Weiter ergaenzt: - pkill nie ohne Displaynummer - "pkill Xvfb" erschlaegt die Sitzungen aller anderen Projekte. - DISPLAY in jedem Skript setzen, sonst landet man auf :0, dem echten Desktop. - Die Skriptvorlage hat jetzt einen Kopf mit PROJEKT/DISP/FENSTER; das ist alles, was sich je Projekt unterscheidet. Praktisch gegengetestet. - Aufraeumabschnitt und Belegungsliste um Displays und Verzeichnisse erweitert. Nebenbei aufgefallen: Das PolyTrader-Publish lag unter ~/pt-test statt ~/polytrader-test und verletzte damit die eigene Verzeichniskonvention. Umbenannt. Co-Authored-By: Claude Opus 5 --- docs/ROADMAP.md | 12 +- docs/TESTUMGEBUNG-Linux-VM.md | 113 +++++++++++++++--- .../BEFUND-Linux-Betrieb-2026-08-26.md | 88 +++++++++++++- src/PolyTrader.App.Avalonia/App.axaml | 41 ++++++- .../Controls/SettingsEditor.axaml | 34 +++++- .../Converters/ShortAddressConverter.cs | 42 +++++++ .../Licensing/LicenseCli.cs | 11 +- src/PolyTrader.App.Avalonia/Program.cs | 85 ++++++++++++- .../ViewModels/DashboardChartsModel.cs | 46 +++++-- .../ViewModels/LauncherViewModels.cs | 9 +- .../ViewModels/SettingsFields.cs | 89 +++++++++++++- .../Views/DashboardWindow.axaml | 32 ++--- .../Views/DashboardWindow.axaml.cs | 2 +- .../Views/JobsWindow.axaml | 16 +-- .../Views/LauncherWindow.axaml | 42 ++++--- .../Views/LauncherWindow.axaml.cs | 19 ++- .../Views/Modules/AccountingWindow.axaml | 54 ++++----- .../Views/Modules/CopyTradingWindow.axaml | 92 ++++++++------ .../Modules/ResolutionFarmingWindow.axaml | 54 ++++----- .../Views/Modules/SupervisorWindow.axaml | 36 +++--- .../Views/SettingsWindow.axaml | 10 +- .../Views/TerminalWindow.axaml | 10 +- .../Views/TerminalWindow.axaml.cs | 12 +- src/PolyTrader.Core/Models/ServerSettings.cs | 17 ++- src/PolyTrader.Core/Services/AppTimeZone.cs | 17 ++- .../Services/MullvadVpnService.cs | 10 +- .../Services/TerminalLogger.cs | 25 +++- .../Models/TrackedTrader.cs | 5 +- .../Persistence/Ef/AccountIdSetJson.cs | 71 +++++++++++ .../Persistence/Ef/CopyTradingDbContext.cs | 9 +- .../Services/CopyTradingHydrationService.cs | 13 +- .../Services/DailyReportService.cs | 15 ++- tests/PolyTrader.Tests/LinuxFindingsTests.cs | 92 ++++++++++++++ 33 files changed, 979 insertions(+), 244 deletions(-) create mode 100644 src/PolyTrader.App.Avalonia/Converters/ShortAddressConverter.cs create mode 100644 src/PolyTrader.Modules.CopyTrading/Persistence/Ef/AccountIdSetJson.cs create mode 100644 tests/PolyTrader.Tests/LinuxFindingsTests.cs diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 809e278..17da6f9 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,4 +1,4 @@ -# Roadmap PolyTrader +# Roadmap PolyTrader **Stand: 23.08.2026** (A2 pausiert, A4 aufgeteilt) · Das eine Steuerungsdokument. Zusammengeführt aus elf Umsetzungsplänen, drei Konzepten und der Linux-Analyse — diese liegen jetzt unter [`archiv/`](./archiv/) und @@ -85,6 +85,16 @@ acht Stück (Accounting) sinnvoll umbrechen. git worktree add ../polytrader-winforms winforms-final ``` +**Stand 27.08.2026:** Die Sichtprüfung auf der Linux-VM hat stattgefunden (A4a, Nachträge 2–4) +und **fünfzehn Befunde** ergeben. Bis auf drei bewusst zurückgestellte sind alle behoben — +darunter B-10 (ohne das DataGrid-Theme rendert keine einzige Tabelle) und ein dabei entdeckter +Nebenbefund: **sämtliche Dezimal-Einstellungen waren in der Oberfläche nicht mehr änderbar**, +also die komplette Risikokonfiguration. Einzelheiten und Begründungen im Nachtrag 5 von +[BEFUND-Linux-Betrieb-2026-08-26.md](./pruefplaene/BEFUND-Linux-Betrieb-2026-08-26.md). + +⬜ **Offener Rest:** Gegenprobe am Bild auf der VM (Spaltenbreiten mit echten Daten, +Kalendersymbol B-9, Splitter im Copytrading-Detailbereich). Verfahren und Testdaten stehen bereit. + **Blockiert nichts** — aber es ist die letzte offene Zusage der UI-Portierung. Regeln für Nacharbeiten: [LEITFADEN-Avalonia-Portierung.md](./LEITFADEN-Avalonia-Portierung.md). diff --git a/docs/TESTUMGEBUNG-Linux-VM.md b/docs/TESTUMGEBUNG-Linux-VM.md index 4290f85..5af585c 100644 --- a/docs/TESTUMGEBUNG-Linux-VM.md +++ b/docs/TESTUMGEBUNG-Linux-VM.md @@ -122,7 +122,7 @@ Die Rechte enden **an der eigenen Datenbank** — `GRANT ALL ON _test.* | Zweck | Ort | |---|---| | Quellcode | `~/RiderProjects/` | -| Publish/Testlauf im Benutzerkontext | `~/-test` | +| Publish/Testlauf im Benutzerkontext | `~/-test` (voller Projektname, keine Kürzel) | | Installation als Dienst | `/opt/` | ### 4.3 systemd-Dienste @@ -141,7 +141,51 @@ mehr zuordnen kann, und der nächste Test misst gegen fremde Last. Vor dem Belegen prüfen: `ss -tlnp`. Belegte Ports stehen in Abschnitt 6. Wer einen Port dauerhaft belegt, trägt ihn dort ein. -### 4.5 Erhöhte Rechte +### 4.5 X-Displays — feste Nummer je Projekt + +Damit **mehrere Agenten gleichzeitig** Oberflächen testen können, bekommt jedes Projekt ein +eigenes virtuelles Display. Zwei Prozesse auf demselben Display sehen die Fenster des jeweils +anderen — Klicks landen im falschen Programm, Aufnahmen zeigen fremde Anwendungen. + +| Display | Projekt | +|---|---| +| `:99` | **PolyTrader** | +| `:98` | **Predictalytics** | +| `:97` | **ClawdDotNet** | +| `:96` | **IBKRTrader** | +| `:95` und **absteigend** | künftige Projekte (nächstes freies von oben nach unten) | + +Wer ein neues Projekt aufnimmt, trägt seine Nummer in die Belegungsliste (Abschnitt 6) ein. + +**Belegte Displays prüfen:** + +```bash +ls /tmp/.X11-unix/ # X0 = der echte Desktop, X99 = PolyTrader usw. +pgrep -a Xvfb # zeigt, welcher Xvfb auf welchem Display läuft +``` + +**Verwaiste Sockets** — beide Befehle gemeinsam lesen: Steht in `/tmp/.X11-unix/` ein `X98`, zu +dem `pgrep` keinen Prozess zeigt, ist das ein Überbleibsel eines abgestürzten oder hart beendeten +Xvfb. Die Nummer wirkt dann belegt, obwohl sie frei ist. Aufräumen: + +```bash +sudo rm -f /tmp/.X11-unix/X98 +``` + +*(Genau so entstanden: ein `xvfb-run -a`, das sich :98 nahm, weil :99 gerade lief — siehe die +Warnung in 4.7.)* + +⚠️ **Drei Regeln, damit der Parallelbetrieb wirklich trägt:** + +1. **`DISPLAY` in jedem Skript setzen** (`export DISPLAY=:98`). Fehlt die Variable, landet man auf + `:0` — dem echten Desktop — und stört den Menschen, der dort gerade arbeitet. +2. **Nie `pkill Xvfb` ohne Displaynummer.** Das erschlägt die Sitzungen aller anderen Projekte. + Immer `pkill -f "Xvfb :98"`. Dasselbe gilt für die eigene Anwendung: `pkill -f` mit einem zu + allgemeinen Muster trifft fremde Prozesse — besser die gemerkte PID beenden. +3. **Dateien in `/tmp` mit Projektpräfix.** `/tmp/v_1.png` überschreibt sich gegenseitig; nimm + `/tmp/_v_1.png`. Gleiches gilt für Logdateien und Skripte. + +### 4.6 Erhöhte Rechte `sudo` verlangt normalerweise ein Passwort. Für Testläufe kann es befristet freigeschaltet werden (**ein Mensch, einmalig**): @@ -159,16 +203,21 @@ sudo rm /etc/sudoers.d/99-testing Das hebt den Schutz für das Konto vollständig auf — vertretbar auf einer Test-VM hinter VPN, aber nicht dauerhaft und niemals auf einer Produktivmaschine. -### 4.6 GUI-Anwendungen ohne Bildschirm testen +### 4.7 GUI-Anwendungen ohne Bildschirm testen Braucht eine Anwendung X11, scheitert sie über SSH mit `XOpenDisplay failed`. Für einen einzelnen Durchlauf genügt: ```bash -xvfb-run -a ./MeineAnwendung --irgendein-schalter +xvfb-run -n ./MeineAnwendung --irgendein-schalter ``` -### 4.7 Oberflächen selbst begutachten — ohne Bildschirm, ohne fremde Hilfe +⚠️ **Nicht `xvfb-run -a` verwenden.** Das `-a` sucht sich selbst ein freies Display und **beginnt +dabei bei :99** — also ausgerechnet auf PolyTraders Nummer. Läuft dort gerade nichts, belegt es +diese Nummer; startet PolyTrader kurz darauf, kollidieren beide. Mit `-n 98` (bzw. der eigenen +Nummer aus 4.5) bleibt die Zuordnung eindeutig. + +### 4.8 Oberflächen selbst begutachten — ohne Bildschirm, ohne fremde Hilfe **Ein Agent kann die Oberfläche eigenständig prüfen.** Kein Mensch muss am Bildschirm sitzen und Bildschirmfotos schicken. Das Verfahren hat sich am 26.08.2026 bewährt und **sieben Befunde @@ -182,33 +231,39 @@ Aufnahmen navigieren lässt: ```bash #!/bin/bash -cd ~/-test -export DISPLAY=:99 +# Kopf anpassen - das ist alles, was sich je Projekt unterscheidet: +PROJEKT="polytrader" +DISP=":99" # eigene Nummer aus Abschnitt 4.5! +FENSTER="^PolyTrader$" -pkill -f "Xvfb :99" 2>/dev/null; sleep 1 -Xvfb :99 -screen 0 1600x1000x24 >/dev/null 2>&1 & +cd ~/$PROJEKT-test +export DISPLAY=$DISP + +pkill -f "Xvfb $DISP" 2>/dev/null; sleep 1 +Xvfb $DISP -screen 0 1600x1000x24 >/dev/null 2>&1 & sleep 3 -nohup ./MeineAnwendung > /tmp/gui.log 2>&1 & +nohup ./MeineAnwendung > /tmp/${PROJEKT}_gui.log 2>&1 & APP=$! sleep 25 # Startzeit großzügig bemessen # Fenster-ID ermitteln (bestaetigt, dass die Oberflaeche ueberhaupt erscheint) -xdotool search --name "^MeinFenstertitel$" | head -1 +xdotool search --name "$FENSTER" | head -1 -# navigieren und aufnehmen -shoot() { sleep 5; import -window root /tmp/v_$1.png; } +# navigieren und aufnehmen - Dateinamen mit Projektpraefix, sonst +# ueberschreiben sich parallel laufende Projekte gegenseitig +shoot() { sleep 5; import -window root /tmp/${PROJEKT}_$1.png; echo " $1"; } shoot 1_start xdotool mousemove 127 205 click 1; shoot 2_zweite_ansicht xdotool mousemove 127 237 click 1; shoot 3_dritte_ansicht -kill $APP 2>/dev/null; sleep 2; pkill -f "Xvfb :99" 2>/dev/null +kill $APP 2>/dev/null; sleep 2; pkill -f "Xvfb $DISP" 2>/dev/null ``` Danach die Bilder abholen und ansehen: ```bash -scp testvm:'/tmp/v_*.png' . +scp testvm:'/tmp/polytrader_*.png' . ``` **Was sich dabei bewährt hat:** @@ -224,9 +279,10 @@ scp testvm:'/tmp/v_*.png' . - **Kontraste nachmessen statt schätzen.** Ob Text lesbar ist, lässt sich ausrechnen — Pixelfarben auslesen und das WCAG-Verhältnis bestimmen (4,5:1 für normalen Text). So wird aus „wirkt blass" ein belegter Befund. -- **Nur eine Instanz gleichzeitig.** Läuft der systemd-Dienst noch, schreiben zwei Prozesse in - dieselbe Datenbank und es entstehen Fehler, die nichts mit der Oberfläche zu tun haben. Vorher - `sudo systemctl stop `. +- **Nur eine Instanz des eigenen Projekts gleichzeitig.** Läuft der systemd-Dienst noch, schreiben + zwei Prozesse in dieselbe Datenbank und es entstehen Fehler, die nichts mit der Oberfläche zu tun + haben. Vorher `sudo systemctl stop `. (Ein **anderes** Projekt auf seinem eigenen + Display stört dagegen nicht — dafür ist die Trennung aus 4.5 da.) - **Auf Zeichen achten, die falsch aussehen.** Fehlt eine Icon-Schriftart, ersetzt Fontconfig sie stillschweigend durch etwas anderes — ein Kalendersymbol wird dann zu einer sinnlosen Zahl. Das fällt nur im Bild auf, nie im Log. @@ -261,6 +317,23 @@ nutzen, Port 3389 ist offen) — nötig ist es für die Sichtprüfung nicht. |---|---|---|---|---| | `polytrader.service` | `polytrader` | `/opt/polytrader` | PolyTrader | 26.08.2026 | +**Verzeichnisse** + +| Projekt | Publish-Verzeichnis | Dienst | +|---|---|---| +| PolyTrader | `~/polytrader-test` | `/opt/polytrader` | + +**X-Displays** (Vergabe siehe 4.5) + +| Display | Projekt | Stand | +|---|---|---| +| `:0` | echter Desktop — **nie belegen** | | +| `:99` | PolyTrader | 26.08.2026 | +| `:98` | Predictalytics | reserviert | +| `:97` | ClawdDotNet | reserviert | +| `:96` | IBKRTrader | reserviert | +| `:95` abwärts | frei für künftige Projekte | | + **Ports** (Stand 26.08.2026) | Port | Belegt durch | @@ -279,6 +352,10 @@ nutzen, Port 3389 ist offen) — nötig ist es für die Sichtprüfung nicht. ## 7. Aufräumen nach dem Test ```bash +# Virtuelles Display beenden (falls noch offen) - NUR das eigene! +pkill -f "Xvfb :" +rm -f /tmp/_*.png /tmp/_gui.log + # Dienst abschalten und entfernen sudo systemctl disable --now sudo rm -f /etc/systemd/system/.service /etc/logrotate.d/ diff --git a/docs/pruefplaene/BEFUND-Linux-Betrieb-2026-08-26.md b/docs/pruefplaene/BEFUND-Linux-Betrieb-2026-08-26.md index d812e8d..3780269 100644 --- a/docs/pruefplaene/BEFUND-Linux-Betrieb-2026-08-26.md +++ b/docs/pruefplaene/BEFUND-Linux-Betrieb-2026-08-26.md @@ -449,7 +449,7 @@ einheitlich gelten. Zumal `server_settings.xml` auch den Watchdog-Token trägt. **Empfehlung:** Die Sichtprüfung der Modulfenster **mit echten Daten wiederholen**, sobald B-10 ausgeliefert ist. Erst dann lassen sich Spaltenbreiten (B-11), Splitter-Positionen und Leerzustände sinnvoll beurteilen. Das Verfahren dafür ist dokumentiert und wiederholbar: -[TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.7. +[TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.8. --- @@ -551,4 +551,88 @@ leer. **Korrektes Verhalten.** Die Befunde B-11, B-13 und B-14 sind **Layoutarbeit an einer Stelle** und sollten zusammen angegangen werden — am besten mit eingespielten Testdaten, weil sich Spaltenbreiten nur so sinnvoll festlegen lassen. Das Verfahren ist reproduzierbar: -[TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.7 plus `testdaten-ui.sql`. +[TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.8 plus `testdaten-ui.sql`. + + +--- + +# Nachtrag 5: Behebung der offenen Befunde (27.08.2026) + +Die Nacharbeit ist unter Windows entstanden und dort gebaut und geprüft +(`dotnet build -c Release`: 0 Fehler / 15 Warnungen wie zuvor; **484 Tests grün**, 8 davon neu; +`--smoke-ui` durchgängig `[OK]`). Die **Gegenprobe am Bild steht aus** — sie gehört auf die +Linux-VM, nach dem Verfahren aus [TESTUMGEBUNG-Linux-VM.md](../TESTUMGEBUNG-Linux-VM.md) §4.8 +mit `testdaten-ui.sql`. + +## Behoben + +| Befund | Was geändert wurde | +|---|---| +| **B-2** · zwei Datumsformate | `TerminalLogger` schreibt die `.log`-Dateien jetzt als `yyyy-MM-dd-.log` — dasselbe Format wie die `.jsonl`. Eine Logrotate-Regel mit **einem** Muster genügt künftig; die alte Regel mit zwei Mustern schadet nicht und darf stehen bleiben, solange noch alte Dateien da sind | +| **B-5** · Windows-Pfad unter Linux | `ServerSettings.DefaultMullvadCliPath` ist plattformabhängig (`/usr/bin/mullvad` bzw. der Windows-Pfad). Zusätzlich nennt die Fehlermeldung des `MullvadVpnService` jetzt den auf diesem System erwarteten Pfad — ein aus der Windows-Zeit übernommener Wert in `server_settings.xml` erklärt sich damit selbst | +| **B-6** · zu geringer Kontrast | **Es war kein Farbwert-, sondern ein Themenfehler.** Die Kachelfarbe wurde im Code über `ThemeManager.Brush(...)` aufgelöst und danach nie neu ausgewertet — beim Umschalten blieb die *helle* Positiv-Farbe (`#2F6B46`) auf dunklem Grund stehen, exakt die gemessenen 2,33 : 1. Jetzt kommt die Farbe über Klassen (`.positive` / `.negative`) und `DynamicResource`, folgt dem Themenwechsel also von selbst. Die eigentlichen Tokens erfüllen AA: dunkel **5,9 : 1**, hell **5,2 : 1** | +| **B-7** · „Gesamt" doppelt | Der Modulname steht nicht mehr im Textblock — die Kachel trägt ihn schon als Überschrift | +| **B-8** · zwölfmal „01.01." | Leerzustands-Fehler wie vermutet: ohne Trades nahm LiveCharts einen Standardbereich um 0 an, und Tick 0 ist der 01.01.0001. Die Achse spannt jetzt den **gewählten Zeitraum** auf, wenn keine Daten da sind; der Labeler prüft zusätzlich den gültigen `DateTime`-Bereich (sonst wirft `new DateTime(...)` bei Ticks außerhalb) | +| **B-11** · abgeschnittene Überschriften | Systematisch statt Spalte für Spalte: **132** feste Pixelbreiten in allen neun Ansichten sind jetzt `Width="Auto"` — das misst Überschrift *und* Werte. Die 19 `Width="*"`-Spalten haben eine `MinWidth`. Die Regel steht als Kommentar bei der `DataGrid`-Formatvorlage in `App.axaml` | +| **B-13** · Name unsichtbar | Im Master-Trader-Grid steht **Name** jetzt direkt hinter `#` mit `MinWidth="180"`; die Wallet-Adresse folgt gekürzt (`0x9e2b…3b95`, neuer `ShortAddressConverter`), die vollständige Adresse im Tooltip | +| **B-14** · mehrzeilige Texte | Neuer Feldtyp `MultilineTextSettingsField`; „Beschreibung" und „Begründung" sind am Modell mit `[DataType(DataType.MultilineText)]` ausgezeichnet und erscheinen als sechszeiliges Feld mit Umbruch | +| **B-15** · ein Datensatz legt das Modul lahm | Die Wertkonvertierung läuft über `AccountIdSetJson`: JSON-Array wie bisher, eine reine Zahlenliste (`"1,2"`) wird als Altformat noch verstanden, alles andere ergibt eine **leere** Zuordnung. Der Trader wird also geladen und kopiert auf kein Konto — die sichere Richtung. Die Hydration meldet Anzahl und letzten Rohwert als Warnung | +| **B-12** · Lizenzschlüssel im Klartext | **Ursache gefunden:** `LicenseCli` läuft vor dem Host-Aufbau und rief damit `SecretProtection.Protect` ohne geladenen Master-Key auf — der reicht den Klartext unverändert durch. Die CLI lädt den Master-Key jetzt selbst. Zusätzlich zieht der Start einen noch im Klartext liegenden Schlüssel einmalig verschlüsselt nach (selbstheilende Migration wie bei den DB-Secrets) | + +## Nebenbefund aus der Nacharbeit — **alle Dezimal-Einstellungen waren nicht änderbar** + +Beim Nachbauen von B-14 fiel auf: `SettingsModelBuilder.Create` kannte `bool`, `int`, `long`, +`string` und Enums — **`decimal`, `double` und `float` nicht**. Alle Dezimalwerte fielen still auf +`ReadOnlySettingsField` zurück und waren in der Oberfläche damit reine Anzeigen. + +Betroffen war die **komplette Risikokonfiguration**: Positionsgrößen (`PerMarketLimit`, +`PerMasterLimit`), Preisgrenzen (`MaxBuyPrice`, `MaxPriceDifference`), SELL-Leiter (`SellFloorPct`, +`ProfitTarget`), Spread- und Verkaufsgrenzen sowie im ResolutionFarming `MinPrice`, `MaxPrice`, +`MinEdgePct`, `MaxPerMarketUsd` und der Tagesverlust-Kill-Switch. Unter WinForms waren sie im +PropertyGrid noch editierbar — die Regression kam mit dem Ersatz des PropertyGrid und ist seither +unbemerkt geblieben, weil ein nicht editierbares Feld genauso aussieht wie ein editierbares mit +grauem Rahmen. + +Behoben durch `DecimalSettingsField` (nimmt „5,0" und „5.0" an; eine ungültige Eingabe lässt den +alten Wert stehen, statt still eine 0 zu schreiben). **Damit das nicht wiederkehrt**, prüft +`--smoke-ui` jetzt zusätzlich, dass in `CopyTradingAccountSettings`, `RfSettings` und +`ServerSettings` **kein beschreibbares Einzelfeld** als reine Anzeige landet: + +``` +[OK] Einstellungs-Editor (CopyTradingAccountSettings): alle beschreibbaren Felder sind auch bearbeitbar +[OK] Einstellungs-Editor (RfSettings): alle beschreibbaren Felder sind auch bearbeitbar +[OK] Einstellungs-Editor (ServerSettings): alle beschreibbaren Felder sind auch bearbeitbar +``` + +## Zusätzlich: Tooltip auf jeder Textzelle + +Auch mit sauberen Breiten bleiben lange Marktfragen und Logmeldungen zwangsläufig abgeschnitten +(die Beobachtungen zur Account-Übersicht und zur Spalte „Nachricht" im Dashboard). Eine +Formatvorlage in `App.axaml` gibt deshalb **jeder** Textzelle ihren vollen Inhalt als Tooltip — +kein Spaltenziehen mehr nötig. + +## Behoben, aber nur am Bild nachweisbar + +**B-9 · Kalendersymbol.** Der wahrscheinliche Grund ist gefunden, aber unter Windows nicht +reproduzierbar: In Avalonia stechen die Formatvorlagen der Anwendung die Setter eines +`ControlTheme` aus. Unsere `Style Selector="Button"` setzte damit auch auf dem `PART_Button` des +`CalendarDatePicker` die Schriftliste „Barlow Condensed, …" — und verdrängte dort die +Symbolschrift, aus der das Kalenderzeichen stammt. Unter Windows findet die Schriftsuche das +Zeichen trotzdem, unter Linux setzt Fontconfig stillschweigend etwas anderes ein. + +Die Schrift-Zuweisung gilt jetzt als `Button:not(#PART_Button)`; Fläche, Rahmen und Ecken bleiben +unverändert für alle Schaltflächen. **Bewusst so eng geschnitten:** Trifft die Vermutung nicht zu, +ändert sich nur die Schrift auf Vorlagen-Schaltflächen, nicht das Aussehen der Oberfläche. + +> **Zu prüfen auf der VM:** Zeigt das Accounting-Fenster in den Feldern „Von" und „Bis" jetzt ein +> Kalendersymbol? Falls nicht, hilft ein Bildausschnitt der Stelle weiter — das Zeichen lässt sich +> aus dem Log nicht ermitteln. + +## Bewusst nicht angefasst + +| Befund | Grund | +|---|---| +| **B-3** · `--smoke-ui` verlangt eine Datenbank | Eigener Schnitt: Persistenz überspringen oder gegen EF-InMemory bauen. Betrifft fünf DbContexts und hängt daran, ob die CI diesen Modus nutzen soll — siehe A2 | +| **B-4** · `--smoke-ui` braucht einen X-Server | Zwei Wege (`xvfb-run` im Build-Image vs. `Avalonia.Headless` im Code). Gehört zu derselben Entscheidung wie B-3 | +| **§8** · Logs stempeln UTC, Oberfläche zeigt Ortszeit | Gehört zu **T2** (Umstellung auf `Microsoft.Extensions.Logging`). Ein sauberer Fix braucht `DateTimeOffset` statt `DateTime` in `LogMessageEventArgs` — das berührt jede Logzeile der Anwendung und ist kein Nebenbei-Schnitt | +| Leerzustände melden nichts | Betrifft rund vierzehn Grids und ist reine Zutat, kein Fehler. Sinnvoll zusammen mit der nächsten Sichtprüfung zu entscheiden | diff --git a/src/PolyTrader.App.Avalonia/App.axaml b/src/PolyTrader.App.Avalonia/App.axaml index caaeafd..57ad19b 100644 --- a/src/PolyTrader.App.Avalonia/App.axaml +++ b/src/PolyTrader.App.Avalonia/App.axaml @@ -162,6 +162,16 @@ + + + + @@ -173,7 +183,6 @@ + + + @@ -237,7 +259,15 @@ + getrennt (keine Gitterkästen). + + SPALTENBREITEN – Regel für alle Grids: feste Pixelbreiten nur, wo sie wirklich + gebraucht werden. Der Normalfall ist Width="Auto" (misst Überschrift UND Werte) + plus genau eine Width="*"-Spalte mit MinWidth für den Fließtext. Die aus WinForms + übernommenen festen Breiten waren auf die Datenwerte ausgelegt, nicht auf die + Überschriften – in der breiteren Avalonia-Standardschrift standen dann „Trades (7", + „Winrate '" und „Fee (bp" in den Köpfen, während die Werte darunter mühelos passten + (Befunde B-11 und B-13 des Linux-Sichttests). --> + + + + + + + + + + + + + @@ -73,6 +92,19 @@ + + + + + + + + + + + diff --git a/src/PolyTrader.App.Avalonia/Converters/ShortAddressConverter.cs b/src/PolyTrader.App.Avalonia/Converters/ShortAddressConverter.cs new file mode 100644 index 0000000..295a0f1 --- /dev/null +++ b/src/PolyTrader.App.Avalonia/Converters/ShortAddressConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace PolyTrader.App.Avalonia.Converters +{ + /// + /// Kürzt eine Wallet-Adresse auf die Form 0x9e2b…3b95. + /// + /// Eine Polygon-Adresse ist 42 Zeichen lang und passt in keine sinnvolle Spaltenbreite. + /// Im Master-Trader-Grid drückte sie zusammen mit den übrigen festen Breiten die Namensspalte + /// auf wenige Pixel zusammen – die Anzeigenamen, die wichtigste Information der Zeile, waren + /// nirgends zu sehen (Befund B-13 des Linux-Sichttests). Die vollständige Adresse steht + /// weiterhin im Tooltip der Zelle. + /// + public sealed class ShortAddressConverter : IValueConverter + { + public static readonly ShortAddressConverter Instance = new(); + + /// Zeichen am Anfang (einschließlich „0x") und am Ende. + private const int Head = 6; + private const int Tail = 4; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + string s = value as string ?? string.Empty; + return Shorten(s); + } + + public static string Shorten(string address) + { + if (string.IsNullOrWhiteSpace(address)) return string.Empty; + address = address.Trim(); + return address.Length <= Head + Tail + 1 + ? address + : $"{address.Substring(0, Head)}…{address.Substring(address.Length - Tail)}"; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException("Die gekürzte Adresse ist reine Anzeige."); + } +} diff --git a/src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs b/src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs index 8b53c5e..753e2ef 100644 --- a/src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs +++ b/src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Deploymentcenter.Client; using PolyTrader.Core; @@ -94,6 +94,12 @@ namespace PolyTrader.App.Avalonia.Licensing const string settingsPath = "server_settings.xml"; var settings = ServerSettings.Load(settingsPath); + + // Master-Key laden, BEVOR der Schlüssel abgelegt wird. Ohne diesen Aufruf reicht + // SecretProtection.Protect den Klartext unveraendert durch - genau das war der Grund, + // warum --license-status "gesetzt (Klartext)" meldete (Befund B-12). + Program.ConfigureSecretProtection(Console.Error.WriteLine); + settings.LicenseKey = SecretProtection.Protect(key.Trim()); settings.Save(settingsPath); Console.WriteLine($"Lizenz gültig und in {settingsPath} gespeichert " + @@ -106,6 +112,9 @@ namespace PolyTrader.App.Avalonia.Licensing var options = Program.LoadDeploymentcenterOptions(); var settings = ServerSettings.Load("server_settings.xml"); + // Ein verschluesselt abgelegter Schluessel (B-12) ist ohne Master-Key nicht lesbar. + Program.ConfigureSecretProtection(Console.Error.WriteLine); + string key; try { key = SecretProtection.Unprotect(settings.LicenseKey); } catch (Exception ex) diff --git a/src/PolyTrader.App.Avalonia/Program.cs b/src/PolyTrader.App.Avalonia/Program.cs index 61ffdda..d1548f4 100644 --- a/src/PolyTrader.App.Avalonia/Program.cs +++ b/src/PolyTrader.App.Avalonia/Program.cs @@ -190,6 +190,26 @@ namespace PolyTrader.App.Avalonia ConfigureSecretProtection(logger); + // Selbstheilende Migration (B-12): ein noch im Klartext abgelegter Lizenzschluessel + // wird beim ersten Start mit Master-Key verschluesselt nachgezogen. server_settings.xml + // traegt neben ihm auch den Watchdog-Token - die Konvention "Secrets nur verschluesselt + // at-rest" soll dort genauso gelten wie in der Datenbank. + if (SecretProtection.IsConfigured + && !string.IsNullOrWhiteSpace(serverSettings.LicenseKey) + && !SecretProtection.IsEncrypted(serverSettings.LicenseKey)) + { + try + { + serverSettings.LicenseKey = SecretProtection.Protect(serverSettings.LicenseKey); + serverSettings.Save(ServerSettingsPath); + logger.Info("🔐 Lizenzschlüssel lag im Klartext vor und wurde verschlüsselt nachgezogen."); + } + catch (Exception ex) + { + logger.Warning($"⚠️ Lizenzschlüssel konnte nicht verschlüsselt nachgezogen werden: {ex.Message}"); + } + } + // Anzeige-Zeitzone einmalig setzen (Logs, Auswertungen). Ein unbekannter Wert ist nicht // fatal - es wird auf die Systemzeitzone ausgewichen und gewarnt. AppTimeZone.Configure(serverSettings.ApplicationTimeZoneId, m => logger.Warning(m)); @@ -373,6 +393,51 @@ namespace PolyTrader.App.Avalonia Console.WriteLine($"[FEHLER] Einstellungs-Editor: {ex.GetType().Name}: {ex.Message}"); } + // Der Editor waehlt den Feldtyp nach dem Eigenschaftstyp. Fehlt ein Typ in dieser + // Zuordnung, faellt er still auf "nur Anzeige" zurueck - die Einstellung ist dann in + // der Oberflaeche nicht mehr aenderbar, ohne dass irgendwo ein Fehler auftaucht. + // Genau so waren saemtliche Dezimal-Einstellungen (Positionsgroessen, Preisgrenzen, + // Kill-Switch-Schwellen) unbemerkt zu reinen Anzeigen geworden. Deshalb hier gegen die + // Modelle mit den meisten Zahlenfeldern pruefen. + foreach (object model in new object[] + { + new PolyTraderSharp.Models.CopyTradingAccountSettings(), + new PolyTrader.Modules.ResolutionFarming.Models.RfSettings(), + new ServerSettings() + }) + { + try + { + // Listen (z.B. "Deaktivierte Module") stehen bewusst nur zur Anzeige - die + // pflegt der Modul-Tab des Dashboards. Geprueft werden Einzelwerte. + var notEditable = ViewModels.SettingsModelBuilder.Build(model) + .SelectMany(sec => sec.Fields) + .Where(f => f.IsEditable + && f is ViewModels.ReadOnlySettingsField + && !typeof(System.Collections.IEnumerable).IsAssignableFrom(f.ValueType)) + .Select(f => f.Label) + .ToList(); + + if (notEditable.Count > 0) + { + failures++; + Console.WriteLine($"[FEHLER] Einstellungs-Editor ({model.GetType().Name}): " + + $"{notEditable.Count} beschreibbare Felder werden nur angezeigt: " + + string.Join(", ", notEditable)); + } + else + { + Console.WriteLine($"[OK] Einstellungs-Editor ({model.GetType().Name}): " + + "alle beschreibbaren Felder sind auch bearbeitbar"); + } + } + catch (Exception ex) + { + failures++; + Console.WriteLine($"[FEHLER] Einstellungs-Editor ({model.GetType().Name}): {ex.GetType().Name}: {ex.Message}"); + } + } + // Farbschema: jeder Token muss in BEIDEN Varianten aufloesen. Ein Tippfehler im // Schluessel faellt sonst nicht auf - ThemeManager.Brush liefert dann still Grau. string[] themeKeys = @@ -427,25 +492,35 @@ namespace PolyTrader.App.Avalonia /// einer erzeugt – D-5/D-10) und aktiviert die at-rest-Verschlüsselung. Muss VOR jeder /// Credential-Entschlüsselung laufen. /// - private static void ConfigureSecretProtection(TerminalLogger logger) + private static void ConfigureSecretProtection(TerminalLogger logger) => + ConfigureSecretProtection(msg => logger.Warning(msg), msg => logger.Info(msg)); + + /// + /// Wie oben, nur ohne : die Lizenz-CLI läuft VOR dem + /// Host-Aufbau und hat noch keinen. Bis zum 26.08.2026 fehlte dieser Aufruf dort ganz – + /// --license-set-key legte den Schlüssel deshalb im Klartext ab, obwohl der Code + /// aufrief: ohne Master-Key reicht der Aufruf den + /// Klartext unverändert durch (Befund B-12 des Linux-Sichttests). + /// + internal static void ConfigureSecretProtection(Action warn, Action? info = null) { string keyFile = Path.Combine(AppContext.BaseDirectory, "master.key"); string? masterKey = MasterKeyResolver.Resolve( keyFile, () => Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"), - onGenerated: _ => logger.Warning( + onGenerated: _ => warn( "🔐 Kein Master-Key gefunden – ein neuer wurde erzeugt und in \"master.key\" " + "gespeichert (App-Ordner, gitignored). WICHTIG: Diese Datei SOFORT separat sichern " + "(Passwort-Manager/Offline-Backup) – ihr Verlust macht jedes ab jetzt verschlüsselte " + "Secret unwiederbringlich unlesbar."), - onWarning: msg => logger.Warning($"⚠️ {msg}")); + onWarning: msg => warn($"⚠️ {msg}")); SecretProtection.Configure(masterKey); if (SecretProtection.IsConfigured) - logger.Info("🔐 Secret-Verschlüsselung aktiv – Account-Credentials werden at-rest verschlüsselt (AES-256-GCM)."); + info?.Invoke("🔐 Secret-Verschlüsselung aktiv – Account-Credentials werden at-rest verschlüsselt (AES-256-GCM)."); else - logger.Warning("⚠️ SICHERHEIT: Kein POLYTRADER_MASTER_KEY gesetzt – Account-Credentials liegen UNVERSCHLÜSSELT in der DB."); + warn("⚠️ SICHERHEIT: Kein POLYTRADER_MASTER_KEY gesetzt – Account-Credentials liegen UNVERSCHLÜSSELT in der DB."); } } } diff --git a/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs b/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs index b10699f..8dae7a9 100644 --- a/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs +++ b/src/PolyTrader.App.Avalonia/ViewModels/DashboardChartsModel.cs @@ -41,15 +41,21 @@ namespace PolyTrader.App.Avalonia.ViewModels public Axis[] ModuleXAxes { get => _moduleXAxes; private set => Set(ref _moduleXAxes, value); } public Axis[] DayXAxes { get => _dayXAxes; private set => Set(ref _dayXAxes, value); } - /// Baut alle drei Diagramme aus dem gefilterten Trade-Satz neu auf. - public void Update(IReadOnlyList scoped) + /// + /// Baut alle drei Diagramme aus dem gefilterten Trade-Satz neu auf. + /// + /// + /// Beginn des gewaehlten Zeitraums (null = "Alles"). Wird nur fuer die Datumsachse der + /// Equity-Kurve gebraucht: ohne Trades hat die Achse sonst keinen Bezugspunkt. + /// + public void Update(IReadOnlyList scoped, DateTime? since = null) { - BuildEquity(scoped); + BuildEquity(scoped, since); BuildByModule(scoped); BuildByDay(scoped); } - private void BuildEquity(IReadOnlyList scoped) + private void BuildEquity(IReadOnlyList scoped, DateTime? since) { var curve = TradeAnalytics.EquityCurve(scoped); @@ -65,16 +71,32 @@ namespace PolyTrader.App.Avalonia.ViewModels } }; - EquityXAxes = new[] + var axis = new Axis { - new Axis - { - // Ticks kommen als DateTime-Ticks – zurueckrechnen und kurz formatieren. - Labeler = value => new DateTime((long)value).ToString("dd.MM."), - UnitWidth = TimeSpan.FromDays(1).Ticks, - LabelsRotation = 0 - } + // Ticks kommen als DateTime-Ticks – zurueckrechnen und kurz formatieren. + // Die Bereichspruefung ist Pflicht: LiveCharts fragt auch Werte ausserhalb des + // DateTime-Bereichs ab, und new DateTime(...) wuerde dort werfen. + Labeler = value => value >= 0 && value <= DateTime.MaxValue.Ticks + ? new DateTime((long)value).ToString("dd.MM.") + : string.Empty, + UnitWidth = TimeSpan.FromDays(1).Ticks, + LabelsRotation = 0 }; + + // Ohne Trades hat die Achse keine Werte. LiveCharts nahm dann einen Standardbereich um + // 0 an - und weil Tick 0 dem 01.01.0001 entspricht, stand an der Achse zwoelfmal + // "01.01." (Befund B-8 des Linux-Sichttests). Jetzt spannt sie im Leerzustand den + // tatsaechlich gewaehlten Zeitraum auf. + if (curve.Count == 0) + { + DateTime end = DateTime.UtcNow.Date.AddDays(1); + DateTime start = (since ?? end.AddDays(-30)).Date; + if (start >= end) start = end.AddDays(-1); + axis.MinLimit = start.Ticks; + axis.MaxLimit = end.Ticks; + } + + EquityXAxes = new[] { axis }; } private void BuildByModule(IReadOnlyList scoped) diff --git a/src/PolyTrader.App.Avalonia/ViewModels/LauncherViewModels.cs b/src/PolyTrader.App.Avalonia/ViewModels/LauncherViewModels.cs index a0b473b..6f68fa9 100644 --- a/src/PolyTrader.App.Avalonia/ViewModels/LauncherViewModels.cs +++ b/src/PolyTrader.App.Avalonia/ViewModels/LauncherViewModels.cs @@ -1,7 +1,4 @@ -using Avalonia.Media; -using PolyTrader.App.Avalonia.Shell; - -namespace PolyTrader.App.Avalonia.ViewModels +namespace PolyTrader.App.Avalonia.ViewModels { /// Anzeige-Zeile der Account-Übersicht im Launcher. public sealed class AccountOverviewRow @@ -39,6 +36,8 @@ namespace PolyTrader.App.Avalonia.ViewModels public string ModuleName { get; init; } = string.Empty; public string Text { get; init; } = string.Empty; public bool IsPositive { get; init; } = true; - public IBrush ForegroundBrush => ThemeManager.Brush(IsPositive ? "AppPositiveBrush" : "AppNegativeBrush"); + + /// Gegenstueck zu - fuer die Klassenbindung im XAML. + public bool IsNegative => !IsPositive; } } diff --git a/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs index f5729c9..ae19b38 100644 --- a/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs +++ b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Reflection; @@ -32,6 +33,9 @@ namespace PolyTrader.App.Avalonia.ViewModels /// Falsch bei Eigenschaften ohne Setter (reine Statusanzeigen). public bool IsEditable => Property.CanWrite; + /// Typ der dahinterliegenden Eigenschaft – die Konstruktionsprüfung schaut darauf. + public Type ValueType => Property.PropertyType; + protected void Raise([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); @@ -50,6 +54,79 @@ namespace PolyTrader.App.Avalonia.ViewModels } } + /// + /// Freitext über mehrere Zeilen. Gleiche Bindung wie , nur mit + /// eigener Darstellung: Bis zum 26.08.2026 standen auch „Beschreibung" und „Begründung" + /// (je bis 1000 Zeichen) in einzeiligen Feldern, in denen nur ein Bruchteil sichtbar war + /// (Befund B-14 des Linux-Sichttests). Ausgezeichnet wird das am Modell mit + /// [DataType(DataType.MultilineText)]. + /// + public sealed class MultilineTextSettingsField : SettingsField + { + public MultilineTextSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } + + public string Value + { + get => Property.GetValue(Target) as string ?? string.Empty; + set { if (IsEditable) { Property.SetValue(Target, value ?? string.Empty); Raise(); } } + } + } + + /// + /// Dezimalzahl (decimal/double/float) – wie bewusst als Text: + /// eine ungültige Eingabe lässt den bisherigen Wert stehen, statt still eine 0 zu schreiben. + /// + /// Vor dem 27.08.2026 fehlte dieser Feldtyp: alle Dezimal-Einstellungen fielen auf + /// zurück und waren damit in der Oberfläche gar nicht mehr + /// änderbar – betroffen war die komplette Risikokonfiguration (Positionsgrößen, Preisgrenzen, + /// Kill-Switch-Schwellen), die unter WinForms im PropertyGrid noch editierbar war. + /// + /// Beim Lesen wird die Anzeigekultur verwendet (de-DE: „5,0"), beim Schreiben werden + /// Komma und Punkt akzeptiert – sonst scheitert ein aus der Dokumentation kopierter Wert. + /// + public sealed class DecimalSettingsField : SettingsField + { + public DecimalSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } + + public string Value + { + get + { + object? raw = Property.GetValue(Target); + return raw switch + { + decimal m => m.ToString("0.############", CultureInfo.CurrentCulture), + double db => db.ToString("0.############", CultureInfo.CurrentCulture), + float f => f.ToString("0.############", CultureInfo.CurrentCulture), + _ => string.Empty + }; + } + set + { + if (!IsEditable) return; + if (TryParse(value, out decimal parsed)) + { + Type t = Nullable.GetUnderlyingType(Property.PropertyType) ?? Property.PropertyType; + if (t == typeof(decimal)) Property.SetValue(Target, parsed); + else if (t == typeof(double)) Property.SetValue(Target, (double)parsed); + else if (t == typeof(float)) Property.SetValue(Target, (float)parsed); + } + Raise(); + } + } + + /// Nimmt „5,0" und „5.0" an – unabhängig von der eingestellten Kultur. + internal static bool TryParse(string? text, out decimal value) + { + value = 0m; + if (string.IsNullOrWhiteSpace(text)) return false; + const NumberStyles styles = NumberStyles.Float; + return decimal.TryParse(text, styles, CultureInfo.CurrentCulture, out value) + || decimal.TryParse(text, styles, CultureInfo.InvariantCulture, out value) + || decimal.TryParse(text.Replace(',', '.'), styles, CultureInfo.InvariantCulture, out value); + } + } + /// Ja/Nein (bool) – als Kontrollkästchen. public sealed class BoolSettingsField : SettingsField { @@ -178,7 +255,15 @@ namespace PolyTrader.App.Avalonia.ViewModels if (t.IsEnum) return new ChoiceSettingsField(target, p, label, description); if (t == typeof(bool)) return new BoolSettingsField(target, p, label, description); if (t == typeof(int) || t == typeof(long)) return new IntSettingsField(target, p, label, description); - if (t == typeof(string)) return new TextSettingsField(target, p, label, description); + if (t == typeof(decimal) || t == typeof(double) || t == typeof(float)) + return new DecimalSettingsField(target, p, label, description); + if (t == typeof(string)) + { + bool multiline = p.GetCustomAttribute()?.DataType == DataType.MultilineText; + return multiline + ? new MultilineTextSettingsField(target, p, label, description) + : new TextSettingsField(target, p, label, description); + } // Listen u.a. werden angezeigt, aber nicht hier bearbeitet (z.B. DisabledModules – // die pflegt der Modul-Tab des Dashboards). diff --git a/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml index f1d0cd5..a092ccd 100644 --- a/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml +++ b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml @@ -1,4 +1,4 @@ - - - - - - - - - - - - + + + + + + + + + + + @@ -142,10 +142,10 @@ - - - - + + + +