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:
@@ -111,36 +111,36 @@ public static class AnomalyEvidenceParser
|
|||||||
private sealed class EvidenceDto
|
private sealed class EvidenceDto
|
||||||
{
|
{
|
||||||
[JsonPropertyName("suspensionGapSeconds")]
|
[JsonPropertyName("suspensionGapSeconds")]
|
||||||
public int SuspensionGapSeconds { get; set; }
|
public int SuspensionGapSeconds { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("preSuspension")]
|
[JsonPropertyName("preSuspension")]
|
||||||
public EvidenceSideDto? PreSuspension { get; set; }
|
public EvidenceSideDto? PreSuspension { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("postSuspension")]
|
[JsonPropertyName("postSuspension")]
|
||||||
public EvidenceSideDto? PostSuspension { get; set; }
|
public EvidenceSideDto? PostSuspension { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class EvidenceSideDto
|
private sealed class EvidenceSideDto
|
||||||
{
|
{
|
||||||
[JsonPropertyName("capturedAt")]
|
[JsonPropertyName("capturedAt")]
|
||||||
public DateTimeOffset CapturedAt { get; set; }
|
public DateTimeOffset CapturedAt { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("p1")]
|
[JsonPropertyName("p1")]
|
||||||
public decimal? P1 { get; set; }
|
public decimal? P1 { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("pDraw")]
|
[JsonPropertyName("pDraw")]
|
||||||
public decimal? PDraw { get; set; }
|
public decimal? PDraw { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("p2")]
|
[JsonPropertyName("p2")]
|
||||||
public decimal? P2 { get; set; }
|
public decimal? P2 { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("rate1")]
|
[JsonPropertyName("rate1")]
|
||||||
public decimal? Rate1 { get; set; }
|
public decimal? Rate1 { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("rateDraw")]
|
[JsonPropertyName("rateDraw")]
|
||||||
public decimal? RateDraw { get; set; }
|
public decimal? RateDraw { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("rate2")]
|
[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.Enums;
|
||||||
using Marathon.Domain.ValueObjects;
|
using Marathon.Domain.ValueObjects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
using DomainEventId = Marathon.Domain.ValueObjects.EventId;
|
|
||||||
using AngleSharpConfig = AngleSharp.Configuration;
|
using AngleSharpConfig = AngleSharp.Configuration;
|
||||||
|
using DomainEventId = Marathon.Domain.ValueObjects.EventId;
|
||||||
|
|
||||||
namespace Marathon.Infrastructure.Scraping.Parsers;
|
namespace Marathon.Infrastructure.Scraping.Parsers;
|
||||||
|
|
||||||
@@ -517,8 +516,11 @@ public sealed partial class EventOddsParser : IEventOddsParser
|
|||||||
value.HasValue ? new OddsValue(value.Value) : null,
|
value.HasValue ? new OddsValue(value.Value) : null,
|
||||||
new OddsRate(rate)));
|
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,
|
_logger.LogDebug(ex,
|
||||||
"Skipping bet ({Type}, {Side}, value={Value}, rate={Rate}) — invariant violation.",
|
"Skipping bet ({Type}, {Side}, value={Value}, rate={Rate}) — invariant violation.",
|
||||||
type, side, value, rate);
|
type, side, value, rate);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
@inject IStringLocalizer<SharedResource> L
|
@inject IStringLocalizer<SharedResource> L
|
||||||
@inject IAnomalyInsightsService InsightsService
|
@inject IAnomalyInsightsService InsightsService
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
@inject ILogger<Insights> Logger
|
||||||
|
|
||||||
<PageTitle>@L["App.Title"] · @L["Nav.Insights"]</PageTitle>
|
<PageTitle>@L["App.Title"] · @L["Nav.Insights"]</PageTitle>
|
||||||
|
|
||||||
@@ -658,8 +659,9 @@
|
|||||||
_vm = report;
|
_vm = report;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { /* superseded */ }
|
catch (OperationCanceledException) { /* superseded */ }
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Logger.LogError(ex, "Insights: failed to build the anomaly outcome report.");
|
||||||
_errored = true;
|
_errored = true;
|
||||||
_vm = null;
|
_vm = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,13 +282,6 @@
|
|||||||
|
|
||||||
private async Task<bool> ConfirmAsync()
|
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(
|
var result = await Dialogs.ShowMessageBox(
|
||||||
title: L["Settings.Confirm.Title"],
|
title: L["Settings.Confirm.Title"],
|
||||||
message: L["Settings.Confirm.Body"],
|
message: L["Settings.Confirm.Body"],
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
@using DomainEventId = Marathon.Domain.ValueObjects.EventId
|
@using DomainEventId = Marathon.Domain.ValueObjects.EventId
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inject IStringLocalizer<SharedResource> L
|
@inject IStringLocalizer<SharedResource> L
|
||||||
|
@inject ILogger<EventListShell> Logger
|
||||||
|
|
||||||
<section class="m-shell">
|
<section class="m-shell">
|
||||||
<header class="m-rise m-rise-1" style="display: grid; gap: var(--m-space-3); max-width: 880px;">
|
<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()
|
private void StartTimer()
|
||||||
{
|
{
|
||||||
_refreshTimer?.Dispose();
|
if (_refreshTimer is not null)
|
||||||
|
{
|
||||||
|
_refreshTimer.Elapsed -= OnRefreshTimerElapsed;
|
||||||
|
_refreshTimer.Dispose();
|
||||||
|
}
|
||||||
var interval = Math.Max(5, AutoRefreshSeconds) * 1000.0;
|
var interval = Math.Max(5, AutoRefreshSeconds) * 1000.0;
|
||||||
_refreshTimer = new System.Timers.Timer(interval) { AutoReset = true };
|
_refreshTimer = new System.Timers.Timer(interval) { AutoReset = true };
|
||||||
_refreshTimer.Elapsed += OnRefreshTimerElapsed;
|
_refreshTimer.Elapsed += OnRefreshTimerElapsed;
|
||||||
@@ -383,10 +388,11 @@
|
|||||||
{
|
{
|
||||||
await InvokeAsync(LoadAsync);
|
await InvokeAsync(LoadAsync);
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Swallowed — LoadAsync already handles its own errors; this catch
|
// Last line of defense for InvokeAsync itself — LoadAsync handles its
|
||||||
// is the last line of defense for InvokeAsync itself.
|
// 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.
|
// 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>();
|
_rows = new List<EventListItem>();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -523,7 +530,11 @@
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_refreshTimer?.Dispose();
|
if (_refreshTimer is not null)
|
||||||
|
{
|
||||||
|
_refreshTimer.Elapsed -= OnRefreshTimerElapsed;
|
||||||
|
_refreshTimer.Dispose();
|
||||||
|
}
|
||||||
_searchCts?.Cancel();
|
_searchCts?.Cancel();
|
||||||
_searchCts?.Dispose();
|
_searchCts?.Dispose();
|
||||||
_loadCts?.Cancel();
|
_loadCts?.Cancel();
|
||||||
|
|||||||
Reference in New Issue
Block a user