diff --git a/plans/initial-implementation/CONTEXT.md b/plans/initial-implementation/CONTEXT.md new file mode 100644 index 0000000..5c0a78e --- /dev/null +++ b/plans/initial-implementation/CONTEXT.md @@ -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` 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) diff --git a/plans/initial-implementation/PLAN.md b/plans/initial-implementation/PLAN.md new file mode 100644 index 0000000..83ec94f --- /dev/null +++ b/plans/initial-implementation/PLAN.md @@ -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 0–8). +> 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) diff --git a/plans/initial-implementation/phase-0-scraping-spike.md b/plans/initial-implementation/phase-0-scraping-spike.md new file mode 100644 index 0000000..973c804 --- /dev/null +++ b/plans/initial-implementation/phase-0-scraping-spike.md @@ -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 1–9 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 2–3 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 + + diff --git a/plans/initial-implementation/phase-1-solution-and-domain.md b/plans/initial-implementation/phase-1-solution-and-domain.md new file mode 100644 index 0000000..7cb27ef --- /dev/null +++ b/plans/initial-implementation/phase-1-solution-and-domain.md @@ -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 + + + net8.0 + enable + enable + 12 + true + latest + + + ``` +- [ ] Add `Directory.Packages.props` for centralized NuGet versions (mark + `true`). +- [ ] 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 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 + + diff --git a/plans/initial-implementation/phase-2-storage.md b/plans/initial-implementation/phase-2-storage.md new file mode 100644 index 0000000..8e52b0e --- /dev/null +++ b/plans/initial-implementation/phase-2-storage.md @@ -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` — generic CRUD: `GetAsync`, `ListAsync`, + `AddAsync`, `UpdateAsync`, `DeleteAsync`, `SaveChangesAsync` + - `IEventRepository : IRepository` — adds `ListByDateRangeAsync`, + `ListBySportAsync` + - `ISnapshotRepository : IRepository` — adds + `ListByEventAsync(EventId, DateTimeOffset from, DateTimeOffset to)` + - `IResultRepository : IRepository` + - `IAnomalyRepository : IRepository` + - `IExcelExporter` — `ExportAsync(DateRange range, ExportKind kind, string outputPath)` + where `ExportKind = PreMatch | Live | Combined` +- [ ] Implement `MarathonDbContext` in `Marathon.Infrastructure/Persistence/`: + - `DbSet`, `DbSet`, `DbSet`, + `DbSet`, `DbSet`, `DbSet`, + `DbSet` + - Configure SQLite with WAL via connection string + - Use `EntityTypeConfiguration` 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__to_.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` and produces a + flat `Dictionary` 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 + + diff --git a/plans/initial-implementation/phase-3-scraping.md b/plans/initial-implementation/phase-3-scraping.md new file mode 100644 index 0000000..ca8384b --- /dev/null +++ b/plans/initial-implementation/phase-3-scraping.md @@ -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> ScrapeUpcomingAsync(SportCode? filter, CancellationToken ct)` + - `Task ScrapeEventOddsAsync(EventId id, OddsSource source, CancellationToken ct)` + - `Task> 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`, `ILogger` + - Methods correspond to `IOddsScraper` interface +- [ ] Implement parsers in `Marathon.Infrastructure/Scraping/Parsers/`: + - `UpcomingEventsParser` — parses listing page → `IReadOnlyList` + - `LiveEventsParser` — parses live listing → `IReadOnlyList` + - `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` + - 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(); + 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().Bind(config.GetSection("Scraping"))` + - `services.AddHttpClient("marathonbet").AddResilienceHandler(...)` + - `services.AddSingleton()` + - `services.AddSingleton()` +- [ ] 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 + + diff --git a/plans/initial-implementation/phase-4-application-and-workers.md b/plans/initial-implementation/phase-4-application-and-workers.md new file mode 100644 index 0000000..a6a2981 --- /dev/null +++ b/plans/initial-implementation/phase-4-application-and-workers.md @@ -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? 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`, 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()`. +- [ ] 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 + + diff --git a/plans/initial-implementation/phase-5-host-theme-i18n.md b/plans/initial-implementation/phase-5-host-theme-i18n.md new file mode 100644 index 0000000..da24d4c --- /dev/null +++ b/plans/initial-implementation/phase-5-host-theme-i18n.md @@ -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 `true`, `false` + - 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` + - `net8.0` 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 `` +- [ ] 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 + + diff --git a/plans/initial-implementation/phase-6-event-browsing-ui.md b/plans/initial-implementation/phase-6-event-browsing-ui.md new file mode 100644 index 0000000..d0f5c10 --- /dev/null +++ b/plans/initial-implementation/phase-6-event-browsing-ui.md @@ -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 + + diff --git a/plans/initial-implementation/phase-7-anomaly-detection.md b/plans/initial-implementation/phase-7-anomaly-detection.md new file mode 100644 index 0000000..3773e12 --- /dev/null +++ b/plans/initial-implementation/phase-7-anomaly-detection.md @@ -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` for an event, returns + `IReadOnlyList` + - 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 + + diff --git a/plans/initial-implementation/phase-8-results-loader.md b/plans/initial-implementation/phase-8-results-loader.md new file mode 100644 index 0000000..3cceac3 --- /dev/null +++ b/plans/initial-implementation/phase-8-results-loader.md @@ -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 + + diff --git a/plans/initial-implementation/phase-9-packaging-polish.md b/plans/initial-implementation/phase-9-packaging-polish.md new file mode 100644 index 0000000..1484db5 --- /dev/null +++ b/plans/initial-implementation/phase-9-packaging-polish.md @@ -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 + +