# 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.)