Two defense-in-depth findings from the I-series security review (both safe today,
neither currently exploitable):
- AddBetForm.Notes was unbounded free-text into SQLite; add a 2000-char sanity cap
in IsValid (covers both the add and edit paths), alongside the existing stake/rate
caps.
- EventId only rejected empty/whitespace; now also reject path separators, '..'
traversal, control/newline chars and over-length input so no current-or-future
consumer that builds a path/filename/log line from an id can be tricked. The
charset stays open for forward-compat with non-numeric bookmaker ids.
AnomalyBrowsingService.TryProject fell back to `new SportCode(0)` when an anomaly's
event was missing — but SportCode throws for 0, which would blow up the whole
feed/dashboard for that row. Anomalies have an FK to events so it was dead in
practice, but an orphaned row now degrades gracefully (skipped, like a row with
unparseable evidence). Closes the flagged latent crash.
- TryProject returns false when the event lookup misses; +1 test.
Adds a detector-kind chip row to the anomaly feed (SuspensionFlip / SteamMove /
SuspensionFreeze / OverroundCompression), multi-select like the sport filter — so
with four detectors live you can slice the feed to a single signal type. The kind
set lives on AnomalyFilter and filters in-memory alongside severity/sport, persisted
via AnomalyBrowsingState like the other filters.
- AnomalyFilter.Kinds + AnomalyBrowsingService in-memory Where clause; feed chip
row + ToggleKind/KindLabel; en/ru resx (Anomaly.Filter.Kind).
- 2 tests: kind-filtered subset + no-filter returns all kinds.
New /anomalies/compare page runs every saved strategy preset over the same
window and ranks them by ROI — bets, W–L, hit-rate, net, and max drawdown side
by side, with the best ROI flagged. Auto-runs on load with an optional date-range refine.
- CompareStrategiesUseCase fans RunBacktestUseCase over saved presets (re-loads the
anomaly set per preset — fine for the handful a user keeps; stays bug-for-bug
identical to a single backtest run).
- StrategyComparisonService.BuildVm (pure) computes per-row hit-rate + a single
best-by-ROI flag; nav entry + en/ru resx.
- 6 tests: use-case fan-out + BuildVm best/tie/no-bets/hit-rate.
Adds the read-only paper-trading page (/paper-trading): settled-only P&L KPIs
(net profit, ROI, hit rate, open count) plus a per-bet ledger table, with a
Forward-test nav entry under Analysis. PaperTradingService batches the
event-title join (no N+1) and folds settled bets into the summary.
Also hardens PaperTradingWorker (review finding): settle now runs in its own
catch so a transient settle failure can't advance the since-marker past an
open window — the window replays until its opens succeed.
- IPaperTradingService / PaperTradingService / PaperTradingVm + PaperBetRowVm.
- en/ru resx (full parity), service registration, nav entry.
- 2 service tests: empty ledger + settled-only aggregation incl. title fallback.
Persist named backtest-strategy presets so a staking config (bankroll,
min-score, stake rule, flat/percent/Kelly params) can be saved, listed,
loaded back into the form, and deleted. The per-run date range is not
part of a preset.
- Domain: SavedStrategy record (name trimmed + bounded to 80 chars,
Create() factory) wrapping the pure BacktestStrategy.
- Persistence: SavedStrategyEntity + config (TEXT decimals, unique
case-insensitive NOCASE index on Name), repository, mapping, and a
hand-trimmed AddSavedStrategies migration (additive — only the new
table). Case-insensitive names mean save-by-name overwrites instead of
creating near-duplicates.
- Application: SaveStrategyUseCase (upsert by name, keeps Id+CreatedAt) +
DeleteStrategyUseCase.
- UI: presets panel on the Backtest page (load/save/delete) + service
methods; fraction<->percent round-trip; en/ru resx.
- Fix: pin Sports.Code as ValueGeneratedNever — it is the bookmaker's
natural sport id, not an autoincrement surrogate. Corrects long-standing
model-snapshot drift; the snapshot is regenerated to match the DB.
- 25 tests across all four layers: domain validation, real-SQLite
round-trip incl. case-insensitive lookup/uniqueness, the upsert use
case, and the service percent mapping.
- Add IDashboardSummaryService/DashboardSummaryService: real event/snapshot/
anomaly counts, top-5 signals, and per-stage pipeline health from worker state.
- Home: replace hard-coded zeros + placeholder feed with live data, a clickable
signal feed, and a first-run empty state with a Settings CTA.
- MainLayout: add an appbar capture-status pill (Capturing/Paused) bound to the
poller toggles, refreshed via IOptionsMonitor.OnChange.
- MyBets: success snackbar on bet submit. Backtest: surface a Cancel button
while a run is in flight.
- Add en/ru localization for all new strings; register IOptionsMonitor<WorkerOptions>
in the bUnit test context for layout-rendering tests.
- Add IEventRepository/IResultRepository.GetManyAsync to kill N+1 lookups at
6 sites (backtest, outcome eval, both bet-journal paths, anomaly browsing,
results selection); guarded by a Received(1).GetManyAsync test.
- Add EventRepository.QueryAsync to push date+sport filtering to SQL (was
load-whole-range-then-filter); search/sort stay in-memory for Cyrillic order.
- Add AnomalyRepository.CountSinceAsync (unread badge) + ListByDateRangeAsync
(feed date filter); add Event/Snapshot count methods for the dashboard.
- Add composite indexes IX_Snapshots_EventCode_CapturedAt and
_EventCode_Source_CapturedAt via a new migration + model snapshot.
- Introduce SqliteDateText as the single source of the O-format date encoding
shared by Mapping (read/write) and the repositories' range predicates.
- Fix LiveOddsPoller cadence drift (budget sleep against cycle time); make
DetectAnomalies dedup O(1) per event; add Event.Title to dedup the title join.
Tests adapted to the batched GetManyAsync via a TestFixtures bridge.
* Pages/Results/ResultsList.razor — completed-events list with date range,
sport/winner filter, search, footer count.
* Pages/Results/ResultsLoader.razor — driver page with two modes (load all
in range / load selected events), live progress reporting via
IProgress<PullResultsProgress>, summary line, cancellable.
* Replaces the Phase 5 Pages/Results.razor placeholder.
Service layer:
* IResultsBrowsingService + ResultsBrowsingService (Scoped, mirrors the
Event/Anomaly browsing-service pattern). Reads IResultRepository +
IEventRepository, projects to immutable view-model records.
* UiServicesExtensions: registers ResultsBrowsingService; also fixes an
unrelated localization resolver bug (drop ResourcesPath since
SharedResource lives in the Marathon.UI.Resources namespace already).
Localization:
* 41 new Results.* keys (RU+EN parity) covering both pages, filter chips,
loader modes, progress states, and footer copy.
Tests:
* ResultsListTests + ResultsLoaderTests — 22 new bUnit tests covering
filter narrowing, mode switching, progress aggregation, and empty
states.
* FakeResultsBrowsingService support type for tests.
* MarathonTestContext registers the fake; TestData adds factories for
EventResult/EventResultListItem.
Creates the 9-project .NET 8 solution (5 src + 4 test) with Marathon.Domain
fully implemented: value objects (SportCode, EventId, OddsRate, OddsValue,
BetScope hierarchy), enums (Side, BetType, OddsSource, AnomalyKind), and
entities (Sport, Country, League, Event, Bet, OddsSnapshot, EventResult,
Anomaly) with all invariants enforced in constructors. 96 domain tests pass
(FluentAssertions + xUnit). Directory.Build.props and Directory.Packages.props
centralise build settings and NuGet versions. Both Marathon.sln and Marathon.slnx
are committed; dotnet build Marathon.sln succeeds with 0 warnings/errors.