diff --git a/src/LageBuch.App/Services/IncidentHostController.cs b/src/LageBuch.App/Services/IncidentHostController.cs index 5f35350..ffad321 100644 --- a/src/LageBuch.App/Services/IncidentHostController.cs +++ b/src/LageBuch.App/Services/IncidentHostController.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Net; using LageBuch.AppLogic; using LageBuch.AppLogic.Services; @@ -39,7 +40,7 @@ public async Task StartAsync(LocalIncidentSession session) // A fresh 4-digit PIN per share session: the host reads it out, joiners type it (§ #64). // Cryptographic RNG so the PIN isn't predictable from a seeded/observed sequence — cheap // hardening even though a 4-digit space is small (brute-force is the accepted, documented risk). - var pin = System.Security.Cryptography.RandomNumberGenerator.GetInt32(0, 10_000).ToString("D4"); + var pin = System.Security.Cryptography.RandomNumberGenerator.GetInt32(0, 10_000).ToString("D4", CultureInfo.InvariantCulture); var host = new IncidentHost(session, _clock, _appVersion, _ui, pin); await host.StartAsync(IPAddress.Any); _host = host; diff --git a/src/LageBuch.AppLogic/ViewModels/HomeViewModel.cs b/src/LageBuch.AppLogic/ViewModels/HomeViewModel.cs index 814eab0..c58c540 100644 --- a/src/LageBuch.AppLogic/ViewModels/HomeViewModel.cs +++ b/src/LageBuch.AppLogic/ViewModels/HomeViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using System.Globalization; using System.Net.Sockets; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -81,7 +82,7 @@ private async Task NewIncidentAsync(NewIncidentRequest request) // Date + time + Stichwort, e.g. "20260819-2217-B3P.fwincident" -- the Einsatznummer is // unknown at creation (#69) and no longer part of the filename; it can be added later from // the workspace header. No Stichwort at all just leaves the timestamp alone. - var timestamp = _clock.Now.ToString("yyyyMMdd-HHmm"); + var timestamp = _clock.Now.ToString("yyyyMMdd-HHmm", CultureInfo.InvariantCulture); var stem = string.IsNullOrWhiteSpace(request.Keyword) ? timestamp : $"{timestamp}-{StripInvalidFileNameChars(request.Keyword.Trim())}"; diff --git a/src/LageBuch.AppLogic/ViewModels/ScbaViewModel.cs b/src/LageBuch.AppLogic/ViewModels/ScbaViewModel.cs index 428d94b..1e2db9d 100644 --- a/src/LageBuch.AppLogic/ViewModels/ScbaViewModel.cs +++ b/src/LageBuch.AppLogic/ViewModels/ScbaViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using System.Globalization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LageBuch.AppLogic.Services; @@ -70,7 +71,7 @@ public ScbaTruppRow( public bool IsAlarm => _trupp.IsAlarm(_clock.Now); public bool IsControlDue => _trupp.IsControlDue(_clock.Now); - public string StartTimeDisplay => _trupp.StartTime is { } s ? s.ToString("HH:mm") : "—"; + public string StartTimeDisplay => _trupp.StartTime is { } s ? s.ToString("HH:mm", CultureInfo.InvariantCulture) : "—"; public string? PressureDisplay => _trupp.LatestPressure is { } p ? $"{p} bar" : null; public string ElapsedDisplay => _trupp.HasStarted ? Clock(_trupp.Elapsed(_clock.Now)) : "—"; diff --git a/src/LageBuch.AppLogic/ViewModels/TasksViewModel.cs b/src/LageBuch.AppLogic/ViewModels/TasksViewModel.cs index b8b95ba..68ceaa5 100644 --- a/src/LageBuch.AppLogic/ViewModels/TasksViewModel.cs +++ b/src/LageBuch.AppLogic/ViewModels/TasksViewModel.cs @@ -72,13 +72,13 @@ public TasksViewModel( // Shared with TaskDialogViewModel (same assembly) so picker wording matches everywhere. internal static IReadOnlyList ImportanceLevels() => Enum.GetValues() - .OrderByDescending(v => Convert.ToInt32(v)) + .OrderByDescending(v => (int)v) .Select(v => new ImportanceOption(v, Formatting.Level(v))) .ToArray(); internal static IReadOnlyList UrgencyLevels() => Enum.GetValues() - .OrderByDescending(v => Convert.ToInt32(v)) + .OrderByDescending(v => (int)v) .Select(v => new UrgencyOption(v, Formatting.Level(v))) .ToArray(); diff --git a/src/LageBuch.Documents/Sections/ForcesSection.cs b/src/LageBuch.Documents/Sections/ForcesSection.cs index 32a4b29..00b10a9 100644 --- a/src/LageBuch.Documents/Sections/ForcesSection.cs +++ b/src/LageBuch.Documents/Sections/ForcesSection.cs @@ -1,3 +1,4 @@ +using System.Globalization; using LageBuch.Domain; using QuestPDF.Fluent; using QuestPDF.Helpers; @@ -40,7 +41,7 @@ public static void Compose(IContainer container, Incident incident) table.Cell().Element(Cells.Body).Text(Formatting.OrDash(unit.CallSign)); // Stärke im 1/1/2-Format: Führungskräfte/Mannschaft/Gesamt (#76). table.Cell().Element(Cells.Body).Text(unit.StrengthText); - table.Cell().Element(Cells.Body).Text(unit.ScbaCount.ToString()); + table.Cell().Element(Cells.Body).Text(unit.ScbaCount.ToString(CultureInfo.InvariantCulture)); table.Cell().Element(Cells.Body).Text(Formatting.OrDash(unit.Status)); table.Cell().Element(Cells.Body).Text(Formatting.OrDash(unit.Notes)); } @@ -56,7 +57,7 @@ public static void Compose(IContainer container, Incident incident) t.Span("Gesamtstärke: ").SemiBold(); t.Span($"{incident.TotalOfficer}/{incident.TotalPersonnel - incident.TotalOfficer}/{incident.TotalPersonnel}"); t.Span(" davon Atemschutzgeräteträger: ").SemiBold(); - t.Span(incident.TotalScba.ToString()); + t.Span(incident.TotalScba.ToString(CultureInfo.InvariantCulture)); }); }); } diff --git a/src/LageBuch.Persistence/IncidentRepository.cs b/src/LageBuch.Persistence/IncidentRepository.cs index ddf0877..f2c42e2 100644 --- a/src/LageBuch.Persistence/IncidentRepository.cs +++ b/src/LageBuch.Persistence/IncidentRepository.cs @@ -1,3 +1,4 @@ +using System.Globalization; using LageBuch.Domain; using LageBuch.Persistence.Sqlite; using Microsoft.Data.Sqlite; @@ -277,7 +278,7 @@ private static void WriteChecklist(SqliteConnection cn, SqliteTransaction tx, IR using var cmd = cn.CreateCommand(); cmd.CommandText = "SELECT state FROM incident_meta LIMIT 1;"; var raw = cmd.ExecuteScalar(); - return raw is null ? null : (IncidentState)Convert.ToInt32(raw); + return raw is null ? null : (IncidentState)Convert.ToInt32(raw, CultureInfo.InvariantCulture); } catch { @@ -308,7 +309,7 @@ public Incident Load(string path) { cmd.CommandText = "SELECT state FROM incident_meta LIMIT 1;"; var raw = cmd.ExecuteScalar() ?? throw new InvalidOperationException("No incident in file."); - state = (IncidentState)Convert.ToInt32(raw); + state = (IncidentState)Convert.ToInt32(raw, CultureInfo.InvariantCulture); } using var cn = state == IncidentState.Closed @@ -408,7 +409,7 @@ public Incident Load(string path) var fd = System.Text.Json.JsonSerializer.Deserialize>(fdJson) ?? new Dictionary(); var fdDict = fd.ToDictionary( - kv => int.Parse(kv.Key), + kv => int.Parse(kv.Key, CultureInfo.InvariantCulture), kv => kv.Value); // apartment_labels is null on rows written before this column existed. var alJson = Str(r, 6); @@ -416,7 +417,7 @@ public Incident Load(string path) ? new Dictionary() : (System.Text.Json.JsonSerializer.Deserialize>(alJson) ?? new Dictionary()) - .ToDictionary(kv => int.Parse(kv.Key), kv => kv.Value); + .ToDictionary(kv => int.Parse(kv.Key, CultureInfo.InvariantCulture), kv => kv.Value); return Domain.CoMeasurement.Building.Rehydrate(Guid.Parse(r.GetString(0)), r.GetString(1), r.GetInt32(2), r.GetInt32(3), fdDict, r.GetInt32(5), alDict); }); @@ -449,7 +450,7 @@ public Incident Load(string path) return Incident.Rehydrate( Guid.Parse((string)meta[0]!), ParseDate((string)meta[1]!), - (IncidentState)Convert.ToInt32(meta[2]), + (IncidentState)Convert.ToInt32(meta[2], CultureInfo.InvariantCulture), incidentNumber, meta[5] as string, meta[6] as string, diff --git a/src/LageBuch.Persistence/Sqlite/Migrations.cs b/src/LageBuch.Persistence/Sqlite/Migrations.cs index 44e0ffb..19c297c 100644 --- a/src/LageBuch.Persistence/Sqlite/Migrations.cs +++ b/src/LageBuch.Persistence/Sqlite/Migrations.cs @@ -1,3 +1,4 @@ +using System.Globalization; using LageBuch.Domain.Atemschutz; using Microsoft.Data.Sqlite; @@ -18,7 +19,7 @@ public static int GetVersion(SqliteConnection cn) using var read = cn.CreateCommand(); read.CommandText = "SELECT version FROM schema_version LIMIT 1;"; var result = read.ExecuteScalar(); - return result is null ? 0 : Convert.ToInt32(result); + return result is null ? 0 : Convert.ToInt32(result, CultureInfo.InvariantCulture); } public static void Migrate(SqliteConnection cn) diff --git a/src/LageBuch.Sync/SnapshotMapper.cs b/src/LageBuch.Sync/SnapshotMapper.cs index 8f53d65..5cdbdb9 100644 --- a/src/LageBuch.Sync/SnapshotMapper.cs +++ b/src/LageBuch.Sync/SnapshotMapper.cs @@ -1,3 +1,4 @@ +using System.Globalization; using LageBuch.Domain; using LageBuch.Domain.Atemschutz; using LageBuch.Domain.CoMeasurement; @@ -47,9 +48,9 @@ public static IncidentSnapshot ToSnapshot(Incident incident) t.CreatedBy, t.CreatedAt, t.DueAt, t.CompletedAt, t.CompletedBy)).ToList(), incident.Buildings.Select(b => new BuildingDto( b.Id, b.Name, b.FloorCount, b.ApartmentsPerFloor, - b.FloorDescriptions.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value), + b.FloorDescriptions.ToDictionary(kv => kv.Key.ToString(CultureInfo.InvariantCulture), kv => kv.Value), b.Ordinal, - b.ApartmentLabels.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value))).ToList(), + b.ApartmentLabels.ToDictionary(kv => kv.Key.ToString(CultureInfo.InvariantCulture), kv => kv.Value))).ToList(), incident.Dwellings.Select(d => new DwellingDto( d.Id, d.BuildingId, d.FloorOrdinal, d.ApartmentNumber, d.ResidentName, d.Status, d.KeyAvailable, d.CoValue)).ToList()); @@ -86,11 +87,11 @@ public static Incident FromSnapshot(IncidentSnapshot snapshot) snapshot.Buildings.Select(b => Building.Rehydrate( b.Id, b.Name, b.FloorCount, b.ApartmentsPerFloor, b.FloorDescriptions.ToDictionary( - kv => int.Parse(kv.Key), + kv => int.Parse(kv.Key, CultureInfo.InvariantCulture), kv => kv.Value), b.Ordinal, b.ApartmentLabels?.ToDictionary( - kv => int.Parse(kv.Key), + kv => int.Parse(kv.Key, CultureInfo.InvariantCulture), kv => kv.Value))), snapshot.Dwellings.Select(d => Dwelling.Rehydrate( d.Id, d.BuildingId, d.FloorOrdinal, d.ApartmentNumber, diff --git a/tests/LageBuch.Acceptance.Tests/ModuleTabsScrollingTests.cs b/tests/LageBuch.Acceptance.Tests/ModuleTabsScrollingTests.cs index 41405b5..f53b05b 100644 --- a/tests/LageBuch.Acceptance.Tests/ModuleTabsScrollingTests.cs +++ b/tests/LageBuch.Acceptance.Tests/ModuleTabsScrollingTests.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Presenters; @@ -56,7 +57,7 @@ public void Nav_rail_stays_one_column_and_scrolls_on_a_short_viewport() .ToArray(); Assert.True(columns.Length == 1, $"nav rail wrapped into {columns.Length} columns at x=" + - string.Join(", ", columns.Select(c => c.ToString("F0"))) + + string.Join(", ", columns.Select(c => c.ToString("F0", CultureInfo.InvariantCulture))) + " -- it must overflow into a scrollbar instead."); // The overflow lands in a ScrollViewer, not silent clipping.