feat(phase-7-frontend): anomaly feed UI + nav badge + Settings toggle (+31 bUnit tests)

Frontend portion of Phase 7. Backend (commit a6ff368) had already shipped
the AnomalyDetector, DetectAnomaliesUseCase, AnomalyDetectionPoller, and
all DI wiring. This commit adds the user-facing surfaces.

New surfaces (Option A routing — folder-per-feature):
- Pages/Anomalies/AnomalyFeed.razor (@page /anomalies) — replaces the
  Phase 5 placeholder with a severity-coded card stream, filter chips
  (severity / sport / date), unread-count summary, 'Mark all read' action.
- Pages/Anomalies/Detail.razor (@page /anomalies/{id:guid}) — m-detail-header
  lockup + AnomalyEvidence panel + back link to /events/{eventCode}.

New components:
- AnomalyCard.razor — severity-tinted left border (signal-red on High,
  amber on Medium, neutral on Low) + SeverityBadge pill + sport icon +
  pre→post tabular-mono rate strip + relative time. Click navigates.
- SeverityBadge.razor — small pill mapping score → bucket per backend
  handoff (Low <0.45, Medium <0.60, High ≥0.60).
- AnomalyEvidence.razor — two-column pre/post panel with implied-prob
  bars + raw rates; favourite-swap callout when argmax(p_pre) ≠ argmax(p_post);
  signal-red 3px left border on the post column. Handles 2-way (no draw).

State + service split mirrors Phase 6's pattern:
- AnomalyViewModels.cs — AnomalyListItem / AnomalyDetailVm / Severity enum
  / AnomalyEvidenceSnapshot record. Severity computed in the view-model
  from Score.
- IAnomalyBrowsingService / AnomalyBrowsingService — wraps IAnomalyRepository,
  parses Anomaly.EvidenceJson into typed view-models, applies filters
  client-side. Methods: ListAsync(filter, ct), GetByIdAsync(id, ct),
  GetUnreadCountAsync(since, ct).
- AnomalyBrowsingState — Singleton holding AnomalyFilter (severity threshold,
  sport set, date range) + LastSeenUtc + cached UnreadCount. OnChange event.

Nav badge:
- NavBody.razor subscribes to AnomalyBrowsingState.OnChange, renders a
  pulsing red m-nav__badge when UnreadCount > 0. Badge resets when the
  user clicks 'Mark all read' on the feed toolbar.

Settings toggle:
- Settings.razor — added Workers:AnomalyDetectionEnabled toggle (backend
  added the flag). Localized via Settings.Worker.AnomalyDetectionEnabled.
- Marathon.UI.Services.WorkerOptions mirror — added AnomalyDetectionEnabled
  (default true).

Localization: +30 RU/EN keys following the dot-segmented convention
(Anomaly.*, Settings.Worker.AnomalyDetectionEnabled). Full key parity verified.

Tests (+31 bUnit, all passing):
- AnomalyFeedTests, AnomalyDetailTests
- AnomalyCardTests, SeverityBadgeTests, AnomalyEvidenceTests
- FakeAnomalyBrowsingService support fake registered in MarathonTestContext.

Routing: deleted the Phase 5 Pages/Anomalies.razor placeholder; new feed
page lives at Pages/Anomalies/AnomalyFeed.razor.

Build: 0 warnings, 0 errors.
Tests: Domain 109 + Application 19 + Infrastructure 80 + UI 68 = 276/276
(baseline 245, +31 new bUnit tests, no regressions).

Phase 7 status:  Done (backend + frontend both complete, awaiting review).

Known deferral: AnomalyBrowsingState.LastSeenUtc is in-memory only; the
unread-count badge resets on app restart. Acceptable for now; Phase 9 may
extend ISettingsWriter or add an ILastSeenStore.
This commit is contained in:
2026-05-05 13:39:39 +03:00
parent a6ff368015
commit 12208a4762
27 changed files with 2273 additions and 32 deletions
+47 -1
View File
@@ -86,7 +86,7 @@ with scraping research, no implementation.
| Phase 4 | phase-implementer | Sonnet 4.6 | ⏭️ Skipped (Big Bang) | — | ✅ Done 2026-05-05. 4 use cases, 3 BackgroundService pollers, InfrastructureModule, ApplicationModule, reflection wiring removed. 202/202 tests green (+17 new). |
| Phase 5 | phase-implementer-frontend | Opus | ⏭️ Skipped (Big Bang) | ✅ With 2 + 3 | Uses frontend-design skill |
| Phase 6 | phase-implementer-frontend | Opus | ⏭️ Skipped (Big Bang) | — | ✅ Done 2026-05-05. PreMatch + Live + Events/Detail pages, EventListShell, SportIcon, OddsCell, OddsTimeline (Plotly.Blazor wrap), ExportDialog. EventBrowsingState + IEventBrowsingService facade. RU+EN strings under PreMatch.* / Live.* / Detail.* / Export.* / Sport.*. 228/228 tests green (+26 new bUnit). |
| Phase 7 | phase-implementer (split if needed) | Sonnet/Opus | ⏭️ Skipped (Big Bang) | — | UI portion uses Opus |
| Phase 7 | phase-implementer (split + UI Opus 1M) | Sonnet/Opus | ⏭️ Skipped (Big Bang) | — | ✅ Done 2026-05-05. Backend (Sonnet, a6ff368): pure `AnomalyDetector` + `DetectAnomaliesUseCase` + `AnomalyDetectionPoller` + 14 backend tests. Frontend (Opus 1M): `AnomalyFeed.razor` + `Detail.razor` + `AnomalyCard`/`SeverityBadge`/`AnomalyEvidence` components + `IAnomalyBrowsingService`/`AnomalyBrowsingService`/`AnomalyBrowsingState`/`AnomalyViewModels`. Nav badge with pulsing signal-red unread count. Settings page wired with `Workers:AnomalyDetectionEnabled`. 28 new `Anomaly.*` localization keys (RU+EN parity). 276/276 tests green (+31 new bUnit). |
| Phase 8 | phase-implementer (split if needed) | Sonnet/Opus | ⏭️ Skipped (Big Bang) | — | UI portion uses Opus |
| Phase 9 | phase-implementer | Sonnet 4.6 | ✅ Final phase tests | — | Full build + test enforced |
@@ -139,6 +139,52 @@ with scraping research, no implementation.
- **Test note:** rates 1.5/2.5 produce a flip score of ~0.25 — BELOW the 0.30 threshold.
Always use 1.3/4.0 (flip score ~0.51) or steeper to guarantee detection in tests.
### Phase 7 Frontend (Anomaly UI, 2026-05-05)
- **Routing — Option A.** Removed the `Pages/Anomalies.razor` placeholder and added
`Pages/Anomalies/AnomalyFeed.razor` (`@page "/anomalies"`) plus
`Pages/Anomalies/Detail.razor` (`@page "/anomalies/{id:guid}"`). Mirrors the
`Pages/Events/Detail.razor` shape from Phase 6.
- **State + Service split mirrors Phase 6** — `AnomalyBrowsingState` (Singleton inside
the RCL; per-circuit in BlazorWebView), `IAnomalyBrowsingService`
`AnomalyBrowsingService` (Scoped). The service does NOT call back into the detector;
it reads `IAnomalyRepository.ListAsync` + `IEventRepository.GetAsync` (per distinct
EventId) and maps to immutable view-model records.
- **`EvidenceJson` parsing** uses `System.Text.Json.JsonSerializer.Deserialize` with
`PropertyNameCaseInsensitive = true` and private nested DTOs. Failures (malformed
JSON, missing pre/post snapshot) drop the row silently — the feed shows the rest.
- **Severity buckets** are defined once in `AnomalySeverityRules` (Low <0.45, Medium
<0.60, High ≥0.60) per the backend handoff. The UI reuses the same enum across
filter chips, the badge pill, and the card border.
- **Signal-red is load-bearing.** High-severity pills, card left borders, evidence
post-suspension column outline, the favourite-swap callout, and the nav badge all
bind to `--m-c-anomaly`. Medium severity uses the editorial amber `--m-c-accent`;
low severity uses the muted `--m-c-ink-soft`. No new color literals introduced.
- **`AnomalyEvidence` panel** renders two columns (pre → arrow → post). Each row
shows the side label, an implied-probability bar (favourite uses amber/red), and
the raw rate in tabular mono. 2-way markets (tennis) skip the Draw row in BOTH
columns based on the parsed `pDraw` being null. The panel highlights a
favourite-swap with a one-line callout above the columns.
- **Nav badge** lives in `NavBody.razor`, driven by `AnomalyBrowsingState.UnreadCount`.
The feed page calls `IAnomalyBrowsingService.GetUnreadCountAsync(LastSeenUtc)` after
each load and pushes the count into state. The user clears it via "Mark all read"
on the feed toolbar (writes `LastSeenUtc = UtcNow`). The badge pulses with
`m-pulse` and respects `prefers-reduced-motion`.
- **Settings page** — added the `Workers:AnomalyDetectionEnabled` toggle inside the
existing WORKERS section, mirroring `LivePollerEnabled` / `UpcomingPollerEnabled`.
Bound via `IOptionsMonitor<WorkerOptions>` already in scope.
- **`Marathon.UI.Services.WorkerOptions`** — added `AnomalyDetectionEnabled` mutable
field (set-able for the form-binding pattern used by the Settings page). The
Infrastructure-side `WorkerOptions` already had the flag.
- **Test infrastructure** — added `FakeAnomalyBrowsingService` with
`MakeItem(...)` / `MakeSnapshot(...)` static factories; registered in
`MarathonTestContext` alongside `AnomalyBrowsingState`.
- **Localization** — 28 new `Anomaly.*` keys (RU+EN parity) under the
`<Surface>.<Element>` convention from Phase 5/6, plus
`Settings.Workers.AnomalyDetectionEnabled` and its `.Hint`.
- **New test count: +31** (9 SeverityBadge + 6 AnomalyCard + 6 AnomalyEvidence +
5 AnomalyFeed + 5 AnomalyDetail). Total: 276/276 passing.
### Phase 6 (Event browsing UI, 2026-05-05)
- **Plotly.Blazor pinned to 5.4.1.** v7.x exists but introduces breaking changes;