* Filter changes from the parent (page-state singleton swap) now trigger
LoadAsync via OnParametersSetAsync once the component has rendered.
Previously the reload happened only on first render, so navigating back
to a list page with a different sport/date filter showed the prior data.
* Refresh-timer Elapsed handler is hoisted to a named async-void method
with try/catch around InvokeAsync. An unhandled exception on the timer
thread used to crash WebView2; now it's logged and swallowed.
* Overlapping ticks are skipped via a _loading short-circuit so a slow
Loader doesn't stack up cancelled-superseded loads.
* EventBrowsingService.BuildListAsync issued one snapshot query per event
on every page render — N+1 against SQLite, with each round-trip hauling
the full bet graph via Include(Bets). Replaced with a single
ISnapshotRepository.ListByEventsAsync batch.
* ListKnownSportCodesAsync / ListKnownCountryCodesAsync used to materialise
every Event row to compute Distinct() in memory. Pushed to EF projection
via two new IEventRepository methods (ListDistinctSportCodesAsync,
ListDistinctCountryCodesAsync) implemented as
.Select(...).Distinct().ToListAsync — single SELECT DISTINCT.
The component had a Dispose() method that detached ThemeState/LocaleState
event handlers, but it was never invoked because the @implements directive
was missing. Each navigation through the layout leaked two subscriptions
on the singleton state objects.
Other state subscribers (NavBody.razor, AnomalyFeed.razor) already declare
the directive correctly.
Cross-cutting polish that surfaced while Phase 8 was being implemented:
* Invariant-culture formatting on every decimal ToString("0.00" / "0.##")
(OddsCell, OddsTimeline, SeverityBadge, AnomalyEvidence,
Pages/Events/Detail) — previously the comma/dot decimal separator
switched with the locale and broke tabular alignment + tests.
* LocaleState constructor no longer mutates process-wide ambient culture;
apply only happens through Set(...). Stops parallel bUnit test runs
leaking ru-RU into each other's threads.
* MainLayout: drawer-open state now offsets main content / footer / appbar
by the drawer width (248px) so the sidebar no longer overlaps content.
Mobile breakpoint (≤720px) keeps the original full-width layout.
* wwwroot/index.html (Marathon.UI RCL): switched from Plotly CDN to the
bundled "_content/Plotly.Blazor/plotly-2.35.3.min.js" — works offline
and matches the Plotly.Blazor 5.4.1 version pin.
* Marathon.Hosts.WpfBlazor/wwwroot/index.html: host-level page that
BlazorWebView's HostPage attribute resolves to. Same Plotly bundle,
no autostart="false" (BlazorWebView auto-starts).
* Pages/Results/ResultsList.razor — completed-events list with date range,
sport/winner filter, search, footer count.
* Pages/Results/ResultsLoader.razor — driver page with two modes (load all
in range / load selected events), live progress reporting via
IProgress<PullResultsProgress>, summary line, cancellable.
* Replaces the Phase 5 Pages/Results.razor placeholder.
Service layer:
* IResultsBrowsingService + ResultsBrowsingService (Scoped, mirrors the
Event/Anomaly browsing-service pattern). Reads IResultRepository +
IEventRepository, projects to immutable view-model records.
* UiServicesExtensions: registers ResultsBrowsingService; also fixes an
unrelated localization resolver bug (drop ResourcesPath since
SharedResource lives in the Marathon.UI.Resources namespace already).
Localization:
* 41 new Results.* keys (RU+EN parity) covering both pages, filter chips,
loader modes, progress states, and footer copy.
Tests:
* ResultsListTests + ResultsLoaderTests — 22 new bUnit tests covering
filter narrowing, mode switching, progress aggregation, and empty
states.
* FakeResultsBrowsingService support type for tests.
* MarathonTestContext registers the fake; TestData adds factories for
EventResult/EventResultListItem.
Three fixes surfaced when launching the WPF host for the first time:
1. App.xaml.cs — call MarathonDbContextInitializer.InitializeAsync()
between Host.Build() and Host.Start() so EF migrations + WAL pragma
are applied BEFORE BackgroundServices race to query the DB. Without
this, all pollers crashed on 'no such table: Events'.
2. wwwroot/index.html — added <script src='https://cdn.plot.ly/plotly-2.35.2.min.js'>
before blazor.webview.js. Phase 6 reviewer flagged this for Phase 9,
but charts are unrenderable without it; better to ship now.
3. Migrations/20260505000000_InitialCreate.cs — added [DbContext] and
[Migration('20260505000000_InitialCreate')] attributes. Phase 2's
hand-written migration was missing both, so EF saw 'no migrations to
apply' even on a fresh DB. With the attributes, the migration runs
on first launch and creates all tables (Events, Snapshots, Bets,
EventResults, Anomalies, Sports, Leagues).
Verified: clean DB → migration applied → all 7 tables created → pollers
run with empty results (no data yet — UpcomingEventsPoller fires every 6h
by default; first scrape will populate the DB).
Phase 4 reviewer (Sonnet) flagged two 🟡 warnings; both addressed:
1. PullLiveOddsUseCase: removed dead 'liveEvents' assignment that called
ScrapeUpcomingAsync but never read the result. Replaced misleading
comment block with a single TODO(phase-6/8) note pointing to the
ListLiveAsync(cutoff) follow-up.
2. Marathon.UI.Services.WorkerOptions.UpcomingScheduleCron: changed default
from '0 */5 * * * *' (every 5 min) to '0 0 */6 * * *' (every 6 hours)
to match Marathon.Infrastructure.Configuration.WorkerOptions and the
appsettings.json default.
PLAN.md: Phase 4 row updated (review status, commit hash); top-level
checkbox in Phases list ticked.
Build 0/0, all 202 tests still passing.
Per Phase 1 reviewer notes — strips Component1.razor, ExampleJsInterop, and
the wwwroot template assets generated by 'dotnet new razorclasslib'. Phase 5
will populate Marathon.UI from scratch with the real layout, components, and
wwwroot/index.html for BlazorWebView. Build still green (0/0).
Creates the 9-project .NET 8 solution (5 src + 4 test) with Marathon.Domain
fully implemented: value objects (SportCode, EventId, OddsRate, OddsValue,
BetScope hierarchy), enums (Side, BetType, OddsSource, AnomalyKind), and
entities (Sport, Country, League, Event, Bet, OddsSnapshot, EventResult,
Anomaly) with all invariants enforced in constructors. 96 domain tests pass
(FluentAssertions + xUnit). Directory.Build.props and Directory.Packages.props
centralise build settings and NuGet versions. Both Marathon.sln and Marathon.slnx
are committed; dotnet build Marathon.sln succeeds with 0 warnings/errors.