diff --git a/src/Marathon.Hosts.WpfBlazor/wwwroot/index.html b/src/Marathon.Hosts.WpfBlazor/wwwroot/index.html
new file mode 100644
index 0000000..162d2ce
--- /dev/null
+++ b/src/Marathon.Hosts.WpfBlazor/wwwroot/index.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+ Marathon — Odds Lab
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Booting
+
Marathon Odds Lab
+
+
+
+
+
An unhandled error has occurred.
+
Reload
+
×
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Marathon.UI/Components/AnomalyEvidence.razor b/src/Marathon.UI/Components/AnomalyEvidence.razor
index f06cc3f..e72d16c 100644
--- a/src/Marathon.UI/Components/AnomalyEvidence.razor
+++ b/src/Marathon.UI/Components/AnomalyEvidence.razor
@@ -217,7 +217,7 @@
builder.OpenElement(12, "span");
builder.AddAttribute(13, "class", "m-evidence__rate");
- builder.AddContent(14, rate is { } r ? r.ToString("0.00") : "—");
+ builder.AddContent(14, rate is { } r ? r.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) : "—");
builder.CloseElement();
builder.CloseElement();
diff --git a/src/Marathon.UI/Components/OddsCell.razor b/src/Marathon.UI/Components/OddsCell.razor
index 74561c0..9846531 100644
--- a/src/Marathon.UI/Components/OddsCell.razor
+++ b/src/Marathon.UI/Components/OddsCell.razor
@@ -87,7 +87,9 @@
[Parameter] public string AriaPrefix { get; set; } = "Odds";
[Parameter] public string EmptyPlaceholder { get; set; } = "—";
- private string Formatted => Rate is { } r ? r.ToString("0.00") : EmptyPlaceholder;
+ private string Formatted => Rate is { } r
+ ? r.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)
+ : EmptyPlaceholder;
private Trend Direction
{
diff --git a/src/Marathon.UI/Components/OddsTimeline.razor b/src/Marathon.UI/Components/OddsTimeline.razor
index 988df3d..2d41aa5 100644
--- a/src/Marathon.UI/Components/OddsTimeline.razor
+++ b/src/Marathon.UI/Components/OddsTimeline.razor
@@ -238,5 +238,6 @@
},
};
- private static string FormatRate(decimal? r) => r is { } v ? v.ToString("0.00") : "—";
+ private static string FormatRate(decimal? r) =>
+ r is { } v ? v.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) : "—";
}
diff --git a/src/Marathon.UI/Components/SeverityBadge.razor b/src/Marathon.UI/Components/SeverityBadge.razor
index 8a98fa6..73ab2ac 100644
--- a/src/Marathon.UI/Components/SeverityBadge.razor
+++ b/src/Marathon.UI/Components/SeverityBadge.razor
@@ -19,7 +19,7 @@
@Label
@if (ShowScore && Score is { } s)
{
- @s.ToString("0.00")
+ @s.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)
}
diff --git a/src/Marathon.UI/MainLayout.razor b/src/Marathon.UI/MainLayout.razor
index 4225975..2c575f4 100644
--- a/src/Marathon.UI/MainLayout.razor
+++ b/src/Marathon.UI/MainLayout.razor
@@ -83,6 +83,25 @@
min-height: 0;
}
+ /* MudDrawer is positioned fixed/absolute by Mud's CSS — push main content
+ right by the drawer's width (248px) so the sidebar doesn't overlap.
+ Same shift on the appbar tools row so the brand isn't covered. */
+ .m-app-frame.is-drawer-open .m-main,
+ .m-app-frame.is-drawer-open .m-footer {
+ padding-left: 248px;
+ transition: padding-left 200ms ease;
+ }
+ .m-app-frame.is-drawer-open .m-appbar {
+ padding-left: calc(248px + clamp(var(--m-space-3), 2vw, var(--m-space-5)));
+ transition: padding-left 200ms ease;
+ }
+ @@media (max-width: 720px) {
+ /* On narrow viewports the drawer becomes a temporary overlay anyway. */
+ .m-app-frame.is-drawer-open .m-main,
+ .m-app-frame.is-drawer-open .m-footer,
+ .m-app-frame.is-drawer-open .m-appbar { padding-left: 0; }
+ }
+
.m-footer {
display: flex;
align-items: center;
diff --git a/src/Marathon.UI/Pages/Events/Detail.razor b/src/Marathon.UI/Pages/Events/Detail.razor
index 292e9aa..4f25fa6 100644
--- a/src/Marathon.UI/Pages/Events/Detail.razor
+++ b/src/Marathon.UI/Pages/Events/Detail.razor
@@ -105,7 +105,7 @@
@BetTypeLabel(row.Type) |
@SideLabel(row.Side) |
@FormatThreshold(row.Threshold) |
- @row.Rate.ToString("0.00") |
+ @row.Rate.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) |
}
@@ -334,6 +334,8 @@
_ => s.ToString(),
};
- private static string FormatRate(decimal? r) => r is { } v ? v.ToString("0.00") : "—";
- private static string FormatThreshold(decimal? v) => v is { } x ? x.ToString("0.##") : "—";
+ private static string FormatRate(decimal? r) =>
+ r is { } v ? v.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) : "—";
+ private static string FormatThreshold(decimal? v) =>
+ v is { } x ? x.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture) : "—";
}
diff --git a/src/Marathon.UI/Services/LocaleState.cs b/src/Marathon.UI/Services/LocaleState.cs
index 12c4fcd..23a2111 100644
--- a/src/Marathon.UI/Services/LocaleState.cs
+++ b/src/Marathon.UI/Services/LocaleState.cs
@@ -15,23 +15,35 @@ public sealed class LocaleState
public static readonly IReadOnlyList Supported = new[] { Russian, English };
- private CultureInfo _culture = CultureInfo.GetCultureInfo(Russian);
+ private CultureInfo _culture;
+
+ public LocaleState()
+ {
+ // Initialise the field only — do NOT mutate process-wide ambient culture
+ // here. Production startup (App.xaml.cs) always calls Set(...) right after
+ // resolving this service, which applies ambient culture explicitly. Doing
+ // it here as well leaks ru-RU across parallel test runs whenever any
+ // bUnit test class instantiates MarathonTestContext.
+ _culture = CultureInfo.GetCultureInfo(Russian);
+ }
public CultureInfo Culture
{
get => _culture;
private set
{
+ // Always re-apply ambient cultures even when the value matches —
+ // App.xaml.cs's startup call uses the same default the constructor
+ // initialized to, so an equality short-circuit here would leave the
+ // actual thread CurrentCulture stuck on the system locale.
+ ApplyAmbientCulture(value);
+
if (string.Equals(_culture.Name, value.Name, StringComparison.OrdinalIgnoreCase))
{
return;
}
_culture = value;
- CultureInfo.DefaultThreadCurrentCulture = value;
- CultureInfo.DefaultThreadCurrentUICulture = value;
- CultureInfo.CurrentCulture = value;
- CultureInfo.CurrentUICulture = value;
OnChange?.Invoke();
}
}
@@ -47,4 +59,12 @@ public sealed class LocaleState
Culture = CultureInfo.GetCultureInfo(cultureName);
}
+
+ private static void ApplyAmbientCulture(CultureInfo c)
+ {
+ CultureInfo.DefaultThreadCurrentCulture = c;
+ CultureInfo.DefaultThreadCurrentUICulture = c;
+ CultureInfo.CurrentCulture = c;
+ CultureInfo.CurrentUICulture = c;
+ }
}
diff --git a/src/Marathon.UI/wwwroot/index.html b/src/Marathon.UI/wwwroot/index.html
index fd229df..ebc4eb2 100644
--- a/src/Marathon.UI/wwwroot/index.html
+++ b/src/Marathon.UI/wwwroot/index.html
@@ -38,8 +38,8 @@
×
-
-
+
+