Watch
1
0
Fork
You've already forked RedFlag
0

fix: dashboard populates Updates-by-Type and gives honest severity bars

The stats handler initialized updates_by_type but never filled it, so the
"Updates by Type" card always rendered empty (Codeberg #10). Add
UpdateQueries.GetUpdatesByType (grouped by package_type, non-terminal scope)
and wire it into GetDashboardStats.

Severity bars were sized against total_updates (all statuses) while the
severity counts are scoped to non-terminal rows — a scope mismatch. Bars now
size against the sum of the scoped severity values, so they form a true
breakdown that always sums to 100% and never overflows.

Also closed the silent error-swallows in GetDashboardStats: each sub-count
failure is now logged [ERROR] [server] [stats] instead of vanishing.
This commit is contained in:
Fimeg 2026-06-13 08:23:20 -04:00
commit 9be8aba073
3 changed files with 53 additions and 1 deletions

View file

@ -1,6 +1,7 @@
package handlers
import (
"log"
"net/http"
"time"
@ -79,19 +80,34 @@ func (h *StatsHandler) GetDashboardStats(c *gin.Context) {
stats.ImportantUpdates = updateStats.ImportantUpdates
stats.ModerateUpdates = updateStats.ModerateUpdates
stats.LowUpdates = updateStats.LowUpdates
} else {
log.Printf("[ERROR] [server] [stats] all_update_stats_failed: %v", err)
}
// Update counts grouped by package type — drives the "Updates by Type" card.
if byType, err := h.updateQueries.GetUpdatesByType(); err == nil {
stats.UpdatesByType = byType
} else {
log.Printf("[ERROR] [server] [stats] updates_by_type_failed: %v", err)
}
// Remediation count — distinct advisories on available version.
if n, err := h.updateQueries.GetAvailableFixCount(); err == nil {
stats.AvailableFixCount = n
} else {
log.Printf("[ERROR] [server] [stats] available_fix_count_failed: %v", err)
}
// Threat count — distinct advisories on installed version.
if n, err := h.updateQueries.GetOpenThreatCount(); err == nil {
stats.OpenThreatCount = n
} else {
log.Printf("[ERROR] [server] [stats] open_threat_count_failed: %v", err)
}
if stats.OpenThreatCount > 0 {
if top, err := h.updateQueries.GetTopOpenThreats(3); err == nil {
stats.TopThreats = top
} else {
log.Printf("[ERROR] [server] [stats] top_open_threats_failed: %v", err)
}
}

View file

@ -1316,6 +1316,36 @@ func (q *UpdateQueries) GetAllUpdateStats() (*models.UpdateStats, error) {
return stats, nil
}
// typeCount is one row of GetUpdatesByType.
type typeCount struct {
PackageType string `db:"package_type"`
Count int `db:"count"`
}
// GetUpdatesByType returns a count of actionable updates grouped by package type.
// Scoped to non-terminal statuses to match the severity breakdown: installed,
// failed, and ignored packages are not actionable workload. The Dashboard's
// "Updates by Type" card reads this — without it the card always renders empty
// because the handler never populated the map. UI-DASHBOARD-AUDIT.
func (q *UpdateQueries) GetUpdatesByType() (map[string]int, error) {
var rows []typeCount
query := `
SELECT package_type, COUNT(*) as count
FROM current_package_state
WHERE status NOT IN ('installed', 'failed', 'ignored')
GROUP BY package_type
`
if err := q.db.Select(&rows, query); err != nil {
return nil, fmt.Errorf("failed to get updates by type: %w", err)
}
out := make(map[string]int, len(rows))
for _, r := range rows {
out[r.PackageType] = r.Count
}
return out, nil
}
// GetAvailableFixCount returns the number of distinct OSV advisory IDs present
// in supply_chain_vulns across all non-terminal package rows. Deduplicates by
// advisory ID so sub-packages sharing one advisory count as one, not many.