Files
maraphon-app/src/Marathon.Hosts.WpfBlazor/App.xaml.cs
T
alexei.dolgolyov 85bc99cac5 fix(host): wire DB migration init + Plotly CDN + attribute fix on hand-written migration
Three fixes surfaced when launching the WPF host for the first time:

1. App.xaml.cs — call MarathonDbContextInitializer.InitializeAsync()
   between Host.Build() and Host.Start() so EF migrations + WAL pragma
   are applied BEFORE BackgroundServices race to query the DB. Without
   this, all pollers crashed on 'no such table: Events'.

2. wwwroot/index.html — added <script src='https://cdn.plot.ly/plotly-2.35.2.min.js'>
   before blazor.webview.js. Phase 6 reviewer flagged this for Phase 9,
   but charts are unrenderable without it; better to ship now.

3. Migrations/20260505000000_InitialCreate.cs — added [DbContext] and
   [Migration('20260505000000_InitialCreate')] attributes. Phase 2's
   hand-written migration was missing both, so EF saw 'no migrations to
   apply' even on a fresh DB. With the attributes, the migration runs
   on first launch and creates all tables (Events, Snapshots, Bets,
   EventResults, Anomalies, Sports, Leagues).

Verified: clean DB → migration applied → all 7 tables created → pollers
run with empty results (no data yet — UpcomingEventsPoller fires every 6h
by default; first scrape will populate the DB).
2026-05-05 13:55:59 +03:00

138 lines
5.2 KiB
C#

using System.Globalization;
using System.IO;
using System.Windows;
using Marathon.Application;
using Marathon.Infrastructure;
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>
/// 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
// Application use cases + Infrastructure (persistence, scraping, workers).
builder.Services.AddMarathonApplication();
builder.Services.AddMarathonInfrastructure(builder.Configuration);
// MainWindow needs the IServiceProvider for BlazorWebView.Services binding.
builder.Services.AddSingleton<MainWindow>();
Host = builder.Build();
// Apply EF migrations + WAL pragma BEFORE Host.Start() so the BackgroundServices
// (LiveOddsPoller, AnomalyDetectionPoller, etc.) don't race the DB schema creation.
// Resolved in a scope because MarathonDbContextInitializer is Scoped (DbContext lifetime).
using (var initScope = Host.Services.CreateScope())
{
var initializer = initScope.ServiceProvider
.GetRequiredService<Marathon.Infrastructure.Persistence.MarathonDbContextInitializer>();
initializer.InitializeAsync().GetAwaiter().GetResult();
}
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;
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);
}
}
}