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.

View file

@ -61,6 +61,12 @@ const Dashboard: React.FC = () => {
{ label: 'Low', value: stats?.low_updates ?? 0, color: 'bg-gray-600' },
];
// Bars show each severity's share of the actionable (non-terminal) severity
// counts the server returns. Sizing against this sum — not total_updates,
// which also counts installed/failed — keeps numerator and denominator in the
// same scope so bars always sum to 100% and never overflow. UI-DASHBOARD-AUDIT #2.
const severityTotal = severityBreakdown.reduce((sum, s) => sum + s.value, 0);
const updateTypeBreakdown = Object.entries(stats?.updates_by_type ?? {}).map(([type, count]) => ({
type: type.charAt(0).toUpperCase() + type.slice(1),
value: count,
@ -151,7 +157,7 @@ const Dashboard: React.FC = () => {
<div
className={`h-2 rounded-full ${severity.color}`}
style={{
width: `${(stats?.total_updates ?? 0) > 0 ? (severity.value / (stats?.total_updates ?? 1)) * 100 : 0}%`
width: `${severityTotal > 0 ? (severity.value / severityTotal) * 100 : 0}%`
}}
></div>
</div>