package main import ( "context" "encoding/json" "fmt" "net/http" "net/url" "time" ) // DashboardData holds aggregated statistics for the dashboard type DashboardData struct { TotalInstalls int `json:"total_installs"` SuccessCount int `json:"success_count"` FailedCount int `json:"failed_count"` InstallingCount int `json:"installing_count"` SuccessRate float64 `json:"success_rate"` TopApps []AppCount `json:"top_apps"` OsDistribution []OsCount `json:"os_distribution"` MethodStats []MethodCount `json:"method_stats"` RecentRecords []TelemetryRecord `json:"recent_records"` DailyStats []DailyStat `json:"daily_stats"` } type AppCount struct { App string `json:"app"` Count int `json:"count"` } type OsCount struct { Os string `json:"os"` Count int `json:"count"` } type MethodCount struct { Method string `json:"method"` Count int `json:"count"` } type DailyStat struct { Date string `json:"date"` Success int `json:"success"` Failed int `json:"failed"` } // FetchDashboardData retrieves aggregated data from PocketBase func (p *PBClient) FetchDashboardData(ctx context.Context, days int) (*DashboardData, error) { if err := p.ensureAuth(ctx); err != nil { return nil, err } data := &DashboardData{} // Calculate date filter since := time.Now().AddDate(0, 0, -days).Format("2006-01-02 00:00:00") filter := url.QueryEscape(fmt.Sprintf("created >= '%s'", since)) // Fetch all records for the period records, err := p.fetchRecords(ctx, filter, 500) if err != nil { return nil, err } // Aggregate statistics appCounts := make(map[string]int) osCounts := make(map[string]int) methodCounts := make(map[string]int) dailySuccess := make(map[string]int) dailyFailed := make(map[string]int) for _, r := range records { data.TotalInstalls++ switch r.Status { case "sucess": data.SuccessCount++ case "failed": data.FailedCount++ case "installing": data.InstallingCount++ } // Count apps if r.NSAPP != "" { appCounts[r.NSAPP]++ } // Count OS if r.OsType != "" { osCounts[r.OsType]++ } // Count methods if r.Method != "" { methodCounts[r.Method]++ } // Daily stats (use Created field if available) if r.Created != "" { date := r.Created[:10] // "2026-02-09" if r.Status == "sucess" { dailySuccess[date]++ } else if r.Status == "failed" { dailyFailed[date]++ } } } // Calculate success rate completed := data.SuccessCount + data.FailedCount if completed > 0 { data.SuccessRate = float64(data.SuccessCount) / float64(completed) * 100 } // Convert maps to sorted slices (top 10) data.TopApps = topN(appCounts, 10) data.OsDistribution = topNOs(osCounts, 10) data.MethodStats = topNMethod(methodCounts, 10) // Daily stats for chart data.DailyStats = buildDailyStats(dailySuccess, dailyFailed, days) // Recent records (last 20) if len(records) > 20 { data.RecentRecords = records[:20] } else { data.RecentRecords = records } return data, nil } // TelemetryRecord includes Created timestamp type TelemetryRecord struct { TelemetryOut Created string `json:"created"` } func (p *PBClient) fetchRecords(ctx context.Context, filter string, limit int) ([]TelemetryRecord, error) { var allRecords []TelemetryRecord page := 1 perPage := 100 for { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/collections/%s/records?filter=%s&sort=-created&page=%d&perPage=%d", p.baseURL, p.targetColl, filter, page, perPage), nil, ) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+p.token) resp, err := p.http.Do(req) if err != nil { return nil, err } var result struct { Items []TelemetryRecord `json:"items"` TotalItems int `json:"totalItems"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { resp.Body.Close() return nil, err } resp.Body.Close() allRecords = append(allRecords, result.Items...) if len(allRecords) >= limit || len(allRecords) >= result.TotalItems { break } page++ } return allRecords, nil } func topN(m map[string]int, n int) []AppCount { result := make([]AppCount, 0, len(m)) for k, v := range m { result = append(result, AppCount{App: k, Count: v}) } // Simple bubble sort for small datasets 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 topNOs(m map[string]int, n int) []OsCount { result := make([]OsCount, 0, len(m)) for k, v := range m { result = append(result, OsCount{Os: k, Count: v}) } 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 topNMethod(m map[string]int, n int) []MethodCount { result := make([]MethodCount, 0, len(m)) for k, v := range m { result = append(result, MethodCount{Method: k, Count: v}) } 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 buildDailyStats(success, failed map[string]int, days int) []DailyStat { result := make([]DailyStat, 0, days) for i := days - 1; i >= 0; i-- { date := time.Now().AddDate(0, 0, -i).Format("2006-01-02") result = append(result, DailyStat{ Date: date, Success: success[date], Failed: failed[date], }) } return result } // DashboardHTML returns the embedded dashboard HTML func DashboardHTML() string { return `
| App | Status | OS | Type | Method | Exit Code | Error |
|---|---|---|---|---|---|---|
| Loading... | ||||||