diff --git a/src/Marathon.Domain/AnomalyDetection/AnomalyEvidenceData.cs b/src/Marathon.Domain/AnomalyDetection/AnomalyEvidenceData.cs index f02b2ab..17d1883 100644 --- a/src/Marathon.Domain/AnomalyDetection/AnomalyEvidenceData.cs +++ b/src/Marathon.Domain/AnomalyDetection/AnomalyEvidenceData.cs @@ -111,36 +111,36 @@ public static class AnomalyEvidenceParser private sealed class EvidenceDto { [JsonPropertyName("suspensionGapSeconds")] - public int SuspensionGapSeconds { get; set; } + public int SuspensionGapSeconds { get; init; } [JsonPropertyName("preSuspension")] - public EvidenceSideDto? PreSuspension { get; set; } + public EvidenceSideDto? PreSuspension { get; init; } [JsonPropertyName("postSuspension")] - public EvidenceSideDto? PostSuspension { get; set; } + public EvidenceSideDto? PostSuspension { get; init; } } private sealed class EvidenceSideDto { [JsonPropertyName("capturedAt")] - public DateTimeOffset CapturedAt { get; set; } + public DateTimeOffset CapturedAt { get; init; } [JsonPropertyName("p1")] - public decimal? P1 { get; set; } + public decimal? P1 { get; init; } [JsonPropertyName("pDraw")] - public decimal? PDraw { get; set; } + public decimal? PDraw { get; init; } [JsonPropertyName("p2")] - public decimal? P2 { get; set; } + public decimal? P2 { get; init; } [JsonPropertyName("rate1")] - public decimal? Rate1 { get; set; } + public decimal? Rate1 { get; init; } [JsonPropertyName("rateDraw")] - public decimal? RateDraw { get; set; } + public decimal? RateDraw { get; init; } [JsonPropertyName("rate2")] - public decimal? Rate2 { get; set; } + public decimal? Rate2 { get; init; } } } diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs b/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs index c71b245..c96bc11 100644 --- a/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs +++ b/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs @@ -6,9 +6,8 @@ using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; using Microsoft.Extensions.Logging; - -using DomainEventId = Marathon.Domain.ValueObjects.EventId; using AngleSharpConfig = AngleSharp.Configuration; +using DomainEventId = Marathon.Domain.ValueObjects.EventId; namespace Marathon.Infrastructure.Scraping.Parsers; @@ -110,7 +109,7 @@ public sealed partial class EventOddsParser : IEventOddsParser // no longer rescans the document with QuerySelector // for every key — that was an O(N) cost paid 6× per // period). - var priceIndex = BuildSelectionPriceIndex(selections); + var priceIndex = BuildSelectionPriceIndex(selections); var elementIndex = BuildSelectionElementIndex(selections); var bets = new List(); @@ -187,19 +186,19 @@ public sealed partial class EventOddsParser : IEventOddsParser // Try each market variant; first match wins foreach (var market in MatchResultMarkets) { - var win1Key = $"{eventId}@{market}.1"; - var drawKey = $"{eventId}@{market}.draw"; - var win2Key = $"{eventId}@{market}.3"; + var win1Key = $"{eventId}@{market}.1"; + var drawKey = $"{eventId}@{market}.draw"; + var win2Key = $"{eventId}@{market}.3"; // Basketball 2-way OT market uses HB_H / HB_A - var hbhKey = $"{eventId}@{market}.HB_H"; - var hbaKey = $"{eventId}@{market}.HB_A"; + var hbhKey = $"{eventId}@{market}.HB_H"; + var hbaKey = $"{eventId}@{market}.HB_A"; - var hasWin1 = idx.TryGetValue(win1Key, out var rate1); - var hasDraw = idx.TryGetValue(drawKey, out var rateDraw); - var hasWin2 = idx.TryGetValue(win2Key, out var rate2); - var hasHbh = idx.TryGetValue(hbhKey, out var rateHbh); - var hasHba = idx.TryGetValue(hbaKey, out var rateHba); + var hasWin1 = idx.TryGetValue(win1Key, out var rate1); + var hasDraw = idx.TryGetValue(drawKey, out var rateDraw); + var hasWin2 = idx.TryGetValue(win2Key, out var rate2); + var hasHbh = idx.TryGetValue(hbhKey, out var rateHbh); + var hasHba = idx.TryGetValue(hbaKey, out var rateHba); if (hasWin1 || hasDraw || hasWin2 || hasHbh || hasHba) { @@ -517,8 +516,11 @@ public sealed partial class EventOddsParser : IEventOddsParser value.HasValue ? new OddsValue(value.Value) : null, new OddsRate(rate))); } - catch (Exception ex) + catch (ArgumentException ex) { + // OddsValue / OddsRate / Bet guard clauses throw ArgumentException and its + // derivatives (ArgumentNullException, ArgumentOutOfRangeException). Catch + // only those — anything else is a real bug that must not be swallowed here. _logger.LogDebug(ex, "Skipping bet ({Type}, {Side}, value={Value}, rate={Rate}) — invariant violation.", type, side, value, rate); diff --git a/src/Marathon.UI/Pages/Anomalies/Insights.razor b/src/Marathon.UI/Pages/Anomalies/Insights.razor index 45de595..5870c1a 100644 --- a/src/Marathon.UI/Pages/Anomalies/Insights.razor +++ b/src/Marathon.UI/Pages/Anomalies/Insights.razor @@ -15,6 +15,7 @@ @inject IStringLocalizer L @inject IAnomalyInsightsService InsightsService @inject NavigationManager Nav +@inject ILogger Logger @L["App.Title"] · @L["Nav.Insights"] @@ -658,8 +659,9 @@ _vm = report; } catch (OperationCanceledException) { /* superseded */ } - catch + catch (Exception ex) { + Logger.LogError(ex, "Insights: failed to build the anomaly outcome report."); _errored = true; _vm = null; } diff --git a/src/Marathon.UI/Pages/Settings.razor b/src/Marathon.UI/Pages/Settings.razor index ec2773a..61f7da3 100644 --- a/src/Marathon.UI/Pages/Settings.razor +++ b/src/Marathon.UI/Pages/Settings.razor @@ -282,13 +282,6 @@ private async Task ConfirmAsync() { - var parameters = new DialogParameters - { - ["ContentText"] = L["Settings.Confirm.Body"].Value, - ["ButtonText"] = L["Settings.Action.Save"].Value, - ["CancelText"] = L["Common.Cancel"].Value, - }; - var result = await Dialogs.ShowMessageBox( title: L["Settings.Confirm.Title"], message: L["Settings.Confirm.Body"], diff --git a/src/Marathon.UI/Pages/Shared/EventListShell.razor b/src/Marathon.UI/Pages/Shared/EventListShell.razor index 9bffc48..9b1a890 100644 --- a/src/Marathon.UI/Pages/Shared/EventListShell.razor +++ b/src/Marathon.UI/Pages/Shared/EventListShell.razor @@ -14,6 +14,7 @@ @using DomainEventId = Marathon.Domain.ValueObjects.EventId @implements IDisposable @inject IStringLocalizer L +@inject ILogger Logger
@@ -364,7 +365,11 @@ private void StartTimer() { - _refreshTimer?.Dispose(); + if (_refreshTimer is not null) + { + _refreshTimer.Elapsed -= OnRefreshTimerElapsed; + _refreshTimer.Dispose(); + } var interval = Math.Max(5, AutoRefreshSeconds) * 1000.0; _refreshTimer = new System.Timers.Timer(interval) { AutoReset = true }; _refreshTimer.Elapsed += OnRefreshTimerElapsed; @@ -383,10 +388,11 @@ { await InvokeAsync(LoadAsync); } - catch + catch (Exception ex) { - // Swallowed — LoadAsync already handles its own errors; this catch - // is the last line of defense for InvokeAsync itself. + // Last line of defense for InvokeAsync itself — LoadAsync handles its + // own errors. Log rather than silently dropping the failure. + Logger.LogError(ex, "EventListShell ({Surface}): auto-refresh tick failed.", Surface); } } @@ -414,9 +420,10 @@ { // Swallow — superseded by a newer load. } - catch + catch (Exception ex) { - // Hide errors from the UI; Phase 9 will add a snackbar. + // Degrade gracefully (clear the rows) but record the failure for diagnosis. + Logger.LogError(ex, "EventListShell ({Surface}): failed to load event rows.", Surface); _rows = new List(); } finally @@ -523,7 +530,11 @@ public void Dispose() { - _refreshTimer?.Dispose(); + if (_refreshTimer is not null) + { + _refreshTimer.Elapsed -= OnRefreshTimerElapsed; + _refreshTimer.Dispose(); + } _searchCts?.Cancel(); _searchCts?.Dispose(); _loadCts?.Cancel();