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:
@@ -1,9 +1,8 @@
|
||||
<Application x:Class="Marathon.Hosts.WpfBlazor.App"
|
||||
<Application x:Class="Marathon.Hosts.WpfBlazor.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Marathon.Hosts.WpfBlazor"
|
||||
StartupUri="MainWindow.xaml">
|
||||
ShutdownMode="OnMainWindowClose">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -1,10 +1,168 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using Marathon.UI.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
|
||||
namespace Marathon.Hosts.WpfBlazor;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// WPF application entry-point. Builds an <see cref="IHost"/> with Serilog,
|
||||
/// configuration (appsettings.json + Local + env vars), and the Marathon UI
|
||||
/// service collection. Composes Application + Infrastructure modules
|
||||
/// optionally — those module entry points may not yet exist while parallel
|
||||
/// Phase 2/3/4 work merges.
|
||||
/// </summary>
|
||||
public partial class App : System.Windows.Application
|
||||
{
|
||||
public IHost? Host { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path to the Local override settings file. Resolved from the
|
||||
/// host's content root (the directory containing <c>appsettings.json</c>).
|
||||
/// </summary>
|
||||
public static string SettingsLocalFileName => "appsettings.Local.json";
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
var contentRoot = AppContext.BaseDirectory;
|
||||
var localSettingsPath = Path.Combine(contentRoot, SettingsLocalFileName);
|
||||
|
||||
var builder = Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder();
|
||||
builder.Environment.ContentRootPath = contentRoot;
|
||||
|
||||
builder.Configuration
|
||||
.SetBasePath(contentRoot)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
|
||||
.AddJsonFile(SettingsLocalFileName, optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables(prefix: "MARATHON_");
|
||||
|
||||
// Serilog — structured rolling-file + console.
|
||||
// Minimum level honours the "Serilog:MinimumLevel:Default" key when
|
||||
// present in configuration; otherwise defaults to Information.
|
||||
var logsDir = Path.Combine(contentRoot, "logs");
|
||||
Directory.CreateDirectory(logsDir);
|
||||
|
||||
var minimumLevel = ParseMinimumLevel(builder.Configuration["Serilog:MinimumLevel:Default"]);
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Is(minimumLevel)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.WriteTo.File(
|
||||
path: Path.Combine(logsDir, "marathon-.log"),
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 14,
|
||||
shared: true)
|
||||
.CreateLogger();
|
||||
|
||||
builder.Services.AddSerilog();
|
||||
|
||||
// Marathon.UI services (Mud, localization, options, theme/locale state, settings writer).
|
||||
builder.Services.AddMarathonUi(builder.Configuration, localSettingsPath);
|
||||
|
||||
// Blazor WebView root services.
|
||||
builder.Services.AddWpfBlazorWebView();
|
||||
#if DEBUG
|
||||
builder.Services.AddBlazorWebViewDeveloperTools();
|
||||
#endif
|
||||
|
||||
// Compose Application + Infrastructure modules if they exist. Parallel
|
||||
// Phase 2/3/4 work may still be merging these; we degrade gracefully.
|
||||
TryAddApplicationAndInfrastructure(builder.Services, builder.Configuration);
|
||||
|
||||
// MainWindow needs the IServiceProvider for BlazorWebView.Services binding.
|
||||
builder.Services.AddSingleton<MainWindow>();
|
||||
|
||||
Host = builder.Build();
|
||||
Host.Start();
|
||||
|
||||
// Apply default culture from configuration before any UI renders.
|
||||
var localeOptions = Host.Services.GetRequiredService<IOptions<LocalizationOptions>>().Value;
|
||||
var locale = Host.Services.GetRequiredService<LocaleState>();
|
||||
try
|
||||
{
|
||||
locale.Set(localeOptions.DefaultCulture);
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
locale.Set(LocaleState.Russian);
|
||||
}
|
||||
|
||||
var window = Host.Services.GetRequiredService<MainWindow>();
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private static Serilog.Events.LogEventLevel ParseMinimumLevel(string? raw) =>
|
||||
Enum.TryParse<Serilog.Events.LogEventLevel>(raw, ignoreCase: true, out var level)
|
||||
? level
|
||||
: Serilog.Events.LogEventLevel.Information;
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort wiring of the Application + Infrastructure DI modules.
|
||||
/// TODO(phase-4): the orchestrator will land a single
|
||||
/// <c>AddMarathonInfrastructure(config)</c> entry point. Until then we use
|
||||
/// reflection to call whichever extension methods exist so partial merges
|
||||
/// don't break compilation of this host.
|
||||
/// </summary>
|
||||
private static void TryAddApplicationAndInfrastructure(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
TryInvokeExtension(services, configuration, "Marathon.Application.DependencyInjection", "AddMarathonApplication");
|
||||
TryInvokeExtension(services, configuration, "Marathon.Infrastructure.DependencyInjection", "AddMarathonInfrastructure");
|
||||
TryInvokeExtension(services, configuration, "Marathon.Infrastructure.Persistence.PersistenceServiceCollectionExtensions", "AddMarathonPersistence");
|
||||
TryInvokeExtension(services, configuration, "Marathon.Infrastructure.Scraping.ScrapingServiceCollectionExtensions", "AddMarathonScraping");
|
||||
}
|
||||
|
||||
private static void TryInvokeExtension(IServiceCollection services, IConfiguration configuration, string typeName, string methodName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Probe across all loaded assemblies — project refs cause them to load on startup.
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
var type = asm.GetType(typeName, throwOnError: false, ignoreCase: false);
|
||||
if (type is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var method = type.GetMethod(methodName, new[] { typeof(IServiceCollection), typeof(IConfiguration) });
|
||||
if (method is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
method.Invoke(null, new object[] { services, configuration });
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Optional module {Type}.{Method} not wired", typeName, methodName);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Host?.StopAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Host shutdown failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Host?.Dispose();
|
||||
Log.CloseAndFlush();
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
<Window x:Class="Marathon.Hosts.WpfBlazor.MainWindow"
|
||||
<Window x:Class="Marathon.Hosts.WpfBlazor.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Marathon.Hosts.WpfBlazor"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="450" Width="800">
|
||||
xmlns:wv="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
|
||||
xmlns:ui="clr-namespace:Marathon.UI;assembly=Marathon.UI"
|
||||
xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.Web;assembly=Microsoft.AspNetCore.Components.Web"
|
||||
Title="Marathon Odds Lab"
|
||||
Height="900"
|
||||
Width="1440"
|
||||
MinHeight="640"
|
||||
MinWidth="960"
|
||||
Background="#0c0a09"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Grid>
|
||||
|
||||
<wv:BlazorWebView x:Name="BlazorWebView"
|
||||
HostPage="wwwroot/index.html">
|
||||
<wv:BlazorWebView.RootComponents>
|
||||
<wv:RootComponent Selector="#app" ComponentType="{x:Type ui:App}" />
|
||||
</wv:BlazorWebView.RootComponents>
|
||||
</wv:BlazorWebView>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Marathon.Hosts.WpfBlazor;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// Hosts the BlazorWebView that renders <see cref="Marathon.UI.App"/>.
|
||||
/// All UI lives in the Razor Class Library — this window is intentionally
|
||||
/// thin so a future ASP.NET Core Blazor Server host can swap in trivially.
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
public MainWindow(IServiceProvider services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
InitializeComponent();
|
||||
BlazorWebView.Services = services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Marathon.UI\Marathon.UI.csproj" />
|
||||
<ProjectReference Include="..\Marathon.Infrastructure\Marathon.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Marathon.Application\Marathon.Application.csproj" />
|
||||
</ItemGroup>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
@@ -12,6 +6,42 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>false</UseWindowsForms>
|
||||
<RootNamespace>Marathon.Hosts.WpfBlazor</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Wpf" />
|
||||
<PackageReference Include="MudBlazor" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Marathon.UI\Marathon.UI.csproj" />
|
||||
<ProjectReference Include="..\Marathon.Application\Marathon.Application.csproj" />
|
||||
<ProjectReference Include="..\Marathon.Infrastructure\Marathon.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="appsettings.Development.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<DependentUpon>appsettings.json</DependentUpon>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug",
|
||||
"Override": {
|
||||
"Microsoft": "Information",
|
||||
"Microsoft.AspNetCore": "Information",
|
||||
"System": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"Scraping": {
|
||||
"PollingIntervalSeconds": 30,
|
||||
"MaxConcurrentRequests": 4,
|
||||
"UserAgents": [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
],
|
||||
"RetryPolicy": {
|
||||
"MaxAttempts": 3,
|
||||
"BaseDelayMs": 500
|
||||
},
|
||||
"RateLimit": {
|
||||
"RequestsPerSecond": 1
|
||||
},
|
||||
"UsePlaywright": false,
|
||||
"BaseUrl": "https://www.marathonbet.by",
|
||||
"RequestTimeoutSeconds": 30
|
||||
},
|
||||
"Workers": {
|
||||
"UpcomingScheduleCron": "0 */5 * * * *",
|
||||
"LivePollerEnabled": true,
|
||||
"UpcomingPollerEnabled": true
|
||||
},
|
||||
"Storage": {
|
||||
"DatabasePath": "./data/marathon.db",
|
||||
"ExportDirectory": "./exports",
|
||||
"SnapshotRetentionDays": 90
|
||||
},
|
||||
"Anomaly": {
|
||||
"SuspensionGapSeconds": 60,
|
||||
"OddsFlipThreshold": 0.30,
|
||||
"MinSnapshotCount": 3,
|
||||
"DetectionIntervalSeconds": 60
|
||||
},
|
||||
"Localization": {
|
||||
"DefaultCulture": "ru-RU"
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"System": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user