Skip to content

Commit 804721d

Browse files
committed
fix: distinguish null from zero for all measurement fields
Drop COALESCE-to-zero on REAL columns across all SQL queries so NULL (unmeasured) propagates through as *float64 nil in Go. Rendering shows "—" for nil, actual value for measured zero. - Remove HasVolumes from PIIPolicy/WastePolicy (nil pointer is the signal) - Make all float fields on AccountSummary, ServiceStatus, PolicyCategoryStatus, WastePolicy, PIIPolicy nullable (*float64) - Adapt waste detail columns based on category benefit type (volume_reduction shows Volume+Bytes, bytes-only shows just Bytes)
1 parent 5663eea commit 804721d

19 files changed

Lines changed: 352 additions & 344 deletions

‎internal/app/chat/chat.go‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -529,14 +529,16 @@ func (m *Model) emptyStateContent() string {
529529

530530
// wastePercent computes waste % preferring bytes.
531531
func wastePercent(s domain.AccountSummary) int {
532-
if s.TotalBytesPerHour > 0 && s.EstimatedBytesPerHour > 0 {
533-
return int(math.Round(s.EstimatedBytesPerHour / s.TotalBytesPerHour * 100))
532+
if s.TotalBytesPerHour != nil && *s.TotalBytesPerHour > 0 &&
533+
s.EstimatedBytesPerHour != nil && *s.EstimatedBytesPerHour > 0 {
534+
return int(math.Round(*s.EstimatedBytesPerHour / *s.TotalBytesPerHour * 100))
534535
}
535536
if s.EstimatedCostPerHour != nil && s.TotalCostPerHour != nil && *s.TotalCostPerHour > 0 {
536537
return int(math.Round(*s.EstimatedCostPerHour / *s.TotalCostPerHour * 100))
537538
}
538-
if s.TotalVolumePerHour > 0 && s.EstimatedVolumePerHour > 0 {
539-
return int(math.Round(s.EstimatedVolumePerHour / s.TotalVolumePerHour * 100))
539+
if s.TotalVolumePerHour != nil && *s.TotalVolumePerHour > 0 &&
540+
s.EstimatedVolumePerHour != nil && *s.EstimatedVolumePerHour > 0 {
541+
return int(math.Round(*s.EstimatedVolumePerHour / *s.TotalVolumePerHour * 100))
540542
}
541543
return 0
542544
}

‎internal/app/statusbar/pii/pii.go‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,12 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
114114
// stateKey builds a string key for change detection.
115115
func (m *Model) stateKey(summary domain.AccountSummary, policies []domain.PIIPolicy, fixedCount int64) string {
116116
var b strings.Builder
117-
fmt.Fprintf(&b, "%.0f:%.0f:%d:%d",
118-
ptrVal(summary.TotalServiceVolumePerHour), summary.TotalVolumePerHour,
117+
fmt.Fprintf(&b, "%v:%v:%d:%d",
118+
summary.TotalServiceVolumePerHour, summary.TotalVolumePerHour,
119119
len(policies), fixedCount)
120120
for _, p := range policies {
121-
fmt.Fprintf(&b, "|%s:%s:%d:%.0f:%v:%v",
122-
p.ServiceName, p.LogEventName, len(p.Fields), p.VolumePerHour, p.AnyObserved, p.HasVolumes)
121+
fmt.Fprintf(&b, "|%s:%s:%d:%v:%v",
122+
p.ServiceName, p.LogEventName, len(p.Fields), p.VolumePerHour, p.AnyObserved)
123123
for _, f := range p.Fields {
124124
fmt.Fprintf(&b, ":%v", f.Observed)
125125
for _, t := range f.PIITypes {
@@ -245,8 +245,8 @@ func (m *Model) renderTable(width int) string {
245245

246246
for _, p := range m.policies {
247247
vol := "—"
248-
if p.HasVolumes {
249-
vol = format.Volume(p.VolumePerHour) + " evt/hr"
248+
if p.VolumePerHour != nil {
249+
vol = format.Volume(*p.VolumePerHour) + " evt/hr"
250250
}
251251
tbl.Row(
252252
p.LogEventName,
@@ -348,16 +348,12 @@ func summaryDiscoveryPercent(s domain.AccountSummary) int {
348348
if s.TotalServiceVolumePerHour == nil || *s.TotalServiceVolumePerHour <= 0 {
349349
return 100
350350
}
351-
pct := int(math.Round(s.TotalVolumePerHour / *s.TotalServiceVolumePerHour * 100))
351+
if s.TotalVolumePerHour == nil {
352+
return 0
353+
}
354+
pct := int(math.Round(*s.TotalVolumePerHour / *s.TotalServiceVolumePerHour * 100))
352355
if pct > 100 {
353356
pct = 100
354357
}
355358
return pct
356359
}
357-
358-
func ptrVal(p *float64) float64 {
359-
if p == nil {
360-
return 0
361-
}
362-
return *p
363-
}

‎internal/app/statusbar/services/services.go‎

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,16 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
108108

109109
// stateKey builds a string key for change detection.
110110
func (m *Model) stateKey(s domain.AccountSummary, services []domain.ServiceStatus) string {
111-
key := fmt.Sprintf("%v:%d:%d:%d:%d:%d:%d:%d:%d:%s:%s:%s:%.0f:%.0f:%d",
111+
key := fmt.Sprintf("%v:%d:%d:%d:%d:%d:%d:%d:%d:%s:%s:%s:%v:%v:%d",
112112
s.ReadyForUse, s.ServiceCount, s.ActiveServices,
113113
s.EventCount, s.AnalyzedCount, s.OkServices,
114114
s.ErrorServices, s.StaleServices, s.QuarantinedCount,
115115
s.Health, s.Error, s.Warning,
116-
ptrVal(s.TotalServiceVolumePerHour), s.TotalVolumePerHour,
116+
s.TotalServiceVolumePerHour, s.TotalVolumePerHour,
117117
len(services))
118118

119119
for _, svc := range services {
120-
key += fmt.Sprintf("|%s:%s:%s:%d:%d:%.0f:%.0f:%.2f",
120+
key += fmt.Sprintf("|%s:%s:%s:%d:%d:%v:%v:%v",
121121
svc.Name, svc.Health, svc.Warning, svc.LogEventCount, svc.LogEventAnalyzedCount,
122122
svc.ServiceVolumePerHour, svc.LogEventVolumePerHour, svc.ServiceCostPerHourVolumeUSD)
123123
}
@@ -199,8 +199,8 @@ func (m *Model) renderSummary() string {
199199
}
200200

201201
// Total volume.
202-
if s.TotalVolumePerHour > 0 {
203-
parts = append(parts, format.Volume(s.TotalVolumePerHour)+" evt/hr")
202+
if s.TotalVolumePerHour != nil && *s.TotalVolumePerHour > 0 {
203+
parts = append(parts, format.Volume(*s.TotalVolumePerHour)+" evt/hr")
204204
}
205205

206206
// Total cost from account summary (pre-aggregated by control plane).
@@ -238,11 +238,19 @@ func (m *Model) renderServiceTable(width, maxRows int) string {
238238

239239
bar := m.discoveryBar()
240240
for _, svc := range visible {
241+
vol := "—"
242+
if svc.ServiceVolumePerHour != nil {
243+
vol = format.Volume(*svc.ServiceVolumePerHour) + "/hr"
244+
}
245+
cost := "—"
246+
if svc.ServiceCostPerHourVolumeUSD != nil {
247+
cost = format.YearlyCost(*svc.ServiceCostPerHourVolumeUSD)
248+
}
241249
tbl.Row(
242250
m.renderServiceName(svc),
243251
m.renderLogEvents(svc, bar),
244-
format.Volume(svc.ServiceVolumePerHour)+"/hr",
245-
format.YearlyCost(svc.ServiceCostPerHourVolumeUSD),
252+
vol,
253+
cost,
246254
)
247255
}
248256

@@ -294,7 +302,7 @@ func (m *Model) discoveryBar() progress.Model {
294302
// never exactly 100% due to throughput fluctuations).
295303
func (m *Model) renderLogEvents(svc domain.ServiceStatus, bar progress.Model) string {
296304
count := fmt.Sprintf("%-5d", svc.LogEventCount)
297-
if svc.ServiceVolumePerHour <= 0 {
305+
if svc.ServiceVolumePerHour == nil || *svc.ServiceVolumePerHour <= 0 {
298306
return count
299307
}
300308
pct := discoveryPercent(svc)
@@ -310,7 +318,10 @@ func summaryDiscoveryPercent(s domain.AccountSummary) int {
310318
if s.TotalServiceVolumePerHour == nil || *s.TotalServiceVolumePerHour <= 0 {
311319
return 100
312320
}
313-
pct := int(math.Round(s.TotalVolumePerHour / *s.TotalServiceVolumePerHour * 100))
321+
if s.TotalVolumePerHour == nil {
322+
return 0
323+
}
324+
pct := int(math.Round(*s.TotalVolumePerHour / *s.TotalServiceVolumePerHour * 100))
314325
if pct > 100 {
315326
pct = 100
316327
}
@@ -319,19 +330,15 @@ func summaryDiscoveryPercent(s domain.AccountSummary) int {
319330

320331
// discoveryPercent computes how much of a service's volume has been classified.
321332
func discoveryPercent(svc domain.ServiceStatus) int {
322-
if svc.ServiceVolumePerHour <= 0 {
333+
if svc.ServiceVolumePerHour == nil || *svc.ServiceVolumePerHour <= 0 {
323334
return 100
324335
}
325-
pct := int(math.Round(svc.LogEventVolumePerHour / svc.ServiceVolumePerHour * 100))
336+
if svc.LogEventVolumePerHour == nil {
337+
return 0
338+
}
339+
pct := int(math.Round(*svc.LogEventVolumePerHour / *svc.ServiceVolumePerHour * 100))
326340
if pct > 100 {
327341
pct = 100
328342
}
329343
return pct
330344
}
331-
332-
func ptrVal(p *float64) float64 {
333-
if p == nil {
334-
return 0
335-
}
336-
return *p
337-
}

‎internal/app/statusbar/waste/detail.go‎

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ func (d *detail) renderHeader() string {
6262
parts = append(parts, warn.Render("●")+" "+warn.Render(fmt.Sprintf("%d pending", d.category.PendingCount)))
6363
}
6464

65-
if d.category.EstimatedCostPerHour > 0 {
66-
parts = append(parts, muted.Render("~"+format.YearlyCost(d.category.EstimatedCostPerHour)))
65+
if d.category.EstimatedCostPerHour != nil && *d.category.EstimatedCostPerHour > 0 {
66+
parts = append(parts, muted.Render("~"+format.YearlyCost(*d.category.EstimatedCostPerHour)))
6767
}
6868

6969
return strings.Join(parts, sep)
@@ -72,38 +72,45 @@ func (d *detail) renderHeader() string {
7272
// renderTable renders the per-policy table.
7373
func (d *detail) renderTable(width int) string {
7474
tbl := table.New(d.theme, table.WithMaxValueWidth(30))
75-
tbl.Headers("Log Event", "Service", "Volume", "Est. Savings")
75+
76+
showVolume := d.category.ReducesVolume()
77+
if showVolume {
78+
tbl.Headers("Log Event", "Service", "Volume", "Bytes", "Est. Savings")
79+
} else {
80+
tbl.Headers("Log Event", "Service", "Bytes", "Est. Savings")
81+
}
7682
tbl.SetWidth(width)
7783

7884
dot := lipgloss.NewStyle().Foreground(d.theme.Warning).Background(d.theme.Bg).Render("●")
7985

8086
for _, p := range d.policies {
81-
vol := "—"
82-
if p.HasVolumes {
83-
vol = format.Volume(p.VolumePerHour) + " evt/hr"
87+
bytes := "—"
88+
if p.BytesPerHour != nil {
89+
bytes = format.Bytes(*p.BytesPerHour) + "/hr"
8490
}
85-
savings := "—"
86-
if p.HasVolumes {
87-
savings = formatPolicyCost(p)
91+
savings := formatPolicyCost(p)
92+
93+
if showVolume {
94+
vol := "—"
95+
if p.VolumePerHour != nil {
96+
vol = format.Volume(*p.VolumePerHour) + " evt/hr"
97+
}
98+
tbl.Row(dot+" "+p.LogEventName, p.ServiceName, vol, bytes, savings)
99+
} else {
100+
tbl.Row(dot+" "+p.LogEventName, p.ServiceName, bytes, savings)
88101
}
89-
tbl.Row(
90-
dot+" "+p.LogEventName,
91-
p.ServiceName,
92-
vol,
93-
savings,
94-
)
95102
}
96103

97104
return tbl.View()
98105
}
99106

100107
// formatPolicyCost returns the estimated yearly cost for a single policy.
101108
func formatPolicyCost(p domain.WastePolicy) string {
102-
if p.EstimatedCostPerHour > 0 {
103-
yearly := p.EstimatedCostPerHour * 8760
109+
if p.EstimatedCostPerHour != nil && *p.EstimatedCostPerHour > 0 {
110+
yearly := *p.EstimatedCostPerHour * 8760
104111
if yearly >= 1 {
105112
return "~" + format.Cost(yearly) + "/yr"
106113
}
107114
}
108-
return "-"
115+
return "—"
109116
}

‎internal/app/statusbar/waste/waste.go‎

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,18 @@ func (m *Model) Update(msg tea.Msg) tea.Cmd {
118118

119119
// stateKey builds a string key for change detection.
120120
func (m *Model) stateKey(s domain.AccountSummary, cats []domain.PolicyCategoryStatus) string {
121-
key := fmt.Sprintf("%v:%d:%d:%d:%d:%d:%v:%v:%v:%.0f:%.0f:%v:%v:%.0f:%.0f:%v:%.0f:%.0f:%d",
121+
key := fmt.Sprintf("%v:%d:%d:%d:%d:%d:%v:%v:%v:%v:%v:%v:%v:%v:%v:%v:%v:%v:%d",
122122
s.ReadyForUse, s.EventCount, s.AnalyzedCount, s.PendingPolicyCount,
123123
s.ApprovedPolicyCount, s.DismissedPolicyCount,
124-
ptrVal(s.EstimatedCostPerHour),
125-
ptrVal(s.EstimatedCostPerHourBytes),
126-
ptrVal(s.EstimatedCostPerHourVolume),
124+
s.EstimatedCostPerHour, s.EstimatedCostPerHourBytes, s.EstimatedCostPerHourVolume,
127125
s.EstimatedVolumePerHour, s.EstimatedBytesPerHour,
128-
ptrVal(s.ObservedCostBefore), ptrVal(s.ObservedCostAfter),
126+
s.ObservedCostBefore, s.ObservedCostAfter,
129127
s.ObservedVolumeBefore, s.ObservedVolumeAfter,
130-
ptrVal(s.TotalCostPerHour), s.TotalVolumePerHour,
128+
s.TotalCostPerHour, s.TotalVolumePerHour,
131129
s.TotalBytesPerHour, len(cats))
132130

133131
for _, c := range cats {
134-
key += fmt.Sprintf("|%s:%d:%d:%d:%.0f:%.0f:%.2f:%.0f:%.0f:%.0f:%.0f:%.2f:%.2f:%d:%d",
132+
key += fmt.Sprintf("|%s:%d:%d:%d:%v:%v:%v:%v:%v:%v:%v:%v:%v:%d:%d",
135133
c.Category, c.PendingCount, c.ApprovedCount, c.DismissedCount,
136134
c.EstimatedVolumePerHour, c.EstimatedBytesPerHour, c.EstimatedCostPerHour,
137135
c.ObservedVolumeBefore, c.ObservedVolumeAfter,
@@ -245,8 +243,9 @@ func (m *Model) CompactView() string {
245243

246244
// wastePercent computes the estimated waste as a percentage of total bytes.
247245
func wastePercent(s domain.AccountSummary) int {
248-
if s.TotalBytesPerHour > 0 && s.EstimatedBytesPerHour > 0 {
249-
return int(math.Round(s.EstimatedBytesPerHour / s.TotalBytesPerHour * 100))
246+
if s.TotalBytesPerHour != nil && *s.TotalBytesPerHour > 0 &&
247+
s.EstimatedBytesPerHour != nil && *s.EstimatedBytesPerHour > 0 {
248+
return int(math.Round(*s.EstimatedBytesPerHour / *s.TotalBytesPerHour * 100))
250249
}
251250
return 0
252251
}
@@ -384,8 +383,8 @@ func (m *Model) discoveryBar() progress.Model {
384383

385384
// formatCategoryCost returns estimated yearly cost for a category.
386385
func formatCategoryCost(c domain.PolicyCategoryStatus) string {
387-
if c.EstimatedCostPerHour > 0 {
388-
yearly := c.EstimatedCostPerHour * 8760
386+
if c.EstimatedCostPerHour != nil && *c.EstimatedCostPerHour > 0 {
387+
yearly := *c.EstimatedCostPerHour * 8760
389388
if yearly >= 1 {
390389
return "~" + format.Cost(yearly) + "/yr"
391390
}
@@ -395,11 +394,13 @@ func formatCategoryCost(c domain.PolicyCategoryStatus) string {
395394

396395
// formatObservedCost returns the observed cost reduction for a category.
397396
func formatObservedCost(c domain.PolicyCategoryStatus) string {
398-
diff := c.ObservedCostBefore - c.ObservedCostAfter
399-
if diff > 0 {
400-
yearly := diff * 8760
401-
if yearly >= 1 {
402-
return "-" + format.Cost(yearly) + "/yr"
397+
if c.ObservedCostBefore != nil && c.ObservedCostAfter != nil {
398+
diff := *c.ObservedCostBefore - *c.ObservedCostAfter
399+
if diff > 0 {
400+
yearly := diff * 8760
401+
if yearly >= 1 {
402+
return "-" + format.Cost(yearly) + "/yr"
403+
}
403404
}
404405
}
405406
return "—"
@@ -414,16 +415,11 @@ func formatObservedSaving(s domain.AccountSummary) string {
414415
}
415416
return ""
416417
}
417-
diff := s.ObservedVolumeBefore - s.ObservedVolumeAfter
418-
if diff > 0 {
419-
return format.Volume(diff) + " evt/hr"
418+
if s.ObservedVolumeBefore != nil && s.ObservedVolumeAfter != nil {
419+
diff := *s.ObservedVolumeBefore - *s.ObservedVolumeAfter
420+
if diff > 0 {
421+
return format.Volume(diff) + " evt/hr"
422+
}
420423
}
421424
return ""
422425
}
423-
424-
func ptrVal(p *float64) float64 {
425-
if p == nil {
426-
return 0
427-
}
428-
return *p
429-
}

‎internal/domain/account_summary.go‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,25 @@ type AccountSummary struct {
4646
TotalServiceVolumePerHour *float64
4747
TotalServiceCostPerHour *float64
4848

49-
// Log event throughput (discovered events).
50-
TotalCostPerHour *float64 // nil when pricing unavailable
49+
// Log event throughput (discovered events). Nil when unmeasured.
50+
TotalCostPerHour *float64
5151
TotalCostPerHourBytes *float64
5252
TotalCostPerHourVolume *float64
53-
TotalVolumePerHour float64
54-
TotalBytesPerHour float64
53+
TotalVolumePerHour *float64
54+
TotalBytesPerHour *float64
5555

56-
// Estimated savings from pending policies.
57-
EstimatedCostPerHour *float64 // nil when pricing unavailable
56+
// Estimated savings from pending policies. Nil when unmeasured.
57+
EstimatedCostPerHour *float64
5858
EstimatedCostPerHourBytes *float64
5959
EstimatedCostPerHourVolume *float64
60-
EstimatedVolumePerHour float64
61-
EstimatedBytesPerHour float64
60+
EstimatedVolumePerHour *float64
61+
EstimatedBytesPerHour *float64
6262

63-
// Observed impact from approved policies (before/after).
64-
ObservedCostBefore *float64 // nil when pricing unavailable
63+
// Observed impact from approved policies (before/after). Nil when unmeasured.
64+
ObservedCostBefore *float64
6565
ObservedCostAfter *float64
66-
ObservedVolumeBefore float64
67-
ObservedVolumeAfter float64
68-
ObservedBytesBefore float64
69-
ObservedBytesAfter float64
66+
ObservedVolumeBefore *float64
67+
ObservedVolumeAfter *float64
68+
ObservedBytesBefore *float64
69+
ObservedBytesAfter *float64
7070
}

0 commit comments

Comments
 (0)