Terminal nach Avalonia + konfigurierbare Zeitzone (P2-Kern)

Alle vier Core-Fenster sind damit portiert.

TERMINAL:
- Live-Ausgabe als virtualisiertes ItemsControl ueber eine begrenzte Zeilenliste statt
  RichTextBox. Damit entfaellt das Auto-Clear der WinForms-Fassung, das bei Erreichen der
  Zeichengrenze den GESAMTEN Verlauf verwarf - jetzt werden nur die aeltesten Zeilen
  verdraengt (Ringpuffer, 5000 Zeilen), der juengste Verlauf bleibt immer sichtbar.
  Die Zeilenzahl steht in der Statuszeile.
- Log-Viewer unveraendert im Funktionsumfang: JSONL-Tagesdateien, Filter nach Datum, Level,
  CID und Volltext, Doppelklick uebernimmt die CID (Signal-Kette verfolgen).

ZEITZONE (Befund aus der Linux-Analyse, hier faellig geworden):
Die Terminal-Ansicht rechnete hart gegen die WINDOWS-ID 'W. Europe Standard Time'. Auf Linux
traegt die nur ueber die ICU-Zuordnung und faellt ganz aus, wenn ICU fehlt oder
InvariantGlobalization gesetzt ist - das Fenster haette beim Oeffnen geworfen.

Neu: PolyTraderSharp.Services.AppTimeZone + ServerSettings.ApplicationTimeZoneId
(Default 'Europe/Berlin', IANA-Schreibweise). Aufloesung versucht die ID direkt, dann die
jeweils andere Schreibweise (IANA<->Windows), zuletzt die Systemzeitzone - ein unbekannter
Wert ist damit nie fatal, sondern erzeugt nur eine Warnung. Beide Programm-Einstiege setzen
sie einmalig beim Start; laut Vorgabe wird sie bei der Installation festgelegt und nicht im
laufenden Betrieb gewechselt (Aenderung verschiebt Logdatei-Tagesgrenzen).

8 neue Tests (AppTimeZoneTests) halten fest: IANA- UND Windows-ID liefern denselben
UTC-Versatz (Winter +1, Sommer +2), unbekannte IDs fallen mit Warnung auf die Systemzeitzone
zurueck, leere Angabe = Systemzeitzone, UTC wird korrekt umgerechnet.

Verifiziert: Solution baut, 450 Tests gruen, --smoke-ui gruen (6 Fenster + Editor-Pruefung,
Zeitzone loest als Europe/Berlin auf), App laeuft real, Linux-Publish laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-06 21:06:24 +02:00
co-authored by Claude Opus 5
parent 4ba8149e64
commit 929fa20ee0
9 changed files with 514 additions and 2 deletions
@@ -0,0 +1,85 @@
using System;
namespace PolyTraderSharp.Services
{
/// <summary>
/// Anzeige-Zeitzone der Anwendung. Wird beim Start EINMAL aus den Server-Settings gesetzt
/// (<c>ApplicationTimeZoneId</c>) und danach überall zum Umrechnen von UTC-Zeitstempeln in die
/// dargestellte Ortszeit verwendet.
///
/// <para><b>Warum das nötig war:</b> Die Terminal-Ansicht rechnete bisher hart gegen
/// <c>"W. Europe Standard Time"</c> eine <i>Windows</i>-Zeitzonen-ID. Auf Linux funktioniert die
/// nur über die ICU-Zuordnung und fällt komplett aus, wenn ICU fehlt oder
/// <c>InvariantGlobalization</c> gesetzt ist. Dann hätte das Fenster beim Öffnen geworfen.</para>
///
/// <para><b>Auflösung:</b> Zuerst die konfigurierte ID direkt, dann über die Umrechnung zwischen
/// IANA- und Windows-Schreibweise (damit dieselbe Konfiguration auf beiden Plattformen trägt),
/// zuletzt die Zeitzone des Systems. Ein Fehlschlag ist damit nie fatal er wird nur gemeldet.</para>
///
/// <para>Die Zeitzone wird bewusst einmalig bei der Installation festgelegt und nicht im
/// laufenden Betrieb gewechselt: eine Änderung verschiebt Logdatei-Tagesgrenzen und
/// Auswertungszeiträume. Änderungen greifen deshalb erst nach einem Neustart.</para>
/// </summary>
public static class AppTimeZone
{
/// <summary>Empfehlung für neue Installationen IANA-Schreibweise, funktioniert auf beiden Plattformen.</summary>
public const string DefaultId = "Europe/Berlin";
/// <summary>Aktuell gültige Anzeige-Zeitzone. Vor <see cref="Configure"/> die des Systems.</summary>
public static TimeZoneInfo Current { get; private set; } = TimeZoneInfo.Local;
/// <summary>Die tatsächlich verwendete ID (kann von der gewünschten abweichen, wenn ausgewichen wurde).</summary>
public static string CurrentId => Current.Id;
/// <summary>
/// Setzt die Anzeige-Zeitzone. Leere Angabe = Systemzeitzone. Meldet über
/// <paramref name="warn"/>, wenn auf etwas anderes als das Gewünschte ausgewichen wurde.
/// </summary>
public static void Configure(string? timeZoneId, Action<string>? warn = null)
{
if (string.IsNullOrWhiteSpace(timeZoneId))
{
Current = TimeZoneInfo.Local;
return;
}
string id = timeZoneId.Trim();
if (TryFind(id, out var tz)) { Current = tz!; return; }
// Andere Schreibweise versuchen: eine unter Windows gepflegte Konfiguration soll auch
// auf Linux tragen und umgekehrt.
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(id, out string? windowsId)
&& TryFind(windowsId!, out tz))
{
Current = tz!;
warn?.Invoke($"Zeitzone „{id}\" wurde als {tz!.Id}\" aufgelöst.");
return;
}
if (TimeZoneInfo.TryConvertWindowsIdToIanaId(id, out string? ianaId)
&& TryFind(ianaId!, out tz))
{
Current = tz!;
warn?.Invoke($"Zeitzone „{id}\" wurde als {tz!.Id}\" aufgelöst.");
return;
}
Current = TimeZoneInfo.Local;
warn?.Invoke($"⚠️ Zeitzone „{id}\" ist auf diesem System unbekannt es wird die Systemzeitzone " +
$"„{TimeZoneInfo.Local.Id}\" verwendet. Empfohlen ist die IANA-Schreibweise, z.B. {DefaultId}\".");
}
/// <summary>Rechnet einen Zeitstempel in die Anzeige-Zeitzone um (UTC-Kennzeichnung wird beachtet).</summary>
public static DateTime ToDisplay(DateTime value) =>
value.Kind == DateTimeKind.Utc
? TimeZoneInfo.ConvertTimeFromUtc(value, Current)
: TimeZoneInfo.ConvertTime(value, Current);
private static bool TryFind(string id, out TimeZoneInfo? tz)
{
try { tz = TimeZoneInfo.FindSystemTimeZoneById(id); return true; }
catch (TimeZoneNotFoundException) { tz = null; return false; }
catch (InvalidTimeZoneException) { tz = null; return false; }
}
}
}