docs(initial-implementation): add feature plan and 10 phase subplans
This commit is contained in:
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
<!-- 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. -->
|
||||||
Reference in New Issue
Block a user