docs(initial-implementation): add feature plan and 10 phase subplans

This commit is contained in:
2026-05-05 00:39:27 +03:00
parent a2396a39a7
commit 8802ddb25b
12 changed files with 1272 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# Feature Context: Initial Implementation
## Configuration
- **Development mode:** Automated
- **Execution mode:** Orchestrator
- **Strategy:** Big Bang
- **Build:** `dotnet build Marathon.sln`
- **Test:** `dotnet test Marathon.sln`
- **Lint:** `dotnet format Marathon.sln --verify-no-changes`
- **Run:** `dotnet run --project src/Marathon.Hosts.WpfBlazor`
- **Implementer models:** Sonnet 4.6 (backend), Opus (frontend)
- **Reviewer model:** Sonnet 4.6
## Customer Constraints
- Source: marathonbet.by — anonymous scraping (no login). ToS risk acknowledged by customer.
- Output: Excel files matching customer's wide-column spec (`Bet_Match_Win_1`,
`Bet_Period-1_Win_Fora_2_Value`, etc.) with date-range filenames.
- Storage: customer accepted SQLite-with-Excel-export instead of Excel-as-database
(decided 2026-05-05).
- UI tech: Blazor Hybrid (changed from initial WPF assumption — better for web migration).
- Locale: RU + EN.
- Scope: analyze-only initially; design `IBetPlacer` extension point for future betting.
- Configurability: every variable parameter (polling, concurrency, retry, UA, retention,
thresholds, locale) goes in `appsettings.json` + Settings UI page.
## Current State
Repo just initialized. Single `main` commit with `.gitignore` + `README.md` + `CLAUDE.md`.
Working on `feature/initial-implementation` branch. No source code yet — Phase 0 starts
with scraping research, no implementation.
## Temporary Workarounds
(none yet)
## Cross-Phase Dependencies
- **Phase 1 (Domain)** is the foundation; all later phases reference domain types.
- **Phase 2 (Storage)** & **Phase 3 (Scraping)** depend only on Phase 1 — can run in parallel.
- **Phase 4 (Application + Workers)** depends on Phase 2 + Phase 3.
- **Phase 5 (UI Shell)** depends on Phase 1 only — can run in parallel with 2/3.
- **Phase 6 (Event Browsing UI)** depends on Phase 4 + Phase 5.
- **Phase 7 (Anomaly)** depends on Phase 4 (snapshot storage) + Phase 6 (UI patterns).
- **Phase 8 (Results)** depends on Phase 6.
- **Phase 9 (Packaging)** is final — runs full build + test suite.
## Deferred Work
- Bet placing (explicit out-of-scope; design extension point only).
- Authenticated scraping (anonymous now; `IOddsScraper` impl is swappable).
- Multi-bookmaker support (only marathonbet.by; abstraction allows future expansion).
- PostgreSQL backend (SQLite for now; `IRepository<T>` abstraction allows swap).
## Failed Approaches
(none yet — phases not started)
## Review Findings Log
(populated by reviewers)
## Phase Execution Log
| Phase | Agent | Model | Test Writer | Parallel | Notes |
|---|---|---|---|---|---|
| Phase 0 | phase-implementer | Sonnet 4.6 | ⏭️ Skipped (research only) | — | Throwaway probe; outputs SCRAPE_FINDINGS.md only |
| Phase 1 | phase-implementer | Sonnet 4.6 | ⏭️ Skipped (Big Bang) | — | — |
| Phase 2 | phase-implementer | Sonnet 4.6 | ⏭️ Skipped (Big Bang) | ✅ With 3 + 5 | — |
| Phase 3 | phase-implementer | Sonnet 4.6 | ⏭️ Skipped (Big Bang) | ✅ With 2 + 5 | — |
| Phase 4 | phase-implementer | Sonnet 4.6 | ⏭️ Skipped (Big Bang) | — | — |
| 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) | — | Uses frontend-design skill |
| Phase 7 | phase-implementer (split if needed) | Sonnet/Opus | ⏭️ Skipped (Big Bang) | — | UI portion uses Opus |
| 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 |
## Environment & Runtime Notes
- Windows 10, PowerShell 5.1 default shell, Bash also available.
- `git` configured globally; remote `origin` = `https://git.dolgolyov-family.by/alexei.dolgolyov/maraphon-app.git`.
- Note: home directory (`C:\Users\Alexei`) is itself a git repo (likely accidental).
The maraphon-app local `.git` overrides it for this directory tree.
- .NET SDK assumed installed; if Phase 1 fails on `dotnet --version`, install or
document in CONTEXT.md.
## Implementation Notes
(populated as we work)
+87
View File
@@ -0,0 +1,87 @@
# Feature: Initial Implementation (maraphon-app)
**Branch:** `feature/initial-implementation`
**Base branch:** `main`
**Created:** 2026-05-05
**Status:** 🟡 In Progress
**Strategy:** Big Bang
**Mode:** Automated
**Execution:** Orchestrator
**Implementer models:** Sonnet 4.6 (backend) · Opus (frontend, with frontend-design skill)
**Reviewer model:** Sonnet 4.6
## Summary
Build the maraphon-app end-to-end: a Blazor Hybrid (.NET 8 + WPF) sports-betting odds
analyzer that scrapes marathonbet.by, persists snapshots in SQLite, exports to Excel
matching the customer spec, and detects coefficient-flip anomalies. Architecture is
Clean Architecture with all UI in a Razor Class Library so the host can later swap to
ASP.NET Core Blazor Server with no UI rewrite. RU + EN localization, every variable
parameter configurable.
## Build & Test Commands
- **Build:** `dotnet build Marathon.sln`
- **Test:** `dotnet test Marathon.sln`
- **Lint:** `dotnet format Marathon.sln --verify-no-changes`
- **Run:** `dotnet run --project src/Marathon.Hosts.WpfBlazor`
> **Big Bang strategy:** Build/tests are NOT run for intermediate phases (Phases 08).
> The full build + test suite must pass at Phase 9 before final review.
> An exception: a `dotnet build` *compile-only smoke check* is allowed after each
> phase to catch syntax/type errors early — this is faster than running tests and
> consistent with Big Bang ("we don't run tests until the end").
## Phases
- [ ] Phase 0: Scraping spike (research, throwaway) [domain: backend] → [subplan](./phase-0-scraping-spike.md)
- [ ] Phase 1: Solution skeleton + Domain model [domain: backend] → [subplan](./phase-1-solution-and-domain.md)
- [ ] Phase 2: Infrastructure — Storage [domain: backend] → [subplan](./phase-2-storage.md)
- [ ] Phase 3: Infrastructure — Scraping [domain: backend] → [subplan](./phase-3-scraping.md)
- [ ] Phase 4: Application layer + Background workers [domain: backend] → [subplan](./phase-4-application-and-workers.md)
- [ ] Phase 5: Blazor Hybrid host + Theme + i18n [domain: frontend] → [subplan](./phase-5-host-theme-i18n.md)
- [ ] Phase 6: Event browsing UI [domain: frontend] → [subplan](./phase-6-event-browsing-ui.md)
- [ ] Phase 7: Anomaly detection [domain: fullstack] → [subplan](./phase-7-anomaly-detection.md)
- [ ] Phase 8: Results loader [domain: fullstack] → [subplan](./phase-8-results-loader.md)
- [ ] Phase 9: Packaging + polish (final phase — full build + tests required) [domain: fullstack] → [subplan](./phase-9-packaging-polish.md)
## Parallelization Plan (Orchestrator mode)
| Round | Phases | Notes |
|---|---|---|
| 1 | Phase 0 | Spike — gating research, no parallelism |
| 2 | Phase 1 | Domain — must finish before Phases 2/3/5 |
| 3 | **Phases 2, 3, 5 in parallel** | Storage, Scraping, UI Shell — disjoint files |
| 4 | Phase 4 | Application + Workers — depends on 2 + 3 |
| 5 | Phase 6 | Event UI — depends on 4 + 5 |
| 6 | Phase 7 | Anomaly detection — depends on 6 |
| 7 | Phase 8 | Results loader — depends on 6 |
| 8 | Phase 9 | Packaging — final, runs full build + tests |
## Phase Progress Log
| Phase | Domain | Status | Review | Build | Committed |
|---|---|---|---|---|---|
| Phase 0: Scraping spike | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 1: Solution + Domain | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 2: Storage | backend | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
| Phase 3: Scraping | backend | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
| Phase 4: Application + Workers | backend | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
| Phase 5: Host + Theme + i18n | frontend | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
| Phase 6: Event browsing UI | frontend | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
| Phase 7: Anomaly detection | fullstack | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
| Phase 8: Results loader | fullstack | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
| Phase 9: Packaging + polish | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
## Final Review
- [ ] Comprehensive code review (final-reviewer agent)
- [ ] Security review (auth N/A, but covers scraping HttpClient, file I/O, user input)
- [ ] Full build passes
- [ ] Full test suite passes
- [ ] User merge approval
- [ ] Merged to `main`
## Amendment Log
(empty)
@@ -0,0 +1,84 @@
# Phase 0: Scraping Spike (Research, Throwaway)
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
**Type:** Research / spike — produces documentation only, NO production code.
## Objective
Determine whether marathonbet.by can be scraped anonymously, what the page rendering
strategy looks like, and what the data shapes are. The output is a documented foundation
that Phases 19 build on. **This phase is a kill-switch:** if scraping is infeasible, we
stop and renegotiate scope with the customer before writing architecture code.
## Tasks
- [ ] Probe `https://www.marathonbet.by/su` (pre-match) anonymously. Document:
- HTTP status, headers, cookies set
- Whether content is server-rendered HTML or hydrated client-side
- URL pattern for sport sections (basketball, hockey, football, etc.)
- Sport group codes (e.g., basketball = 6 per spec)
- [ ] Probe `https://www.marathonbet.by/su/live` (live events). Document:
- Same as above
- Whether odds update via XHR/fetch/WebSocket — capture network calls
- [ ] Identify event-detail URL pattern and inspect a sample event's full odds page.
- [ ] For 3 events across 3 sports (e.g., basketball, hockey, tennis), capture:
- Event metadata (sport, country, league, category, scheduled time, event ID)
- Match-level bets: Win-1 / Draw / Win-2, Win-Fora-1/2 (with handicap value),
Total Less/More (with threshold)
- Period-N bets where the sport has periods
- [ ] Identify any anti-bot measures: Cloudflare challenges, JS challenges, rate
limiting, header requirements, fingerprinting hints.
- [ ] Test rate behavior: ~10 sequential requests, observe latency / blocks. Do NOT
hammer — be respectful.
- [ ] Document API endpoints if marathonbet.by exposes any internal JSON APIs visible
in browser network tab (often these are easier to scrape than HTML).
- [ ] Decide: HtmlClient + AngleSharp sufficient, or Playwright required (or both)?
- [ ] Save 23 representative HTML/JSON samples under `spike/captures/` (gitignored;
for local reference only).
- [ ] Write `spike/SCRAPE_FINDINGS.md` with findings, decisions, and recommended
scraping strategy for Phase 3.
- [ ] Write `spike/SCHEMA_DRAFT.md` with concrete proposed domain field mappings —
marathonbet.by terms → spec field names (`Bet_Match_Win_1`, etc.).
## Files to Modify/Create
- `spike/SCRAPE_FINDINGS.md` — research output (committed to repo)
- `spike/SCHEMA_DRAFT.md` — proposed domain mapping (committed to repo)
- `spike/captures/*.html` / `.json` — local samples (gitignored, NOT committed)
## Acceptance Criteria
- `SCRAPE_FINDINGS.md` exists and answers:
- Is anonymous scraping feasible? (yes/no/conditional)
- What scraping technology is required? (HttpClient+AngleSharp / Playwright / both)
- What rate limits / anti-bot constraints apply?
- What URL patterns and endpoints will Phase 3 target?
- `SCHEMA_DRAFT.md` maps real marathonbet.by data to the customer-spec field names.
- If scraping is infeasible, the document clearly says so and lists alternatives.
- **No production C# code is written in this phase.**
## Notes
- Use WebFetch tool for initial probing; supplement with curl/Bash if Playwright-style
behavior needs investigation.
- Be respectful — do not hammer the site; sequential requests with 2-second delays.
- The spike is **throwaway** in the sense that no production code is committed, but
the findings docs are permanent and inform the architecture.
- If marathonbet.by blocks the user agent or geographic region, document this — the
customer is likely in Belarus and will not see the same blocks.
## Review Checklist
- [ ] `SCRAPE_FINDINGS.md` answers all required questions above
- [ ] `SCHEMA_DRAFT.md` covers all bet types in the customer spec
(Win/Draw/Win_Fora/Total at Match + Period-N scope)
- [ ] No production code committed
- [ ] Recommended Phase 3 strategy is concrete and actionable
- [ ] Risk register updated if anti-bot or rate-limit issues found
## Handoff to Next Phase
<!-- Filled by Phase 0 implementer. Critical: list anything Phase 1+ implementers must know,
especially deviations from the customer spec field names due to real bookmaker data. -->
@@ -0,0 +1,122 @@
# Phase 1: Solution Skeleton + Domain Model
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Create the .NET 8 solution structure (5 source projects + 4 test projects) and implement
the core domain model — entities, value objects, enums, and invariants — with no
external dependencies. This establishes the foundation that all later phases reference.
## Tasks
- [ ] Create `Marathon.sln` with these projects:
- `src/Marathon.Domain/Marathon.Domain.csproj` (classlib, .NET 8, no deps)
- `src/Marathon.Application/Marathon.Application.csproj` (classlib, refs Domain)
- `src/Marathon.Infrastructure/Marathon.Infrastructure.csproj` (classlib, refs Domain + Application)
- `src/Marathon.UI/Marathon.UI.csproj` (Razor Class Library, refs Domain + Application)
- `src/Marathon.Hosts.WpfBlazor/Marathon.Hosts.WpfBlazor.csproj` (WPF + BlazorWebView,
refs Marathon.UI + Marathon.Infrastructure + Marathon.Application)
- `tests/Marathon.Domain.Tests/Marathon.Domain.Tests.csproj` (xUnit)
- `tests/Marathon.Application.Tests/Marathon.Application.Tests.csproj` (xUnit)
- `tests/Marathon.Infrastructure.Tests/Marathon.Infrastructure.Tests.csproj` (xUnit)
- `tests/Marathon.UI.Tests/Marathon.UI.Tests.csproj` (bUnit + xUnit)
- [ ] Add `Directory.Build.props` at repo root with shared settings:
```xml
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>12</LangVersion>
<TreatWarningsAsErrors Condition="'$(Configuration)'=='Release'">true</TreatWarningsAsErrors>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
</Project>
```
- [ ] Add `Directory.Packages.props` for centralized NuGet versions (mark
`<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>`).
- [ ] Add `.editorconfig` at repo root with C# formatting rules consistent with
CLAUDE.md conventions (file-scoped namespaces, 4-space indent, etc.).
- [ ] Implement `Marathon.Domain` types:
- **Value objects (records):**
- `SportCode(int Value)` — must be > 0
- `EventId(string Value)` — bookmaker's event identifier (string, not int)
- `Side` enum: `Side1, Side2, Draw, Less, More`
- `BetScope` discriminated union: `Match | Period(int Number)` (use record hierarchy)
- `BetType` enum: `Win, Draw, WinFora, Total`
- `OddsRate(decimal Value)` — must be > 1.0
- `OddsValue(decimal Value)` — handicap or total threshold (e.g., -5.5, 220.5)
- **Entities (use records or classes with private setters as appropriate):**
- `Sport(SportCode Code, string NameRu, string NameEn)`
- `Country(string Code, string NameRu, string NameEn)`
- `League(string Id, SportCode Sport, string Country, string NameRu, string NameEn,
string Category)`
- `Event(EventId Id, SportCode Sport, string CountryCode, string LeagueId,
string Category, DateTimeOffset ScheduledAt, string Side1Name, string Side2Name)`
- `Bet(BetScope Scope, BetType Type, Side Side, OddsValue? Value, OddsRate Rate)`
- `OddsSnapshot(EventId EventId, DateTimeOffset CapturedAt, OddsSource Source,
IReadOnlyList<Bet> Bets)` where `OddsSource = PreMatch | Live`
- `EventResult(EventId EventId, int Side1Score, int Side2Score, Side WinnerSide,
DateTimeOffset CompletedAt)`
- `Anomaly(Guid Id, EventId EventId, DateTimeOffset DetectedAt, AnomalyKind Kind,
decimal Score, string EvidenceJson)` where `AnomalyKind = SuspensionFlip`
- [ ] Implement domain invariants in record constructors / static factory methods.
- [ ] Implement `Marathon.Domain.Tests` — TDD tests for invariants:
- `OddsRate` rejects ≤ 1.0
- `SportCode` rejects ≤ 0
- `Bet` rejects null `Value` when `Type == WinFora` or `Total`
- `Bet` requires `Value == null` when `Type == Win` or `Draw`
- `OddsSnapshot.Bets` is non-empty
- `Event.ScheduledAt` is UTC (`Offset == TimeSpan.Zero`)
- Domain types are immutable (no settable public properties)
## Files to Modify/Create
- `Marathon.sln`
- `Directory.Build.props`
- `Directory.Packages.props`
- `.editorconfig`
- `src/Marathon.Domain/**` — entities, VOs, enums, invariants
- `src/Marathon.Application/Marathon.Application.csproj` — empty stub csproj
- `src/Marathon.Infrastructure/Marathon.Infrastructure.csproj` — empty stub
- `src/Marathon.UI/Marathon.UI.csproj` — empty RCL stub
- `src/Marathon.Hosts.WpfBlazor/Marathon.Hosts.WpfBlazor.csproj` — empty stub
- `tests/Marathon.Domain.Tests/**` — invariant tests
- `tests/Marathon.{Application,Infrastructure,UI}.Tests/*.csproj` — empty xUnit stubs
## Acceptance Criteria
- `dotnet build Marathon.sln` succeeds (compile-only smoke check, allowed in Big Bang).
- All domain tests pass (`dotnet test tests/Marathon.Domain.Tests` is allowed even in
Big Bang since this is the foundation phase and the test project is self-contained).
- Domain types are public, immutable records with invariants enforced in constructors.
- No EF Core, scraping, or UI code in this phase.
## Notes
- Use file-scoped namespaces and one type per file (except small enum + record groups).
- Domain types must NOT reference `System.Net.Http`, EF Core, or any infrastructure.
- For the discriminated union `BetScope`, use a record hierarchy:
```csharp
public abstract record BetScope { /* private ctor */ }
public sealed record MatchScope : BetScope;
public sealed record PeriodScope(int Number) : BetScope;
```
Or a single record with a nullable `PeriodNumber` — implementer's choice, document it.
- Test framework: xUnit with FluentAssertions. Don't add Mockito/NSubstitute yet
(no abstractions to mock in Domain).
## Review Checklist
- [ ] Solution builds (`dotnet build`)
- [ ] Domain tests all pass
- [ ] No external deps in `Marathon.Domain.csproj` except framework packages
- [ ] Public API surface is minimal — only what later phases need
- [ ] All types follow CLAUDE.md naming/style conventions
## Handoff to Next Phase
<!-- Filled by Phase 1 implementer. -->
@@ -0,0 +1,115 @@
# Phase 2: Infrastructure — Storage
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Implement persistent storage: EF Core + SQLite (WAL) with migrations, repository
implementations of the Application layer's interfaces, and a ClosedXML-based Excel
exporter that produces files matching the customer's wide-column spec with date-range
filenames.
## Tasks
- [ ] Add packages to `Marathon.Infrastructure` (via `Directory.Packages.props`):
- `Microsoft.EntityFrameworkCore`
- `Microsoft.EntityFrameworkCore.Sqlite`
- `Microsoft.EntityFrameworkCore.Design`
- `ClosedXML`
- [ ] Add Application-layer abstractions in `Marathon.Application/Abstractions/`:
- `IRepository<TKey, TEntity>` — generic CRUD: `GetAsync`, `ListAsync`,
`AddAsync`, `UpdateAsync`, `DeleteAsync`, `SaveChangesAsync`
- `IEventRepository : IRepository<EventId, Event>` — adds `ListByDateRangeAsync`,
`ListBySportAsync`
- `ISnapshotRepository : IRepository<Guid, OddsSnapshot>` — adds
`ListByEventAsync(EventId, DateTimeOffset from, DateTimeOffset to)`
- `IResultRepository : IRepository<EventId, EventResult>`
- `IAnomalyRepository : IRepository<Guid, Anomaly>`
- `IExcelExporter``ExportAsync(DateRange range, ExportKind kind, string outputPath)`
where `ExportKind = PreMatch | Live | Combined`
- [ ] Implement `MarathonDbContext` in `Marathon.Infrastructure/Persistence/`:
- `DbSet<EventEntity>`, `DbSet<SnapshotEntity>`, `DbSet<BetEntity>`,
`DbSet<EventResultEntity>`, `DbSet<AnomalyEntity>`, `DbSet<SportEntity>`,
`DbSet<LeagueEntity>`
- Configure SQLite with WAL via connection string
- Use `EntityTypeConfiguration<T>` classes (one per entity in `Configurations/`)
- Map domain types ↔ EF entities via mapping helpers (don't pollute domain)
- Indexes: `(EventId)` on `Snapshots` and `Bets`; `(Sport, ScheduledAt)` on `Events`
- [ ] Implement `Migrations/InitialCreate` migration (EF Core CLI):
```
dotnet ef migrations add InitialCreate --project src/Marathon.Infrastructure
```
- [ ] Implement repositories in `Marathon.Infrastructure/Persistence/Repositories/`:
- `EventRepository`, `SnapshotRepository`, `ResultRepository`, `AnomalyRepository`
- Each maps EF entity ↔ domain type at the boundary
- [ ] Implement `ExcelExporter` in `Marathon.Infrastructure/Export/`:
- Uses ClosedXML
- Output filename: `Marathon_<from yyyy-MM-dd>_to_<to yyyy-MM-dd>.xlsx`
- Two sheets: `PreMatch` and `Live` (or only the selected one based on `ExportKind`)
- Wide columns matching customer spec exactly:
- Event metadata: `RowNum`, `SportCode`, `Sport`, `Country`, `League`, `Category`,
`DateFull`, `Day`, `Month`, `Year`, `Time`, `EventId`
- Match-level bets: `Bet_Match_Win_1`, `Bet_Match_Draw`, `Bet_Match_Win_2`,
`Bet_Match_Win_Fora_1_Value`, `Bet_Match_Win_Fora_1_Rate`, etc.
- Period-N bets: dynamically generated for max periods seen (`Bet_Period-1_Win_1`, ...)
- For Live export, prefix with `Live_` instead of `Bet_`
- Final column: `WinnerSide` (1 or 2 based on lowest pre-match Win rate, per spec
§1.2.4 / §2.2.4)
- Implement a `BetRowDenormalizer` helper that takes a `List<Bet>` and produces a
flat `Dictionary<string, object?>` keyed by spec column names.
- [ ] Add a DI extension `AddMarathonInfrastructure(IServiceCollection, IConfiguration)`
in `Marathon.Infrastructure/DependencyInjection.cs` that wires up DbContext +
repositories + exporter using `IConfiguration` for `Storage:DatabasePath` and
`Storage:ExportDirectory`.
- [ ] Tests in `Marathon.Infrastructure.Tests`:
- In-memory SQLite (`Microsoft.Data.Sqlite` with `Mode=Memory;Cache=Shared`)
- Test: insert + retrieve `Event`, `OddsSnapshot`, `Anomaly` round-trip preserves all
domain fields
- Test: `ExcelExporter` generates a workbook with the expected sheet names, headers
matching spec, and row count matching event count
- Test: filename pattern matches `Marathon_yyyy-MM-dd_to_yyyy-MM-dd.xlsx`
- Test: WAL mode is enabled after open
## Files to Modify/Create
- `src/Marathon.Application/Abstractions/I*.cs` — repository interfaces
- `src/Marathon.Application/ExportKind.cs`, `DateRange.cs`
- `src/Marathon.Infrastructure/Persistence/MarathonDbContext.cs`
- `src/Marathon.Infrastructure/Persistence/Entities/*.cs`
- `src/Marathon.Infrastructure/Persistence/Configurations/*Configuration.cs`
- `src/Marathon.Infrastructure/Persistence/Repositories/*Repository.cs`
- `src/Marathon.Infrastructure/Persistence/Mapping.cs` — entity ↔ domain
- `src/Marathon.Infrastructure/Export/ExcelExporter.cs`
- `src/Marathon.Infrastructure/Export/BetRowDenormalizer.cs`
- `src/Marathon.Infrastructure/Migrations/*` — EF migrations
- `src/Marathon.Infrastructure/DependencyInjection.cs`
- `tests/Marathon.Infrastructure.Tests/**`
## Acceptance Criteria
- All Infrastructure code compiles (Big Bang: compile-only smoke check OK).
- DbContext + repositories cover all domain types.
- Excel exporter output matches customer spec column names exactly (no typos in
`Bet_Match_Win_Fora_1_Value`, hyphens in `Period-1`, etc.).
- Filename includes inclusive date range from event scheduling.
## Notes
- This phase is parallelizable with Phase 3 (Scraping) — they touch disjoint files.
- `ExcelExporter` uses normalized DB data and produces wide columns — DO NOT store
data in wide format in SQLite.
- Big Bang: do NOT run full test suite. A `dotnet build` smoke check is acceptable.
## Review Checklist
- [ ] Solution builds (compile-only)
- [ ] Excel column names match customer spec exactly (cross-check against TZ §1.2 / §2.2)
- [ ] Filename pattern matches `Marathon_yyyy-MM-dd_to_yyyy-MM-dd.xlsx`
- [ ] No domain types polluted with EF attributes — mapping is in `Configurations/`
- [ ] WAL mode enabled in connection string
## Handoff to Next Phase
<!-- Filled by Phase 2 implementer. -->
@@ -0,0 +1,129 @@
# Phase 3: Infrastructure — Scraping
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
## Objective
Implement the scraping pipeline: HttpClient + AngleSharp for HTML pages with a Playwright
fallback for JS-rendered content, all wrapped in resilient policies (retry, circuit
breaker, rate limiter). All parsing logic is informed by Phase 0's `SCRAPE_FINDINGS.md`
and `SCHEMA_DRAFT.md`.
## Tasks
- [ ] Read `spike/SCRAPE_FINDINGS.md` and `spike/SCHEMA_DRAFT.md` from Phase 0 to
determine which strategy applies (HTML / Playwright / hybrid).
- [ ] Add packages:
- `AngleSharp`
- `Microsoft.Extensions.Http`
- `Microsoft.Extensions.Http.Resilience` (Polly v8 wrapper)
- `Microsoft.Playwright` (only if Phase 0 decided Playwright is needed)
- [ ] Define abstractions in `Marathon.Application/Abstractions/`:
- `IOddsScraper`:
- `Task<IReadOnlyList<Event>> ScrapeUpcomingAsync(SportCode? filter, CancellationToken ct)`
- `Task<OddsSnapshot> ScrapeEventOddsAsync(EventId id, OddsSource source, CancellationToken ct)`
- `Task<IReadOnlyList<EventResult>> ScrapeResultsAsync(DateRange range, CancellationToken ct)`
- `IBetPlacer` — empty marker interface for future betting feature (extension point)
- [ ] Implement `Marathon.Infrastructure/Scraping/MarathonbetScraper.cs`:
- Composes parsers + HttpClient + (optional) Playwright per Phase 0 strategy
- Constructor takes `IHttpClientFactory`, `IOptions<ScrapingOptions>`, `ILogger`
- Methods correspond to `IOddsScraper` interface
- [ ] Implement parsers in `Marathon.Infrastructure/Scraping/Parsers/`:
- `UpcomingEventsParser` — parses listing page → `IReadOnlyList<Event>`
- `LiveEventsParser` — parses live listing → `IReadOnlyList<Event>`
- `EventOddsParser` — parses event detail page → `OddsSnapshot` (handles all bet types
in spec: Win/Draw/WinFora/Total at Match + Period-N scope)
- `ResultsParser` — parses completed events → `IReadOnlyList<EventResult>`
- Each parser is unit-testable: takes `string html` (or `IDocument`), returns domain types
- [ ] `ScrapingOptions` POCO bound to `appsettings.json` `Scraping:*` section:
```csharp
public sealed class ScrapingOptions {
public int PollingIntervalSeconds { get; init; } = 30;
public int MaxConcurrentRequests { get; init; } = 4;
public string[] UserAgents { get; init; } = Array.Empty<string>();
public RetryPolicyOptions RetryPolicy { get; init; } = new();
public RateLimitOptions RateLimit { get; init; } = new();
public bool EnablePlaywrightFallback { get; init; } = false;
public string BaseUrl { get; init; } = "https://www.marathonbet.by";
}
```
- [ ] Configure named `HttpClient` "marathonbet" in DI with:
- `BaseAddress` = `Scraping:BaseUrl`
- `User-Agent` rotation via `DelegatingHandler` (`UserAgentRotatorHandler`)
- Polly resilience (`AddResilienceHandler` from `Microsoft.Extensions.Http.Resilience`):
- Retry: exponential backoff, max attempts from config
- Circuit breaker: 5 failures → 30s open
- Rate limiter: token bucket (configurable RPS)
- Timeout: per-request from config
- [ ] (Optional, if Phase 0 needs it) Implement `PlaywrightScraper` for SPA-rendered
pages — used as fallback if HTML scraping detects empty/dynamic content.
- [ ] Add DI registration in `Marathon.Infrastructure/DependencyInjection.cs`:
- `services.AddOptions<ScrapingOptions>().Bind(config.GetSection("Scraping"))`
- `services.AddHttpClient("marathonbet").AddResilienceHandler(...)`
- `services.AddSingleton<IOddsScraper, MarathonbetScraper>()`
- `services.AddSingleton<UserAgentRotatorHandler>()`
- [ ] Add `appsettings.json` template under `src/Marathon.Hosts.WpfBlazor/appsettings.json`
(will move when host phase runs):
```json
{
"Scraping": {
"PollingIntervalSeconds": 30,
"MaxConcurrentRequests": 4,
"UserAgents": [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..."
],
"RetryPolicy": { "MaxAttempts": 3, "BaseDelayMs": 500 },
"RateLimit": { "RequestsPerSecond": 1 },
"EnablePlaywrightFallback": false,
"BaseUrl": "https://www.marathonbet.by"
}
}
```
- [ ] Tests in `Marathon.Infrastructure.Tests/Scraping/`:
- Use recorded HTML fixtures (committed under
`tests/Marathon.Infrastructure.Tests/Fixtures/marathonbet/*.html` — small samples
only) — copy from `spike/captures/` if appropriate
- Test each parser produces expected domain output for the fixtures
- Test `MarathonbetScraper` handles network errors gracefully (Polly mock)
- DO NOT make real network calls in tests
## Files to Modify/Create
- `src/Marathon.Application/Abstractions/IOddsScraper.cs`
- `src/Marathon.Application/Abstractions/IBetPlacer.cs` (marker interface)
- `src/Marathon.Infrastructure/Scraping/MarathonbetScraper.cs`
- `src/Marathon.Infrastructure/Scraping/Parsers/*.cs` — 4 parsers
- `src/Marathon.Infrastructure/Scraping/UserAgentRotatorHandler.cs`
- `src/Marathon.Infrastructure/Scraping/Playwright/PlaywrightScraper.cs` (conditional)
- `src/Marathon.Infrastructure/Configuration/ScrapingOptions.cs`
- `tests/Marathon.Infrastructure.Tests/Scraping/Parsers/*Tests.cs`
- `tests/Marathon.Infrastructure.Tests/Fixtures/marathonbet/*.html`
## Acceptance Criteria
- Compiles (Big Bang).
- All parser logic is unit-testable without network.
- `IOddsScraper` is the only public surface used by Application layer.
- `appsettings.json` template covers every variable parameter.
- `IBetPlacer` exists as a future-proof extension point.
## Notes
- This phase is parallelizable with Phase 2 — disjoint files.
- DO NOT hammer marathonbet.by — tests use local fixtures.
- If Phase 0 found that scraping requires headless browser only, skip the AngleSharp
parsers and implement Playwright-only.
- Big Bang: compile-only smoke check after this phase; tests deferred to Phase 9.
## Review Checklist
- [ ] Compiles
- [ ] Parser interface is clean (`string html → domain types`)
- [ ] All `Scraping:*` config keys are wired through `ScrapingOptions`
- [ ] No real network calls in tests
## Handoff to Next Phase
<!-- Filled by Phase 3 implementer. -->
@@ -0,0 +1,94 @@
# Phase 4: Application Layer + Background Workers
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** backend
**Depends on:** Phase 1 (Domain), Phase 2 (Storage), Phase 3 (Scraping)
## Objective
Wire scraping + storage together via use-case orchestrators in the Application layer
and background services that execute pollers on configurable intervals.
## Tasks
- [ ] Implement use cases in `Marathon.Application/UseCases/`:
- `PullUpcomingEventsUseCase(IOddsScraper, IEventRepository, ISnapshotRepository)`
- `ExecuteAsync(CancellationToken)` → fetch upcoming events, persist new ones,
capture initial pre-match snapshots for each
- `PullLiveOddsUseCase(IOddsScraper, IEventRepository, ISnapshotRepository)`
- `ExecuteAsync(CancellationToken)` → for each currently-live event, fetch a
fresh snapshot, persist it
- `PullResultsUseCase(IOddsScraper, IEventRepository, IResultRepository)`
- `ExecuteAsync(DateRange range, IReadOnlyList<EventId>? selection, CancellationToken)`
→ fetch results for completed events (all or selected)
- `ExportToExcelUseCase(IExcelExporter, IEventRepository)`
- `ExecuteAsync(DateRange, ExportKind, CancellationToken)`
- [ ] Implement background services in `Marathon.Infrastructure/Workers/`:
- `UpcomingEventsPoller : BackgroundService` — runs `PullUpcomingEventsUseCase` on
a configurable cron-like schedule (default: every 6 hours)
- `LiveOddsPoller : BackgroundService` — runs `PullLiveOddsUseCase` every
`Scraping:PollingIntervalSeconds` seconds
- Both honor `CancellationToken`, log via `ILogger<T>`, and skip cycles gracefully
on errors (don't crash the host)
- [ ] Add `WorkerOptions` POCO bound to `Workers:*` config:
```csharp
public sealed class WorkerOptions {
public string UpcomingScheduleCron { get; init; } = "0 0 */6 * * *"; // every 6h
public bool LivePollerEnabled { get; init; } = true;
public bool UpcomingPollerEnabled { get; init; } = true;
}
```
Use `Cronos` package or simple TimeSpan for upcoming schedule.
- [ ] Add DI extension `AddMarathonApplication(IServiceCollection, IConfiguration)`
in `Marathon.Application/DependencyInjection.cs`:
- Registers all use cases
- [ ] Update `Marathon.Infrastructure/DependencyInjection.cs` to also register
`BackgroundService`s under `services.AddHostedService<T>()`.
- [ ] Tests in `Marathon.Application.Tests`:
- Mock `IOddsScraper` + repos with NSubstitute
- Test: `PullUpcomingEventsUseCase` persists new events, skips duplicates
- Test: `PullLiveOddsUseCase` writes a snapshot per live event
- Test: `PullResultsUseCase` respects `selection` filter (when null, fetches all)
- Test: `ExportToExcelUseCase` invokes `IExcelExporter.ExportAsync` with correct
date range
- [ ] Tests in `Marathon.Infrastructure.Tests/Workers/`:
- Test: `LiveOddsPoller` invokes use case at configured interval (use FakeTimeProvider)
- Test: poller continues after a use-case exception (logs, doesn't propagate)
## Files to Modify/Create
- `src/Marathon.Application/UseCases/*.cs`
- `src/Marathon.Application/DependencyInjection.cs`
- `src/Marathon.Infrastructure/Workers/UpcomingEventsPoller.cs`
- `src/Marathon.Infrastructure/Workers/LiveOddsPoller.cs`
- `src/Marathon.Infrastructure/Configuration/WorkerOptions.cs`
- `tests/Marathon.Application.Tests/UseCases/**`
- `tests/Marathon.Infrastructure.Tests/Workers/**`
## Acceptance Criteria
- Compiles (Big Bang).
- Use cases depend only on Application abstractions (no Infrastructure refs).
- Workers honor cancellation and don't crash on transient errors.
- All variable timing/enabling is configurable.
## Notes
- Use `IHostedService` from `Microsoft.Extensions.Hosting` — works in WPF host via
`Host.CreateApplicationBuilder()` pattern (Phase 5 will expose this).
- For the cron-style upcoming poller, prefer the `Cronos` package (small, mature)
over hand-rolled scheduling.
- Big Bang: compile-only smoke check.
## Review Checklist
- [ ] Use cases have no Infrastructure dependencies
- [ ] Both pollers configurable (interval, enable/disable)
- [ ] Cancellation propagated correctly
- [ ] Errors logged, not propagated out of `ExecuteAsync`
## Handoff to Next Phase
<!-- Filled by Phase 4 implementer. Phase 5 needs to know how to start the host
including these BackgroundServices. -->
@@ -0,0 +1,121 @@
# Phase 5: Blazor Hybrid Host + Theme + Localization
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./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 — e.g., Inter or Manrope which
have full Cyrillic coverage)
- 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:MainLayout}" />`
- [ ] In `src/Marathon.Hosts.WpfBlazor/App.xaml.cs`:
- Build `IHost` via `Host.CreateApplicationBuilder()`
- Call `services.AddMarathonInfrastructure(config)`
- Call `services.AddMarathonApplication(config)`
- 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/` (move from Phase 3 if
placed there) with all sections. Add `appsettings.Development.json` template.
- [ ] Tests in `Marathon.UI.Tests` (using bUnit):
- Test: `MainLayout` renders without errors
- Test: locale switcher changes culture
- Test: theme tokens are applied (CSS variables present in DOM)
## Files to Modify/Create
- `src/Marathon.UI/_Imports.razor`
- `src/Marathon.UI/MainLayout.razor`
- `src/Marathon.UI/Pages/Home.razor`, `Pages/Settings.razor`
- `src/Marathon.UI/Theme/MarathonTheme.cs`, `Theme/app.css`
- `src/Marathon.UI/wwwroot/index.html`
- `src/Marathon.UI/Resources/SharedResource.{cs,ru.resx,en.resx}`
- `src/Marathon.UI/Components/LocaleSwitcher.razor`
- `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`
- `src/Marathon.Hosts.WpfBlazor/Properties/AssemblyInfo.cs`
- `tests/Marathon.UI.Tests/MainLayoutTests.cs`, `LocaleSwitcherTests.cs`
## Acceptance Criteria
- Host project compiles (Big Bang smoke check).
- `Marathon.UI` is a clean RCL — usable from any host (verifies portability).
- Theme is distinct, not generic — implementer should follow `frontend-design` skill
guidance for typography, color, motion, spatial composition.
- Locale switcher works (toggles between RU and EN strings on the same page).
- Settings page surfaces every configurable parameter from `appsettings.json`.
## Notes
- This phase is parallelizable with Phases 2 and 3 (only depends on Phase 1 Domain,
but the orchestrator can run all three after Phase 1 completes).
- The frontend-design skill content is provided to the agent in `FRONTEND_DESIGN_SKILL`
context block. Follow it precisely.
- Use Cyrillic-friendly fonts (Inter, Manrope, IBM Plex Sans, JetBrains Mono).
- For BlazorWebView in WPF, the project SDK MUST be `Microsoft.NET.Sdk.Razor` and
the OutputType set to `WinExe` with WPF enabled.
## Review Checklist
- [ ] Compiles
- [ ] `Marathon.UI` references no host-specific code (BlazorWebView, WPF)
- [ ] Theme not generic — distinctive palette + typography
- [ ] All `appsettings.json` keys reachable via the Settings page
- [ ] RU + EN both renderable (placeholder strings ok for later phases)
- [ ] Accessibility: keyboard navigation in nav drawer, focus indicators
## Handoff to Next Phase
<!-- Filled by Phase 5 implementer. Critical: document the theme tokens, component
layout patterns, and the IStringLocalizer key naming convention so Phase 6
remains consistent. -->
@@ -0,0 +1,108 @@
# Phase 6: Event Browsing UI
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
**Implementer:** Opus + frontend-design skill
**Depends on:** Phase 4 (use cases) + Phase 5 (UI shell)
## Objective
Build the user-facing browsing experience: pre-match list, live list (auto-refreshing),
event-detail view with odds-over-time chart, plus an Excel export trigger. Visual
quality must match the design system established in Phase 5 — distinctive, accessible,
information-dense without being cluttered.
## Tasks
- [ ] Create `Marathon.UI/Pages/PreMatch/EventsList.razor`:
- Server-paginated table of upcoming `Event`s (use `IEventRepository` via injected
use case wrapper)
- Filters: sport (multi-select), country (multi-select), date range, free-text
search (league/team)
- Sort: scheduled time, sport, country, league
- Each row shows compact odds preview (Match Win-1 / Draw / Win-2)
- Click row → navigate to `Pages/Events/Detail.razor?id=...`
- [ ] Create `Marathon.UI/Pages/Live/LiveList.razor`:
- Same shell as PreMatch but data source is live snapshots
- Auto-refresh every `Scraping:PollingIntervalSeconds` (use a timer + state subscription
pattern; do NOT poll the scraper directly — read from snapshot repo)
- Visual indicator when odds change since last refresh (subtle pulse / arrow)
- [ ] Create `Marathon.UI/Pages/Events/Detail.razor`:
- Event header: sport icon, league, scheduled time, sides 1 & 2
- Tabs: "Match" | "Period 1" | "Period 2" | ...
- For each scope, show all bet types in a tabular layout
- Charts panel: odds-over-time using Plotly.Blazor — three traces for Win-1/Draw/Win-2,
secondary axis for handicap value
- Snapshot history table beneath the chart
- Excel export button (single event or full date range)
- [ ] Create `Marathon.UI/Components/SportIcon.razor` — small SVG-based component with
recognizable icons per sport (basketball, football, hockey, tennis, volleyball, etc.).
- [ ] Create `Marathon.UI/Components/OddsCell.razor` — formats `OddsRate` with delta
arrow (↑ green, ↓ red) when value changes from previous render.
- [ ] Create `Marathon.UI/Components/OddsTimeline.razor` — wraps Plotly.Blazor with
consistent theming and tooltip behavior.
- [ ] Create `Marathon.UI/Components/ExportDialog.razor` — modal: pick `DateRange`,
pick `ExportKind`, click Export → calls `ExportToExcelUseCase`. Show success toast
with output file path.
- [ ] State management: small `EventBrowsingState` service (singleton scoped to UI)
holding active filters per page. Inject via DI in pages. No Redux/Fluxor — keep
simple.
- [ ] Add packages to `Marathon.UI`:
- `Plotly.Blazor`
- [ ] Update `Marathon.UI/Resources/SharedResource.{ru,en}.resx` with all new strings.
Establish key naming convention from Phase 5 handoff notes.
- [ ] Performance:
- Virtualized rows for large event lists (`MudVirtualize` or `MudTable` virtual
pagination)
- Debounce filter inputs (300ms)
- Memoize chart data — recompute only when snapshot list changes
- [ ] Accessibility:
- Table semantics with proper headers + ARIA labels
- Keyboard navigation for row selection
- Focus visible on all interactive elements
- Charts include data table fallback for screen readers
## Files to Modify/Create
- `src/Marathon.UI/Pages/PreMatch/EventsList.razor`
- `src/Marathon.UI/Pages/Live/LiveList.razor`
- `src/Marathon.UI/Pages/Events/Detail.razor`
- `src/Marathon.UI/Components/SportIcon.razor`, `OddsCell.razor`,
`OddsTimeline.razor`, `ExportDialog.razor`
- `src/Marathon.UI/Services/EventBrowsingState.cs`
- `src/Marathon.UI/Resources/SharedResource.{ru,en}.resx` — append new keys
- `src/Marathon.UI/Components/_Imports.razor` — register Plotly.Blazor
- Tests: `tests/Marathon.UI.Tests/Pages/**`, `Components/**`
## Acceptance Criteria
- Compiles (Big Bang).
- Live list visually conveys odds changes between refreshes.
- Detail page chart renders 3 traces (Win-1/Draw/Win-2) with smooth interpolation
and clear tooltip showing exact rate at any point in time.
- Excel export from the dialog reaches `ExportToExcelUseCase` correctly.
- Both RU and EN render correctly across all new UI.
- Distinctive visual identity — implementer should follow frontend-design guidance.
## Notes
- The frontend-design skill content is provided to the agent in `FRONTEND_DESIGN_SKILL`.
Apply its principles — typography, color, motion, spatial composition.
- Use Plotly.Blazor for charts (smooth, themable, professional look).
- Keep components small (<200 lines) and composable.
- Big Bang: compile-only smoke check.
## Review Checklist
- [ ] Compiles
- [ ] No mutation of domain types in UI components
- [ ] Filters/sort persist within page session via `EventBrowsingState`
- [ ] Chart accessible (data table fallback)
- [ ] All new strings localized in RU + EN
- [ ] Visual consistency with Phase 5 theme tokens
## Handoff to Next Phase
<!-- Filled by Phase 6 implementer. Phase 7 (anomaly detection UI) needs to know
the table/card patterns established here so the anomaly feed is consistent. -->
@@ -0,0 +1,107 @@
# Phase 7: Anomaly Detection (Suspension + Flip)
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
**Implementer:** Sonnet (backend portion) + Opus (UI portion, with frontend-design)
**Depends on:** Phase 4 (snapshot pipeline) + Phase 6 (UI patterns)
## Objective
Detect the **odds-flip anomaly** described in customer TZ §3: bookmaker freezes betting
on a live event, then re-opens with inverted underdog/favorite odds. Persist anomalies
and surface them in a dedicated UI feed page so the user can act on them.
## Tasks
### Backend (Sonnet)
- [ ] Implement `Marathon.Domain/AnomalyDetection/AnomalyDetector.cs`:
- Pure domain logic — takes `IReadOnlyList<OddsSnapshot>` for an event, returns
`IReadOnlyList<Anomaly>`
- Detect suspension intervals: gaps between snapshots > `SuspensionGapSeconds`
(configurable)
- For each suspension, compute pre-suspension and post-suspension implied
probability vectors `(p1, pDraw, p2)` from Win-1/Draw/Win-2 rates
- Compute flip score: `max(|p_post[i] - p_pre[i]|)` across i ∈ {1, draw, 2}
- If flip score ≥ `OddsFlipThreshold` AND the favorite changed (argmax differs),
emit an `Anomaly(Kind=SuspensionFlip, Score, EvidenceJson)` where `EvidenceJson`
contains the snapshots bracketing the suspension
- [ ] Add `AnomalyOptions` POCO bound to `Anomaly:*`:
```csharp
public sealed class AnomalyOptions {
public int SuspensionGapSeconds { get; init; } = 60;
public decimal OddsFlipThreshold { get; init; } = 0.30m;
public int MinSnapshotCount { get; init; } = 3;
}
```
- [ ] Implement `DetectAnomaliesUseCase` in `Marathon.Application/UseCases/`:
- Iterate over events with new snapshots since last detection run
- Invoke `AnomalyDetector` per event
- Persist new anomalies via `IAnomalyRepository`
- [ ] Implement `AnomalyDetectionPoller : BackgroundService` in
`Marathon.Infrastructure/Workers/`:
- Runs every `Anomaly:DetectionIntervalSeconds` (default 60s)
- Calls `DetectAnomaliesUseCase`
- [ ] Backend tests in `Marathon.Domain.Tests/AnomalyDetection/`:
- Synthetic snapshot timeline with no flip → 0 anomalies
- Snapshot timeline with suspension + small odds shift → 0 anomalies (below threshold)
- Snapshot timeline with suspension + large flip (favorite ↔ underdog) → 1 anomaly
- Score calculation matches expected value
### Frontend (Opus + frontend-design)
- [ ] Create `Marathon.UI/Pages/Anomalies/AnomalyFeed.razor`:
- List of anomalies sorted by `DetectedAt` descending
- Each card shows: severity (color-coded by score), event identity, sport icon,
detected timestamp, mini sparkline of pre/post odds
- Click card → expand to show evidence timeline (snapshots before/after suspension)
- Filter: severity threshold, sport, date range
- [ ] Create `Marathon.UI/Components/AnomalyCard.razor` — visually distinctive,
attention-grabbing without being garish; follows frontend-design guidance for
information hierarchy.
- [ ] Add navigation entry to `MainLayout` drawer with notification badge showing
unread anomaly count.
- [ ] Localize all strings in RU + EN.
- [ ] Frontend tests in `Marathon.UI.Tests/Pages/Anomalies/`:
- bUnit: anomaly card renders evidence timeline
- bUnit: filter narrows the list correctly
## Files to Modify/Create
- `src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs`
- `src/Marathon.Domain/AnomalyDetection/SuspensionInterval.cs`
- `src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs`
- `src/Marathon.Application/Configuration/AnomalyOptions.cs` (or in Infra)
- `src/Marathon.Infrastructure/Workers/AnomalyDetectionPoller.cs`
- `src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor`
- `src/Marathon.UI/Components/AnomalyCard.razor`
- `tests/Marathon.Domain.Tests/AnomalyDetection/AnomalyDetectorTests.cs`
- `tests/Marathon.UI.Tests/Pages/Anomalies/**`
## Acceptance Criteria
- Compiles (Big Bang).
- `AnomalyDetector` is a pure function — no I/O, no DI dependencies.
- Configurable thresholds via `appsettings.json` (visible in Settings page).
- UI clearly distinguishes high/medium/low severity anomalies.
- Evidence timeline shows the actual snapshots that triggered the detection.
## Notes
- This is the **product's actual differentiator** — quality of detection logic and
evidence presentation matters. Spend time getting the score formula right.
- Implied probability formula: `p = 1 / odds` (then normalize so they sum to 1).
- Big Bang: compile-only smoke check.
## Review Checklist
- [ ] Detector is deterministic and pure
- [ ] Score calculation correct (verify against hand-computed example)
- [ ] No false positives on synthetic "normal" timelines
- [ ] UI evidence timeline matches stored `EvidenceJson`
- [ ] All strings localized
## Handoff to Next Phase
<!-- Filled by Phase 7 implementer. -->
@@ -0,0 +1,82 @@
# Phase 8: Results Loader
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
**Implementer:** Sonnet (backend) + Opus (UI)
**Depends on:** Phase 6 (UI patterns)
## Objective
Per customer TZ §4: scrape and persist results of completed events, with a UI that
allows the user to load all results in a date range OR pick specific events to load
selectively.
## Tasks
### Backend (Sonnet)
- [ ] `PullResultsUseCase` was scaffolded in Phase 4 — extend it here:
- When `selection` is null/empty, fetch results for ALL completed events in range
that don't have a stored `EventResult` yet
- When `selection` provided, fetch results only for those events
- Idempotent — re-running for already-loaded results is a no-op
- [ ] Add `IResultsScraper`-related parser methods (or extend `IOddsScraper` with
`ScrapeResultsAsync`) — implementation may already exist from Phase 3.
- [ ] After persisting results, infer `WinnerSide` and update the `Event` accordingly
(or store derived `WinnerSide` on `EventResult` only — implementer's choice, document
in handoff).
- [ ] Tests in `Marathon.Application.Tests`:
- `PullResultsUseCase` with selection list pulls only those events
- With null selection, pulls all completed events missing results in range
- Idempotency: running twice produces no duplicates
### Frontend (Opus + frontend-design)
- [ ] Create `Marathon.UI/Pages/Results/ResultsLoader.razor`:
- Date range picker
- Two modes: "All in range" (default) | "Selected events"
- Selected events mode: searchable multi-select of completed events lacking results
- "Load Results" button → invokes `PullResultsUseCase`
- Progress indicator (number of events processed / total)
- Result table on completion showing what was loaded (event identity, score,
winner side)
- [ ] Create `Marathon.UI/Pages/Results/ResultsList.razor`:
- Browse already-loaded results
- Filter by sport, date range, winner-side-1 / winner-side-2 / draw
- Link back to event detail page (Phase 6)
- [ ] Add `Results` entry to navigation drawer.
- [ ] Localize all strings RU + EN.
- [ ] Frontend tests:
- bUnit: loader page invokes use case with correct parameters in both modes
- bUnit: results list filter narrows correctly
## Files to Modify/Create
- `src/Marathon.Application/UseCases/PullResultsUseCase.cs` — extend
- `src/Marathon.UI/Pages/Results/ResultsLoader.razor`
- `src/Marathon.UI/Pages/Results/ResultsList.razor`
- `tests/Marathon.Application.Tests/UseCases/PullResultsUseCaseTests.cs`
- `tests/Marathon.UI.Tests/Pages/Results/**`
## Acceptance Criteria
- Compiles (Big Bang).
- Selective loading respects user's selection.
- Bulk loading skips events that already have results.
- UI shows progress during a multi-event load.
## Notes
- Big Bang: compile-only smoke check.
## Review Checklist
- [ ] Idempotent — no duplicate `EventResult` rows
- [ ] UI handles empty range gracefully (no events match)
- [ ] All strings localized
## Handoff to Next Phase
<!-- Filled by Phase 8 implementer. Phase 9 is packaging — note any runtime requirements
(e.g., Playwright browser binaries) that need to be bundled with the installer. -->
@@ -0,0 +1,133 @@
# Phase 9: Packaging + Polish (FINAL PHASE — full build + tests required)
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** fullstack
**Implementer:** Sonnet 4.6
**Type:** **Final phase — Big Bang strategy mandates full build + full test suite pass.**
## Objective
Make the application shippable: comprehensive logging, finalized configuration UX,
deployment artifact (single-file exe and/or MSIX installer), README with end-user
setup, screenshots, and a final pass for any cross-cutting polish (error UI,
empty states, loading states, telemetry).
## Tasks
### Verification (FIRST — gate before any new work)
- [ ] `dotnet restore Marathon.sln` — succeeds
- [ ] `dotnet build Marathon.sln` — succeeds with NO warnings in Release mode
- [ ] `dotnet test Marathon.sln` — ALL tests pass (this is the first time the full
suite runs end-to-end since Big Bang strategy was used)
- [ ] `dotnet format Marathon.sln --verify-no-changes` — passes
- [ ] **If any of the above fails, fix before proceeding.** This is the only phase
where build + tests are mandatory under Big Bang.
### Logging
- [ ] Configure Serilog in `Marathon.Hosts.WpfBlazor/App.xaml.cs`:
- Rolling file: `./logs/marathon-.log`, 7-day retention, 50 MB per file cap
- Console sink (debug builds only)
- Enrichers: `FromLogContext`, `WithThreadId`, `WithProcessId`
- Minimum level via config: `Logging:MinimumLevel` (default `Information`)
- [ ] Add structured logging at key points:
- Scraping cycles start/end (sport, count, duration)
- Snapshot persisted (event ID, snapshot ID)
- Anomaly detected (event ID, score)
- Excel export completed (path, row count)
- All exceptions with stack + context
### Settings UX polish
- [ ] Settings page validates input client-side (e.g., polling interval ≥ 5s)
- [ ] Confirmation dialog before saving settings that require restart
- [ ] "Reset to defaults" button per section
- [ ] Live-edit of polling intervals takes effect within the next cycle
### Empty states & loading states
- [ ] Every page that loads data shows a skeleton/spinner during fetch
- [ ] Every list shows an empty-state illustration + helpful copy when no data
- [ ] Network errors surface a clear toast with retry action
### Error UI
- [ ] Global error boundary in `MainLayout.razor` catches Blazor exceptions
- [ ] Display friendly message + "report issue" copy (with log path)
- [ ] Errors logged to Serilog with full stack
### Packaging
- [ ] Add `dotnet publish` profile for single-file self-contained exe:
```
dotnet publish src/Marathon.Hosts.WpfBlazor -c Release -r win-x64 --self-contained \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
```
- [ ] (Optional) MSIX packaging via `Microsoft.Windows.SDK.BuildTools` — only if time
permits and customer wants installer flow.
- [ ] If Playwright is bundled, ensure browser binaries are included via
`playwright.exe install` step in publish output.
- [ ] Bundle `appsettings.json` in publish output; `appsettings.Local.json` is
generated on first run.
### Documentation
- [ ] Expand `README.md`:
- System requirements (Windows 10+, .NET 8 runtime if not self-contained)
- Installation instructions
- First-run configuration walkthrough
- Excel export sheet/column reference
- Troubleshooting section (logs location, common errors)
- Screenshots of main UI surfaces
- [ ] Create `docs/USER_GUIDE.md` (RU) and `docs/USER_GUIDE_EN.md` for end users.
- [ ] Update `CLAUDE.md` with final permanent learnings.
- [ ] Capture screenshots: pre-match list, live list, event detail with chart,
anomaly feed, settings page. Place in `docs/screenshots/`.
### Final commit hygiene
- [ ] No commented-out code anywhere
- [ ] No `TODO(phase-N)` markers remaining (Phase 9 IS the resolution phase)
- [ ] `dotnet format` applied to entire solution
- [ ] No NuGet vulnerabilities (`dotnet list package --vulnerable --include-transitive`)
## Files to Modify/Create
- `src/Marathon.Hosts.WpfBlazor/App.xaml.cs` — Serilog config
- `src/Marathon.Hosts.WpfBlazor/Properties/PublishProfiles/win-x64-self-contained.pubxml`
- `src/Marathon.UI/Components/EmptyState.razor`, `LoadingSpinner.razor`,
`ErrorBoundary.razor`
- `README.md` — expanded
- `docs/USER_GUIDE.md`, `USER_GUIDE_EN.md`
- `docs/screenshots/*.png`
- `CLAUDE.md` — final updates
## Acceptance Criteria
- **Build passes**: `dotnet build` clean, zero warnings in Release.
- **All tests pass**: `dotnet test` green.
- **Lint passes**: `dotnet format --verify-no-changes` clean.
- **Publish succeeds**: single-file exe produced and launches without errors.
- **Documentation complete**: README + user guides + screenshots.
- **No vulnerabilities**: `dotnet list package --vulnerable` returns nothing.
## Notes
- This is the FINAL phase. The next step after this is the comprehensive review
agent + security review + user merge approval.
- If new bugs surface during full-suite testing, fix them here. Document each fix
in CLAUDE.md if it reveals a permanent project lesson.
## Review Checklist
- [ ] Build, tests, lint all green
- [ ] Single-file publish works
- [ ] Logs land in expected location with sensible content
- [ ] No remaining `TODO(phase-N)` markers
- [ ] Screenshots match current UI
## Handoff to Next Phase
<!-- This is the final phase. The "next phase" is the final-reviewer + merge step. -->