Skip to content

Commit 957ef0c

Browse files
committed
refactor: unify CatalogSummary + PolicySummary into AccountSummary
One struct, one query, one method. Both types queried the same datadog_account_statuses_cache table but arbitrarily split the columns. - New AccountSummary with all fields from both structs plus 6 missing columns from the schema (DismissedPolicyCount, CleanCount, etc) - Single GetAccountSummary SQL query replaces GetCatalogStatus + GetPolicyStatus - GetSummary() replaces GetCatalogSummary() + GetPolicySummary() - Show analysis progress in catalogstatus compact view - Delete catalog_status.go and policy_summary.go
1 parent a3bf473 commit 957ef0c

9 files changed

Lines changed: 235 additions & 159 deletions

File tree

‎internal/app/chat/chat.go‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ type Model struct {
7171
originY int
7272

7373
// Empty state
74-
policySummary *domain.PolicySummary
74+
policySummary *domain.AccountSummary
7575

7676
// Dependencies
7777
db sqlite.DB
@@ -149,7 +149,7 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
149149
if !m.hasMessages() && m.db != nil {
150150
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
151151
defer cancel()
152-
summary, err := m.db.DatadogAccountStatuses().GetPolicySummary(ctx)
152+
summary, err := m.db.DatadogAccountStatuses().GetSummary(ctx)
153153
if err == nil && summary.ReadyForUse {
154154
m.policySummary = &summary
155155
}
@@ -170,7 +170,35 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
170170
cmds = append(cmds, m.handlePersistedMessage(msg))
171171

172172
case msgs.StreamFailed:
173+
// Forward to round so it transitions to StateFailed.
174+
cmds = append(cmds, m.messageList.Update(msg))
175+
176+
// Clean up orphaned messages from DB (async to avoid blocking UI).
177+
if last := m.messageList.LastRound(); last != nil {
178+
ids := last.LastTurnMessageIDs()
179+
if len(ids) > 0 {
180+
db := m.db
181+
scope := m.scope
182+
cmds = append(cmds, func() tea.Msg {
183+
for _, id := range ids {
184+
if err := db.Messages().Delete(context.Background(), id); err != nil {
185+
scope.Error("failed to delete orphaned message", "id", id, "error", err)
186+
}
187+
}
188+
return nil
189+
})
190+
}
191+
192+
// Turn 1: remove round entirely, input bar restores text via pendingText.
193+
if !last.HasAssistantContent() {
194+
m.messageList.RemoveLastRound()
195+
}
196+
// Turn 2+: round stays visible with red error divider.
197+
}
198+
199+
cmds = append(cmds, m.inputBar.Update(msg))
173200
cmds = append(cmds, appmsg.ErrorCmd("Failed to get response", msg.Err, false))
201+
return tea.Batch(cmds...)
174202

175203
case tea.MouseClickMsg:
176204
// Click on the message list area focuses it
@@ -493,7 +521,7 @@ func (m *Model) emptyStateContent() string {
493521
}
494522

495523
// wastePercent computes waste % preferring bytes.
496-
func wastePercent(s domain.PolicySummary) int {
524+
func wastePercent(s domain.AccountSummary) int {
497525
if s.TotalBytesPerHour > 0 && s.EstimatedBytesPerHour > 0 {
498526
return int(math.Round(s.EstimatedBytesPerHour / s.TotalBytesPerHour * 100))
499527
}

‎internal/app/statusbar/catalogstatus/catalogstatus.go‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type Model struct {
2828
theme styles.Theme
2929
db sqlite.DB
3030

31-
summary domain.CatalogSummary
31+
summary domain.AccountSummary
3232
services []domain.ServiceStatus
3333
hasData bool
3434
lastState string
@@ -72,7 +72,7 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
7272
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
7373
defer cancel()
7474

75-
summary, err := m.db.DatadogAccountStatuses().GetCatalogSummary(ctx)
75+
summary, err := m.db.DatadogAccountStatuses().GetSummary(ctx)
7676
if err != nil {
7777
return m.poll()
7878
}
@@ -97,7 +97,7 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
9797
}
9898

9999
// stateKey builds a string key for change detection.
100-
func (m *Model) stateKey(s domain.CatalogSummary, services []domain.ServiceStatus) string {
100+
func (m *Model) stateKey(s domain.AccountSummary, services []domain.ServiceStatus) string {
101101
key := fmt.Sprintf("%v:%d:%d:%d:%d:%d:%d:%d:%d:%s:%.0f:%s:%d",
102102
s.ReadyForUse, s.ServiceCount, s.ActiveServices,
103103
s.EventCount, s.AnalyzedCount, s.AnalyzingCount,
@@ -142,7 +142,11 @@ func (m *Model) CompactView() string {
142142
case domain.ServiceLogStatusDiscovering:
143143
suffix = " · discovering"
144144
case domain.ServiceLogStatusAnalyzing:
145-
suffix = " · analyzing"
145+
if s.EventCount > 0 {
146+
suffix = fmt.Sprintf(" · analyzing %d/%d events", s.AnalyzedCount, s.EventCount)
147+
} else {
148+
suffix = " · analyzing"
149+
}
146150
case domain.ServiceLogStatusDisabled, domain.ServiceLogStatusInactive,
147151
domain.ServiceLogStatusBroken, domain.ServiceLogStatusStale,
148152
domain.ServiceLogStatusReady:
@@ -290,7 +294,7 @@ func (m *Model) serviceName(svc domain.ServiceStatus) string {
290294
// renderStatus renders the status badge, with percent for discovering/analyzing.
291295
func (m *Model) renderStatus(svc domain.ServiceStatus) string {
292296
s := status.Service(m.theme, svc.Status, true)
293-
if svc.PercentComplete > 0 && svc.Status == domain.ServiceLogStatusDiscovering {
297+
if svc.PercentComplete > 0 && (svc.Status == domain.ServiceLogStatusDiscovering || svc.Status == domain.ServiceLogStatusAnalyzing) {
294298
muted := lipgloss.NewStyle().Foreground(m.theme.TextMuted).Background(m.theme.Bg)
295299
s += " " + muted.Render(fmt.Sprintf("%.0f%%", svc.PercentComplete))
296300
}

‎internal/app/statusbar/policystatus/policystatus.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type Model struct {
2727
theme styles.Theme
2828
db sqlite.DB
2929

30-
summary domain.PolicySummary
30+
summary domain.AccountSummary
3131
categories []domain.PolicyCategoryStatus
3232
hasData bool
3333
lastState string
@@ -71,7 +71,7 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
7171
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
7272
defer cancel()
7373

74-
summary, err := m.db.DatadogAccountStatuses().GetPolicySummary(ctx)
74+
summary, err := m.db.DatadogAccountStatuses().GetSummary(ctx)
7575
if err != nil {
7676
return m.poll()
7777
}
@@ -96,7 +96,7 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
9696
}
9797

9898
// stateKey builds a string key for change detection.
99-
func (m *Model) stateKey(s domain.PolicySummary, cats []domain.PolicyCategoryStatus) string {
99+
func (m *Model) stateKey(s domain.AccountSummary, cats []domain.PolicyCategoryStatus) string {
100100
key := fmt.Sprintf("%v:%d:%d:%d:%v:%v:%v:%.0f:%.0f:%v:%v:%.0f:%.0f:%v:%.0f:%.0f:%d",
101101
s.ReadyForUse, s.PendingPolicyCount,
102102
s.PolicyCount, s.ApprovedPolicyCount,
@@ -165,7 +165,7 @@ func (m *Model) CompactView() string {
165165
}
166166

167167
// wastePercent computes the estimated waste as a percentage of total bytes.
168-
func wastePercent(s domain.PolicySummary) int {
168+
func wastePercent(s domain.AccountSummary) int {
169169
if s.TotalBytesPerHour > 0 && s.EstimatedBytesPerHour > 0 {
170170
return int(math.Round(s.EstimatedBytesPerHour / s.TotalBytesPerHour * 100))
171171
}
@@ -233,7 +233,7 @@ func (m *Model) renderHeadline() string {
233233

234234
// formatSavingsBreakdown returns per-dimension savings segments for joining
235235
// into the headline, e.g. ["bytes: ~$4.6k/yr", "volume: ~$7.3k/yr", "~$11.9k/yr total"].
236-
func formatSavingsBreakdown(s domain.PolicySummary, sep string) []string {
236+
func formatSavingsBreakdown(s domain.AccountSummary, sep string) []string {
237237
if s.EstimatedCostPerHour == nil {
238238
return nil
239239
}
@@ -389,7 +389,7 @@ func formatBenefitLabel(benefits string) string {
389389
}
390390

391391
// formatObservedSaving returns the observed savings from approved policies.
392-
func formatObservedSaving(s domain.PolicySummary) string {
392+
func formatObservedSaving(s domain.AccountSummary) string {
393393
if s.ObservedCostBefore != nil && s.ObservedCostAfter != nil {
394394
diff := (*s.ObservedCostBefore - *s.ObservedCostAfter) * 8760
395395
if diff >= 1 {

‎internal/domain/account_summary.go‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package domain
2+
3+
// PolicyLogStatus is the lifecycle status for a policy.
4+
type PolicyLogStatus string
5+
6+
const (
7+
PolicyLogStatusPending PolicyLogStatus = "PENDING"
8+
PolicyLogStatusApproved PolicyLogStatus = "APPROVED"
9+
PolicyLogStatusDismissed PolicyLogStatus = "DISMISSED"
10+
)
11+
12+
func (s PolicyLogStatus) String() string { return string(s) }
13+
14+
// AccountSummary is the aggregated status across all Datadog accounts.
15+
// Sourced from a single query against datadog_account_statuses_cache.
16+
type AccountSummary struct {
17+
ReadyForUse bool
18+
19+
// Catalog: services
20+
ServiceCount int64
21+
ActiveServices int64
22+
ReadyServices int64
23+
BrokenServices int64
24+
StaleServices int64
25+
DiscoveringCount int64
26+
AnalyzingCount int64
27+
28+
// Catalog: events
29+
EventCount int64
30+
AnalyzedCount int64
31+
CleanCount int64
32+
PendingCount int64
33+
ResolvedCount int64
34+
QuarantinedCount int64
35+
PercentComplete float64
36+
WorstStatus ServiceLogStatus
37+
LogError string
38+
39+
// Policies
40+
PolicyCount int64
41+
PendingPolicyCount int64
42+
ApprovedPolicyCount int64
43+
DismissedPolicyCount int64
44+
45+
// Cost & volume: estimated savings from pending policies
46+
EstimatedCostPerHour *float64 // nil when pricing unavailable
47+
EstimatedCostPerHourBytes *float64
48+
EstimatedCostPerHourVolume *float64
49+
EstimatedVolumePerHour float64
50+
EstimatedBytesPerHour float64
51+
52+
// Cost & volume: observed impact from approved policies
53+
ObservedCostBefore *float64 // nil when pricing unavailable
54+
ObservedCostAfter *float64
55+
ObservedVolumeBefore float64
56+
ObservedVolumeAfter float64
57+
58+
// Cost & volume: totals
59+
TotalCostPerHour *float64 // nil when pricing unavailable
60+
TotalVolumePerHour float64
61+
TotalBytesPerHour float64
62+
}

‎internal/domain/catalog_status.go‎

Lines changed: 0 additions & 17 deletions
This file was deleted.

‎internal/domain/policy_summary.go‎

Lines changed: 0 additions & 32 deletions
This file was deleted.

‎internal/sqlite/datadog_account_statuses.go‎

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,61 +10,66 @@ import (
1010

1111
// DatadogAccountStatuses provides access to Datadog account status data.
1212
type DatadogAccountStatuses interface {
13-
GetCatalogSummary(ctx context.Context) (domain.CatalogSummary, error)
14-
GetPolicySummary(ctx context.Context) (domain.PolicySummary, error)
13+
GetSummary(ctx context.Context) (domain.AccountSummary, error)
1514
}
1615

1716
// datadogAccountStatusesImpl implements DatadogAccountStatuses.
1817
type datadogAccountStatusesImpl struct {
1918
queries *gen.Queries
2019
}
2120

22-
// GetCatalogSummary returns aggregated catalog status across all Datadog accounts.
23-
func (d *datadogAccountStatusesImpl) GetCatalogSummary(ctx context.Context) (domain.CatalogSummary, error) {
24-
row, err := d.queries.GetCatalogStatus(ctx)
21+
// GetSummary returns aggregated status across all Datadog accounts.
22+
func (d *datadogAccountStatusesImpl) GetSummary(ctx context.Context) (domain.AccountSummary, error) {
23+
row, err := d.queries.GetAccountSummary(ctx)
2524
if err != nil {
26-
return domain.CatalogSummary{}, WrapSQLiteError(err, "get catalog summary")
25+
return domain.AccountSummary{}, WrapSQLiteError(err, "get account summary")
2726
}
2827

29-
return domain.CatalogSummary{
30-
ReadyForUse: row.ReadyForUse != 0,
28+
return domain.AccountSummary{
29+
ReadyForUse: row.ReadyForUse != 0,
30+
31+
// Services
3132
ServiceCount: row.ServiceCount,
3233
ActiveServices: row.ActiveServices,
33-
EventCount: row.EventCount,
34-
AnalyzedCount: row.AnalyzedCount,
35-
AnalyzingCount: row.AnalyzingCount,
36-
DiscoveringCount: row.DiscoveringCount,
34+
ReadyServices: row.ReadyServices,
3735
BrokenServices: row.BrokenServices,
3836
StaleServices: row.StaleServices,
37+
DiscoveringCount: row.DiscoveringCount,
38+
AnalyzingCount: row.AnalyzingCount,
39+
40+
// Events
41+
EventCount: row.EventCount,
42+
AnalyzedCount: row.AnalyzedCount,
43+
CleanCount: row.CleanCount,
44+
PendingCount: row.PendingCount,
45+
ResolvedCount: row.ResolvedCount,
46+
QuarantinedCount: row.QuarantinedCount,
3947
PercentComplete: row.PercentComplete,
4048
WorstStatus: domain.ServiceLogStatus(row.WorstStatus),
4149
LogError: fmt.Sprint(row.LogError),
42-
}, nil
43-
}
4450

45-
// GetPolicySummary returns aggregated policy/savings status across all Datadog accounts.
46-
func (d *datadogAccountStatusesImpl) GetPolicySummary(ctx context.Context) (domain.PolicySummary, error) {
47-
row, err := d.queries.GetPolicyStatus(ctx)
48-
if err != nil {
49-
return domain.PolicySummary{}, WrapSQLiteError(err, "get policy summary")
50-
}
51+
// Policies
52+
PolicyCount: row.PolicyCount,
53+
PendingPolicyCount: row.PendingPolicyCount,
54+
ApprovedPolicyCount: row.ApprovedPolicyCount,
55+
DismissedPolicyCount: row.DismissedPolicyCount,
5156

52-
return domain.PolicySummary{
53-
ReadyForUse: row.ReadyForUse != 0,
54-
PendingPolicyCount: row.PendingPolicyCount,
55-
PolicyCount: row.PolicyCount,
56-
ApprovedPolicyCount: row.ApprovedPolicyCount,
57+
// Estimated savings
5758
EstimatedCostPerHour: row.EstimatedCostPerHour,
5859
EstimatedCostPerHourBytes: row.EstimatedCostPerHourBytes,
5960
EstimatedCostPerHourVolume: row.EstimatedCostPerHourVolume,
6061
EstimatedVolumePerHour: row.EstimatedVolumePerHour,
6162
EstimatedBytesPerHour: row.EstimatedBytesPerHour,
62-
ObservedCostBefore: row.ObservedCostBefore,
63-
ObservedCostAfter: row.ObservedCostAfter,
64-
ObservedVolumeBefore: row.ObservedVolumeBefore,
65-
ObservedVolumeAfter: row.ObservedVolumeAfter,
66-
TotalCostPerHour: row.TotalCostPerHour,
67-
TotalVolumePerHour: row.TotalVolumePerHour,
68-
TotalBytesPerHour: row.TotalBytesPerHour,
63+
64+
// Observed impact
65+
ObservedCostBefore: row.ObservedCostBefore,
66+
ObservedCostAfter: row.ObservedCostAfter,
67+
ObservedVolumeBefore: row.ObservedVolumeBefore,
68+
ObservedVolumeAfter: row.ObservedVolumeAfter,
69+
70+
// Totals
71+
TotalCostPerHour: row.TotalCostPerHour,
72+
TotalVolumePerHour: row.TotalVolumePerHour,
73+
TotalBytesPerHour: row.TotalBytesPerHour,
6974
}, nil
7075
}

0 commit comments

Comments
 (0)