12208a4762
Frontend portion of Phase 7. Backend (commit a6ff368) had already shipped
the AnomalyDetector, DetectAnomaliesUseCase, AnomalyDetectionPoller, and
all DI wiring. This commit adds the user-facing surfaces.
New surfaces (Option A routing — folder-per-feature):
- Pages/Anomalies/AnomalyFeed.razor (@page /anomalies) — replaces the
Phase 5 placeholder with a severity-coded card stream, filter chips
(severity / sport / date), unread-count summary, 'Mark all read' action.
- Pages/Anomalies/Detail.razor (@page /anomalies/{id:guid}) — m-detail-header
lockup + AnomalyEvidence panel + back link to /events/{eventCode}.
New components:
- AnomalyCard.razor — severity-tinted left border (signal-red on High,
amber on Medium, neutral on Low) + SeverityBadge pill + sport icon +
pre→post tabular-mono rate strip + relative time. Click navigates.
- SeverityBadge.razor — small pill mapping score → bucket per backend
handoff (Low <0.45, Medium <0.60, High ≥0.60).
- AnomalyEvidence.razor — two-column pre/post panel with implied-prob
bars + raw rates; favourite-swap callout when argmax(p_pre) ≠ argmax(p_post);
signal-red 3px left border on the post column. Handles 2-way (no draw).
State + service split mirrors Phase 6's pattern:
- AnomalyViewModels.cs — AnomalyListItem / AnomalyDetailVm / Severity enum
/ AnomalyEvidenceSnapshot record. Severity computed in the view-model
from Score.
- IAnomalyBrowsingService / AnomalyBrowsingService — wraps IAnomalyRepository,
parses Anomaly.EvidenceJson into typed view-models, applies filters
client-side. Methods: ListAsync(filter, ct), GetByIdAsync(id, ct),
GetUnreadCountAsync(since, ct).
- AnomalyBrowsingState — Singleton holding AnomalyFilter (severity threshold,
sport set, date range) + LastSeenUtc + cached UnreadCount. OnChange event.
Nav badge:
- NavBody.razor subscribes to AnomalyBrowsingState.OnChange, renders a
pulsing red m-nav__badge when UnreadCount > 0. Badge resets when the
user clicks 'Mark all read' on the feed toolbar.
Settings toggle:
- Settings.razor — added Workers:AnomalyDetectionEnabled toggle (backend
added the flag). Localized via Settings.Worker.AnomalyDetectionEnabled.
- Marathon.UI.Services.WorkerOptions mirror — added AnomalyDetectionEnabled
(default true).
Localization: +30 RU/EN keys following the dot-segmented convention
(Anomaly.*, Settings.Worker.AnomalyDetectionEnabled). Full key parity verified.
Tests (+31 bUnit, all passing):
- AnomalyFeedTests, AnomalyDetailTests
- AnomalyCardTests, SeverityBadgeTests, AnomalyEvidenceTests
- FakeAnomalyBrowsingService support fake registered in MarathonTestContext.
Routing: deleted the Phase 5 Pages/Anomalies.razor placeholder; new feed
page lives at Pages/Anomalies/AnomalyFeed.razor.
Build: 0 warnings, 0 errors.
Tests: Domain 109 + Application 19 + Infrastructure 80 + UI 68 = 276/276
(baseline 245, +31 new bUnit tests, no regressions).
Phase 7 status: ✅ Done (backend + frontend both complete, awaiting review).
Known deferral: AnomalyBrowsingState.LastSeenUtc is in-memory only; the
unread-count badge resets on app restart. Acceptable for now; Phase 9 may
extend ISettingsWriter or add an ILastSeenStore.
291 lines
19 KiB
Markdown
291 lines
19 KiB
Markdown
# 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
|
||
|
||
- **Public results / archive endpoint** — does NOT exist. Tested
|
||
`https://www.marathonbet.by/su/results`, `/su/results/`, `/su/results.htm` —
|
||
all return HTTP 404. No `/archive`, `/history` links anywhere in the public
|
||
HTML either. **Phase 8 deviation:** the Results loader cannot back-fill from
|
||
an archive — it must poll each event detail page until
|
||
`eventJsonInfo.matchIsComplete=true` and snapshot `resultDescription` at that
|
||
moment. Phase 8 implementer must revise the subplan accordingly.
|
||
- **JSONP `/su/liveupdate/popular/` endpoint** — exposes only refresh signals
|
||
(`{"modified":[{"type":"refreshPage"}],"updated":<ts>}`), not actual odds. Cannot
|
||
be used as a JSON odds source. Use it only as a "something changed" hint to
|
||
trigger a full event-detail re-scrape.
|
||
- **Anonymous WebSocket (STOMP)** at `/su/websocket/endpoint` is documented in
|
||
`initData.stomp` but appears to require an authenticated session
|
||
(`PUNTER-SESSION-HASH` cookie); we did not test it but the customer's anonymous
|
||
scraping constraint makes it unsuitable anyway.
|
||
|
||
## Review Findings Log
|
||
|
||
(populated by reviewers)
|
||
|
||
## Phase Execution Log
|
||
|
||
| Phase | Agent | Model | Test Writer | Parallel | Notes |
|
||
|---|---|---|---|---|---|
|
||
| Phase 0 | phase-implementer | Opus | ⏭️ Skipped (research only) | — | ✅ Done 2026-05-05. Outputs: spike/SCRAPE_FINDINGS.md + spike/SCHEMA_DRAFT.md + 7 local fixtures. Anonymous scraping confirmed feasible; HttpClient+AngleSharp recommended; no Playwright needed; no public results page found (Phase 8 deviation noted). |
|
||
| Phase 1 | phase-implementer | Sonnet 4.6 | ⏭️ Skipped (Big Bang) | — | ✅ Done 2026-05-05. 9 projects (5 src + 4 test). 96 domain tests passed. Key decisions: BetScope sealed hierarchy, ScheduledAt=UTC+3 (Moscow), OddsValue rejects zero. Deviations: slnx auto-created alongside sln, WPF App.xaml.cs needs FQ Application type. |
|
||
| 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) | — | ✅ Done 2026-05-05. 4 use cases, 3 BackgroundService pollers, InfrastructureModule, ApplicationModule, reflection wiring removed. 202/202 tests green (+17 new). |
|
||
| 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) | — | ✅ Done 2026-05-05. PreMatch + Live + Events/Detail pages, EventListShell, SportIcon, OddsCell, OddsTimeline (Plotly.Blazor wrap), ExportDialog. EventBrowsingState + IEventBrowsingService facade. RU+EN strings under PreMatch.* / Live.* / Detail.* / Export.* / Sport.*. 228/228 tests green (+26 new bUnit). |
|
||
| Phase 7 | phase-implementer (split + UI Opus 1M) | Sonnet/Opus | ⏭️ Skipped (Big Bang) | — | ✅ Done 2026-05-05. Backend (Sonnet, a6ff368): pure `AnomalyDetector` + `DetectAnomaliesUseCase` + `AnomalyDetectionPoller` + 14 backend tests. Frontend (Opus 1M): `AnomalyFeed.razor` + `Detail.razor` + `AnomalyCard`/`SeverityBadge`/`AnomalyEvidence` components + `IAnomalyBrowsingService`/`AnomalyBrowsingService`/`AnomalyBrowsingState`/`AnomalyViewModels`. Nav badge with pulsing signal-red unread count. Settings page wired with `Workers:AnomalyDetectionEnabled`. 28 new `Anomaly.*` localization keys (RU+EN parity). 276/276 tests green (+31 new bUnit). |
|
||
| 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
|
||
|
||
### Phase 7 Backend (Anomaly detection, 2026-05-05)
|
||
|
||
- **`AnomalyDetector` is pure domain — no I/O, no DI.** Constructor takes three ints/decimals
|
||
from `AnomalyOptions`; the caller (use case) materialises it per cycle.
|
||
The UI evidence panel can reconstruct the same probabilities from `EvidenceJson` without
|
||
needing to re-invoke the detector.
|
||
- **Implied probability formula:** `p_i = 1 / rate_i`, then normalise so all `p_i` sum to 1
|
||
(divorcé of the bookmaker's margin). This is the standard European odds conversion.
|
||
- **Flip score** = `max(|p_post[i] − p_pre[i]|)` over Match-Win sides (p1, pDraw?, p2).
|
||
Score is clamped to `[0, 1]` before constructing `Anomaly` (domain invariant enforces ≤1).
|
||
- **Two-part gate** — an anomaly requires BOTH: (a) `flipScore ≥ OddsFlipThreshold` AND
|
||
(b) `argmax(p_pre) != argmax(p_post)`. This prevents spurious detections when one side's
|
||
probability shifts a lot but it was never the favourite.
|
||
- **Tennis / 2-way markets** — `pDraw` is `null` when no `BetType.Draw` bet is present.
|
||
The detector and `EvidenceJson` gracefully handle this (JSON field is omitted when null via
|
||
`DefaultIgnoreCondition.WhenWritingNull`).
|
||
- **`EvidenceJson` uses `System.Text.Json` with custom `JsonPropertyName` attributes** on
|
||
sealed nested records (`EvidencePayload`, `SnapshotEvidence`). Source generation was not
|
||
used at this scale — the payload is small and created infrequently.
|
||
- **`DetectAnomaliesUseCase` loads all events + last-24-h live snapshots per cycle.**
|
||
This is a deliberate simplification; a future optimisation is to track `last_run_at` per
|
||
event. Documented as 🟡 in the handoff.
|
||
- **Dedup strategy:** two anomalies are considered duplicates if they share `EventId`, `Kind`,
|
||
and their `DetectedAt` values fall within a 1-minute window. This prevents the same
|
||
suspension triggering re-insertion on consecutive detection cycles while the gap snapshot
|
||
pair remains in the 24-hour window.
|
||
- **`AnomalyOptions` placed in `Marathon.Application/Configuration/`** (not Infrastructure).
|
||
The `AnomalyDetector` itself is in `Marathon.Domain/AnomalyDetection/` but requires no
|
||
options binding — it takes plain constructor parameters.
|
||
- **`AnomalyDetectionPoller` reads `IOptionsMonitor<AnomalyOptions>` per cycle** so that
|
||
hot-reload of `DetectionIntervalSeconds` takes effect without a restart. Same pattern as
|
||
`LiveOddsPoller` reading `WorkerOptions`.
|
||
- **`Workers:AnomalyDetectionEnabled`** added to `WorkerOptions` (default `true`) and
|
||
`appsettings.json`. UI agent must add a Settings toggle for this flag.
|
||
- **New test count: +17** (13 domain + 4 application). Total: 245/245 passing.
|
||
- **Test note:** rates 1.5/2.5 produce a flip score of ~0.25 — BELOW the 0.30 threshold.
|
||
Always use 1.3/4.0 (flip score ~0.51) or steeper to guarantee detection in tests.
|
||
|
||
### Phase 7 Frontend (Anomaly UI, 2026-05-05)
|
||
|
||
- **Routing — Option A.** Removed the `Pages/Anomalies.razor` placeholder and added
|
||
`Pages/Anomalies/AnomalyFeed.razor` (`@page "/anomalies"`) plus
|
||
`Pages/Anomalies/Detail.razor` (`@page "/anomalies/{id:guid}"`). Mirrors the
|
||
`Pages/Events/Detail.razor` shape from Phase 6.
|
||
- **State + Service split mirrors Phase 6** — `AnomalyBrowsingState` (Singleton inside
|
||
the RCL; per-circuit in BlazorWebView), `IAnomalyBrowsingService` →
|
||
`AnomalyBrowsingService` (Scoped). The service does NOT call back into the detector;
|
||
it reads `IAnomalyRepository.ListAsync` + `IEventRepository.GetAsync` (per distinct
|
||
EventId) and maps to immutable view-model records.
|
||
- **`EvidenceJson` parsing** uses `System.Text.Json.JsonSerializer.Deserialize` with
|
||
`PropertyNameCaseInsensitive = true` and private nested DTOs. Failures (malformed
|
||
JSON, missing pre/post snapshot) drop the row silently — the feed shows the rest.
|
||
- **Severity buckets** are defined once in `AnomalySeverityRules` (Low <0.45, Medium
|
||
<0.60, High ≥0.60) per the backend handoff. The UI reuses the same enum across
|
||
filter chips, the badge pill, and the card border.
|
||
- **Signal-red is load-bearing.** High-severity pills, card left borders, evidence
|
||
post-suspension column outline, the favourite-swap callout, and the nav badge all
|
||
bind to `--m-c-anomaly`. Medium severity uses the editorial amber `--m-c-accent`;
|
||
low severity uses the muted `--m-c-ink-soft`. No new color literals introduced.
|
||
- **`AnomalyEvidence` panel** renders two columns (pre → arrow → post). Each row
|
||
shows the side label, an implied-probability bar (favourite uses amber/red), and
|
||
the raw rate in tabular mono. 2-way markets (tennis) skip the Draw row in BOTH
|
||
columns based on the parsed `pDraw` being null. The panel highlights a
|
||
favourite-swap with a one-line callout above the columns.
|
||
- **Nav badge** lives in `NavBody.razor`, driven by `AnomalyBrowsingState.UnreadCount`.
|
||
The feed page calls `IAnomalyBrowsingService.GetUnreadCountAsync(LastSeenUtc)` after
|
||
each load and pushes the count into state. The user clears it via "Mark all read"
|
||
on the feed toolbar (writes `LastSeenUtc = UtcNow`). The badge pulses with
|
||
`m-pulse` and respects `prefers-reduced-motion`.
|
||
- **Settings page** — added the `Workers:AnomalyDetectionEnabled` toggle inside the
|
||
existing WORKERS section, mirroring `LivePollerEnabled` / `UpcomingPollerEnabled`.
|
||
Bound via `IOptionsMonitor<WorkerOptions>` already in scope.
|
||
- **`Marathon.UI.Services.WorkerOptions`** — added `AnomalyDetectionEnabled` mutable
|
||
field (set-able for the form-binding pattern used by the Settings page). The
|
||
Infrastructure-side `WorkerOptions` already had the flag.
|
||
- **Test infrastructure** — added `FakeAnomalyBrowsingService` with
|
||
`MakeItem(...)` / `MakeSnapshot(...)` static factories; registered in
|
||
`MarathonTestContext` alongside `AnomalyBrowsingState`.
|
||
- **Localization** — 28 new `Anomaly.*` keys (RU+EN parity) under the
|
||
`<Surface>.<Element>` convention from Phase 5/6, plus
|
||
`Settings.Workers.AnomalyDetectionEnabled` and its `.Hint`.
|
||
- **New test count: +31** (9 SeverityBadge + 6 AnomalyCard + 6 AnomalyEvidence +
|
||
5 AnomalyFeed + 5 AnomalyDetail). Total: 276/276 passing.
|
||
|
||
### Phase 6 (Event browsing UI, 2026-05-05)
|
||
|
||
- **Plotly.Blazor pinned to 5.4.1.** v7.x exists but introduces breaking changes;
|
||
5.4.1 is the latest on the .NET 8 line and works with our existing MudBlazor
|
||
7.15.0 / .NET 8.0.12 stack. The `Plotly.Blazor.LayoutLib.Margin` type clashes
|
||
with `MudBlazor.Margin` — fully qualify the layout-side type.
|
||
- **Razor source generator does NOT accept C# 11 raw string literals (`"""…"""`)**
|
||
inside `@code` blocks. The parser sees the leading `"""` as the start of a
|
||
normal string and never finds the close, producing an "Unterminated string
|
||
literal" RZ1000. Use concatenated single-quoted attribute strings instead
|
||
(see `SportIcon.razor` SVG constants).
|
||
- **Razor reserves the identifier `code`.** A `@foreach (var code in ...)`
|
||
loop is parsed as the `@code` directive, not as iteration. Use any other
|
||
identifier (`var sportCode in ...`).
|
||
- **`MudBlazor.DateRange` shadows `Marathon.Application.Storage.DateRange`**
|
||
in any file whose `_Imports.razor` brings both namespaces in. Add
|
||
`using AppDateRange = Marathon.Application.Storage.DateRange;` per-file
|
||
where the application's `DateRange` is constructed (already done in
|
||
`ExportDialog.razor` and `ExportDialogTests.cs`).
|
||
- **EventBrowsingService is Scoped, EventBrowsingState is Singleton.** The
|
||
service captures the per-circuit DI scope so EF Core's `DbContext` lifetime
|
||
works correctly; the state object holds the per-page filter records and
|
||
fires `OnChange` only when the new value !equals the old one. This split
|
||
matches Phase 5's split between `ThemeState` (singleton) and per-circuit
|
||
data services.
|
||
- **View-models, not domain entities, cross the UI boundary.** Pages bind to
|
||
`EventListItem` / `EventDetail` / `BetRow` / `OddsTimelinePoint`
|
||
records (defined in `Marathon.UI.Services`). Repositories are not exposed
|
||
to Razor components. This keeps the UI free of EF tracked graphs and
|
||
preserves Phase 5's "RCL is host-agnostic" invariant.
|
||
- **Live page reads polling cadence from `IOptionsMonitor<ScrapingSettingsForm>`.**
|
||
Phase 4's `WorkerOptions.LivePollIntervalSeconds` (drives the poller) is a
|
||
separate setting from the UI's display refresh; the latter intentionally
|
||
follows `Scraping:PollingIntervalSeconds` per the Phase 6 subplan.
|
||
- **Plotly chart memoization.** Computed signature = `(count, first ticks,
|
||
last ticks, first/last rate triples)`. Sufficient to invalidate the trace
|
||
list on any meaningful change while staying cheap during live polling.
|
||
- **bUnit shared `MarathonTestContext` now registers the fake browsing service
|
||
and the browsing state.** Phase 7 tests can extend it directly or follow the same pattern.
|
||
`Support/TestData.MoscowToday(int hour)` produces correctly-offset
|
||
`DateTimeOffset` values — domain `Event.ScheduledAt` will reject any other
|
||
offset.
|
||
|
||
### Phase 1 (Solution skeleton + Domain model, 2026-05-05)
|
||
|
||
- **.NET 10 SDK creates `.slnx` by default.** `dotnet new sln` produces `Marathon.slnx`
|
||
(new XML format), not `Marathon.sln`. A hand-crafted `Marathon.sln` was added alongside
|
||
it so that `dotnet build Marathon.sln` works as specified in the plan. Both files are
|
||
kept; prefer `Marathon.sln` for CLI commands.
|
||
- **`BetScope` is a sealed record hierarchy:** `abstract record BetScope` with
|
||
`sealed record MatchScope : BetScope` (singleton `Instance`) and
|
||
`sealed record PeriodScope(int Number) : BetScope`. Use pattern matching, not
|
||
an enum+nullable approach.
|
||
- **`Event.ScheduledAt` must be UTC+3 (Moscow), not UTC.** The domain enforces
|
||
`Offset == TimeSpan.FromHours(3)`. Phase 3 must construct `DateTimeOffset` with
|
||
`+03:00` before passing to `Event`; do NOT convert to UTC first.
|
||
- **`Directory.Build.props` must NOT set `TargetFramework`** — WpfBlazor needs
|
||
`net8.0-windows` while all other projects use `net8.0`. Each csproj owns its TFM.
|
||
- **`Marathon.Application` namespace conflicts with `System.Windows.Application`**
|
||
in WPF `App.xaml.cs`. Fix: use `System.Windows.Application` fully qualified.
|
||
Phase 5 must keep this qualification.
|
||
- **Central package management:** all `PackageReference` elements in test csproj files
|
||
must NOT include `Version=`. Versions live exclusively in `Directory.Packages.props`.
|
||
- **96 domain tests, 0 failures.** All invariants covered: SportCode, EventId,
|
||
OddsRate, OddsValue, BetScope, Bet (all 4 type combinations), OddsSnapshot,
|
||
Event (ScheduledAt offset), Anomaly.
|
||
|
||
### Phase 0 (Scraping spike, 2026-05-05)
|
||
|
||
- **Anonymous scraping is feasible** from a non-Belarus IP. No Cloudflare, no JS
|
||
challenge, no UA filtering observed. `Server: nginx`. Standard cookies only.
|
||
- **Site is fully SSR.** All needed data (event grid, full odds, breadcrumbs,
|
||
period markets) is in the raw HTML. No SPA hydration required.
|
||
- **Recommended scraper stack: HttpClient + AngleSharp + Polly v8.** Playwright is
|
||
not required for read-only scraping — keep it as an optional fallback flag
|
||
(`Scraping:UsePlaywright`) for future-proofing only.
|
||
- **Polling cadence:** site itself polls live updates every 3 s; for our analyzer,
|
||
pre-match 30 s and live 5–10 s is sufficient.
|
||
- **Rate-limit:** 5 sequential requests at 1 req/s pacing all returned 200 in <1 s,
|
||
no throttling. Recommend default `RequestsPerSecond=1`, `MaxConcurrent=4`.
|
||
- **Sport ID semantics:** customer's "Sport_Code = 6" (Basketball) maps to
|
||
`data-sport-treeId="6"` in the breadcrumb-canonical sport listing
|
||
(`/su/betting/Basketball+-+6`). Some sports also have a separate "category tree
|
||
ID" used inside the live grouping (e.g., 45356 for Basketball-live) — ignore
|
||
those, use only the canonical breadcrumb ID.
|
||
- **Selection key format:** `<eventId>@<MarketName>{LineIndex?}.<Outcome>`. The
|
||
market name is sport-specific (`Match_Result`, `1st_Half_Result`, `Total_Goals`,
|
||
`Total_Points`, `Total_Games`, `To_Win_Match_With_Handicap`, etc.). Total
|
||
thresholds are encoded in the outcome (`Under_3.5`, `Over_213.5`). Handicap
|
||
values are NOT in the key — they're in `<span class="middle-simple">` text.
|
||
- **Tennis has no Draw outcome** — domain `Bet_Match_Draw` must be nullable.
|
||
- **Date display ambiguity:** listing shows `HH:MM` (today) or `DD <ru-month> HH:MM`
|
||
(future). Anchor the parser on `initData.serverTime` (Moscow TZ, format
|
||
`YYYY,MM,DD,HH,MM,SS`).
|
||
- **No public results page** (`/su/results` → 404). Final scores are exposed only
|
||
on the event detail page itself via `eventJsonInfo` JSON
|
||
(`matchIsComplete`, `resultDescription`). Phase 8 must poll until completion;
|
||
cannot back-fill from an archive endpoint.
|
||
- **Probe environment:** Windows 10 + curl, geo-routed as Poland (`countryCode: PL`).
|
||
Customer in Belarus may see slightly different KYC overlays — parser must be
|
||
defensive (treat missing markets as null, never throw).
|
||
- **Captures saved locally** at `spike/captures/*.html` (gitignored): 7 fixtures
|
||
for offline parser development in Phase 3.
|