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:
parent
b8cdb91a21
commit
9be8aba073
3 changed files with 53 additions and 1 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue