4.2 KiB
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 (
<Nullable>enable</Nullable>) - Implicit usings: enabled
- Treat warnings as errors in
Releasebuilds - File-scoped namespaces
- One public type per file (except small DTOs/records grouped in a feature folder)
- Domain entities: prefer
recordfor immutable data; class with private setters when identity matters - No mutation of domain objects after construction — return new instances
- Repositories return
IReadOnlyList<T>, notList<T>orIEnumerable<T>(clarity on enumeration cost) - Tests follow
Given_When_ThenorShould_<expected>_When_<condition>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(defaultru-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>)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_<YYYY-MM-DD>_to_<YYYY-MM-DD>.xlsx
Recurring Issues & Patterns
(Populated as we work — leave empty until something repeats.)