refactor: hoist Moscow offset + sport labels into shared helpers (HIGH)

* New Marathon.Domain.ValueObjects.MoscowTime with Offset, Now, and
  EndOfMoscowDay(DateOnly) — replaces ~15 inline TimeSpan.FromHours(3)
  literals across Domain/Application/Infrastructure/UI.
* New Marathon.UI.Services.SportLabels.Resolve(IStringLocalizer, int) —
  replaces 6 near-identical SportLabel switch bodies in EventListShell,
  Events/Detail, Anomalies/AnomalyFeed, Results/ResultsList,
  Results/ResultsLoader, and AnomalyCard. Single source of truth for the
  6/11/22723/43658 sport-code mapping. Pages keep a one-liner wrapper so
  the call sites stay terse.
This commit is contained in:
2026-05-09 15:40:35 +03:00
parent d1e6ce7ce2
commit fed3a09695
18 changed files with 92 additions and 96 deletions
@@ -2,6 +2,7 @@ using Marathon.Application.Abstractions;
using Marathon.Application.Configuration;
using Marathon.Domain.AnomalyDetection;
using Marathon.Domain.Entities;
using Marathon.Domain.ValueObjects;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -24,7 +25,6 @@ namespace Marathon.Application.UseCases;
/// </remarks>
public sealed class DetectAnomaliesUseCase
{
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
private static readonly TimeSpan SnapshotLookback = TimeSpan.FromHours(24);
// Dedup window: two anomalies for the same event within this window are considered duplicates.
@@ -67,7 +67,7 @@ public sealed class DetectAnomaliesUseCase
var events = await _eventRepo.ListAsync(ct);
int newAnomalyCount = 0;
var now = DateTimeOffset.UtcNow.ToOffset(MoscowOffset);
var now = MoscowTime.Now;
var from = now - SnapshotLookback;
// Hoisted outside the per-event loop: load existing anomalies ONCE per cycle