diff --git a/src/Trax.Dashboard/Components/Pages/Data/DeadLetterDetailPage.razor.cs b/src/Trax.Dashboard/Components/Pages/Data/DeadLetterDetailPage.razor.cs index ae4e528..7f1dfb1 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/DeadLetterDetailPage.razor.cs +++ b/src/Trax.Dashboard/Components/Pages/Data/DeadLetterDetailPage.razor.cs @@ -80,10 +80,7 @@ private async Task RequeueManifest() var registration = TrainDiscovery .DiscoverTrains() - .FirstOrDefault(r => - r.ServiceType.FullName == manifest.Name - || r.ImplementationType.FullName == manifest.Name - ); + .FirstOrDefault(r => r.ServiceType.FullName == manifest.Name); if (registration is null) { diff --git a/src/Trax.Dashboard/Components/Pages/Data/DeadLettersPage.razor b/src/Trax.Dashboard/Components/Pages/Data/DeadLettersPage.razor index d659fcd..a8f531f 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/DeadLettersPage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/DeadLettersPage.razor @@ -1,4 +1,5 @@ @page "/trax/data/dead-letters" +@using System.Linq.Dynamic.Core @inherits PollingComponentBase @inject IDataContextProviderFactory DataContextFactory @inject NavigationManager Navigation @@ -8,9 +9,9 @@ Train executions that exceeded their maximum retry count and require manual intervention. Resolve by retrying or acknowledging. - + ServerLoadData="@LoadPageAsync"> @code { - private List _items = []; + private TraxDataGrid? _grid; protected override async Task LoadDataAsync(CancellationToken cancellationToken) + { + if (_grid is not null) + await _grid.ReloadAsync(); + } + + private async Task> LoadPageAsync( + LoadDataArgs args, + CancellationToken cancellationToken) { using var context = await DataContextFactory.CreateDbContextAsync(cancellationToken); - _items = await context.DeadLetters.AsNoTracking().ToListAsync(cancellationToken); + IQueryable query = context.DeadLetters.AsNoTracking(); + + if (!string.IsNullOrEmpty(args.Filter)) + query = query.Where(args.Filter); + + if (!string.IsNullOrEmpty(args.OrderBy)) + query = query.OrderBy(args.OrderBy); + else + query = query.OrderByDescending(d => d.Id); + + var count = await query.CountAsync(cancellationToken); + + if (args.Skip.HasValue) + query = query.Skip(args.Skip.Value); + if (args.Top.HasValue) + query = query.Take(args.Top.Value); + + var items = await query.ToListAsync(cancellationToken); + return new ServerDataResult(items, count); } } diff --git a/src/Trax.Dashboard/Components/Pages/Data/LogsPage.razor b/src/Trax.Dashboard/Components/Pages/Data/LogsPage.razor index 5c9c450..7698a57 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/LogsPage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/LogsPage.razor @@ -1,4 +1,5 @@ @page "/trax/data/logs" +@using System.Linq.Dynamic.Core @inherits PollingComponentBase @inject IDataContextProviderFactory DataContextFactory @@ -7,9 +8,9 @@ Log entries captured during train execution, including messages, exceptions, and diagnostic information. - + ServerLoadData="@LoadPageAsync"> @code { - private List _items = []; + private TraxDataGrid? _grid; protected override async Task LoadDataAsync(CancellationToken cancellationToken) + { + if (_grid is not null) + await _grid.ReloadAsync(); + } + + private async Task> LoadPageAsync( + LoadDataArgs args, + CancellationToken cancellationToken) { using var context = await DataContextFactory.CreateDbContextAsync(cancellationToken); - _items = await context.Logs.AsNoTracking().ToListAsync(cancellationToken); + IQueryable query = context.Logs.AsNoTracking(); + + if (!string.IsNullOrEmpty(args.Filter)) + query = query.Where(args.Filter); + + if (!string.IsNullOrEmpty(args.OrderBy)) + query = query.OrderBy(args.OrderBy); + else + query = query.OrderByDescending(l => l.Id); + + var count = await query.CountAsync(cancellationToken); + + if (args.Skip.HasValue) + query = query.Skip(args.Skip.Value); + if (args.Top.HasValue) + query = query.Take(args.Top.Value); + + var items = await query.ToListAsync(cancellationToken); + return new ServerDataResult(items, count); } } diff --git a/src/Trax.Dashboard/Components/Pages/Data/ManifestDetailPage.razor.cs b/src/Trax.Dashboard/Components/Pages/Data/ManifestDetailPage.razor.cs index 063eca6..0ee3ae1 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/ManifestDetailPage.razor.cs +++ b/src/Trax.Dashboard/Components/Pages/Data/ManifestDetailPage.razor.cs @@ -52,6 +52,7 @@ protected override async Task LoadDataAsync(CancellationToken cancellationToken) .Metadatas.AsNoTracking() .Where(m => m.ManifestId == ManifestId) .OrderByDescending(m => m.StartTime) + .Take(500) .ToListAsync(cancellationToken); } } diff --git a/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupsPage.razor b/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupsPage.razor index faded50..e5be7a8 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupsPage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupsPage.razor @@ -107,8 +107,6 @@ _items = await context.ManifestGroups .AsNoTracking() - .Include(g => g.Manifests) - .ThenInclude(m => m.Metadatas) .Select(g => new GroupSummary { Id = g.Id, diff --git a/src/Trax.Dashboard/Components/Pages/Data/MetadataDetailPage.razor.cs b/src/Trax.Dashboard/Components/Pages/Data/MetadataDetailPage.razor.cs index ebbf8fd..9727695 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/MetadataDetailPage.razor.cs +++ b/src/Trax.Dashboard/Components/Pages/Data/MetadataDetailPage.razor.cs @@ -113,10 +113,7 @@ private async Task RequeueTrain() { var registration = TrainDiscovery .DiscoverTrains() - .FirstOrDefault(r => - r.ServiceType.FullName == _metadata.Name - || r.ImplementationType.FullName == _metadata.Name - ); + .FirstOrDefault(r => r.ServiceType.FullName == _metadata.Name); if (registration is null) { diff --git a/src/Trax.Dashboard/Components/Pages/Index.razor b/src/Trax.Dashboard/Components/Pages/Index.razor index 5c22cbe..8a29c88 100644 --- a/src/Trax.Dashboard/Components/Pages/Index.razor +++ b/src/Trax.Dashboard/Components/Pages/Index.razor @@ -95,67 +95,102 @@ } -@* ── Executions Chart (with time range toggle) ── *@ -@if (DashboardSettings.ShowExecutionsChart) +@* ── Executions Chart + Avg Execution Duration ── *@ +@if (DashboardSettings.ShowExecutionsChart || DashboardSettings.ShowAvgDuration) { - - - - Executions - - - - - - - - @if (_executionsOverTime.Any(e => e.Completed > 0 || e.Failed > 0 || e.Cancelled > 0)) - { - - - - - - - - - - - - - - - - - - - - } - else - { - - No executions in this time range + @if (DashboardSettings.ShowExecutionsChart) + { + + + + Executions + + + + + + - } - - + @if (ExecutionsOverTime.Any(e => e.Completed > 0 || e.Failed > 0 || e.Cancelled > 0)) + { + + + + + + + + + + + + + + + + + + + + } + else + { + + No executions in this time range + + } + + + } + @if (DashboardSettings.ShowAvgDuration) + { + + + Avg Execution Duration (7d) + @if (_avgDurations.Count > 0) + { + + + + + + + + + + + + } + else + { + + No completed trains in the last 7 days + + } + + + } } -@* ── Failures (Top Failures chart + Recent Failures table) ── *@ +@* ── Failures + Throughput ── *@ @if (DashboardSettings.ShowFailures) { @@ -190,56 +225,29 @@ - Recent Failures - @if (_recentFailures.Count > 0) - { - - - - - - - - - - } - else - { - No recent failures - } - - - -} - -@* ── Avg Execution Duration ── *@ -@if (DashboardSettings.ShowAvgDuration) -{ - - - - Avg Execution Duration (7d) - @if (_avgDurations.Count > 0) + Throughput (7d) + @if (_throughputSeries.Count > 0) { - - - - + @foreach (var series in _throughputSeries) + { + + + + } + + } else diff --git a/src/Trax.Dashboard/Components/Pages/Index.razor.cs b/src/Trax.Dashboard/Components/Pages/Index.razor.cs index 5cd4838..1d8562a 100644 --- a/src/Trax.Dashboard/Components/Pages/Index.razor.cs +++ b/src/Trax.Dashboard/Components/Pages/Index.razor.cs @@ -23,9 +23,6 @@ public partial class Index [Inject] private IServiceProvider ServiceProvider { get; set; } = default!; - [Inject] - private NavigationManager Navigation { get; set; } = default!; - // KPI card values private int _executionsToday; private double _successRate; @@ -33,15 +30,17 @@ public partial class Index private int _unresolvedDeadLetters; // Chart data - private List _executionsOverTime = []; private List _hourlyData = []; private List _minuteData = []; + + private List ExecutionsOverTime => + _selectedTimeRange == TimeRange1H ? _minuteData : _hourlyData; private string _selectedTimeRange = TimeRange24H; private List _topFailures = []; private List _avgDurations = []; - // Tables - private List _recentFailures = []; + // Throughput sparkline (7d) — one series per top train + "Other" + private List _throughputSeries = []; // Server health private double _cpuPercent; @@ -190,7 +189,8 @@ int CountForState(TrainState s) => var targetMinute = minuteStart.Minute; return new ExecutionTimePoint { - Label = i % 10 == 0 ? minuteStart.ToString("HH:mm") : " ", + Label = + i % 5 == 0 ? minuteStart.ToString("HH:mm") : $"\u2009{minuteStart:HH:mm}", Completed = minuteStats .Where(x => x.Date == targetDate @@ -219,8 +219,6 @@ int CountForState(TrainState s) => }) .ToList(); - _executionsOverTime = _selectedTimeRange == TimeRange1H ? _minuteData : _hourlyData; - // Top failing trains (last 7 days) var last7d = now.AddDays(-7); var failuresQuery = context @@ -273,29 +271,100 @@ await failuresQuery }) .ToList(); - // Recent failures - var recentFailuresQuery = context + // Throughput sparkline (completed per 6h block over 7d, by train) + var throughputQuery = context .Metadatas.AsNoTracking() - .Where(m => m.TrainState == TrainState.Failed); + .Where(m => m.TrainState == TrainState.Completed && m.StartTime >= last7d); if (hideAdmin) - recentFailuresQuery = recentFailuresQuery.ExcludeAdmin(adminNames); + throughputQuery = throughputQuery.ExcludeAdmin(adminNames); - _recentFailures = await recentFailuresQuery - .OrderByDescending(m => m.StartTime) - .Take(20) + var throughputStats = await throughputQuery + .GroupBy(m => new + { + m.StartTime.Date, + Block = m.StartTime.Hour / 6, + m.Name, + }) + .Select(g => new + { + g.Key.Date, + g.Key.Block, + g.Key.Name, + Count = g.Count(), + }) .ToListAsync(cancellationToken); - } - private void OnTimeRangeChanged(string value) - { - _selectedTimeRange = value; - _executionsOverTime = _selectedTimeRange == TimeRange1H ? _minuteData : _hourlyData; + // Identify top 3 trains by total count + var top3Names = throughputStats + .GroupBy(x => x.Name) + .OrderByDescending(g => g.Sum(x => x.Count)) + .Take(3) + .Select(g => g.Key) + .ToList(); + + var top3Set = new HashSet(top3Names); + + // Build time block labels + var blockLabels = Enumerable + .Range(0, 28) + .Select(i => + { + var blockStart = now.AddHours(-((27 - i) * 6)); + return new + { + Date = blockStart.Date, + Block = blockStart.Hour / 6, + Label = blockStart.Hour == 0 + ? blockStart.ToString("MMM dd") + : $"\u2009{blockStart:MMM dd HH}", + }; + }) + .ToList(); + + // Build one series per top train + "Other" + var seriesNames = top3Names.Append("Other").ToList(); + string[] seriesColors = ["#2E7D32", "#1565C0", "#F9A825", "#78909C"]; + + _throughputSeries = seriesNames + .Select( + (name, idx) => + new ThroughputSeries + { + Name = name == "Other" ? "Other" : ShortName(name), + Color = seriesColors[idx], + Points = blockLabels + .Select(b => new ThroughputPoint + { + Label = b.Label, + Count = + name == "Other" + ? throughputStats + .Where(x => + x.Date == b.Date + && x.Block == b.Block + && !top3Set.Contains(x.Name) + ) + .Sum(x => x.Count) + : throughputStats + .Where(x => + x.Date == b.Date + && x.Block == b.Block + && x.Name == name + ) + .Sum(x => x.Count), + }) + .ToList(), + } + ) + .Where(s => s.Points.Any(p => p.Count > 0)) + .ToList(); } - private void OnRecentFailureRowClick(DataGridRowMouseEventArgs args) + private string FormatCategoryLabel(object value) { - Navigation.NavigateTo($"trax/data/metadata/{args.Data.Id}"); + var label = value?.ToString() ?? ""; + return label.StartsWith('\u2009') ? "" : label; } private void CollectServerHealthMetrics() diff --git a/src/Trax.Dashboard/Models/ChartModels.cs b/src/Trax.Dashboard/Models/ChartModels.cs index b38cd2c..580ee68 100644 --- a/src/Trax.Dashboard/Models/ChartModels.cs +++ b/src/Trax.Dashboard/Models/ChartModels.cs @@ -19,3 +19,16 @@ public class TrainDuration public string Name { get; init; } = ""; public double AvgMs { get; init; } } + +public class ThroughputPoint +{ + public string Label { get; init; } = ""; + public int Count { get; init; } +} + +public class ThroughputSeries +{ + public string Name { get; init; } = ""; + public string Color { get; init; } = ""; + public List Points { get; init; } = []; +} diff --git a/src/Trax.Dashboard/Trax.Dashboard.csproj b/src/Trax.Dashboard/Trax.Dashboard.csproj index 32fdf97..7d5849f 100644 --- a/src/Trax.Dashboard/Trax.Dashboard.csproj +++ b/src/Trax.Dashboard/Trax.Dashboard.csproj @@ -20,9 +20,6 @@ - - - - - +