refactor: log silenced UI errors, fix timer leak, narrow exception catch

- EventListShell: detach the Elapsed handler before disposing the refresh timer
  (both StartTimer and Dispose) to stop a leaked subscription firing on a
  torn-down component; log the two previously-silent catches.
- Insights: log the previously-silent report-load catch.
- EventOddsParser: narrow catch(Exception) to catch(ArgumentException) so only
  the OddsRate/OddsValue/Bet guard-clause throws are swallowed.
- AnomalyEvidenceData: make the JSON DTOs init-only per the immutability convention.
- Settings: remove a dead DialogParameters block.
This commit is contained in:
2026-05-28 22:34:17 +03:00
parent f294255f10
commit 0501f9c39c
5 changed files with 47 additions and 39 deletions
@@ -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; }
}
}
@@ -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;
@@ -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);
@@ -15,6 +15,7 @@
@inject IStringLocalizer<SharedResource> L
@inject IAnomalyInsightsService InsightsService
@inject NavigationManager Nav
@inject ILogger<Insights> Logger
<PageTitle>@L["App.Title"] · @L["Nav.Insights"]</PageTitle>
@@ -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;
}
-7
View File
@@ -282,13 +282,6 @@
private async Task<bool> 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"],
@@ -14,6 +14,7 @@
@using DomainEventId = Marathon.Domain.ValueObjects.EventId
@implements IDisposable
@inject IStringLocalizer<SharedResource> L
@inject ILogger<EventListShell> Logger
<section class="m-shell">
<header class="m-rise m-rise-1" style="display: grid; gap: var(--m-space-3); max-width: 880px;">
@@ -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<EventListItem>();
}
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();