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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user