@@ -96,6 +96,42 @@ function getWorkspaceActiveHours(workspace: CoderWorkspace): number {
9696 return 0 ;
9797}
9898
99+ /**
100+ * Calculate total active hours for a group of workspaces, ensuring logical consistency
101+ *
102+ * Active hours from Insights API are per-user across all time (including deleted workspaces).
103+ * To ensure active hours ≤ total hours, we cap each user's active hours at their total workspace hours.
104+ *
105+ * @param workspaces - Array of workspace metrics to aggregate
106+ * @returns Total active hours across all unique users, capped at their workspace totals
107+ */
108+ function calculateTotalActiveHours ( workspaces : WorkspaceMetrics [ ] ) : number {
109+ // Track active hours and total hours per unique user
110+ const userActiveHours = new Map < string , number > ( ) ;
111+ const userTotalHours = new Map < string , number > ( ) ;
112+
113+ workspaces . forEach ( ( workspace ) => {
114+ const user = workspace . owner_github_handle ;
115+
116+ // Active hours are per-user (same across all their workspaces), so take max
117+ const existingActive = userActiveHours . get ( user ) || 0 ;
118+ userActiveHours . set ( user , Math . max ( existingActive , workspace . active_hours ) ) ;
119+
120+ // Total hours are per-workspace, so sum them up
121+ const existingTotal = userTotalHours . get ( user ) || 0 ;
122+ userTotalHours . set ( user , existingTotal + workspace . workspace_hours ) ;
123+ } ) ;
124+
125+ // Cap each user's active hours at their total hours (handles deleted workspaces)
126+ let totalActive = 0 ;
127+ userActiveHours . forEach ( ( activeHours , user ) => {
128+ const totalHours = userTotalHours . get ( user ) || 0 ;
129+ totalActive += Math . min ( activeHours , totalHours ) ;
130+ } ) ;
131+
132+ return totalActive ;
133+ }
134+
99135/**
100136 * Classify activity status based on days since last active
101137 */
@@ -278,8 +314,8 @@ export function aggregateByTeam(workspaces: WorkspaceMetrics[]): TeamMetrics[] {
278314 // Total workspace hours (sum of all workspace lifetime hours)
279315 const totalWorkspaceHours = teamWorkspaces . reduce ( ( sum , w ) => sum + w . workspace_hours , 0 ) ;
280316
281- // Total active hours (sum of actual interaction hours from Insights API )
282- const totalActiveHours = teamWorkspaces . reduce ( ( sum , w ) => sum + w . active_hours , 0 ) ;
317+ // Total active hours (aggregated correctly per-user, capped at their workspace totals )
318+ const totalActiveHours = calculateTotalActiveHours ( teamWorkspaces ) ;
283319
284320 // Average workspace hours
285321 const avgWorkspaceHours =
@@ -432,8 +468,8 @@ export function calculateTemplateMetrics(
432468 // Total workspace hours (sum of all workspace lifetime hours)
433469 const totalWorkspaceHours = templateWorkspaces . reduce ( ( sum , w ) => sum + w . workspace_hours , 0 ) ;
434470
435- // Total active hours (sum of actual interaction hours from Insights API )
436- const totalActiveHours = templateWorkspaces . reduce ( ( sum , w ) => sum + w . active_hours , 0 ) ;
471+ // Total active hours (aggregated correctly per-user, capped at their workspace totals )
472+ const totalActiveHours = calculateTotalActiveHours ( templateWorkspaces ) ;
437473
438474 // Average workspace hours
439475 const avgWorkspaceHours =
0 commit comments