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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user