diff --git a/misc/data/dashboard.go b/misc/data/dashboard.go index acba2a89d..80f06e465 100644 --- a/misc/data/dashboard.go +++ b/misc/data/dashboard.go @@ -191,6 +191,40 @@ func (p *PBClient) FetchDashboardData(ctx context.Context, days int) (*Dashboard typeCounts[r.Type]++ } + // === Extended metrics tracking === + + // Track tool executions + if r.Type == "tool" && r.ToolName != "" { + toolCounts[r.ToolName]++ + data.TotalTools++ + } + + // Track addon installations + if r.Type == "addon" { + addonCounts[r.NSAPP]++ + data.TotalAddons++ + } + + // Track GPU usage + if r.GPUVendor != "" { + key := r.GPUVendor + if r.GPUPassthrough != "" { + key += "|" + r.GPUPassthrough + } + gpuCounts[key]++ + } + + // Track error categories + if r.Status == "failed" && r.ErrorCategory != "" { + errorCatCounts[r.ErrorCategory]++ + } + + // Track install duration (for averaging) + if r.InstallDuration > 0 { + totalDuration += r.InstallDuration + durationCount++ + } + // Daily stats (use Created field if available) if r.Created != "" { date := r.Created[:10] // "2026-02-09" @@ -224,6 +258,25 @@ func (p *PBClient) FetchDashboardData(ctx context.Context, days int) (*Dashboard // Daily stats for chart data.DailyStats = buildDailyStats(dailySuccess, dailyFailed, days) + // === Extended metrics === + + // GPU stats + data.GPUStats = buildGPUStats(gpuCounts) + + // Error categories + data.ErrorCategories = buildErrorCategories(errorCatCounts) + + // Top tools + data.TopTools = buildToolStats(toolCounts, 10) + + // Top addons + data.TopAddons = buildAddonStats(addonCounts, 10) + + // Average install duration + if durationCount > 0 { + data.AvgInstallDuration = float64(totalDuration) / float64(durationCount) + } + // Recent records (last 20) if len(records) > 20 { data.RecentRecords = records[:20] @@ -502,6 +555,97 @@ func buildDailyStats(success, failed map[string]int, days int) []DailyStat { return result } +// === Extended metrics helper functions === + +func buildGPUStats(gpuCounts map[string]int) []GPUCount { + result := make([]GPUCount, 0, len(gpuCounts)) + for key, count := range gpuCounts { + parts := strings.Split(key, "|") + vendor := parts[0] + passthrough := "" + if len(parts) > 1 { + passthrough = parts[1] + } + result = append(result, GPUCount{ + Vendor: vendor, + Passthrough: passthrough, + Count: count, + }) + } + // Sort by count descending + for i := 0; i < len(result)-1; i++ { + for j := i + 1; j < len(result); j++ { + if result[j].Count > result[i].Count { + result[i], result[j] = result[j], result[i] + } + } + } + return result +} + +func buildErrorCategories(catCounts map[string]int) []ErrorCatCount { + result := make([]ErrorCatCount, 0, len(catCounts)) + for cat, count := range catCounts { + result = append(result, ErrorCatCount{ + Category: cat, + Count: count, + }) + } + // Sort by count descending + for i := 0; i < len(result)-1; i++ { + for j := i + 1; j < len(result); j++ { + if result[j].Count > result[i].Count { + result[i], result[j] = result[j], result[i] + } + } + } + return result +} + +func buildToolStats(toolCounts map[string]int, n int) []ToolCount { + result := make([]ToolCount, 0, len(toolCounts)) + for tool, count := range toolCounts { + result = append(result, ToolCount{ + Tool: tool, + Count: count, + }) + } + // Sort by count descending + for i := 0; i < len(result)-1; i++ { + for j := i + 1; j < len(result); j++ { + if result[j].Count > result[i].Count { + result[i], result[j] = result[j], result[i] + } + } + } + if len(result) > n { + return result[:n] + } + return result +} + +func buildAddonStats(addonCounts map[string]int, n int) []AddonCount { + result := make([]AddonCount, 0, len(addonCounts)) + for addon, count := range addonCounts { + result = append(result, AddonCount{ + Addon: addon, + Count: count, + }) + } + // Sort by count descending + for i := 0; i < len(result)-1; i++ { + for j := i + 1; j < len(result); j++ { + if result[j].Count > result[i].Count { + result[i], result[j] = result[j], result[i] + } + } + } + if len(result) > n { + return result[:n] + } + return result +} + // DashboardHTML returns the embedded dashboard HTML func DashboardHTML() string { return `