@@ -16,15 +16,17 @@ import (
1616
1717// repoStatus holds aggregated status for a single repository
1818type repoStatus struct {
19- FullName string
20- Org string
21- RepoName string
22- Stars int
23- Forks int
24- OpenIssues int
25- OpenPRs int
26- TotalAlerts int
27- LastSyncAt * time.Time
19+ FullName string
20+ Org string
21+ RepoName string
22+ Stars int
23+ Forks int
24+ OpenIssues int
25+ OpenPRs int
26+ TotalAlerts int
27+ LastSyncAt * time.Time
28+ LastPushedAt * time.Time
29+ CoveragePercent * float64
2830}
2931
3032// newAnalyticsStatusCmd creates the analytics status command
@@ -106,6 +108,16 @@ func displayAllRepositories(ctx context.Context, analyticsRepo db.AnalyticsRepo)
106108 status .Forks = snapshot .Forks
107109 status .OpenIssues = snapshot .OpenIssues
108110 status .OpenPRs = snapshot .OpenPRs
111+ status .LastPushedAt = snapshot .PushedAt
112+ }
113+
114+ // Get latest CI snapshot for coverage
115+ ciSnap , ciErr := analyticsRepo .GetLatestCISnapshot (ctx , repo .ID )
116+ if ciErr != nil && ! errors .Is (ciErr , gorm .ErrRecordNotFound ) {
117+ return fmt .Errorf ("failed to get CI snapshot for %s: %w" , repo .FullName (), ciErr )
118+ }
119+ if ciSnap != nil {
120+ status .CoveragePercent = ciSnap .CoveragePercent
109121 }
110122
111123 // Get alert counts
@@ -143,34 +155,63 @@ func displayAllRepositories(ctx context.Context, analyticsRepo db.AnalyticsRepo)
143155 for _ , org := range orgOrder {
144156 group := orgGroups [org ]
145157 output .Plain ("" )
158+ output .Plain ("" )
146159 repoWord := "repositories"
147160 if len (group ) == 1 {
148161 repoWord = "repository"
149162 }
150163 output .Info (fmt .Sprintf ("%s (%d %s)" , org , len (group ), repoWord ))
151- output .Plain (strings .Repeat ("─" , 80 ))
152- output .Plain (fmt .Sprintf ("%-35s %6s %6s %7s %5s %7s %s" ,
153- "Repository" , "Stars" , "Forks" , "Issues" , "PRs" , "Alerts" , "Last Sync" ))
154- output .Plain (strings .Repeat ("─" , 80 ))
164+ output .Plain (strings .Repeat ("─" , 115 ))
165+ output .Plain (fmt .Sprintf ("%-35s %6s %6s %7s %5s %7s %8s %-16s %s" ,
166+ "Repository" , "Stars" , "Forks" , "Issues" , "PRs" , "Alerts" , "Coverage" , "Last Push" , " Last Sync" ))
167+ output .Plain (strings .Repeat ("─" , 115 ))
155168
156169 for _ , status := range group {
157170 lastSync := "Never"
158171 if status .LastSyncAt != nil {
159172 lastSync = formatTimeAgo (status .LastSyncAt )
160173 }
161174
162- output .Plain (fmt .Sprintf ("%-35s %6d %6d %7d %5d %7d %s" ,
175+ output .Plain (fmt .Sprintf ("%-35s %6d %6d %7d %5d %7d %8s %-16s %s" ,
163176 truncate (status .RepoName , 35 ),
164177 status .Stars ,
165178 status .Forks ,
166179 status .OpenIssues ,
167180 status .OpenPRs ,
168181 status .TotalAlerts ,
182+ formatCoverage (status .CoveragePercent ),
183+ formatTimeAgoShort (status .LastPushedAt ),
169184 lastSync ))
170185 }
186+
187+ // Summary row
188+ var sumStars , sumForks , sumIssues , sumPRs , sumAlerts int
189+ var sumCoverage float64
190+ var covCount int
191+ for _ , s := range group {
192+ sumStars += s .Stars
193+ sumForks += s .Forks
194+ sumIssues += s .OpenIssues
195+ sumPRs += s .OpenPRs
196+ sumAlerts += s .TotalAlerts
197+ if s .CoveragePercent != nil {
198+ sumCoverage += * s .CoveragePercent
199+ covCount ++
200+ }
201+ }
202+ avgCoverage := "-"
203+ if covCount > 0 {
204+ avg := sumCoverage / float64 (covCount )
205+ avgCoverage = fmt .Sprintf ("%.1f%%" , avg )
206+ }
207+ output .Plain (strings .Repeat ("─" , 115 ))
208+ output .Plain (fmt .Sprintf ("%-35s %6d %6d %7d %5d %7d %8s %-16s %s" ,
209+ "Total" , sumStars , sumForks , sumIssues , sumPRs , sumAlerts , avgCoverage , "" , "" ))
171210 }
172211
173212 // Display summary
213+ output .Plain (strings .Repeat ("─" , 115 ))
214+
174215 output .Plain ("" )
175216 if reposWithAlerts > 0 {
176217 output .Info (fmt .Sprintf ("Total: %d repositories, %d with %d open alerts" ,
@@ -189,7 +230,7 @@ func displayAllRepositories(ctx context.Context, analyticsRepo db.AnalyticsRepo)
189230 output .Info (fmt .Sprintf ("Last sync: %s (%s)" , formatTimeAgo (lastSync .CompletedAt ), duration ))
190231 }
191232
192- output .Plain (strings .Repeat ("─" , 80 ))
233+ output .Plain (strings .Repeat ("─" , 115 ))
193234 output .Plain ("" )
194235
195236 return nil
@@ -212,6 +253,12 @@ func displaySingleRepository(ctx context.Context, analyticsRepo db.AnalyticsRepo
212253 return fmt .Errorf ("failed to get snapshot: %w" , err )
213254 }
214255
256+ // Get latest CI snapshot for coverage
257+ ciSnap , err := analyticsRepo .GetLatestCISnapshot (ctx , repo .ID )
258+ if err != nil && ! errors .Is (err , gorm .ErrRecordNotFound ) {
259+ return fmt .Errorf ("failed to get CI snapshot: %w" , err )
260+ }
261+
215262 // Get alert counts by type
216263 alertsByType , err := analyticsRepo .GetAlertCountsByType (ctx , repo .ID )
217264 if err != nil && ! errors .Is (err , gorm .ErrRecordNotFound ) {
@@ -268,6 +315,12 @@ func displaySingleRepository(ctx context.Context, analyticsRepo db.AnalyticsRepo
268315 if snapshot .BranchCount > 0 {
269316 output .Plain (fmt .Sprintf (" Branches: %d" , snapshot .BranchCount ))
270317 }
318+ if ciSnap != nil && ciSnap .CoveragePercent != nil {
319+ output .Plain (fmt .Sprintf (" Coverage: %s" , formatCoverage (ciSnap .CoveragePercent )))
320+ }
321+ if snapshot .PushedAt != nil {
322+ output .Plain (fmt .Sprintf (" Last Push: %s" , formatTimeAgo (snapshot .PushedAt )))
323+ }
271324 } else {
272325 output .Warn ("No metrics available yet. Run 'analytics sync' to collect data." )
273326 }
@@ -381,6 +434,22 @@ func formatTimeAgo(t *time.Time) string {
381434 }
382435}
383436
437+ // formatTimeAgoShort is like formatTimeAgo but returns "never" instead of "unknown" for nil.
438+ func formatTimeAgoShort (t * time.Time ) string {
439+ if t == nil {
440+ return "never"
441+ }
442+ return formatTimeAgo (t )
443+ }
444+
445+ // formatCoverage formats a nullable coverage percentage for display.
446+ func formatCoverage (p * float64 ) string {
447+ if p == nil {
448+ return "-"
449+ }
450+ return fmt .Sprintf ("%.1f%%" , * p )
451+ }
452+
384453// formatDuration converts milliseconds to a human-readable duration format
385454func formatDuration (ms int64 ) string {
386455 if ms == 0 {
0 commit comments