Files
alexei.dolgolyov e4d8476782 WIP(initial-implementation): parallel batch P2/P3/P5 — code complete, unreviewed
Snapshot of the parallel batch (Phases 2 + 3 + 5) at session pause. Solution does
NOT build cleanly yet — known cross-phase compile issues remain to be resolved
before review. See plans/initial-implementation/PLAN.md "Resume Notes" section
for the exact tomorrow-morning action list.

Phase 2 (Storage):
- Repository interfaces in Marathon.Application/Abstractions
- DateRange, ExportKind, StorageOptions in Marathon.Application/Storage
- EF Core 8 + SQLite (WAL) persistence: 7 entities + configurations + 4 repos
- Hand-written InitialCreate migration (dotnet ef blocked by parallel work)
- ClosedXML ExcelExporter with exact customer-spec wide columns
- PersistenceModule.AddMarathonPersistence DI extension
- Round-trip + export tests (cannot run yet — see cross-phase issues)

Phase 3 (Scraping):
- IOddsScraper, IBetPlacer in Marathon.Application/Abstractions
- ScrapingOptions in Marathon.Infrastructure/Configuration
- MarathonbetScraper with 4 parsers (Upcoming, Live, EventOdds, Results)
- Helpers: ServerTimeProvider, PeriodScopeMapper, OutcomeCodeMapper, MoscowDateParser
- UserAgentRotatorHandler + Polly v8 resilience pipeline
- ScrapingModule.AddMarathonScraping DI extension
- GlobalUsings.cs aliases for EventId / Configuration disambiguation
- Parser tests with trimmed HTML fixtures
- ScrapeResultsAsync interim no-op (Phase 8 will replace via watch-list polling)

Phase 5 (UI shell — killed mid-final-verify, assumed ~95%):
- Marathon.UI populated: MainLayout, App.razor, Pages (Home, Settings),
  Components, Theme (MarathonTheme.cs + Tokens.cs + app.css), Resources
  (SharedResource.{cs,ru.resx,en.resx}), Services (ISettingsWriter), wwwroot
- WPF host: App.xaml(.cs), MainWindow.xaml(.cs), Marathon.Hosts.WpfBlazor.csproj
  with Microsoft.AspNetCore.Components.WebView.Wpf + MudBlazor + Serilog
- appsettings.json + appsettings.Development.json with all sections wired
- bUnit tests: MainLayoutTests, LocaleSwitcherTests, ThemeToggleTests,
  JsonSettingsWriterTests + Support helpers

Cross-phase issues to resolve at next session:
1. Phase 2 repository classes are 'internal' — Phase 3's tests can't reference
   them. Fix: add InternalsVisibleTo to Marathon.Infrastructure.csproj.
2. Phase 5: LocalizationOptions namespace ambiguity (AspNetCore vs Extensions).
3. Phase 5: WpfBlazor Serilog API mismatch.

Reviewer has NOT run on this batch. Move to Phase 4 only after build is green
and a combined parallel-batch reviewer passes.
2026-05-05 01:56:53 +03:00

18 KiB
Raw Permalink Blame History

Phase 5: Blazor Hybrid Host + Theme + Localization

Status: Done Parent plan: PLAN.md Domain: frontend Implementer: Opus + frontend-design skill

Objective

Create the WPF + BlazorWebView host that loads Marathon.UI (Razor Class Library), establish the design system / theme using MudBlazor, set up bilingual (RU/EN) localization end-to-end, and wire up DI to compose Application + Infrastructure layers.

Tasks

  • In src/Marathon.Hosts.WpfBlazor/Marathon.Hosts.WpfBlazor.csproj:
    • Set <UseWPF>true</UseWPF>, <UseWindowsForms>false</UseWindowsForms>
    • SDK: Microsoft.NET.Sdk.Razor (so Razor + WPF interop works)
    • Add packages:
      • Microsoft.AspNetCore.Components.WebView.Wpf
      • MudBlazor
      • Microsoft.Extensions.Hosting
      • Serilog.Extensions.Hosting
      • Serilog.Sinks.File
      • Serilog.Sinks.Console
  • In src/Marathon.UI/Marathon.UI.csproj:
    • SDK: Microsoft.NET.Sdk.Razor
    • <TargetFramework>net8.0</TargetFramework> with WebView for Razor Components
    • Add MudBlazor (so components in this RCL can use MudBlazor)
  • Create Marathon.UI/_Imports.razor with namespace and component imports (Microsoft.AspNetCore.Components.*, MudBlazor, project namespaces).
  • Create Marathon.UI/wwwroot/index.html (Blazor host HTML for the WebView).
  • Create Marathon.UI/MainLayout.razor with MudBlazor MudLayout + MudAppBar + MudDrawer navigation. Include locale switcher (RU/EN) in the AppBar.
  • Create Marathon.UI/Pages/Home.razor placeholder dashboard.
  • Create Marathon.UI/Pages/Settings.razor — bound to all appsettings.json options (ScrapingOptions, WorkerOptions, StorageOptions, AnomalyOptions, LocalizationOptions). Live save via IOptionsMonitor + writing back to appsettings.Local.json.
  • Establish theme tokens in Marathon.UI/Theme/MarathonTheme.cs — distinctive palette per frontend-design guidance, NOT generic AI-default. Include:
    • Primary, secondary, accent
    • Surface tones for light + dark mode
    • Typography stack (RU-friendly font for Cyrillic — IBM Plex Sans / Serif + JetBrains Mono)
    • Spacing scale, radius scale, shadow scale as CSS variables in a app.css
  • Wire MudBlazor theme via MudThemeProvider in MainLayout.razor.
  • Localization:
    • Add Microsoft.Extensions.Localization to Marathon.UI
    • Create Marathon.UI/Resources/SharedResource.cs (marker class for IStringLocalizer)
    • Add Marathon.UI/Resources/SharedResource.ru.resx and SharedResource.en.resx with all UI strings used in this phase + placeholders for later phases
    • Configure supported cultures in host: ru-RU, en-US
    • Locale switcher persists choice to appsettings.Local.json and reloads UI
  • In src/Marathon.Hosts.WpfBlazor/MainWindow.xaml:
    • Single BlazorWebView filling the window
    • HostPage="wwwroot/index.html"
    • RootComponents add <RootComponent Selector="#app" ComponentType="{x:Type ui:App}" /> (uses App.razor Router instead of MainLayout directly so navigation works)
  • In src/Marathon.Hosts.WpfBlazor/App.xaml.cs:
    • Build IHost via Host.CreateApplicationBuilder()
    • Call services.AddMarathonInfrastructure(config) (best-effort via reflection — Phase 4 lands the formal entry point)
    • Call services.AddMarathonApplication(config) (best-effort, same)
    • Call services.AddWpfBlazorWebView()
    • Add MudBlazor: services.AddMudServices()
    • Configure Serilog (rolling file at ./logs/marathon-.log, console)
    • Start the host on OnStartup, stop on OnExit
  • Add appsettings.json to Marathon.Hosts.WpfBlazor/ with all sections. Add appsettings.Development.json template.
  • Tests in Marathon.UI.Tests (using bUnit):
    • Test: MainLayout renders brand + navigation; toggles theme via state
    • Test: locale switcher changes culture and persists to settings
    • Test: theme toggle flips state and notifies subscribers only on real change
    • Test (bonus): JsonSettingsWriter round-trip + section reset

Files to Modify/Create

  • src/Marathon.UI/_Imports.razor
  • src/Marathon.UI/App.razor
  • src/Marathon.UI/MainLayout.razor
  • src/Marathon.UI/Pages/Home.razor, Pages/Settings.razor, Pages/PreMatch.razor, Pages/Live.razor, Pages/Anomalies.razor, Pages/Results.razor, Pages/Placeholders.razor
  • src/Marathon.UI/Theme/MarathonTheme.cs, Theme/Tokens.cs
  • src/Marathon.UI/wwwroot/index.html, wwwroot/app.css
  • src/Marathon.UI/Resources/SharedResource.{cs,ru.resx,en.resx}
  • src/Marathon.UI/Components/LocaleSwitcher.razor, ThemeToggle.razor, AppBrand.razor, NavBody.razor, StatCard.razor, PipelineStep.razor, Field.razor, SectionFooter.razor
  • src/Marathon.UI/Services/UiServicesExtensions.cs, ThemeState.cs, LocaleState.cs, LocalizationOptions.cs, WorkerOptions.cs, AnomalyOptions.cs, ScrapingSettingsForm.cs, ISettingsWriter.cs, JsonSettingsWriter.cs
  • src/Marathon.Hosts.WpfBlazor/App.xaml, App.xaml.cs
  • src/Marathon.Hosts.WpfBlazor/MainWindow.xaml, MainWindow.xaml.cs
  • src/Marathon.Hosts.WpfBlazor/appsettings.json, appsettings.Development.json
  • tests/Marathon.UI.Tests/MainLayoutTests.cs, LocaleSwitcherTests.cs, ThemeToggleTests.cs, JsonSettingsWriterTests.cs, Support/MarathonTestContext.cs, Support/TestSettingsWriter.cs, Support/TestLocalizer.cs

Acceptance Criteria

  • Host project compiles (Big Bang smoke check). All Phase-5-owned projects build clean.
  • Marathon.UI is a clean RCL — references only Domain + Application, no WPF/BlazorWebView. Verified by dotnet build src/Marathon.UI/Marathon.UI.csproj.
  • Theme is distinct: editorial-quant aesthetic. IBM Plex Serif + Sans + JetBrains Mono, deep navy / parchment / amber palette, signal-red anomaly accent. No Inter, no purple gradients.
  • Locale switcher works (segmented RU/EN control wired through LocaleState, flips CultureInfo.CurrentUICulture, persists to appsettings.Local.json).
  • Settings page surfaces every configurable parameter from appsettings.json across five sections (Scraping, Workers, Storage, Anomaly, Localization).

Notes

  • This phase ran parallel with Phases 2 and 3 per the plan.
  • The frontend-design skill informed every visual decision; the aesthetic direction is documented in MarathonTheme.cs header and the Handoff section below.
  • Cyrillic-friendly fonts: IBM Plex Serif/Sans + JetBrains Mono are loaded from Google Fonts in wwwroot/index.html with display=swap.
  • For BlazorWebView in WPF, the project SDK is Microsoft.NET.Sdk.Razor and OutputType is WinExe with WPF enabled.

Review Checklist

  • Compiles (Marathon.UI, Marathon.UI.Tests, Marathon.Hosts.WpfBlazor all green)
  • Marathon.UI references no host-specific code (BlazorWebView, WPF)
  • Theme not generic — distinctive palette + serif display + mono numerals
  • All appsettings.json keys reachable via the Settings page
  • RU + EN both renderable (full key parity)
  • Accessibility: keyboard nav, visible amber focus rings, ARIA labels on icon buttons and segmented controls

Handoff to Next Phase

Aesthetic direction — "Editorial-Quant"

Inspired by long-form data journalism (FT, Quartz) and trading terminals (Bloomberg). Confident, dense, serif-led on display surfaces. Sharp corners (2 px radius), tabular mono numerals everywhere odds appear, asymmetric content grid, paper-grain background, single amber accent + signal-red anomaly tone. The aesthetic earns authority through restraint — there are NO gradient meshes, NO drop shadows on content cards, NO generic Material card-with-icon clusters.

Typography

Role Stack
Display (H1H3) "IBM Plex Serif", "PT Serif", Georgia, serif
Body (H4H6, Body, Subtitle, Button) "IBM Plex Sans", "PT Sans", system-ui, sans-serif
Numerals / Caption / Overline / kicker "JetBrains Mono", "IBM Plex Mono", "Fira Code", Consolas, monospace

All three families have full Cyrillic coverage. Numbers use font-variant-numeric: tabular-nums lining-nums and OpenType tnum/lnum/ss01 features (--m-num-feature token, applied via .m-num, .m-mono, all Mud table cells, and any element with data-numeric).

Theme tokens (CSS variables in app.css, mirrored in Theme/Tokens.cs)

Token Light Dark Purpose
--m-c-ink #0f172a #f5f5f4 Primary text / ink
--m-c-paper #fafaf7 #1c1917 Surface
--m-c-paper-2 #f5f4ef #0c0a09 Background
--m-c-rule #e7e5e4 #292524 Dividers, borders
--m-c-accent #d97706 #fbbf24 Amber accent (kickers, focus rings, hover)
--m-c-anomaly #dc2626 #f87171 Load-bearing for Phase 7 anomaly UI
--m-c-positive #15803d #4ade80 Confirmations, OK status
--m-c-info #0369a1 #38bdf8 Informational accents

Spacing scale: --m-space-1--m-space-9 (4 → 96 px). Radius scale: --m-radius-sharp (0) → --m-radius-lg (10 px) — defaults to --m-radius-xs (2 px). Shadow scale: defined inline in MarathonTheme.cs::MarathonShadows. Use sparingly; the language is borders, not shadows.

The MudBlazor MudTheme is built in Marathon.UI.Theme.MarathonTheme.Build(). Phase 6 should consume the Mud palette via Color.Primary, Color.Tertiary (= amber accent), Color.Error (= anomaly signal). Do NOT hard-code hexes outside MarathonTheme.cs and app.css.

Component primitives available to Phase 6+

Component Path Purpose
<AppBrand /> Components/AppBrand.razor Wordmark + dateline lockup for the AppBar
<NavBody /> Components/NavBody.razor Drawer navigation (dark surface, amber active state)
<LocaleSwitcher /> Components/LocaleSwitcher.razor RU/EN segmented control
<ThemeToggle /> Components/ThemeToggle.razor Light/dark icon button
<StatCard Label Value Delta Anomaly /> Components/StatCard.razor Editorial stat block (kicker + mono value + delta)
<PipelineStep Index Label Status /> Components/PipelineStep.razor Numbered status row (ok/warn/error/idle)
<Field Label Hint>... Components/Field.razor 240 px label column + control column with hint text
<SectionFooter OnSave /> Components/SectionFooter.razor Right-aligned save bar inside .m-section

CSS primitives (raw classes in app.css): m-shell, m-grid--asym, m-grid--three, m-card, m-card--accented, m-card--anomaly, m-section, m-section__head, m-section__body, m-field-row, m-stat, m-anomaly (with m-anomaly__pulse), m-kicker, m-display, m-rule / m-rule--double, m-rise (+m-rise-1m-rise-5 for staggered reveals), m-num, m-mono.

Localization key naming convention

Dot-segmented <Surface>.<Element> (sub-segmented as needed):

  • App.* — application chrome (App.Title, App.BrandMark, App.Dateline, App.Tagline)
  • Nav.* — primary navigation labels and section headings (Nav.Section.Analysis, Nav.Dashboard, Nav.PreMatch, Nav.Live, Nav.Anomalies, Nav.Results, Nav.Settings, Nav.Section.System)
  • Home.* — dashboard surfaces (Home.Kicker, Home.Title, Home.Lede, Home.Stat.*, Home.Section.*, Home.Pipeline.Step1..4, Home.Empty)
  • Settings.* — settings page; further nested by section (Settings.Section.Scraping, Settings.Scraping.<Field>, Settings.Scraping.<Field>.Hint, etc.)
  • Locale.* — locale switcher labels (Locale.Russian, Locale.English, Locale.Tooltip.Switch)
  • Theme.* — theme toggle (Theme.Toggle.Light, Theme.Toggle.Dark)
  • Common.* — shared verbs/nouns (Common.Save, Common.Cancel, Common.Reset, Common.Loading, Common.Empty, Common.Yes, Common.No)
  • Anomaly.* — anomaly feed placeholders (Anomaly.Live, Anomaly.Kind.SuspensionFlip, Anomaly.Score)

Add new keys to BOTH SharedResource.ru.resx AND SharedResource.en.resx. Phase 6 should follow the same scheme; e.g. event browsing keys go under PreMatch.*, Live.* matching the route names in PLAN.

Settings reload mechanism

  1. Host registers appsettings.json + appsettings.{Env}.json + appsettings.Local.json (gitignored, optional, reloadOnChange: true) + MARATHON_* env vars in App.xaml.cs::OnStartup.
  2. Marathon.UI.Services.UiServicesExtensions.AddMarathonUi(IConfiguration, settingsLocalPath) binds:
    • LocalizationOptions (Localization:*)
    • WorkerOptions (Workers:*) — drives Phase 4 pollers
    • AnomalyOptions (Anomaly:*) — drives Phase 7 detector
    • StorageOptions (Storage:*) — Phase 2's options class, lives in Marathon.Application.Storage
    • ScrapingSettingsForm (Scraping:*) — UI-side mirror of Marathon.Infrastructure.Configuration.ScrapingOptions so the RCL stays host-agnostic. Phase 4 may bind the same JSON section to both forms.
  3. JsonSettingsWriter writes user edits as a single section into appsettings.Local.json via atomic temp-file rename. Other sections in that file are preserved (round-trip tested).
  4. Components inject IOptionsMonitor<T> and re-read on demand. The Settings page snapshots a clone of CurrentValue into local edit state, then writes the whole section.
  5. LocaleState and ThemeState are singletons with Action OnChange events; MainLayout.razor, LocaleSwitcher.razor, and ThemeToggle.razor subscribe and call StateHasChanged. Setting the locale also flips CultureInfo.DefaultThreadCurrent{,UI}Culture so newly created IStringLocalizer<T> instances pick up the new culture.

Marathon.UI portability invariant — verified

Marathon.UI.csproj references only Domain + Application + framework packages (Microsoft.AspNetCore.Components.Web, MudBlazor, Microsoft.Extensions.Localization, Microsoft.Extensions.Options*, Microsoft.Extensions.Configuration*, Microsoft.Extensions.Logging.Abstractions). It does NOT reference Infrastructure or any WPF/WebView assembly. A future ASP.NET Core Blazor Server host can register AddMarathonUi(...) and mount <App /> at #app with no UI changes.

The ScrapingSettingsForm mirror in Marathon.UI.Services is intentional — keeping Infrastructure.Configuration.ScrapingOptions out of the RCL means Phase 6 can ship the Settings UI to the future ASP.NET Core host without dragging in EF Core, AngleSharp, or Polly.

What Phase 4 needs to know

  • UiServicesExtensions.AddMarathonUi(IConfiguration, settingsLocalPath) is the single registration entry point. The host already calls it.
  • Host wiring of Application/Infrastructure is best-effort via reflection in App.xaml.cs::TryAddApplicationAndInfrastructure. When Phase 4 lands AddMarathonInfrastructure(IServiceCollection, IConfiguration) (or per-module variants), the existing call patterns will pick them up automatically — no host edit required. Replace the reflection with a direct call when Phase 4 commits.
  • WorkerOptions lives in Marathon.UI.Services (WorkerOptions.SectionName == "Workers"). Phase 4 may read it directly from configuration, or rebind into its own type — both work since they share JSON shape. The Settings page already exposes its three keys (UpcomingScheduleCron, LivePollerEnabled, UpcomingPollerEnabled).
  • AnomalyOptions likewise (Anomaly:*).
  • appsettings.Local.json is the "user-facing" override file. Phase 4 services should depend on IOptionsMonitor<T> so they react to user edits within seconds (file watcher is enabled on all three JSON sources).

What Phase 6 needs to know

  • Use the existing primitives. <StatCard>, <Field>, <PipelineStep>, the m-card / m-section / m-grid--asym / m-grid--three / m-shell classes form the layout language. Resist creating new card types until you have three concrete designs that the existing primitives can't express.
  • Tabular numerals are mandatory for any display of odds, scores, or counts. Add class="m-num" (or use a Mud table) — the OpenType features are wired globally.
  • Anomaly visual language must hang off --m-c-anomaly / Color.Error / .m-anomaly / .m-anomaly__pulse. Phase 7 inherits these.
  • Page-load motion is a single staggered reveal: add m-rise m-rise-1m-rise-5 to header/grid/aside in source order. Respects prefers-reduced-motion.
  • Routes and nav labels are pre-wired: /, /prematch, /live, /anomalies, /results, /settings. Phase 6/7/8 just replace the Placeholders body with real content — the nav drawer, breadcrumbs, AppBar, and locale switcher are already in MainLayout.

Deviations / known gaps

  1. Settings persistence reload. IOptionsMonitor<T> triggers when the JSON file changes. The Settings page snapshots a copy of CurrentValue into local state on initialisation, so a save-then-rebind cycle requires the user to navigate away and back (or for Phase 6 to hook OnChange and refresh local state). Acceptable for Phase 5; Phase 6 may add the listener.
  2. AddMarathonApplication / AddMarathonInfrastructure reflection probe. Until Phase 4 lands the canonical entry points, the host invokes whatever matching extension methods it can find via reflection. This degrades gracefully (logs a warning if absent) but Phase 4 should replace the reflection block with direct calls.
  3. bUnit version auto-resolved from 1.35.6 → 1.36.0 (NU1603). Updated Directory.Packages.props accordingly.
  4. Settings dialog confirmation uses Dialogs.ShowMessageBox(...). The DialogParameters block is currently dead code — left in place because future dialogs may want to use a custom layout instead of the message box.
  5. Pre-existing build failures outside Phase 5 scope: tests/Marathon.Infrastructure.Tests references internal repository classes (Phase 2 scope). Marathon.UI / Marathon.UI.Tests / Marathon.Hosts.WpfBlazor build clean. All 11 bUnit tests pass.