@page "/anomalies" @using Marathon.UI.Components @implements IDisposable @inject IStringLocalizer L @inject IAnomalyBrowsingService Anomalies @inject AnomalyBrowsingState State @inject NavigationManager Nav @L["App.Title"] ยท @L["Anomaly.Title"]
@L["Nav.Section.Analysis"]

@L["Anomaly.Title"]

@L["Anomaly.Lede"]

@L["Anomaly.Stat.Total"]
@_items.Count
@L["Anomaly.Severity.High"]
@_items.Count(i => i.Severity == AnomalySeverity.High)
@L["Anomaly.Severity.Medium"]
@_items.Count(i => i.Severity == AnomalySeverity.Medium)
@L["Anomaly.Severity.Low"]
@_items.Count(i => i.Severity == AnomalySeverity.Low)
@if (_loading && _items.Count == 0) {
@L["Common.Loading"]
} else if (_items.Count == 0) {
@L["Common.Empty"]

@L["Anomaly.Empty.NoneInRange"]

} else {
@foreach (var item in _items) { }
}
@code { private static readonly AnomalySeverity[] _severityOptions = { AnomalySeverity.Low, AnomalySeverity.Medium, AnomalySeverity.High }; private static readonly AnomalyKind[] _kindOptions = { AnomalyKind.SuspensionFlip, AnomalyKind.SteamMove, AnomalyKind.SuspensionFreeze, AnomalyKind.OverroundCompression }; private List _items = new(); private IReadOnlyList _availableSports = Array.Empty(); private bool _loading = true; private CancellationTokenSource? _loadCts; private AnomalyFilter _filter = new(); protected override async Task OnInitializedAsync() { _filter = State.Filter; State.OnChange += OnStateChanged; try { _availableSports = await Anomalies.ListKnownSportCodesAsync(CancellationToken.None); } catch { _availableSports = Array.Empty(); } await LoadAsync(); } private void OnStateChanged() { InvokeAsync(StateHasChanged); } private async Task LoadAsync() { _loadCts?.Cancel(); _loadCts = new CancellationTokenSource(); var ct = _loadCts.Token; _loading = true; try { var rows = await Anomalies.ListAsync(_filter, ct); if (ct.IsCancellationRequested) return; _items = rows.ToList(); var unread = await Anomalies.GetUnreadCountAsync(State.LastSeenUtc, ct); State.SetUnreadCount(unread); } catch (OperationCanceledException) { /* superseded */ } catch { _items = new List(); } finally { _loading = false; StateHasChanged(); } } private async Task UpdateFilter(AnomalyFilter next) { _filter = next; State.UpdateFilter(next); await LoadAsync(); } private Task ToggleSeverity(AnomalySeverity severity) => UpdateFilter(_filter with { MinSeverity = _filter.MinSeverity == severity ? null : severity }); private Task ClearSeverity() => UpdateFilter(_filter with { MinSeverity = null }); private Task ToggleSport(int code) { var existing = _filter.SportCodes?.ToList() ?? new List(); if (!existing.Remove(code)) existing.Add(code); return UpdateFilter(_filter with { SportCodes = existing.Count == 0 ? null : existing }); } private Task ToggleKind(AnomalyKind kind) { var existing = _filter.Kinds?.ToList() ?? new List(); if (!existing.Remove(kind)) existing.Add(kind); return UpdateFilter(_filter with { Kinds = existing.Count == 0 ? null : existing }); } private string KindLabel(AnomalyKind kind) => kind switch { AnomalyKind.SuspensionFlip => L["Anomaly.Kind.SuspensionFlip"], AnomalyKind.SteamMove => L["Anomaly.Kind.SteamMove"], AnomalyKind.SuspensionFreeze => L["Anomaly.Kind.SuspensionFreeze"], AnomalyKind.OverroundCompression => L["Anomaly.Kind.OverroundCompression"], _ => kind.ToString(), }; private async Task OnFromChanged(ChangeEventArgs e) { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, MoscowTime.Offset) }); } } private async Task OnToChanged(ChangeEventArgs e) { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { await UpdateFilter(_filter with { To = MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(v.Date)) }); } } private void MarkAllRead() { State.MarkAllSeen(DateTimeOffset.UtcNow); } private void OpenInsights() { Nav.NavigateTo("/anomalies/insights"); } private void HandleClick(AnomalyListItem item) { Nav.NavigateTo($"/anomalies/{item.Id}"); } private string SeverityLabel(AnomalySeverity s) => s switch { AnomalySeverity.High => L["Anomaly.Severity.High"], AnomalySeverity.Medium => L["Anomaly.Severity.Medium"], _ => L["Anomaly.Severity.Low"], }; private string SportLabel(int code) => SportLabels.Resolve(L, code); private static string FormatDate(DateTimeOffset? value) => value?.ToString("yyyy-MM-dd") ?? string.Empty; public void Dispose() { State.OnChange -= OnStateChanged; _loadCts?.Cancel(); _loadCts?.Dispose(); } }