diff --git a/ai/metrics.go b/ai/metrics.go index 4bb256c..e2427d3 100644 --- a/ai/metrics.go +++ b/ai/metrics.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "slices" + "sort" "sync" "time" @@ -152,11 +153,21 @@ func Summary(events []Event) map[string]any { } } + recent := make([]Event, len(events)) + copy(recent, events) + sort.SliceStable(recent, func(i, j int) bool { + return recent[i].Timestamp.After(recent[j].Timestamp) + }) + if len(recent) > 10 { + recent = recent[:10] + } + return map[string]any{ "total": len(events), "by_type": sortedMap(byType), "by_repo": sortedMap(byRepo), "by_agent": sortedMap(byAgent), + "events": briefEvents(recent), } } @@ -181,3 +192,22 @@ func sortedMap(m map[string]int) []map[string]any { } return result } + +// briefEvents converts events into the compact shape used by metrics_query. +func briefEvents(events []Event) []map[string]any { + result := make([]map[string]any, len(events)) + for i, ev := range events { + item := map[string]any{ + "type": ev.Type, + "timestamp": ev.Timestamp, + } + if ev.AgentID != "" { + item["agent_id"] = ev.AgentID + } + if ev.Repo != "" { + item["repo"] = ev.Repo + } + result[i] = item + } + return result +} diff --git a/ai/metrics_test.go b/ai/metrics_test.go index d4363c2..260f3cc 100644 --- a/ai/metrics_test.go +++ b/ai/metrics_test.go @@ -228,9 +228,9 @@ func TestSummary_Good_Empty(t *testing.T) { func TestSummary_Good(t *testing.T) { events := []Event{ - {Type: "build", Repo: "core-php", AgentID: "agent-1"}, - {Type: "build", Repo: "core-php", AgentID: "agent-2"}, - {Type: "test", Repo: "core-api", AgentID: "agent-1"}, + {Type: "build", Repo: "core-php", AgentID: "agent-1", Timestamp: time.Date(2026, 3, 15, 9, 0, 0, 0, time.UTC)}, + {Type: "build", Repo: "core-php", AgentID: "agent-2", Timestamp: time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)}, + {Type: "test", Repo: "core-api", AgentID: "agent-1", Timestamp: time.Date(2026, 3, 15, 11, 0, 0, 0, time.UTC)}, } s := Summary(events) @@ -248,6 +248,39 @@ func TestSummary_Good(t *testing.T) { if byType[0]["key"] != "build" || byType[0]["count"] != 2 { t.Errorf("expected build:2 first, got %v:%v", byType[0]["key"], byType[0]["count"]) } + + recent, _ := s["events"].([]map[string]any) + if len(recent) != 3 { + t.Fatalf("expected 3 recent events, got %d", len(recent)) + } + if recent[0]["type"] != "test" { + t.Errorf("expected newest event first, got %v", recent[0]["type"]) + } + if _, ok := recent[0]["timestamp"].(time.Time); !ok { + t.Errorf("expected timestamp to be time.Time, got %T", recent[0]["timestamp"]) + } +} + +func TestSummary_Good_RecentEventsLimit(t *testing.T) { + events := make([]Event, 0, 12) + for i := 0; i < 12; i++ { + events = append(events, Event{ + Type: "type", + Timestamp: time.Date(2026, 3, 15, 12, i, 0, 0, time.UTC), + }) + } + + s := Summary(events) + recent, _ := s["events"].([]map[string]any) + if len(recent) != 10 { + t.Fatalf("expected 10 recent events, got %d", len(recent)) + } + if recent[0]["timestamp"].(time.Time).Minute() != 11 { + t.Errorf("expected newest event first, got minute %d", recent[0]["timestamp"].(time.Time).Minute()) + } + if recent[9]["timestamp"].(time.Time).Minute() != 2 { + t.Errorf("expected tenth newest event last, got minute %d", recent[9]["timestamp"].(time.Time).Minute()) + } } // --- sortedMap --- diff --git a/cmd/metrics/cmd.go b/cmd/metrics/cmd.go index e1b9b27..bae083d 100644 --- a/cmd/metrics/cmd.go +++ b/cmd/metrics/cmd.go @@ -6,10 +6,10 @@ import ( "fmt" "time" - "forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/ai/ai" "dappco.re/go/core/i18n" coreerr "dappco.re/go/core/log" + "forge.lthn.ai/core/cli/pkg/cli" ) var ( @@ -94,6 +94,23 @@ func runMetrics() error { cli.Blank() } + // Recent events + if recent, ok := summary["events"].([]map[string]any); ok && len(recent) > 0 { + cli.Print("%s\n", cli.DimStyle.Render("Recent events:")) + for _, entry := range recent { + ts, _ := entry["timestamp"].(time.Time) + agent, _ := entry["agent_id"].(string) + repo, _ := entry["repo"].(string) + cli.Print(" %-20s %-24s %-20s %-20s\n", + ts.Format(time.RFC3339), + entry["type"], + agent, + repo, + ) + } + cli.Blank() + } + if len(events) == 0 { cli.Text(i18n.T("cmd.ai.metrics.none_found")) }