Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions control-plane/internal/handlers/agentic/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -641,3 +641,23 @@ func (m *mockStatusStorage) GetInboundEventByWorkflowID(context.Context, string)
func (m *mockStatusStorage) TriggerMetrics(context.Context) (*types.TriggerMetrics, error) {
return &types.TriggerMetrics{}, nil
}

func (m *mockStatusStorage) CountExecutionsByStatus(ctx context.Context, start, end time.Time) (types.ExecutionStatusCounts, error) {
return types.ExecutionStatusCounts{}, nil
}

func (m *mockStatusStorage) QueryRecentFailures(ctx context.Context, start, end time.Time, limit int) ([]*types.Execution, error) {
return nil, nil
}

func (m *mockStatusStorage) QueryDeveloperHome(ctx context.Context, q types.DeveloperHomeQuery) (*types.DeveloperHomeData, error) {
return &types.DeveloperHomeData{}, nil
}

func (m *mockStatusStorage) QueryBlastRadius(ctx context.Context, agentNodeID string, start, end time.Time) ([]types.DeveloperHomeEntryPoint, error) {
return nil, nil
}

func (m *mockStatusStorage) CountForwarderDeadLetters(ctx context.Context) (int64, error) {
return 0, nil
}
20 changes: 20 additions & 0 deletions control-plane/internal/handlers/config_storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,3 +715,23 @@ func (m *configStorageMock) GetInboundEventByWorkflowID(context.Context, string)
func (m *configStorageMock) TriggerMetrics(context.Context) (*types.TriggerMetrics, error) {
return &types.TriggerMetrics{}, nil
}

func (m *configStorageMock) CountExecutionsByStatus(ctx context.Context, start, end time.Time) (types.ExecutionStatusCounts, error) {
return types.ExecutionStatusCounts{}, nil
}

func (m *configStorageMock) QueryRecentFailures(ctx context.Context, start, end time.Time, limit int) ([]*types.Execution, error) {
return nil, nil
}

func (m *configStorageMock) QueryDeveloperHome(ctx context.Context, q types.DeveloperHomeQuery) (*types.DeveloperHomeData, error) {
return &types.DeveloperHomeData{}, nil
}

func (m *configStorageMock) QueryBlastRadius(ctx context.Context, agentNodeID string, start, end time.Time) ([]types.DeveloperHomeEntryPoint, error) {
return nil, nil
}

func (m *configStorageMock) CountForwarderDeadLetters(ctx context.Context) (int64, error) {
return 0, nil
}
20 changes: 20 additions & 0 deletions control-plane/internal/handlers/connector/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1382,3 +1382,23 @@ func (m *mockStorage) GetInboundEventByWorkflowID(context.Context, string) (*typ
func (m *mockStorage) TriggerMetrics(context.Context) (*types.TriggerMetrics, error) {
return &types.TriggerMetrics{}, nil
}

func (m *mockStorage) CountExecutionsByStatus(ctx context.Context, start, end time.Time) (types.ExecutionStatusCounts, error) {
return types.ExecutionStatusCounts{}, nil
}

func (m *mockStorage) QueryRecentFailures(ctx context.Context, start, end time.Time, limit int) ([]*types.Execution, error) {
return nil, nil
}

func (m *mockStorage) QueryDeveloperHome(ctx context.Context, q types.DeveloperHomeQuery) (*types.DeveloperHomeData, error) {
return &types.DeveloperHomeData{}, nil
}

func (m *mockStorage) QueryBlastRadius(ctx context.Context, agentNodeID string, start, end time.Time) ([]types.DeveloperHomeEntryPoint, error) {
return nil, nil
}

func (m *mockStorage) CountForwarderDeadLetters(ctx context.Context) (int64, error) {
return 0, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -151,31 +152,35 @@ func TestDashboardSummaryHandlerAdditionalCoverage(t *testing.T) {
func TestExecutionsSummarySuccessRateRolling24h(t *testing.T) {
gin.SetMode(gin.TestMode)

store := &dashboardOverrideStorage{overrideStorage: &overrideStorage{StorageProvider: setupTestStorage(t)}}
base := setupTestStorage(t)
store := &dashboardOverrideStorage{overrideStorage: &overrideStorage{StorageProvider: base}}
handler := NewDashboardHandler(store, &mockLifecycleAgentService{})

// 01:00 UTC: the calendar-today window is only an hour old, so most of the
// rolling 24h window falls in yesterday's calendar window.
now := time.Date(2026, 7, 17, 1, 0, 0, 0, time.UTC)
today := time.Date(2026, 7, 17, 0, 0, 0, 0, time.UTC)

runningToday := &types.Execution{Status: string(types.ExecutionStatusRunning), StartedAt: now.Add(-15 * time.Minute)}
failedWithin24h := &types.Execution{Status: string(types.ExecutionStatusFailed), StartedAt: now.Add(-23 * time.Hour)}
succeededWithin24h := &types.Execution{Status: string(types.ExecutionStatusSucceeded), StartedAt: now.Add(-23*time.Hour - 30*time.Minute)}
succeededOutside24h := &types.Execution{Status: string(types.ExecutionStatusSucceeded), StartedAt: now.Add(-24*time.Hour - 30*time.Minute)}

store.queryExecutionRecordsFn = func(ctx context.Context, filter types.ExecutionFilter) ([]*types.Execution, error) {
if filter.StartTime != nil && filter.StartTime.Equal(today) {
return []*types.Execution{runningToday}, nil
}
return []*types.Execution{failedWithin24h, succeededWithin24h, succeededOutside24h}, nil
for i, rec := range []struct {
status types.ExecutionStatus
started time.Time
}{
{types.ExecutionStatusRunning, now.Add(-15 * time.Minute)}, // today, in flight
{types.ExecutionStatusFailed, now.Add(-23 * time.Hour)}, // yesterday, in the 24h window
{types.ExecutionStatusSucceeded, now.Add(-23*time.Hour - 30*time.Minute)}, // yesterday, in the 24h window
{types.ExecutionStatusSucceeded, now.Add(-24*time.Hour - 30*time.Minute)}, // yesterday, before the 24h window
} {
id := fmt.Sprintf("rolling-%d", i)
require.NoError(t, base.CreateExecutionRecord(context.Background(), &types.Execution{
ExecutionID: id, RunID: id, AgentNodeID: "agent", ReasonerID: "r", NodeID: "agent",
Status: string(rec.status), StartedAt: rec.started,
}))
}

summary, rate, err := handler.getExecutionsSummaryAndSuccessRate(context.Background(), now)
require.NoError(t, err)
require.Equal(t, 1, summary.Today)
require.Equal(t, 3, summary.Yesterday)
// Rate basis: failedWithin24h + succeededWithin24h. The in-flight run does
// not count against the rate and the pre-window success is excluded.
// Rate basis: the failure and the success inside the window. The in-flight
// run does not count against the rate and the pre-window success is excluded.
require.Equal(t, 50.0, rate)
}
96 changes: 74 additions & 22 deletions control-plane/internal/handlers/ui/dashboard.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ui

import (
"context"
"net/http"
"sort"
"sync"
Expand All @@ -21,6 +22,9 @@ type DashboardHandler struct {
agentService interfaces.AgentService
cache *DashboardCache
enhancedCache *EnhancedDashboardCache
homeCache homeCache
// now overrides the clock in tests.
now func() time.Time
}

// NewDashboardHandler creates a new DashboardHandler.
Expand Down Expand Up @@ -239,19 +243,33 @@ func (h *DashboardHandler) GetEnhancedDashboardSummaryHandler(c *gin.Context) {
return
}

// Query executions for the specified time range
filters := types.ExecutionFilter{
StartTime: &startTime,
EndTime: &endTime,
Limit: 50000,
SortBy: "started_at",
SortDescending: false,
ExcludePayloads: true,
// The per-reasoner breakdowns (trends, hotspots, heatmap, top workflows)
// are built from a sample of the window's executions. The sample is the
// NEWEST rows, so a window with more executions than the sample holds
// still describes the latest part of the window, and detail_sampled says
// so. The counts and incidents below never come from the sample.
executions, sampled, err := h.queryDashboardSample(ctx, startTime, endTime)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to query workflow executions for enhanced dashboard")
RespondInternalError(c, "failed to load workflow execution data")
return
}

executions, err := h.store.QueryExecutionRecords(ctx, filters)
rangeCounts, err := h.storage.CountExecutionsByStatus(ctx, startTime, endTime)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to query workflow executions for enhanced dashboard")
logger.Logger.Error().Err(err).Msg("failed to count executions for enhanced dashboard")
RespondInternalError(c, "failed to load workflow execution data")
return
}
weekCounts, err := h.storage.CountExecutionsByStatus(ctx, endTime.AddDate(0, 0, -7), endTime)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to count executions for enhanced dashboard")
RespondInternalError(c, "failed to load workflow execution data")
return
}
recentFailures, err := h.storage.QueryRecentFailures(ctx, startTime, endTime, 10)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to query incidents for enhanced dashboard")
RespondInternalError(c, "failed to load workflow execution data")
return
}
Expand Down Expand Up @@ -324,10 +342,11 @@ func (h *DashboardHandler) GetEnhancedDashboardSummaryHandler(c *gin.Context) {
}

overview := h.buildEnhancedOverviewForRange(agents, executions, startTime, endTime)
applyExactCounts(&overview, rangeCounts, weekCounts)
trends := buildExecutionTrendsForRange(executions, startTime, endTime, preset)
agentHealth := h.buildAgentHealthSummary(ctx, agents)
workflows := buildWorkflowInsights(executions, activeExecutions)
incidents := buildIncidentItems(executions, 10)
incidents := buildIncidentItems(recentFailures, 10)
hotspots := buildHotspotSummary(executions)
activityPatterns := buildActivityPatterns(executions)

Expand All @@ -341,27 +360,60 @@ func (h *DashboardHandler) GetEnhancedDashboardSummaryHandler(c *gin.Context) {
Incidents: incidents,
Hotspots: hotspots,
ActivityPatterns: activityPatterns,
DetailSampled: sampled,
}

// Calculate comparison data if requested
if enableComparison {
prevStart, prevEnd := calculateComparisonPeriod(startTime, endTime)
prevFilters := types.ExecutionFilter{
StartTime: &prevStart,
EndTime: &prevEnd,
Limit: 50000,
SortBy: "started_at",
SortDescending: false,
ExcludePayloads: true,
}

prevExecutions, err := h.store.QueryExecutionRecords(ctx, prevFilters)
prevExecutions, _, err := h.queryDashboardSample(ctx, prevStart, prevEnd)
if err == nil {
prevOverview := h.buildEnhancedOverviewForRange(agents, prevExecutions, prevStart, prevEnd)
response.Comparison = buildComparisonData(overview, prevOverview, prevStart, prevEnd)
prevCounts, countErr := h.storage.CountExecutionsByStatus(ctx, prevStart, prevEnd)
prevWeek, weekErr := h.storage.CountExecutionsByStatus(ctx, prevEnd.AddDate(0, 0, -7), prevEnd)
if countErr == nil && weekErr == nil {
applyExactCounts(&prevOverview, prevCounts, prevWeek)
response.Comparison = buildComparisonData(overview, prevOverview, prevStart, prevEnd)
}
}
}

h.enhancedCache.Set(cacheKey, response)
c.JSON(http.StatusOK, response)
}

// dashboardSampleLimit bounds how many executions the enhanced dashboard
// loads to build its per-reasoner breakdowns.
const dashboardSampleLimit = 50000

// queryDashboardSample loads the newest executions of a window, oldest first,
// and reports whether the window held more than the sample.
func (h *DashboardHandler) queryDashboardSample(ctx context.Context, start, end time.Time) ([]*types.Execution, bool, error) {
executions, err := h.store.QueryExecutionRecords(ctx, types.ExecutionFilter{
StartTime: &start,
EndTime: &end,
Limit: dashboardSampleLimit,
SortBy: "started_at",
SortDescending: true,
ExcludePayloads: true,
})
if err != nil {
return nil, false, err
}
for i, j := 0, len(executions)-1; i < j; i, j = i+1, j-1 {
executions[i], executions[j] = executions[j], executions[i]
}
return executions, len(executions) >= dashboardSampleLimit, nil
}

// applyExactCounts replaces the sample-derived counts in an overview with
// COUNT results: executions in the requested range, executions in the seven
// days ending with it, and the range's success rate.
func applyExactCounts(overview *EnhancedOverview, inRange, lastWeek types.ExecutionStatusCounts) {
overview.ExecutionsLast24h = int(inRange.Total)
overview.ExecutionsLast7d = int(lastWeek.Total)
overview.SuccessRate24h = 0
if inRange.Total > 0 {
overview.SuccessRate24h = float64(inRange.Succeeded) / float64(inRange.Total) * 100
}
}
Loading
Loading