feat(ui): live dashboard, capture-status pill, bet/backtest UX
- Add IDashboardSummaryService/DashboardSummaryService: real event/snapshot/ anomaly counts, top-5 signals, and per-stage pipeline health from worker state. - Home: replace hard-coded zeros + placeholder feed with live data, a clickable signal feed, and a first-run empty state with a Settings CTA. - MainLayout: add an appbar capture-status pill (Capturing/Paused) bound to the poller toggles, refreshed via IOptionsMonitor.OnChange. - MyBets: success snackbar on bet submit. Backtest: surface a Cancel button while a run is in flight. - Add en/ru localization for all new strings; register IOptionsMonitor<WorkerOptions> in the bUnit test context for layout-rendering tests.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
using Marathon.Application.Abstractions;
|
||||
using Marathon.Domain.ValueObjects;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Marathon.UI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Repository-backed implementation of <see cref="IDashboardSummaryService"/>.
|
||||
/// Composes server-side counts (no full-table materialisation) with the top few
|
||||
/// anomaly signals and the worker-toggle state. Scoped — captures the per-circuit
|
||||
/// repository scope like the other browsing services.
|
||||
/// </summary>
|
||||
public sealed class DashboardSummaryService : IDashboardSummaryService
|
||||
{
|
||||
private const int LatestSignalCount = 5;
|
||||
|
||||
private readonly IEventRepository _events;
|
||||
private readonly ISnapshotRepository _snapshots;
|
||||
private readonly IAnomalyRepository _anomalies;
|
||||
private readonly IAnomalyBrowsingService _anomalyBrowsing;
|
||||
private readonly IOptionsMonitor<WorkerOptions> _workers;
|
||||
|
||||
public DashboardSummaryService(
|
||||
IEventRepository events,
|
||||
ISnapshotRepository snapshots,
|
||||
IAnomalyRepository anomalies,
|
||||
IAnomalyBrowsingService anomalyBrowsing,
|
||||
IOptionsMonitor<WorkerOptions> workers)
|
||||
{
|
||||
_events = events ?? throw new ArgumentNullException(nameof(events));
|
||||
_snapshots = snapshots ?? throw new ArgumentNullException(nameof(snapshots));
|
||||
_anomalies = anomalies ?? throw new ArgumentNullException(nameof(anomalies));
|
||||
_anomalyBrowsing = anomalyBrowsing ?? throw new ArgumentNullException(nameof(anomalyBrowsing));
|
||||
_workers = workers ?? throw new ArgumentNullException(nameof(workers));
|
||||
}
|
||||
|
||||
public async Task<DashboardSummary> GetAsync(CancellationToken ct)
|
||||
{
|
||||
var todayStart = new DateTimeOffset(MoscowTime.Now.Date, MoscowTime.Offset);
|
||||
|
||||
var eventsTracked = await _events.CountAsync(ct).ConfigureAwait(false);
|
||||
var snapshotsToday = await _snapshots.CountSinceAsync(todayStart, ct).ConfigureAwait(false);
|
||||
// DateTimeOffset.MinValue lower bound counts every row (year dominates the
|
||||
// lexical comparison regardless of offset).
|
||||
var anomaliesTotal = await _anomalies.CountSinceAsync(DateTimeOffset.MinValue, ct).ConfigureAwait(false);
|
||||
var anomaliesToday = await _anomalies.CountSinceAsync(todayStart, ct).ConfigureAwait(false);
|
||||
var sports = await _events.ListDistinctSportCodesAsync(ct).ConfigureAwait(false);
|
||||
|
||||
var latest = anomaliesTotal == 0
|
||||
? (IReadOnlyList<AnomalyListItem>)Array.Empty<AnomalyListItem>()
|
||||
: (await _anomalyBrowsing.ListAsync(new AnomalyFilter(), ct).ConfigureAwait(false))
|
||||
.Take(LatestSignalCount)
|
||||
.ToList();
|
||||
|
||||
var w = _workers.CurrentValue;
|
||||
|
||||
return new DashboardSummary(
|
||||
EventsTracked: eventsTracked,
|
||||
SnapshotsToday: snapshotsToday,
|
||||
AnomaliesTotal: anomaliesTotal,
|
||||
AnomaliesToday: anomaliesToday,
|
||||
SportsCovered: sports.Count,
|
||||
LatestSignals: latest,
|
||||
ScheduleStatus: StageStatus(w.UpcomingPollerEnabled, eventsTracked > 0),
|
||||
SnapshotStatus: StageStatus(w.LivePollerEnabled, snapshotsToday > 0),
|
||||
DetectorStatus: StageStatus(w.AnomalyDetectionEnabled, anomaliesTotal > 0),
|
||||
ExportStatus: eventsTracked > 0 ? "ok" : "idle");
|
||||
}
|
||||
|
||||
// Maps a worker stage to the PipelineStep token: disabled → idle, enabled but
|
||||
// not yet producing → warn (waiting), enabled and producing → ok.
|
||||
private static string StageStatus(bool enabled, bool producing) =>
|
||||
!enabled ? "idle" : producing ? "ok" : "warn";
|
||||
}
|
||||
Reference in New Issue
Block a user