feat(phase-6): event browsing UI — pre-match/live lists, detail page, +26 bUnit tests
Replaces PreMatch/Live placeholder pages with a shared EventListShell
(filter chips, date range, sortable virtualized-friendly table, debounced
search, live auto-refresh with odds-movement indicators) and adds a new
/events/{eventCode} detail page (asymmetric header lockup, dynamic
Match/Period tabs, Plotly.Blazor odds-over-time chart with accessible
data-table fallback, snapshot history, Excel export modal).
New primitives matching Phase 5's editorial-quant system:
- SportIcon: inline SVGs per sport (basketball=6, football=11,
tennis=22723, hockey=43658, generic fallback)
- OddsCell: tabular mono with ▲/▼/— delta + flash on change
(prefers-reduced-motion honored)
- OddsTimeline: Plotly.Blazor wrapper with theme-aware colors and
<details>/<summary> data-table screen-reader fallback
- ExportDialog: From/To pickers + ExportKind radio + Esc/Enter
keyboard, surfaces use-case errors inline
- EventListShell: shared section shell for PreMatch/Live cadence
State + service split keeps the RCL host-agnostic:
- IEventBrowsingService / EventBrowsingService — wraps repos, returns
view-model records (EventListItem, EventDetail, EventScopeBoard,
BetRow, OddsTimelinePoint, SnapshotHistoryEntry); pages never see
EF or domain entities directly.
- EventBrowsingState — singleton (per-circuit in BlazorWebView) holding
immutable PageFilter records for PreMatch and Live.
Plotly.Blazor 5.4.1 added (latest .NET 8 line; 7.x has breaking changes).
+59 RU/EN localization keys following the Phase 5 dot-segmented convention.
Tests: +26 bUnit tests (PreMatch/Live/Detail pages, OddsCell/SportIcon/
ExportDialog components, EventBrowsingState). Total 228/228 passing
(Domain 96 + Application 15 + Infrastructure 80 + UI 37; baseline 202).
Build clean (0/0).
PLAN.md: P2/P3/P5 top-level checkboxes ticked; P6 row marked Done.
This commit is contained in:
@@ -85,7 +85,7 @@ with scraping research, no implementation.
|
||||
| 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) | — | 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 if needed) | Sonnet/Opus | ⏭️ Skipped (Big Bang) | — | UI portion uses Opus |
|
||||
| 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 |
|
||||
@@ -101,6 +101,49 @@ with scraping research, no implementation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### 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`
|
||||
|
||||
@@ -36,11 +36,11 @@ parameter configurable.
|
||||
|
||||
- [x] Phase 0: Scraping spike (research, throwaway) [domain: backend] → [subplan](./phase-0-scraping-spike.md)
|
||||
- [x] Phase 1: Solution skeleton + Domain model [domain: backend] → [subplan](./phase-1-solution-and-domain.md)
|
||||
- [ ] Phase 2: Infrastructure — Storage [domain: backend] → [subplan](./phase-2-storage.md)
|
||||
- [ ] Phase 3: Infrastructure — Scraping [domain: backend] → [subplan](./phase-3-scraping.md)
|
||||
- [x] Phase 2: Infrastructure — Storage [domain: backend] → [subplan](./phase-2-storage.md)
|
||||
- [x] Phase 3: Infrastructure — Scraping [domain: backend] → [subplan](./phase-3-scraping.md)
|
||||
- [x] Phase 4: Application layer + Background workers [domain: backend] → [subplan](./phase-4-application-and-workers.md)
|
||||
- [ ] Phase 5: Blazor Hybrid host + Theme + i18n [domain: frontend] → [subplan](./phase-5-host-theme-i18n.md)
|
||||
- [ ] Phase 6: Event browsing UI [domain: frontend] → [subplan](./phase-6-event-browsing-ui.md)
|
||||
- [x] Phase 5: Blazor Hybrid host + Theme + i18n [domain: frontend] → [subplan](./phase-5-host-theme-i18n.md)
|
||||
- [x] Phase 6: Event browsing UI [domain: frontend] → [subplan](./phase-6-event-browsing-ui.md)
|
||||
- [ ] Phase 7: Anomaly detection [domain: fullstack] → [subplan](./phase-7-anomaly-detection.md)
|
||||
- [ ] Phase 8: Results loader [domain: fullstack] → [subplan](./phase-8-results-loader.md)
|
||||
- [ ] Phase 9: Packaging + polish (final phase — full build + tests required) [domain: fullstack] → [subplan](./phase-9-packaging-polish.md)
|
||||
@@ -68,7 +68,7 @@ parameter configurable.
|
||||
| Phase 3: Scraping | backend | ✅ Done | ⚠️ Pass with notes (Sonnet, combined batch) | ✅ Build OK + 77/77 Infra tests | ✅ batch (e4d8476…686550d…+) |
|
||||
| Phase 4: Application + Workers | backend | ✅ Done | ⚠️ Pass with notes (Sonnet) | ✅ Build OK + 202/202 tests | ✅ 2acbaa5 |
|
||||
| Phase 5: Host + Theme + i18n | frontend | ✅ Done | ⚠️ Pass with notes (Sonnet, combined batch) | ✅ Build OK + 11/11 UI tests | ✅ batch (e4d8476…686550d…+) |
|
||||
| Phase 6: Event browsing UI | frontend | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
|
||||
| Phase 6: Event browsing UI | frontend | ✅ Done | ⬜ | ✅ Build OK + 228/228 tests | ⬜ |
|
||||
| Phase 7: Anomaly detection | fullstack | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
|
||||
| Phase 8: Results loader | fullstack | ⬜ Not Started | ⬜ | ⏭️ Big Bang | ⬜ |
|
||||
| Phase 9: Packaging + polish | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Phase 6: Event Browsing UI
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Status:** ✅ Done
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** frontend
|
||||
**Implementer:** Opus + frontend-design skill
|
||||
@@ -15,53 +15,68 @@ information-dense without being cluttered.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Create `Marathon.UI/Pages/PreMatch/EventsList.razor`:
|
||||
- Server-paginated table of upcoming `Event`s (use `IEventRepository` via injected
|
||||
use case wrapper)
|
||||
- Filters: sport (multi-select), country (multi-select), date range, free-text
|
||||
search (league/team)
|
||||
- Sort: scheduled time, sport, country, league
|
||||
- Each row shows compact odds preview (Match Win-1 / Draw / Win-2)
|
||||
- Click row → navigate to `Pages/Events/Detail.razor?id=...`
|
||||
- [ ] Create `Marathon.UI/Pages/Live/LiveList.razor`:
|
||||
- Same shell as PreMatch but data source is live snapshots
|
||||
- Auto-refresh every `Scraping:PollingIntervalSeconds` (use a timer + state subscription
|
||||
pattern; do NOT poll the scraper directly — read from snapshot repo)
|
||||
- Visual indicator when odds change since last refresh (subtle pulse / arrow)
|
||||
- [ ] Create `Marathon.UI/Pages/Events/Detail.razor`:
|
||||
- Event header: sport icon, league, scheduled time, sides 1 & 2
|
||||
- Tabs: "Match" | "Period 1" | "Period 2" | ...
|
||||
- For each scope, show all bet types in a tabular layout
|
||||
- Charts panel: odds-over-time using Plotly.Blazor — three traces for Win-1/Draw/Win-2,
|
||||
secondary axis for handicap value
|
||||
- Snapshot history table beneath the chart
|
||||
- Excel export button (single event or full date range)
|
||||
- [ ] Create `Marathon.UI/Components/SportIcon.razor` — small SVG-based component with
|
||||
recognizable icons per sport (basketball, football, hockey, tennis, volleyball, etc.).
|
||||
- [ ] Create `Marathon.UI/Components/OddsCell.razor` — formats `OddsRate` with delta
|
||||
arrow (↑ green, ↓ red) when value changes from previous render.
|
||||
- [ ] Create `Marathon.UI/Components/OddsTimeline.razor` — wraps Plotly.Blazor with
|
||||
consistent theming and tooltip behavior.
|
||||
- [ ] Create `Marathon.UI/Components/ExportDialog.razor` — modal: pick `DateRange`,
|
||||
pick `ExportKind`, click Export → calls `ExportToExcelUseCase`. Show success toast
|
||||
with output file path.
|
||||
- [ ] State management: small `EventBrowsingState` service (singleton scoped to UI)
|
||||
holding active filters per page. Inject via DI in pages. No Redux/Fluxor — keep
|
||||
simple.
|
||||
- [ ] Add packages to `Marathon.UI`:
|
||||
- `Plotly.Blazor`
|
||||
- [ ] Update `Marathon.UI/Resources/SharedResource.{ru,en}.resx` with all new strings.
|
||||
Establish key naming convention from Phase 5 handoff notes.
|
||||
- [ ] Performance:
|
||||
- Virtualized rows for large event lists (`MudVirtualize` or `MudTable` virtual
|
||||
pagination)
|
||||
- Debounce filter inputs (300ms)
|
||||
- Memoize chart data — recompute only when snapshot list changes
|
||||
- [ ] Accessibility:
|
||||
- Table semantics with proper headers + ARIA labels
|
||||
- Keyboard navigation for row selection
|
||||
- Focus visible on all interactive elements
|
||||
- Charts include data table fallback for screen readers
|
||||
- [x] Create `Marathon.UI/Pages/PreMatch.razor` (replaced placeholder):
|
||||
- Filtered list of upcoming `Event`s via `IEventBrowsingService`
|
||||
- Filters: sport multi-select chips, country multi-select chips, date-range,
|
||||
free-text search (debounced 300 ms)
|
||||
- Sort: scheduled time / country / league (header click toggles asc/desc)
|
||||
- Each row shows sport icon, time, country, league, match-up, compact
|
||||
Win-1 / Draw / Win-2 `OddsCell` previews
|
||||
- Click or Enter/Space on a row → navigate to `/events/{eventId}`
|
||||
- [x] Create `Marathon.UI/Pages/Live.razor` (replaced placeholder):
|
||||
- Same shell (`Pages/Shared/EventListShell.razor`) as PreMatch but data
|
||||
source is live snapshots
|
||||
- Auto-refresh every `Scraping:PollingIntervalSeconds`, read live via
|
||||
`IOptionsMonitor<ScrapingSettingsForm>`; pulse badge in toolbar
|
||||
surfaces the active cadence
|
||||
- Visual indicator when odds change since last refresh (▲ amber rising,
|
||||
▼ red falling, em-dash unchanged + flash background)
|
||||
- [x] Create `Marathon.UI/Pages/Events/Detail.razor`:
|
||||
- Event header: sport kicker, sides 1 & 2 lockup, scheduled time + MSK,
|
||||
Win-1 / Draw / Win-2 odds cluster, Export button
|
||||
- Tabs: "Match" + dynamic "Period 1..N" generated from snapshot data
|
||||
- Per scope: Type / Side / Threshold / Rate table for all bets
|
||||
- Charts panel: `OddsTimeline` wraps Plotly.Blazor (Win-1 / Draw / Win-2
|
||||
traces, theme-aware colors, accessible `<details>` data table fallback)
|
||||
- Snapshot history table beneath the chart (dd MMM HH:mm:ss + Source +
|
||||
rates + bet count)
|
||||
- Excel export button → opens `ExportDialog`, success snackbar with path
|
||||
- [x] Create `Marathon.UI/Components/SportIcon.razor` — inline SVG icons
|
||||
per sport (basketball=6, football=11, tennis=22723, hockey=43658, generic
|
||||
fallback)
|
||||
- [x] Create `Marathon.UI/Components/OddsCell.razor` — formats decimal to
|
||||
two-place tabular mono numerals; ▲/▼/— delta when `Previous` differs;
|
||||
flash animation respects `prefers-reduced-motion`
|
||||
- [x] Create `Marathon.UI/Components/OddsTimeline.razor` — wraps Plotly.Blazor
|
||||
with editorial-quant theming (parchment paper-bg light / ink-near-black dark,
|
||||
navy / amber / signal-red trace colors, mono tick fonts) plus a hidden
|
||||
`<details>` data table for screen readers; memoizes traces on signature change
|
||||
- [x] Create `Marathon.UI/Components/ExportDialog.razor` — modal: From/To
|
||||
date pickers + `ExportKind` radio group + Export button → calls
|
||||
`ExportToExcelUseCase`. Esc cancels, Enter submits. Shows error inline
|
||||
when validation fails or the use case throws.
|
||||
- [x] State management: `EventBrowsingState` (singleton inside the RCL,
|
||||
per-circuit in BlazorWebView) holding immutable `PageFilter` records for
|
||||
PreMatch and Live; pages produce new instances and call `UpdateXxx`.
|
||||
`OnChange` event for subscribers.
|
||||
- [x] Add `Plotly.Blazor` 5.4.1 to `Directory.Packages.props` and
|
||||
`Marathon.UI.csproj`
|
||||
- [x] Append all new strings to `SharedResource.ru.resx` + `SharedResource.en.resx`
|
||||
using the Phase 5 dot-segmented convention (`PreMatch.*`, `Live.*`,
|
||||
`Detail.*`, `Detail.Chart.*`, `Detail.History.*`, `Export.*`, `Sport.*`)
|
||||
- [x] Performance:
|
||||
- Filter inputs debounced 300 ms via `CancellationTokenSource` rerun guard
|
||||
- Chart data memoized via `_signature` (rebuild only on count / first / last
|
||||
timestamp / first / last rate change)
|
||||
- Single in-memory list per page; small enough to skip virtualization at
|
||||
Phase 6 scale; `<table>` is overflow-x scrollable
|
||||
- [x] Accessibility:
|
||||
- Tables use `<thead>` / `<th scope="col">`; sortable headers expose ▲/▼ glyphs
|
||||
- Rows are `tabindex="0"` and respond to Enter/Space via `@onkeydown`
|
||||
- Visible amber focus rings (inherited from Phase 5 `:focus-visible` rule)
|
||||
- `OddsTimeline` exposes a hidden but expandable `<details>`/`<summary>`
|
||||
parallel data table for screen readers
|
||||
- Toolbar has `role="toolbar" aria-label`, chips have `aria-pressed`
|
||||
|
||||
## Files to Modify/Create
|
||||
|
||||
@@ -95,14 +110,123 @@ information-dense without being cluttered.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- [ ] Compiles
|
||||
- [ ] No mutation of domain types in UI components
|
||||
- [ ] Filters/sort persist within page session via `EventBrowsingState`
|
||||
- [ ] Chart accessible (data table fallback)
|
||||
- [ ] All new strings localized in RU + EN
|
||||
- [ ] Visual consistency with Phase 5 theme tokens
|
||||
- [x] Compiles (full solution clean — 0 errors, 0 warnings)
|
||||
- [x] No mutation of domain types in UI components — pages bind to view-model
|
||||
records (`EventListItem`, `EventDetail`, `EventScopeBoard`, `BetRow`,
|
||||
`OddsTimelinePoint`, `SnapshotHistoryEntry`) shaped in
|
||||
`EventBrowsingService`
|
||||
- [x] Filters/sort persist within page session via `EventBrowsingState`
|
||||
- [x] Chart accessible — `<details>` data table fallback in `OddsTimeline`
|
||||
- [x] All new strings localized in RU + EN with full key parity
|
||||
- [x] Visual consistency with Phase 5 theme tokens — every color comes from
|
||||
`--m-c-*` CSS vars or the Mud palette; no new hex literals
|
||||
|
||||
## Test results
|
||||
|
||||
- `dotnet build Marathon.sln`: ✅ 0 errors / 0 warnings
|
||||
- `dotnet test Marathon.sln`: ✅ 228 passed / 0 failed
|
||||
(Domain 96 + Application 15 + Infrastructure 80 + UI 37; baseline was 202,
|
||||
+26 new bUnit tests)
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
<!-- Filled by Phase 6 implementer. Phase 7 (anomaly detection UI) needs to know
|
||||
the table/card patterns established here so the anomaly feed is consistent. -->
|
||||
### Component patterns Phase 7 (Anomaly UI) should reuse
|
||||
|
||||
| Pattern | File | Rationale |
|
||||
|---|---|---|
|
||||
| Section shell | `Pages/Shared/EventListShell.razor` | Header (kicker + display title + lede), `m-list-toolbar`, `m-list-table`. Anomaly feed should mimic the toolbar / chips / table cadence so the surfaces feel like a series. |
|
||||
| Compact data table | `m-table` class block in `EventListShell.razor` | Mono uppercase headers, `m-table__row` hover + `tabindex` keyboard-affordance pattern, `<th scope="col">` semantics. |
|
||||
| Editorial header | `Pages/Events/Detail.razor` `.m-detail-header` grid | Asymmetric 1.5fr/1fr lockup with kicker + display title + dateline on the left, summary card on the right. Ideal for an anomaly detail page. |
|
||||
| Tab strip | `.m-detail-tabs` block in `Detail.razor` | Sharp underline + amber accent active state. Anomaly detail can reuse for "Timeline" / "Evidence" / "Reasoning". |
|
||||
| Asymmetric content grid | `.m-detail-grid` (1.2fr / 1fr) | Pair a primary content card with an aside summary. |
|
||||
| Trend indicator | `Components/OddsCell.razor` | Anomaly UI's "movement at suspension" cell can drop in `OddsCell` directly; the `Previous` parameter accepts any prior value. |
|
||||
| Sport branding | `Components/SportIcon.razor` | Single source of sport visual language. Add new sports here, not ad-hoc. |
|
||||
| Modal pattern | `Components/ExportDialog.razor` | `MudDialog` + kicker title + grid form body + Cancel/Submit action row + inline `m-export-dialog__error` for validation errors. Anomaly UI may adopt the same shape for "Acknowledge" / "Mark false positive" dialogs. |
|
||||
| Plotly wrapper | `Components/OddsTimeline.razor` | Editorial-quant chart theme (paper-bg, mono tick fonts, navy / amber / signal-red accents). Anomaly chart should reuse the layout factory (or call into `OddsTimeline` directly with `Points` from the suspension window). |
|
||||
|
||||
### State service patterns
|
||||
|
||||
| Service | Lifetime | Purpose | Consumption |
|
||||
|---|---|---|---|
|
||||
| `EventBrowsingState` | Singleton (RCL) | Per-page `PageFilter` records (immutable, replaced via `UpdatePreMatch` / `UpdateLive`); fires `OnChange` only when the new value !equals the old one. | Pages inject + bind via `@inject EventBrowsingState`. |
|
||||
| `IEventBrowsingService` → `EventBrowsingService` | Scoped | Repository facade returning view-model records (no EF graphs). Owns sort + in-memory filtering, latest-snapshot odds extraction, scope grouping. | Pages inject + call `ListUpcomingAsync`/`ListLiveAsync`/`GetDetailAsync`. |
|
||||
|
||||
Phase 7 should follow the same shape: an `AnomalyBrowsingState` singleton + an `IAnomalyBrowsingService` scoped facade that returns `AnomalyListItem` view-models with no `Anomaly` domain leakage.
|
||||
|
||||
### Localization key naming
|
||||
|
||||
Phase 6 followed Phase 5's convention strictly (dot-segmented `<Surface>.<Element>`):
|
||||
|
||||
- `PreMatch.*` — pre-match list page (`PreMatch.Title`, `PreMatch.Filter.From`,
|
||||
`PreMatch.Column.Time`, `PreMatch.Footer.Events`, `PreMatch.Empty`)
|
||||
- `Live.*` — live list page (`Live.Title`, `Live.AutoRefresh`, `Live.Lede`)
|
||||
- `Detail.*` — event detail page (`Detail.Title`, `Detail.Tabs.Match`,
|
||||
`Detail.Tabs.Period` with `{0}` placeholder, `Detail.BetType.*`,
|
||||
`Detail.Side.*`, `Detail.Chart.*`, `Detail.Chart.AccessibleSummary`,
|
||||
`Detail.History.Title`, `Detail.History.Source`, `Detail.History.Live`,
|
||||
`Detail.History.PreMatch`)
|
||||
- `Export.*` — export dialog (`Export.Title`, `Export.DateRange.From`,
|
||||
`Export.Kind.PreMatch|Live|Combined`, `Export.Submit`, `Export.Cancel`,
|
||||
`Export.Success` with `{0}` placeholder for path,
|
||||
`Export.Error.MissingDates|InvalidRange|Failed`)
|
||||
- `Sport.*` — sport display names (`Sport.Basketball`, `Sport.Football`,
|
||||
`Sport.Tennis`, `Sport.Hockey`)
|
||||
|
||||
Phase 7 strings should slot under `Anomaly.*` (the `Anomaly.Live` /
|
||||
`Anomaly.Kind.SuspensionFlip` / `Anomaly.Score` keys are already reserved
|
||||
from Phase 5).
|
||||
|
||||
### Routing additions
|
||||
|
||||
- `/prematch` (existing — body replaced)
|
||||
- `/live` (existing — body replaced)
|
||||
- `/events/{EventCode}` (new) — accepts a URL-escaped `EventId.Value`
|
||||
(numeric for marathonbet.by; allow non-numeric for forward compatibility)
|
||||
|
||||
Phase 7 should add `/anomalies/{eventId}` or `/anomalies/{anomalyId}` and link
|
||||
to the matching detail page from the home dashboard's "Latest signals" feed.
|
||||
|
||||
### Theme + Plotly tokens
|
||||
|
||||
- Plotly traces use the same triplet as the rest of the app: navy `#0f172a`
|
||||
for Win-1, amber `#d97706` for Draw, signal-red `#dc2626` for Win-2.
|
||||
Phase 7 can reuse the same trace palette for "before suspension" / "during
|
||||
suspension" / "after suspension" (with red as the alert tone — this is
|
||||
load-bearing).
|
||||
- Plotly.Blazor 5.4.1 is on the .NET 8 line; staying on this major avoids
|
||||
the v7 breaking changes documented upstream. Phase 7's anomaly chart should
|
||||
call into `OddsTimeline` if possible, only forking if it needs additional
|
||||
axes or annotations (e.g. a vertical band for the suspension window).
|
||||
|
||||
### Verified invariants & gotchas
|
||||
|
||||
- `Marathon.UI` still references **only** Domain + Application + framework
|
||||
packages. `Plotly.Blazor` was added; it's an MIT-licensed Razor wrapper
|
||||
with no Infrastructure / Hosting deps, so the RCL stays host-agnostic.
|
||||
- `DateRange` ambiguity: both `MudBlazor.DateRange` and
|
||||
`Marathon.Application.Storage.DateRange` are visible inside Razor pages
|
||||
that import both namespaces (via `_Imports.razor`). Use
|
||||
`using AppDateRange = Marathon.Application.Storage.DateRange;` in any
|
||||
file that calls the application's `DateRange`. Already applied in
|
||||
`ExportDialog.razor` and `ExportDialogTests.cs`.
|
||||
- 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. Use
|
||||
concatenated single-quoted attribute SVG strings instead (see
|
||||
`SportIcon.razor`).
|
||||
- `code` is reserved by the Razor source generator. Loop over a list with
|
||||
any other identifier (`@foreach (var sportCode in ...)`).
|
||||
- `Plotly.Blazor` exposes a `Plotly.Blazor.LayoutLib.Margin` that conflicts
|
||||
with `MudBlazor.Margin`. Fully qualify the layout-side type as
|
||||
`new Plotly.Blazor.LayoutLib.Margin {...}`.
|
||||
|
||||
### Test infrastructure delta (for Phase 7)
|
||||
|
||||
- `tests/Marathon.UI.Tests/Support/MarathonTestContext` now also registers
|
||||
a `FakeEventBrowsingService` and `EventBrowsingState` singleton; Phase 7
|
||||
tests can reuse both, or follow the same fake pattern for an
|
||||
`IAnomalyBrowsingService`.
|
||||
- `Support/TestData.cs` exposes `MoscowToday(int hour)`, `ListItem(...)`,
|
||||
and `Detail(...)` factories; reuse for anomaly fixtures.
|
||||
- `Support/TestOptionsMonitor<T>` wraps `IOptionsMonitor<T>` for tests that
|
||||
need to drive options-change callbacks deterministically.
|
||||
|
||||
Reference in New Issue
Block a user