commit a2396a39a703048cc812a4d920905d8f5b4b4ced Author: alexei.dolgolyov Date: Tue May 5 00:31:54 2026 +0300 chore: initial repo setup (.gitignore, README, CLAUDE.md) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4456e65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# ============================================================================= +# .NET +# ============================================================================= +bin/ +obj/ +out/ +*.user +*.suo +*.userprefs +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +*.lock.json +*.nuget.props +*.nuget.targets +project.lock.json +project.fragment.lock.json +artifacts/ +TestResults/ +coverage/ +*.coverage +*.coveragexml +*.opencover.xml + +# ============================================================================= +# Visual Studio / Rider / VS Code +# ============================================================================= +.vs/ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.idea/ +*.swp +*.swo +*~ + +# ============================================================================= +# OS +# ============================================================================= +Thumbs.db +ehthumbs.db +Desktop.ini +.DS_Store +$RECYCLE.BIN/ + +# ============================================================================= +# Build / Tooling artifacts +# ============================================================================= +*.log +*.tlog +*.tmp +*.cache +*.dll +*.exe +*.pdb +node_modules/ +.playwright/ +playwright-report/ +test-results/ + +# ============================================================================= +# Project-specific +# ============================================================================= +# Local SQLite databases (never commit data) +*.db +*.db-shm +*.db-wal +data/ +exports/ + +# Local secrets / runtime config overrides +appsettings.Local.json +appsettings.*.local.json +.env +.env.* +!.env.example + +# Scraping fixtures captured during Phase 0 spike (kept locally, not in repo) +spike/captures/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..99e5d4e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,105 @@ +# CLAUDE.md — maraphon-app + +> Project memory for Claude Code sessions on this repository. Keep entries concise. +> Per-feature learnings are appended below by the feature-planner workflow. + +## Project Overview + +**maraphon-app** is a sports betting odds analyzer for marathonbet.by. It scrapes +pre-match (`/su`) and live (`/su/live`) events, persists odds snapshots over time, and +detects anomalies — especially the **odds-flip** pattern (bookmaker freezes bets then +inverts underdog/favorite coefficients). + +## Architecture (Clean Architecture, 5 projects + tests) + +``` +Marathon.Domain ← entities, value objects, no external deps + ↑ +Marathon.Application ← use cases + abstractions (IOddsScraper, IRepository, ...) + ↑ +Marathon.Infrastructure ← EF Core (SQLite), scraping (AngleSharp/Playwright), Excel, Polly +Marathon.UI ← Razor Class Library (all Blazor components — host-agnostic) + ↑ +Marathon.Hosts.WpfBlazor ← WPF + BlazorWebView host (replaceable for ASP.NET Core later) +``` + +**Key portability invariant:** All UI lives in `Marathon.UI` (Razor Class Library). The +host project (`Marathon.Hosts.WpfBlazor`) is the *only* thing that changes when migrating +to a web app — drop in an ASP.NET Core Blazor Server host that references the same RCL. + +## Tech stack + +- **.NET 8 LTS**, C# 12 +- **EF Core 8** + SQLite (WAL mode) +- **AngleSharp** (HTML), **Playwright for .NET** (SPA fallback) +- **Polly v8** (`Microsoft.Extensions.Http.Resilience`) +- **MudBlazor** components, **Plotly.Blazor** charts +- **Serilog** logging (rolling file + console) +- **xUnit + FluentAssertions + NSubstitute**, in-memory SQLite for repo tests + +## Build & test + +| Command | Purpose | +|---|---| +| `dotnet build Marathon.sln` | Build all projects | +| `dotnet test Marathon.sln` | Run all tests | +| `dotnet format Marathon.sln --verify-no-changes` | Lint | +| `dotnet run --project src/Marathon.Hosts.WpfBlazor` | Run desktop app | + +## Coding conventions + +- Nullable reference types: **enabled** (`enable`) +- Implicit usings: enabled +- Treat warnings as errors in `Release` builds +- File-scoped namespaces +- One public type per file (except small DTOs/records grouped in a feature folder) +- Domain entities: prefer `record` for immutable data; class with private setters when + identity matters +- No mutation of domain objects after construction — return new instances +- Repositories return `IReadOnlyList`, not `List` or `IEnumerable` (clarity on + enumeration cost) +- Tests follow `Given_When_Then` or `Should__When_` naming + +## Configuration + +Every variable parameter is configurable via `appsettings.json` and overridable via +`appsettings.Local.json` (gitignored) or environment variables: + +- `Scraping:PollingIntervalSeconds` (default 30) +- `Scraping:MaxConcurrentRequests` (default 4) +- `Scraping:UserAgents[]` (rotated per request) +- `Scraping:RetryPolicy:*` (Polly settings) +- `Scraping:RateLimit:RequestsPerSecond` (default 1) +- `Storage:DatabasePath` (default `./data/marathon.db`) +- `Storage:ExportDirectory` (default `./exports`) +- `Storage:SnapshotRetentionDays` (default 90) +- `Anomaly:SuspensionGapSeconds` (default 60) +- `Anomaly:OddsFlipThreshold` (default 0.30 — implied probability delta) +- `Localization:DefaultCulture` (default `ru-RU`) + +A future Settings page in the UI binds to these. + +## Domain model summary + +- `Sport(Code, Name)` — e.g., `(6, "Баскетбол")` +- `Event(Id, SportCode, CountryCode, LeagueId, CategoryId, ScheduledAt, EventCodeFromBookmaker)` +- `OddsSnapshot(EventId, CapturedAt, Source: Pre|Live, Bets: List)` +- `Bet(Scope: Match|Period[N], Type: Win|Draw|WinFora|Total, Side: 1|2|Less|More, Value?, Rate)` +- `EventResult(EventId, FinalScore, WinnerSide)` +- `Anomaly(EventId, DetectedAt, Kind: SuspensionFlip, Score, EvidenceTimeline)` + +## Excel export schema (compliance with customer spec) + +Customer TZ requires wide-table layout with columns like `Bet_Match_Win_1`, +`Bet_Period-1_Win_Fora_2_Value`, etc. + +**Internal storage is normalized** (one row per Bet in `OddsSnapshots`). The Excel +exporter denormalizes to the wide format on demand. Filename pattern: + +``` +Marathon__to_.xlsx +``` + +## Recurring Issues & Patterns + +(Populated as we work — leave empty until something repeats.) diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa63e9c --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# maraphon-app + +Sports betting odds analyzer for [marathonbet.by](https://www.marathonbet.by). + +Scrapes pre-match (`/su`) and live (`/su/live`) sports events, tracks coefficient changes +over time, and detects anomalies — in particular the "odds-flip" pattern where the +bookmaker freezes betting and then inverts underdog/favorite odds. + +## Tech stack + +- **.NET 8** + C# 12 +- **Blazor Hybrid** — WPF shell hosting `BlazorWebView` (designed to migrate to ASP.NET + Core Blazor Server with no UI rewrite) +- **EF Core + SQLite** (WAL mode) for local storage +- **ClosedXML** for Excel export +- **AngleSharp** for HTML scraping (with **Playwright** fallback for JS-rendered pages) +- **Polly v8** for retry / circuit breaker / rate limiting +- **MudBlazor** UI components, **Plotly.Blazor** for charts +- **Serilog** structured logging +- **xUnit / FluentAssertions / NSubstitute** for tests + +## Project layout + +``` +src/ + Marathon.Domain/ entities, value objects, no dependencies + Marathon.Application/ use cases, abstractions (IOddsScraper, IRepository, ...) + Marathon.Infrastructure/ EF Core, scraping, Polly, Excel, Playwright + Marathon.UI/ Razor Class Library — all Blazor components live here + Marathon.Hosts.WpfBlazor/ WPF + BlazorWebView host (replaceable for web) +tests/ + Marathon.*.Tests/ unit + integration tests per layer +``` + +## Build & run + +```pwsh +dotnet build Marathon.sln +dotnet test Marathon.sln +dotnet run --project src/Marathon.Hosts.WpfBlazor +``` + +## Configuration + +All variable parameters (polling intervals, concurrency, user-agents, retry policy, +snapshot retention, locale) are exposed via `appsettings.json` and live-editable via +the in-app **Settings** page. + +## Status + +🟡 In active development. See [`plans/initial-implementation/PLAN.md`](plans/initial-implementation/PLAN.md) +for the current phase plan and progress. + +## License + +Private — customer project.