Skip to content

Commit 094d0b6

Browse files
authored
Merge branch 'main' into fix/ssrf-approval-callback-435
2 parents afcd0e9 + 9b685fe commit 094d0b6

22 files changed

Lines changed: 304 additions & 48 deletions

CHANGELOG.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,74 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
66

77
<!-- changelog:entries -->
88

9+
## [0.1.110-rc.4] - 2026-07-17
10+
11+
12+
### Fixed
13+
14+
- Fix(control-plane): dashboard success rate defaults to 100% and actually covers 24h (#792)
15+
16+
* fix(control-plane): dashboard success rate covers a rolling 24h window
17+
18+
The summary endpoint computed success_rate from calendar-today (UTC)
19+
executions only, returned 0 when nothing had run, and counted in-flight
20+
executions as failures. An idle system therefore showed a red 0% under a
21+
label claiming "last 24 hours".
22+
23+
Compute the rate over executions started in the rolling last-24h window,
24+
count only terminal executions in the denominator, and report 100 when
25+
none have finished - no completed runs means nothing has failed.
26+
27+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
28+
29+
* fix(web): stop double-scaling the dashboard success rate
30+
31+
The summary endpoint returns success_rate as a 0-100 percentage, but the
32+
dashboard multiplied it by 100 again before display. The test fixtures
33+
mirrored the same wrong 0-1 scale, so the tautology passed while a real
34+
server response would have rendered 9100%. Align the fixtures with the
35+
actual API contract.
36+
37+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
38+
39+
---------
40+
41+
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> (623084e)
42+
43+
- Fix(desktop): correct Windows tray icon theme and DPI rendering (#791)
44+
45+
* feat(desktop): generate multi-size ICO tray glyphs
46+
47+
The Windows tray ignores PNG scale-factor representations
48+
(electron/electron#33044) and upscales the 16px bitmap on >100% displays,
49+
so make-icons.mjs now also emits one .ico per tray variant with
50+
16/20/24/32/48 frames (20 covers the common 125% scaling). The PNGs stay
51+
for the Linux representation path.
52+
53+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
54+
55+
* fix(desktop): render the Windows tray glyph correctly
56+
57+
Two fixes for the washed-out / blurry tray icon on Windows:
58+
59+
- Pick the light/dark glyph from the *system* (taskbar) theme via
60+
nativeTheme.shouldUseDarkColorsForSystemIntegratedUI instead of
61+
shouldUseDarkColors, which tracks the separately-configurable *apps*
62+
theme. With mixed themes (dark taskbar + light apps is common) the
63+
tray wore a near-invisible glyph that read as the offline icon. The
64+
poll re-checks the theme too, since Windows does not reliably emit
65+
nativeTheme 'updated' for system-theme-only flips.
66+
- Load the tray image from the multi-size .ico on win32 so Electron
67+
serves a DPI-correct frame; scale-factor PNG representations are
68+
ignored by the Windows tray (electron/electron#33044) and the 16px
69+
bitmap got upscaled on >100% displays.
70+
71+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72+
73+
---------
74+
75+
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> (6da0607)
76+
977
## [0.1.110-rc.3] - 2026-07-17
1078

1179

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.110-rc.3
1+
0.1.110-rc.4

control-plane/internal/handlers/ui/coverage_dashboard_additional_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,15 @@ func TestDashboardPureHelpersCoverage(t *testing.T) {
215215
{Status: string(types.ExecutionStatusSucceeded)},
216216
{Status: string(types.ExecutionStatusFailed)},
217217
}))
218+
require.Equal(t, 100.0, (&DashboardHandler{}).calculateSuccessRate(nil))
219+
require.Equal(t, 100.0, (&DashboardHandler{}).calculateSuccessRate([]*types.Execution{
220+
{Status: string(types.ExecutionStatusRunning)},
221+
}))
222+
require.Equal(t, 50.0, (&DashboardHandler{}).calculateSuccessRate([]*types.Execution{
223+
{Status: string(types.ExecutionStatusSucceeded)},
224+
{Status: string(types.ExecutionStatusFailed)},
225+
{Status: string(types.ExecutionStatusRunning)},
226+
}))
218227
})
219228
}
220229

control-plane/internal/handlers/ui/coverage_nodes_dashboard_summary_test.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package ui
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"net/http"
78
"net/http/httptest"
89
"testing"
10+
"time"
911

1012
"github.com/Agent-Field/agentfield/control-plane/internal/core/domain"
1113
"github.com/Agent-Field/agentfield/control-plane/internal/services"
@@ -23,7 +25,7 @@ func (s *dashboardOverrideStorage) QueryExecutionRecords(ctx context.Context, fi
2325
if s.queryExecutionRecordsFn != nil {
2426
return s.queryExecutionRecordsFn(ctx, filter)
2527
}
26-
return s.overrideStorage.StorageProvider.QueryExecutionRecords(ctx, filter)
28+
return s.StorageProvider.QueryExecutionRecords(ctx, filter)
2729
}
2830

2931
func TestNodesHandlerAdditionalCoverage(t *testing.T) {
@@ -98,6 +100,32 @@ func TestDashboardSummaryHandlerAdditionalCoverage(t *testing.T) {
98100
require.Equal(t, http.StatusOK, resp.Code)
99101
})
100102

103+
t.Run("idle system reports vacuous 100 percent success", func(t *testing.T) {
104+
store := &dashboardOverrideStorage{overrideStorage: &overrideStorage{StorageProvider: setupTestStorage(t)}}
105+
handler := NewDashboardHandler(store, &mockLifecycleAgentService{})
106+
router := gin.New()
107+
router.GET("/api/ui/v1/dashboard", handler.GetDashboardSummaryHandler)
108+
109+
store.queryExecutionRecordsFn = func(ctx context.Context, filter types.ExecutionFilter) ([]*types.Execution, error) {
110+
return nil, nil
111+
}
112+
store.queryAgentPackagesFn = func(ctx context.Context, filters types.PackageFilters) ([]*types.AgentPackage, error) {
113+
return nil, nil
114+
}
115+
store.listAgentsFn = func(ctx context.Context, filters types.AgentFilters) ([]*types.AgentNode, error) {
116+
return nil, nil
117+
}
118+
119+
resp := httptest.NewRecorder()
120+
router.ServeHTTP(resp, httptest.NewRequest(http.MethodGet, "/api/ui/v1/dashboard", nil))
121+
require.Equal(t, http.StatusOK, resp.Code)
122+
123+
var body DashboardSummaryResponse
124+
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &body))
125+
require.Equal(t, 100.0, body.SuccessRate)
126+
require.Equal(t, 0, body.Executions.Today)
127+
})
128+
101129
t.Run("error when collector fails", func(t *testing.T) {
102130
store := &dashboardOverrideStorage{overrideStorage: &overrideStorage{StorageProvider: setupTestStorage(t)}}
103131
handler := NewDashboardHandler(store, &mockLifecycleAgentService{})
@@ -119,3 +147,35 @@ func TestDashboardSummaryHandlerAdditionalCoverage(t *testing.T) {
119147
require.Equal(t, http.StatusInternalServerError, resp.Code)
120148
})
121149
}
150+
151+
func TestExecutionsSummarySuccessRateRolling24h(t *testing.T) {
152+
gin.SetMode(gin.TestMode)
153+
154+
store := &dashboardOverrideStorage{overrideStorage: &overrideStorage{StorageProvider: setupTestStorage(t)}}
155+
handler := NewDashboardHandler(store, &mockLifecycleAgentService{})
156+
157+
// 01:00 UTC: the calendar-today window is only an hour old, so most of the
158+
// rolling 24h window falls in yesterday's calendar window.
159+
now := time.Date(2026, 7, 17, 1, 0, 0, 0, time.UTC)
160+
today := time.Date(2026, 7, 17, 0, 0, 0, 0, time.UTC)
161+
162+
runningToday := &types.Execution{Status: string(types.ExecutionStatusRunning), StartedAt: now.Add(-15 * time.Minute)}
163+
failedWithin24h := &types.Execution{Status: string(types.ExecutionStatusFailed), StartedAt: now.Add(-23 * time.Hour)}
164+
succeededWithin24h := &types.Execution{Status: string(types.ExecutionStatusSucceeded), StartedAt: now.Add(-23*time.Hour - 30*time.Minute)}
165+
succeededOutside24h := &types.Execution{Status: string(types.ExecutionStatusSucceeded), StartedAt: now.Add(-24*time.Hour - 30*time.Minute)}
166+
167+
store.queryExecutionRecordsFn = func(ctx context.Context, filter types.ExecutionFilter) ([]*types.Execution, error) {
168+
if filter.StartTime != nil && filter.StartTime.Equal(today) {
169+
return []*types.Execution{runningToday}, nil
170+
}
171+
return []*types.Execution{failedWithin24h, succeededWithin24h, succeededOutside24h}, nil
172+
}
173+
174+
summary, rate, err := handler.getExecutionsSummaryAndSuccessRate(context.Background(), now)
175+
require.NoError(t, err)
176+
require.Equal(t, 1, summary.Today)
177+
require.Equal(t, 3, summary.Yesterday)
178+
// Rate basis: failedWithin24h + succeededWithin24h. The in-flight run does
179+
// not count against the rate and the pre-window success is excluded.
180+
require.Equal(t, 50.0, rate)
181+
}

control-plane/internal/handlers/ui/dashboard.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ func NewDashboardHandler(storage storage.StorageProvider, agentService interface
3636

3737
// DashboardSummaryResponse represents the dashboard summary response
3838
type DashboardSummaryResponse struct {
39-
Agents AgentsSummary `json:"agents"`
40-
Executions ExecutionsSummary `json:"executions"`
41-
SuccessRate float64 `json:"success_rate"`
42-
Packages PackagesSummary `json:"packages"`
39+
Agents AgentsSummary `json:"agents"`
40+
Executions ExecutionsSummary `json:"executions"`
41+
// SuccessRate is the percentage (0-100) of terminal executions started in
42+
// the last 24 hours that succeeded; 100 when none have finished.
43+
SuccessRate float64 `json:"success_rate"`
44+
Packages PackagesSummary `json:"packages"`
4345
}
4446

4547
// AgentsSummary represents agent statistics

control-plane/internal/handlers/ui/dashboard_aggregate.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -779,29 +779,48 @@ func (h *DashboardHandler) getExecutionsSummaryAndSuccessRate(ctx context.Contex
779779
return ExecutionsSummary{}, 0, err
780780
}
781781

782-
// Calculate success rate from today's executions
783-
successRate := h.calculateSuccessRate(todayExecutions)
782+
// Success rate covers the rolling last-24h window, which spans the tail of
783+
// yesterday's calendar window plus all of today's.
784+
cutoff := now.Add(-24 * time.Hour)
785+
last24h := make([]*types.Execution, 0, len(todayExecutions)+len(yesterdayExecutions))
786+
for _, exec := range todayExecutions {
787+
if !exec.StartedAt.Before(cutoff) {
788+
last24h = append(last24h, exec)
789+
}
790+
}
791+
for _, exec := range yesterdayExecutions {
792+
if !exec.StartedAt.Before(cutoff) {
793+
last24h = append(last24h, exec)
794+
}
795+
}
796+
successRate := h.calculateSuccessRate(last24h)
784797

785798
return ExecutionsSummary{
786799
Today: len(todayExecutions),
787800
Yesterday: len(yesterdayExecutions),
788801
}, successRate, nil
789802
}
790803

791-
// calculateSuccessRate calculates the success rate from executions
804+
// calculateSuccessRate returns the percentage of terminal executions that
805+
// succeeded. In-flight executions don't count against the rate, and with no
806+
// terminal executions at all there is nothing failing, so it reports 100.
792807
func (h *DashboardHandler) calculateSuccessRate(executions []*types.Execution) float64 {
793-
if len(executions) == 0 {
794-
return 0.0
795-
}
796-
797808
successCount := 0
809+
terminalCount := 0
798810
for _, exec := range executions {
811+
if !types.IsTerminalExecutionStatus(exec.Status) {
812+
continue
813+
}
814+
terminalCount++
799815
if types.NormalizeExecutionStatus(exec.Status) == types.ExecutionStatusSucceeded {
800816
successCount++
801817
}
802818
}
819+
if terminalCount == 0 {
820+
return 100.0
821+
}
803822

804-
return float64(successCount) / float64(len(executions)) * 100.0
823+
return float64(successCount) / float64(terminalCount) * 100.0
805824
}
806825

807826
// getPackagesSummary collects package statistics

control-plane/internal/templates/go/go.mod.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ module {{.GoModule}}
22

33
go 1.23
44

5-
require github.com/Agent-Field/agentfield/sdk/go v0.1.110-rc.3
5+
require github.com/Agent-Field/agentfield/sdk/go v0.1.110-rc.4

control-plane/web/client/src/pages/NewDashboardPage.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ describe("NewDashboardPage", () => {
130130
});
131131
vi.mocked(getDashboardSummary).mockResolvedValue({
132132
executions: { today: 10 },
133-
success_rate: 0.9,
133+
success_rate: 90,
134134
agents: { running: 1 },
135135
});
136136
});
@@ -279,7 +279,7 @@ describe("NewDashboardPage", () => {
279279
});
280280
vi.mocked(getDashboardSummary).mockResolvedValue({
281281
executions: { today: 123 },
282-
success_rate: 0.88,
282+
success_rate: 88,
283283
agents: { running: 5 },
284284
});
285285

control-plane/web/client/src/pages/NewDashboardPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ export function NewDashboardPage() {
782782
<Separator orientation="vertical" className="hidden h-6 sm:block" />
783783
<div className="flex items-center gap-1.5">
784784
<span className="text-2xl font-semibold tabular-nums">
785-
{successRate != null ? `${(successRate * 100).toFixed(0)}%` : "—"}
785+
{successRate != null ? `${successRate.toFixed(0)}%` : "—"}
786786
</span>
787787
<span className="text-muted-foreground">success</span>
788788
</div>

control-plane/web/client/src/test/pages/NewDashboardPage.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const pageState = vi.hoisted(() => ({
1010
useQueryResult: {
1111
data: {
1212
executions: { today: 12 },
13-
success_rate: 0.91,
13+
success_rate: 91,
1414
agents: { running: 3 },
1515
},
1616
isLoading: false,
@@ -147,7 +147,7 @@ describe("NewDashboardPage", () => {
147147
pageState.useQueryResult = {
148148
data: {
149149
executions: { today: 12 },
150-
success_rate: 0.91,
150+
success_rate: 91,
151151
agents: { running: 3 },
152152
},
153153
isLoading: false,

0 commit comments

Comments
 (0)