@@ -14,6 +14,7 @@ import (
1414 "github.com/usetero/cli/internal/domain"
1515 "github.com/usetero/cli/internal/sqlite"
1616 "github.com/usetero/cli/internal/styles"
17+ "github.com/usetero/cli/internal/tea/components/status"
1718 "github.com/usetero/cli/internal/tea/components/table"
1819)
1920
@@ -180,12 +181,9 @@ func (m *Model) renderHeadline() string {
180181
181182 var parts []string
182183
183- text := lipgloss .NewStyle ().Foreground (colors .Page .Text )
184-
185184 // Waste percentage with colored dot.
186- if wp := wastePercent (s ); wp > 0 {
187- dot := lipgloss .NewStyle ().Foreground (colors .Warning .Fg ).Render ("●" )
188- parts = append (parts , dot + " " + text .Render (fmt .Sprintf ("%d%% waste" , wp )))
185+ if w := status .Waste (m .theme , wastePercent (s )); w != "" {
186+ parts = append (parts , w )
189187 }
190188
191189 // Observed savings.
@@ -196,6 +194,7 @@ func (m *Model) renderHeadline() string {
196194
197195 // Pending count.
198196 if s .PendingPolicyCount > 0 {
197+ text := lipgloss .NewStyle ().Foreground (colors .Page .Text )
199198 est := formatEstimated (s )
200199 if est != "" {
201200 parts = append (parts , text .Render (fmt .Sprintf ("%d pending" , s .PendingPolicyCount ))+ sep + muted .Render ("~" + est ))
@@ -213,36 +212,68 @@ func (m *Model) renderHeadline() string {
213212
214213// renderCategoryTable renders the per-category breakdown.
215214func (m * Model ) renderCategoryTable (width int ) string {
216- // Only show categories with pending policies.
217- var pending []domain.PolicyCategoryStatus
218- for _ , c := range m .categories {
219- if c .PendingCount > 0 {
220- pending = append (pending , c )
221- }
222- }
223-
224- if len (pending ) == 0 {
215+ if len (m .categories ) == 0 {
225216 muted := lipgloss .NewStyle ().Foreground (m .theme .Colors .Page .TextMuted )
226- return muted .Render ("No pending policies " )
217+ return muted .Render ("No policy data " )
227218 }
228219
229220 tbl := table .New (m .theme , table .WithMaxValueWidth (30 ))
230- tbl .Headers ("Category" , "Pending" , "Impact " , "Benefit " , "Risk" )
221+ tbl .Headers ("Category" , "Pending" , "Waste " , "Impact " , "Risk" , "Benefit " )
231222 tbl .SetWidth (width )
232223
233- for _ , c := range pending {
224+ for _ , c := range m . categories {
234225 tbl .Row (
235226 c .Category ,
236227 fmt .Sprintf ("%d" , c .PendingCount ),
228+ m .formatWaste (c ),
237229 formatCategoryImpact (c ),
230+ m .renderRisk (c .RiskLevel ),
238231 formatBenefitLabel (c .Benefit ),
239- c .RiskLevel ,
240232 )
241233 }
242234
243235 return tbl .View ()
244236}
245237
238+ // categoryWastePercent computes a single category's waste as a percentage of total.
239+ func (m * Model ) categoryWastePercent (c domain.PolicyCategoryStatus ) int {
240+ s := m .summary
241+ if s .TotalBytesPerHour > 0 && c .EstimatedBytesPerHour > 0 {
242+ return int (math .Round (c .EstimatedBytesPerHour / s .TotalBytesPerHour * 100 ))
243+ }
244+ if s .TotalVolumePerHour > 0 && c .EstimatedVolumePerHour > 0 {
245+ return int (math .Round (c .EstimatedVolumePerHour / s .TotalVolumePerHour * 100 ))
246+ }
247+ return 0
248+ }
249+
250+ // formatWaste renders the waste % with a colored dot, matching the statusbar style.
251+ func (m * Model ) formatWaste (c domain.PolicyCategoryStatus ) string {
252+ if w := status .WasteShort (m .theme , m .categoryWastePercent (c )); w != "" {
253+ return w
254+ }
255+ if c .PendingCount == 0 {
256+ return lipgloss .NewStyle ().Foreground (m .theme .Colors .Success .Fg ).Render ("●" )
257+ }
258+ return "—"
259+ }
260+
261+ // renderRisk returns a color-coded risk label.
262+ func (m * Model ) renderRisk (level domain.RiskLevel ) string {
263+ colors := m .theme .Colors
264+ s := level .String ()
265+ switch level {
266+ case domain .RiskLevelHigh :
267+ return lipgloss .NewStyle ().Foreground (colors .Error .Fg ).Render (s )
268+ case domain .RiskLevelMedium :
269+ return lipgloss .NewStyle ().Foreground (colors .Warning .Fg ).Render (s )
270+ case domain .RiskLevelLow :
271+ return lipgloss .NewStyle ().Foreground (colors .Success .Fg ).Render (s )
272+ default :
273+ return lipgloss .NewStyle ().Foreground (colors .Page .TextMuted ).Render (s )
274+ }
275+ }
276+
246277// wastePercent computes the estimated waste as a percentage.
247278// Prefers bytes (where most waste lives), then cost, then volume.
248279func wastePercent (s domain.PolicySummary ) int {
@@ -270,22 +301,38 @@ func formatCategoryImpact(c domain.PolicyCategoryStatus) string {
270301 return "—"
271302}
272303
273- // formatBenefitLabel returns a human-readable benefit label.
274- func formatBenefitLabel (benefit string ) string {
275- switch {
276- case strings .Contains (benefit , "volume_reduction" ):
277- return "cost"
278- case strings .Contains (benefit , "bytes_reduction" ):
279- return "bytes"
280- case strings .Contains (benefit , "signal_quality" ):
281- return "signal quality"
282- case strings .Contains (benefit , "compliance" ):
283- return "compliance"
284- case strings .Contains (benefit , "resilience" ):
285- return "resilience"
286- default :
287- return benefit
304+ // formatBenefitLabel returns human-readable benefit labels.
305+ // The input may contain JSON arrays like '["volume_reduction","signal_quality"]'
306+ // concatenated by GROUP_CONCAT.
307+ func formatBenefitLabel (benefits string ) string {
308+ labelFor := map [string ]string {
309+ "volume_reduction" : "cost" ,
310+ "bytes_reduction" : "bytes" ,
311+ "signal_quality" : "signal" ,
312+ "compliance" : "compliance" ,
313+ "resilience" : "resilience" ,
314+ }
315+
316+ // Strip JSON array noise: brackets, quotes, whitespace
317+ cleaned := strings .NewReplacer ("[" , "" , "]" , "" , "\" " , "" ).Replace (benefits )
318+
319+ seen := make (map [string ]bool )
320+ var labels []string
321+ for _ , part := range strings .Split (cleaned , "," ) {
322+ key := strings .TrimSpace (part )
323+ label , ok := labelFor [key ]
324+ if ! ok {
325+ continue
326+ }
327+ if ! seen [label ] {
328+ seen [label ] = true
329+ labels = append (labels , label )
330+ }
331+ }
332+ if len (labels ) == 0 {
333+ return ""
288334 }
335+ return strings .Join (labels , ", " )
289336}
290337
291338// formatEstimated returns the estimated impact of pending policies.
0 commit comments