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:
2026-05-05 01:56:53 +03:00
parent 144c936e90
commit e4d8476782
129 changed files with 8524 additions and 121 deletions
+294
View File
@@ -0,0 +1,294 @@
using MudBlazor;
using MudBlazor.Utilities;
namespace Marathon.UI.Theme;
/// <summary>
/// The Marathon design system, expressed as a MudBlazor theme.
///
/// Aesthetic direction: editorial-quant. Inspired by Bloomberg terminals,
/// FT.com long-reads, and Quartz dashboards. Confident, information-dense,
/// reveals patterns. Pairs IBM Plex Sans (Cyrillic-capable display + body)
/// with JetBrains Mono for tabular numerals. Anomaly accent is a load-bearing
/// signal red so Phase 7 can hang the entire anomaly visual language off
/// <c>palette.Error</c> without coupling to a hard-coded hex.
/// </summary>
public static class MarathonTheme
{
/// <summary>The full theme — both light and dark palettes plus typography.</summary>
public static MudTheme Build() => new()
{
PaletteLight = LightPalette,
PaletteDark = DarkPalette,
Typography = MarathonTypography,
LayoutProperties = LayoutProps,
Shadows = MarathonShadows,
ZIndex = new ZIndex(),
};
// ------------------------------------------------------------------
// Palettes — single accent (amber #d97706), single signal (red #ef4444)
// on a deep navy/parchment chassis. No purple gradients, no cliche.
// ------------------------------------------------------------------
private static readonly PaletteLight LightPalette = new()
{
Primary = "#0f172a", // deep navy / ink
PrimaryContrastText = "#fafaf7",
Secondary = "#334155", // slate
SecondaryContrastText = "#fafaf7",
Tertiary = "#d97706", // amber accent
TertiaryContrastText = "#1c1917",
Info = "#0369a1",
Success = "#15803d",
Warning = "#b45309",
Error = "#dc2626", // anomaly signal
ErrorContrastText = "#fff7ed",
Black = "#1c1917",
White = "#fafaf7",
Surface = "#fafaf7", // warm parchment
Background = "#f5f4ef", // a half-step warmer than surface
BackgroundGray = "#ebe9e1",
DrawerBackground = "#0f172a", // dark drawer on light app — editorial contrast
DrawerText = "#e7e5e4",
DrawerIcon = "#d6d3d1",
AppbarBackground = "#fafaf7",
AppbarText = "#0f172a",
TextPrimary = "#0f172a",
TextSecondary = "#475569",
TextDisabled = "#94a3b8",
ActionDefault = "#334155",
ActionDisabled = "#cbd5e1",
ActionDisabledBackground = "#e2e8f0",
LinesDefault = "#e7e5e4",
LinesInputs = "#cbd5e1",
TableLines = "#e7e5e4",
TableStriped = "#f5f4ef",
TableHover = "#ebe9e1",
Divider = "#e7e5e4",
DividerLight = "#f1f5f9",
OverlayDark = new MudColor("#0f172a99").Value,
OverlayLight = new MudColor("#fafaf7cc").Value,
};
private static readonly PaletteDark DarkPalette = new()
{
Primary = "#fbbf24", // amber, promoted in dark mode
PrimaryContrastText = "#0c0a09",
Secondary = "#94a3b8",
SecondaryContrastText = "#0c0a09",
Tertiary = "#fbbf24",
TertiaryContrastText = "#0c0a09",
Info = "#38bdf8",
Success = "#4ade80",
Warning = "#fbbf24",
Error = "#f87171", // anomaly signal — softened for dark
ErrorContrastText = "#0c0a09",
Black = "#0c0a09",
White = "#fafaf7",
Surface = "#1c1917", // ink-stained paper
Background = "#0c0a09", // near-black
BackgroundGray = "#1c1917",
DrawerBackground = "#0c0a09",
DrawerText = "#e7e5e4",
DrawerIcon = "#a8a29e",
AppbarBackground = "#0c0a09",
AppbarText = "#fafaf7",
TextPrimary = "#f5f5f4",
TextSecondary = "#a8a29e",
TextDisabled = "#57534e",
ActionDefault = "#a8a29e",
ActionDisabled = "#44403c",
ActionDisabledBackground = "#1c1917",
LinesDefault = "#292524",
LinesInputs = "#44403c",
TableLines = "#292524",
TableStriped = "#1c1917",
TableHover = "#292524",
Divider = "#292524",
DividerLight = "#1c1917",
OverlayDark = new MudColor("#0c0a09cc").Value,
OverlayLight = new MudColor("#fafaf722").Value,
};
// ------------------------------------------------------------------
// Typography — IBM Plex Sans / JetBrains Mono / IBM Plex Serif (display)
// All have full Cyrillic coverage. Numerals are tabular.
// ------------------------------------------------------------------
private static readonly string[] DisplayStack = { "IBM Plex Serif", "PT Serif", "Georgia", "serif" };
private static readonly string[] BodyStack = { "IBM Plex Sans", "PT Sans", "system-ui", "sans-serif" };
private static readonly string[] MonoStack = { "JetBrains Mono", "IBM Plex Mono", "Fira Code", "Consolas", "monospace" };
private static readonly Typography MarathonTypography = new()
{
Default = new Default
{
FontFamily = BodyStack,
FontWeight = 400,
FontSize = "0.9375rem", // 15px — denser than MUD default 16
LineHeight = 1.55,
LetterSpacing = "0",
},
H1 = new H1
{
FontFamily = DisplayStack,
FontWeight = 300,
FontSize = "clamp(2.25rem, 4vw, 3.5rem)",
LineHeight = 1.05,
LetterSpacing = "-0.022em",
},
H2 = new H2
{
FontFamily = DisplayStack,
FontWeight = 400,
FontSize = "clamp(1.75rem, 2.5vw, 2.25rem)",
LineHeight = 1.15,
LetterSpacing = "-0.018em",
},
H3 = new H3
{
FontFamily = DisplayStack,
FontWeight = 500,
FontSize = "1.5rem",
LineHeight = 1.25,
LetterSpacing = "-0.012em",
},
H4 = new H4
{
FontFamily = BodyStack,
FontWeight = 600,
FontSize = "1.25rem",
LineHeight = 1.3,
LetterSpacing = "-0.005em",
},
H5 = new H5
{
FontFamily = BodyStack,
FontWeight = 600,
FontSize = "1.0625rem",
LineHeight = 1.35,
},
H6 = new H6
{
FontFamily = BodyStack,
FontWeight = 600,
FontSize = "0.9375rem",
LineHeight = 1.4,
LetterSpacing = "0.02em",
},
Subtitle1 = new Subtitle1
{
FontFamily = BodyStack,
FontWeight = 500,
FontSize = "0.9375rem",
LineHeight = 1.5,
},
Subtitle2 = new Subtitle2
{
FontFamily = BodyStack,
FontWeight = 500,
FontSize = "0.8125rem",
LineHeight = 1.5,
LetterSpacing = "0.01em",
},
Body1 = new Body1
{
FontFamily = BodyStack,
FontWeight = 400,
FontSize = "0.9375rem",
LineHeight = 1.55,
},
Body2 = new Body2
{
FontFamily = BodyStack,
FontWeight = 400,
FontSize = "0.8125rem",
LineHeight = 1.5,
},
Button = new Button
{
FontFamily = BodyStack,
FontWeight = 500,
FontSize = "0.8125rem",
LineHeight = 1.4,
LetterSpacing = "0.06em",
TextTransform = "uppercase",
},
Caption = new Caption
{
FontFamily = MonoStack,
FontWeight = 400,
FontSize = "0.75rem",
LineHeight = 1.4,
LetterSpacing = "0.04em",
TextTransform = "uppercase",
},
Overline = new Overline
{
FontFamily = MonoStack,
FontWeight = 500,
FontSize = "0.6875rem",
LineHeight = 1.4,
LetterSpacing = "0.18em",
TextTransform = "uppercase",
},
};
// ------------------------------------------------------------------
// Layout — sharp corners, narrow drawer. The aesthetic earns its
// authority through restraint.
// ------------------------------------------------------------------
private static readonly LayoutProperties LayoutProps = new()
{
DefaultBorderRadius = "2px",
AppbarHeight = "60px",
DrawerWidthLeft = "248px",
DrawerWidthRight = "248px",
DrawerMiniWidthLeft = "60px",
DrawerMiniWidthRight = "60px",
};
// ------------------------------------------------------------------
// Shadows — flat by default, one accent shadow for floating panels.
// Override only the slots Mud actually uses; keep first/last as-is.
// ------------------------------------------------------------------
private static readonly Shadow MarathonShadows = new()
{
Elevation = new[]
{
"none",
"0 1px 0 0 rgba(15,23,42,0.06)",
"0 1px 2px 0 rgba(15,23,42,0.08)",
"0 2px 4px -1px rgba(15,23,42,0.10)",
"0 4px 8px -2px rgba(15,23,42,0.12)",
"0 6px 14px -4px rgba(15,23,42,0.14)",
"0 8px 18px -6px rgba(15,23,42,0.16)",
"0 10px 22px -8px rgba(15,23,42,0.18)",
"0 12px 28px -10px rgba(15,23,42,0.20)",
"0 14px 32px -12px rgba(15,23,42,0.22)",
"0 16px 36px -14px rgba(15,23,42,0.24)",
"0 18px 40px -16px rgba(15,23,42,0.26)",
"0 20px 44px -18px rgba(15,23,42,0.28)",
"0 22px 48px -20px rgba(15,23,42,0.30)",
"0 24px 52px -22px rgba(15,23,42,0.32)",
"0 26px 56px -24px rgba(15,23,42,0.34)",
"0 28px 60px -26px rgba(15,23,42,0.36)",
"0 30px 64px -28px rgba(15,23,42,0.38)",
"0 32px 68px -30px rgba(15,23,42,0.40)",
"0 34px 72px -32px rgba(15,23,42,0.42)",
"0 36px 76px -34px rgba(15,23,42,0.44)",
"0 38px 80px -36px rgba(15,23,42,0.46)",
"0 40px 84px -38px rgba(15,23,42,0.48)",
"0 42px 88px -40px rgba(15,23,42,0.50)",
"0 44px 92px -42px rgba(15,23,42,0.52)",
"0 46px 96px -44px rgba(15,23,42,0.54)",
},
};
}
+38
View File
@@ -0,0 +1,38 @@
namespace Marathon.UI.Theme;
/// <summary>
/// Design tokens exposed to C# code (e.g. for chart colors, custom shapes,
/// Razor components that need to reach beyond the MudTheme palette).
/// Mirrors the values declared as CSS variables in <c>wwwroot/app.css</c>.
/// </summary>
public static class Tokens
{
public static class Colors
{
public const string AnomalySignal = "#dc2626";
public const string AnomalySignalDark = "#f87171";
public const string Accent = "#d97706";
public const string AccentDark = "#fbbf24";
public const string InkPrimary = "#0f172a";
public const string Parchment = "#fafaf7";
public const string ParchmentDeep = "#f5f4ef";
public const string InkDeep = "#0c0a09";
}
public static class Spacing
{
public const string Xs = "4px";
public const string Sm = "8px";
public const string Md = "16px";
public const string Lg = "24px";
public const string Xl = "40px";
public const string Xxl = "64px";
}
public static class Typography
{
public const string DisplayStack = "\"IBM Plex Serif\", \"PT Serif\", Georgia, serif";
public const string BodyStack = "\"IBM Plex Sans\", \"PT Sans\", system-ui, sans-serif";
public const string MonoStack = "\"JetBrains Mono\", \"IBM Plex Mono\", \"Fira Code\", Consolas, monospace";
}
}