WIP(initial-implementation): parallel batch P2/P3/P5 — code complete, unreviewed
Snapshot of the parallel batch (Phases 2 + 3 + 5) at session pause. Solution does
NOT build cleanly yet — known cross-phase compile issues remain to be resolved
before review. See plans/initial-implementation/PLAN.md "Resume Notes" section
for the exact tomorrow-morning action list.
Phase 2 (Storage):
- Repository interfaces in Marathon.Application/Abstractions
- DateRange, ExportKind, StorageOptions in Marathon.Application/Storage
- EF Core 8 + SQLite (WAL) persistence: 7 entities + configurations + 4 repos
- Hand-written InitialCreate migration (dotnet ef blocked by parallel work)
- ClosedXML ExcelExporter with exact customer-spec wide columns
- PersistenceModule.AddMarathonPersistence DI extension
- Round-trip + export tests (cannot run yet — see cross-phase issues)
Phase 3 (Scraping):
- IOddsScraper, IBetPlacer in Marathon.Application/Abstractions
- ScrapingOptions in Marathon.Infrastructure/Configuration
- MarathonbetScraper with 4 parsers (Upcoming, Live, EventOdds, Results)
- Helpers: ServerTimeProvider, PeriodScopeMapper, OutcomeCodeMapper, MoscowDateParser
- UserAgentRotatorHandler + Polly v8 resilience pipeline
- ScrapingModule.AddMarathonScraping DI extension
- GlobalUsings.cs aliases for EventId / Configuration disambiguation
- Parser tests with trimmed HTML fixtures
- ScrapeResultsAsync interim no-op (Phase 8 will replace via watch-list polling)
Phase 5 (UI shell — killed mid-final-verify, assumed ~95%):
- Marathon.UI populated: MainLayout, App.razor, Pages (Home, Settings),
Components, Theme (MarathonTheme.cs + Tokens.cs + app.css), Resources
(SharedResource.{cs,ru.resx,en.resx}), Services (ISettingsWriter), wwwroot
- WPF host: App.xaml(.cs), MainWindow.xaml(.cs), Marathon.Hosts.WpfBlazor.csproj
with Microsoft.AspNetCore.Components.WebView.Wpf + MudBlazor + Serilog
- appsettings.json + appsettings.Development.json with all sections wired
- bUnit tests: MainLayoutTests, LocaleSwitcherTests, ThemeToggleTests,
JsonSettingsWriterTests + Support helpers
Cross-phase issues to resolve at next session:
1. Phase 2 repository classes are 'internal' — Phase 3's tests can't reference
them. Fix: add InternalsVisibleTo to Marathon.Infrastructure.csproj.
2. Phase 5: LocalizationOptions namespace ambiguity (AspNetCore vs Extensions).
3. Phase 5: WpfBlazor Serilog API mismatch.
Reviewer has NOT run on this batch. Move to Phase 4 only after build is green
and a combined parallel-batch reviewer passes.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
@inject IStringLocalizer<SharedResource> L
|
||||
|
||||
<a href="/" class="m-brand @Class" aria-label="@L["App.Title"]">
|
||||
<span class="m-brand__mark">@L["App.BrandMark"]</span>
|
||||
<span class="m-brand__dateline">@L["App.Dateline"]</span>
|
||||
</a>
|
||||
|
||||
@code {
|
||||
[Parameter] public string? Class { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="m-field-row">
|
||||
<div>
|
||||
<label style="font-weight: 500; font-size: 0.9375rem;">@Label</label>
|
||||
@if (!string.IsNullOrEmpty(Hint))
|
||||
{
|
||||
<div class="m-field-row__hint">@Hint</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
@ChildContent
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public string Label { get; set; } = string.Empty;
|
||||
[Parameter] public string? Hint { get; set; }
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
@using LocalizationOptions = Marathon.UI.Services.LocalizationOptions
|
||||
@inject LocaleState LocaleState
|
||||
@inject ISettingsWriter SettingsWriter
|
||||
@inject IStringLocalizer<SharedResource> L
|
||||
@inject ILogger<LocaleSwitcher> Logger
|
||||
|
||||
<div class="m-segmented" role="group" aria-label="@L["Locale.Tooltip.Switch"]">
|
||||
<button type="button"
|
||||
class="m-segmented__btn @(IsActive(LocaleState.Russian) ? "is-active" : null)"
|
||||
aria-pressed="@IsActive(LocaleState.Russian).ToString().ToLowerInvariant()"
|
||||
@onclick="@(() => SwitchAsync(LocaleState.Russian))">
|
||||
@L["Locale.Russian"]
|
||||
</button>
|
||||
<button type="button"
|
||||
class="m-segmented__btn @(IsActive(LocaleState.English) ? "is-active" : null)"
|
||||
aria-pressed="@IsActive(LocaleState.English).ToString().ToLowerInvariant()"
|
||||
@onclick="@(() => SwitchAsync(LocaleState.English))">
|
||||
@L["Locale.English"]
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsActive(string culture) =>
|
||||
string.Equals(LocaleState.Culture.Name, culture, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private async Task SwitchAsync(string culture)
|
||||
{
|
||||
if (IsActive(culture))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LocaleState.Set(culture);
|
||||
await SettingsWriter.SaveSectionAsync(
|
||||
LocalizationOptions.SectionName,
|
||||
new LocalizationOptions { DefaultCulture = culture });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to persist locale {Culture}", culture);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
@inject IStringLocalizer<SharedResource> L
|
||||
|
||||
<nav class="m-nav" aria-label="primary">
|
||||
<div style="padding: var(--m-space-5) var(--m-space-4) var(--m-space-3); border-bottom: 1px solid rgba(231,229,228,0.10);">
|
||||
<div style="font-family: var(--m-font-display); font-size: 1.25rem; color: #fafaf7;">
|
||||
<span style="color: var(--m-c-accent);">M</span>arathon
|
||||
</div>
|
||||
<div style="font-family: var(--m-font-mono); font-size: 0.6875rem; letter-spacing: 0.18em; text-transform: uppercase; color: rgba(231,229,228,0.55); margin-top: 4px;">
|
||||
Odds Lab · v0.1
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-nav__group">@L["Nav.Section.Analysis"]</div>
|
||||
<NavLink class="m-nav__link" href="" Match="NavLinkMatch.All">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.GridView" Size="Size.Small" />
|
||||
<span>@L["Nav.Dashboard"]</span>
|
||||
</NavLink>
|
||||
<NavLink class="m-nav__link" href="prematch">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Schedule" Size="Size.Small" />
|
||||
<span>@L["Nav.PreMatch"]</span>
|
||||
</NavLink>
|
||||
<NavLink class="m-nav__link" href="live">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Bolt" Size="Size.Small" />
|
||||
<span>@L["Nav.Live"]</span>
|
||||
</NavLink>
|
||||
<NavLink class="m-nav__link" href="anomalies">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Warning" Size="Size.Small" />
|
||||
<span>@L["Nav.Anomalies"]</span>
|
||||
</NavLink>
|
||||
<NavLink class="m-nav__link" href="results">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Done" Size="Size.Small" />
|
||||
<span>@L["Nav.Results"]</span>
|
||||
</NavLink>
|
||||
|
||||
<div class="m-nav__group" style="margin-top: var(--m-space-5);">@L["Nav.Section.System"]</div>
|
||||
<NavLink class="m-nav__link" href="settings">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Tune" Size="Size.Small" />
|
||||
<span>@L["Nav.Settings"]</span>
|
||||
</NavLink>
|
||||
</nav>
|
||||
@@ -0,0 +1,30 @@
|
||||
<li style="display: grid; grid-template-columns: 36px 1fr auto; gap: var(--m-space-3); align-items: center; padding: var(--m-space-2) 0;">
|
||||
<span class="m-mono" style="font-size: 0.75rem; color: var(--m-c-ink-soft); letter-spacing: 0.12em;">@Index</span>
|
||||
<span style="font-size: 0.9375rem;">@Label</span>
|
||||
<span style="display: inline-flex; align-items: center; gap: 6px; font-family: var(--m-font-mono); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.14em; color: @StatusColor;">
|
||||
<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: @StatusColor;"></span>
|
||||
@StatusLabel
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public string Index { get; set; } = string.Empty;
|
||||
[Parameter, EditorRequired] public string Label { get; set; } = string.Empty;
|
||||
[Parameter] public string Status { get; set; } = "idle";
|
||||
|
||||
private string StatusColor => Status switch
|
||||
{
|
||||
"ok" => "var(--m-c-positive)",
|
||||
"warn" => "var(--m-c-accent)",
|
||||
"error" => "var(--m-c-anomaly)",
|
||||
_ => "var(--m-c-ink-soft)",
|
||||
};
|
||||
|
||||
private string StatusLabel => Status switch
|
||||
{
|
||||
"ok" => "OK",
|
||||
"warn" => "WAIT",
|
||||
"error" => "FAIL",
|
||||
_ => "IDLE",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@inject IStringLocalizer<SharedResource> L
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; gap: var(--m-space-3); padding-top: var(--m-space-3); border-top: 1px solid var(--m-c-rule); margin-top: var(--m-space-2);">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OnSave">
|
||||
@L["Settings.Action.Save"]
|
||||
</MudButton>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public EventCallback OnSave { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="m-card @(Anomaly ? "m-card--anomaly" : null)" style="display: flex; flex-direction: column; gap: var(--m-space-2);">
|
||||
<span class="m-stat__label">@Label</span>
|
||||
<span class="m-stat__value">@Value</span>
|
||||
@if (!string.IsNullOrEmpty(Delta))
|
||||
{
|
||||
<span class="m-stat__delta @(Anomaly ? "m-stat__delta--down" : null)" style="color: @(Anomaly ? "var(--m-c-anomaly)" : "var(--m-c-ink-soft)");">
|
||||
@Delta
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public string Label { get; set; } = string.Empty;
|
||||
[Parameter, EditorRequired] public string Value { get; set; } = string.Empty;
|
||||
[Parameter] public string? Delta { get; set; }
|
||||
[Parameter] public bool Anomaly { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@inject ThemeState ThemeState
|
||||
@inject IStringLocalizer<SharedResource> L
|
||||
|
||||
<MudTooltip Text="@(ThemeState.IsDark ? L["Theme.Toggle.Light"] : L["Theme.Toggle.Dark"])">
|
||||
<MudIconButton
|
||||
Icon="@(ThemeState.IsDark ? Icons.Material.Outlined.LightMode : Icons.Material.Outlined.DarkMode)"
|
||||
Color="Color.Inherit"
|
||||
OnClick="OnToggle"
|
||||
aria-label="@(ThemeState.IsDark ? L["Theme.Toggle.Light"] : L["Theme.Toggle.Dark"])" />
|
||||
</MudTooltip>
|
||||
|
||||
@code {
|
||||
private void OnToggle() => ThemeState.Toggle();
|
||||
}
|
||||
Reference in New Issue
Block a user