repo-wrapped 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildComparisonSection = buildComparisonSection;
4
+ const date_fns_1 = require("date-fns");
5
+ function buildComparisonSection(comparison) {
6
+ const { range1, range2, changes, summary } = comparison;
7
+ return `
8
+ <div class="comparison-section">
9
+ <h2>📊 Period Comparison</h2>
10
+
11
+ <div class="comparison-header">
12
+ <div class="period-label period-1">
13
+ <span class="period-name">${range1.label}</span>
14
+ <span class="period-dates">${(0, date_fns_1.format)(range1.start, 'MMM d, yyyy')} - ${(0, date_fns_1.format)(range1.end, 'MMM d, yyyy')}</span>
15
+ </div>
16
+ <div class="vs-divider">VS</div>
17
+ <div class="period-label period-2">
18
+ <span class="period-name">${range2.label}</span>
19
+ <span class="period-dates">${(0, date_fns_1.format)(range2.start, 'MMM d, yyyy')} - ${(0, date_fns_1.format)(range2.end, 'MMM d, yyyy')}</span>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="comparison-summary">
24
+ <p>${summary}</p>
25
+ </div>
26
+
27
+ <div class="comparison-grid">
28
+ ${buildComparisonCard('Commits', range1.metrics.totalCommits, range2.metrics.totalCommits, changes.commits, '📝')}
29
+ ${buildComparisonCard('Velocity', range1.metrics.commitsPerWeek, range2.metrics.commitsPerWeek, changes.velocity, '🚀', '/week')}
30
+ ${buildComparisonCard('Quality', range1.metrics.qualityScore, range2.metrics.qualityScore, changes.quality, '✨', '/10')}
31
+ ${buildComparisonCard('Active Days', range1.metrics.activeDays, range2.metrics.activeDays, changes.activeDays, '📅')}
32
+ ${buildComparisonCard('Contributors', range1.metrics.totalAuthors, range2.metrics.totalAuthors, changes.authors, '👥')}
33
+ </div>
34
+
35
+ ${buildTypeDistributionComparison(range1, range2)}
36
+ </div>
37
+ `;
38
+ }
39
+ function buildComparisonCard(label, value1, value2, change, emoji, suffix = '') {
40
+ const trendClass = change.trend === 'up' ? 'positive' : change.trend === 'down' ? 'negative' : 'neutral';
41
+ const trendArrow = change.trend === 'up' ? '↑' : change.trend === 'down' ? '↓' : '→';
42
+ const sign = change.percentage >= 0 ? '+' : '';
43
+ return `
44
+ <div class="comparison-card">
45
+ <div class="card-header">
46
+ <span class="card-emoji">${emoji}</span>
47
+ <span class="card-label">${label}</span>
48
+ </div>
49
+ <div class="card-values">
50
+ <div class="value-box period-1">
51
+ <span class="value">${value1}${suffix}</span>
52
+ </div>
53
+ <div class="change-indicator ${trendClass}">
54
+ <span class="change-arrow">${trendArrow}</span>
55
+ <span class="change-percent">${sign}${change.percentage}%</span>
56
+ </div>
57
+ <div class="value-box period-2">
58
+ <span class="value">${value2}${suffix}</span>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ `;
63
+ }
64
+ function buildTypeDistributionComparison(range1, range2) {
65
+ const types = ['feat', 'fix', 'docs', 'refactor', 'test', 'chore', 'other'];
66
+ const typeLabels = {
67
+ feat: 'Features',
68
+ fix: 'Fixes',
69
+ docs: 'Docs',
70
+ refactor: 'Refactor',
71
+ test: 'Tests',
72
+ chore: 'Chores',
73
+ other: 'Other',
74
+ };
75
+ const typeColors = {
76
+ feat: '#69db7c',
77
+ fix: '#ff6b6b',
78
+ docs: '#4dabf7',
79
+ refactor: '#ffa94d',
80
+ test: '#da77f2',
81
+ chore: '#868e96',
82
+ other: '#495057',
83
+ };
84
+ const total1 = Object.values(range1.metrics.commitsByType).reduce((a, b) => a + b, 0) || 1;
85
+ const total2 = Object.values(range2.metrics.commitsByType).reduce((a, b) => a + b, 0) || 1;
86
+ const bars = types.map(type => {
87
+ const count1 = range1.metrics.commitsByType[type] || 0;
88
+ const count2 = range2.metrics.commitsByType[type] || 0;
89
+ const pct1 = Math.round((count1 / total1) * 100);
90
+ const pct2 = Math.round((count2 / total2) * 100);
91
+ return `
92
+ <div class="type-row">
93
+ <div class="type-label" style="color: ${typeColors[type]}">${typeLabels[type]}</div>
94
+ <div class="type-bars">
95
+ <div class="bar-container period-1">
96
+ <div class="bar" style="width: ${pct1}%; background: ${typeColors[type]}"></div>
97
+ <span class="bar-value">${count1} (${pct1}%)</span>
98
+ </div>
99
+ <div class="bar-container period-2">
100
+ <div class="bar" style="width: ${pct2}%; background: ${typeColors[type]}"></div>
101
+ <span class="bar-value">${count2} (${pct2}%)</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ `;
106
+ }).join('');
107
+ return `
108
+ <div class="type-distribution-comparison">
109
+ <h3>Commit Type Distribution</h3>
110
+ <div class="distribution-legend">
111
+ <span class="legend-item period-1">${range1.label}</span>
112
+ <span class="legend-item period-2">${range2.label}</span>
113
+ </div>
114
+ <div class="type-bars-container">
115
+ ${bars}
116
+ </div>
117
+ </div>
118
+ `;
119
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildEventsSection = buildEventsSection;
4
+ const date_fns_1 = require("date-fns");
5
+ const EVENT_ICONS = {
6
+ disruption: '⚠️',
7
+ attrition: '👋',
8
+ growth: '🌱',
9
+ release: '🚀',
10
+ incident: '🔥',
11
+ external: '📅',
12
+ custom: '📌'
13
+ };
14
+ const EVENT_COLORS = {
15
+ disruption: '#f59e0b',
16
+ attrition: '#ef4444',
17
+ growth: '#10b981',
18
+ release: '#3b82f6',
19
+ incident: '#dc2626',
20
+ external: '#6b7280',
21
+ custom: '#8b5cf6'
22
+ };
23
+ function buildEventsSection(events) {
24
+ if (!events || events.length === 0) {
25
+ return '<p class="no-data">No events annotated</p>';
26
+ }
27
+ const sortedEvents = [...events].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
28
+ return `
29
+ <div class="events-section">
30
+ <!-- Event Legend -->
31
+ <div class="events-legend">
32
+ ${Object.entries(EVENT_ICONS).map(([type, icon]) => `
33
+ <span class="legend-item">
34
+ <span class="event-icon">${icon}</span>
35
+ <span class="legend-label">${type}</span>
36
+ </span>
37
+ `).join('')}
38
+ </div>
39
+
40
+ <!-- Event Stats Summary -->
41
+ <div class="events-summary">
42
+ <div class="summary-stat">
43
+ <span class="stat-value">${events.length}</span>
44
+ <span class="stat-label">Total Events</span>
45
+ </div>
46
+ ${buildEventTypeCounts(events)}
47
+ </div>
48
+
49
+ <!-- Events Timeline -->
50
+ <div class="events-timeline">
51
+ ${sortedEvents.map(event => buildEventCard(event)).join('')}
52
+ </div>
53
+ </div>
54
+ `;
55
+ }
56
+ function buildEventTypeCounts(events) {
57
+ const counts = events.reduce((acc, e) => {
58
+ acc[e.type] = (acc[e.type] || 0) + 1;
59
+ return acc;
60
+ }, {});
61
+ const topTypes = Object.entries(counts)
62
+ .sort((a, b) => b[1] - a[1])
63
+ .slice(0, 3);
64
+ return topTypes.map(([type, count]) => `
65
+ <div class="summary-stat">
66
+ <span class="stat-value">${EVENT_ICONS[type] || '📌'} ${count}</span>
67
+ <span class="stat-label">${type}</span>
68
+ </div>
69
+ `).join('');
70
+ }
71
+ function buildEventCard(event) {
72
+ const icon = EVENT_ICONS[event.type] || '📌';
73
+ const color = EVENT_COLORS[event.type] || '#6b7280';
74
+ const dateStr = (0, date_fns_1.format)(new Date(event.date), 'MMM d, yyyy');
75
+ let correlationHtml = '';
76
+ if (event.correlation) {
77
+ const { velocityChange, qualityChange, activeAuthorsChange } = event.correlation.impact;
78
+ correlationHtml = `
79
+ <div class="event-correlation">
80
+ <div class="correlation-metrics">
81
+ <div class="correlation-metric ${velocityChange >= 0 ? 'positive' : 'negative'}">
82
+ <span class="metric-label">Velocity</span>
83
+ <span class="metric-value">${velocityChange >= 0 ? '+' : ''}${velocityChange.toFixed(0)}%</span>
84
+ </div>
85
+ <div class="correlation-metric ${qualityChange >= 0 ? 'positive' : 'negative'}">
86
+ <span class="metric-label">Quality</span>
87
+ <span class="metric-value">${qualityChange >= 0 ? '+' : ''}${qualityChange.toFixed(1)}</span>
88
+ </div>
89
+ <div class="correlation-metric ${activeAuthorsChange >= 0 ? 'positive' : 'negative'}">
90
+ <span class="metric-label">Contributors</span>
91
+ <span class="metric-value">${activeAuthorsChange >= 0 ? '+' : ''}${activeAuthorsChange}</span>
92
+ </div>
93
+ </div>
94
+ <div class="correlation-assessment">
95
+ <span class="assessment-icon">💡</span>
96
+ ${event.correlation.assessment}
97
+ </div>
98
+ </div>
99
+ `;
100
+ }
101
+ return `
102
+ <div class="event-card" style="--event-color: ${color}">
103
+ <div class="event-header">
104
+ <span class="event-icon">${icon}</span>
105
+ <span class="event-date">${dateStr}</span>
106
+ <span class="event-type-badge">${event.type}</span>
107
+ </div>
108
+ <div class="event-label">${event.label}</div>
109
+ ${event.description ? `<div class="event-description">${event.description}</div>` : ''}
110
+ ${correlationHtml}
111
+ </div>
112
+ `;
113
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildExecutiveSummarySection = buildExecutiveSummarySection;
4
+ const date_fns_1 = require("date-fns");
5
+ function buildExecutiveSummarySection(summary) {
6
+ return `
7
+ <div class="executive-summary-section">
8
+ <div class="executive-header">
9
+ <h2>📊 Executive Summary</h2>
10
+ <div class="summary-meta">
11
+ <span class="repo-name">${summary.repoName}</span>
12
+ <span class="period">${(0, date_fns_1.format)(summary.period.start, 'MMM d, yyyy')} - ${(0, date_fns_1.format)(summary.period.end, 'MMM d, yyyy')}</span>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="kpi-grid">
17
+ ${summary.kpis.map(kpi => buildKPICard(kpi)).join('')}
18
+ </div>
19
+
20
+ <div class="insights-risks-grid">
21
+ <div class="insights-column">
22
+ <h3>💡 Key Insights</h3>
23
+ <div class="insights-list">
24
+ ${summary.keyInsights.map(insight => `
25
+ <div class="insight-item ${insight.type}">
26
+ <span class="insight-icon">${insight.type === 'positive' ? '✅' : insight.type === 'negative' ? '⚠️' : 'ℹ️'}</span>
27
+ <span class="insight-text">${insight.insight}</span>
28
+ </div>
29
+ `).join('')}
30
+ </div>
31
+ </div>
32
+
33
+ <div class="risks-column">
34
+ <h3>🚨 Risk Assessment</h3>
35
+ <div class="risks-list">
36
+ ${summary.risks.length > 0 ? summary.risks.map(risk => `
37
+ <div class="risk-item ${risk.level}">
38
+ <span class="risk-badge">${risk.level.toUpperCase()}</span>
39
+ <div class="risk-content">
40
+ <span class="risk-category">${risk.category}</span>
41
+ <span class="risk-description">${risk.description}</span>
42
+ </div>
43
+ </div>
44
+ `).join('') : '<div class="no-risks">No significant risks identified</div>'}
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="recommendations-section">
50
+ <h3>📋 Recommendations</h3>
51
+ <ol class="recommendations-list">
52
+ ${summary.recommendations.map(rec => `<li>${rec}</li>`).join('')}
53
+ </ol>
54
+ </div>
55
+
56
+ <div class="summary-footer">
57
+ Generated: ${(0, date_fns_1.format)(summary.generatedAt, 'MMM d, yyyy HH:mm')}
58
+ </div>
59
+ </div>
60
+ `;
61
+ }
62
+ function buildKPICard(kpi) {
63
+ const statusColors = {
64
+ green: '#69db7c',
65
+ yellow: '#ffd43b',
66
+ red: '#ff6b6b',
67
+ };
68
+ const trendArrow = kpi.trend === 'up' ? '↑' : kpi.trend === 'down' ? '↓' : '→';
69
+ const trendClass = kpi.trend === 'up' ? 'positive' : kpi.trend === 'down' ? 'negative' : 'neutral';
70
+ return `
71
+ <div class="kpi-card" style="--status-color: ${statusColors[kpi.status]}">
72
+ <div class="kpi-status-indicator" title="${kpi.status}"></div>
73
+ <div class="kpi-content">
74
+ <div class="kpi-label">${kpi.name}</div>
75
+ <div class="kpi-value">${kpi.value}</div>
76
+ <div class="kpi-trend ${trendClass}">
77
+ <span class="trend-arrow">${trendArrow}</span>
78
+ <span class="trend-value">${kpi.trendValue}</span>
79
+ </div>
80
+ ${kpi.benchmark ? `<div class="kpi-benchmark">${kpi.benchmark}</div>` : ''}
81
+ </div>
82
+ </div>
83
+ `;
84
+ }
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildGapSection = buildGapSection;
4
+ const date_fns_1 = require("date-fns");
5
+ function buildGapSection(gapAnalysis) {
6
+ const statsHtml = buildGapStats(gapAnalysis);
7
+ const timelineHtml = buildGapTimeline(gapAnalysis);
8
+ const gapsListHtml = buildGapsList(gapAnalysis.gaps);
9
+ return `
10
+ <div class="gap-section">
11
+ <h2>🕳️ Gap & Blocker Analysis</h2>
12
+
13
+ ${statsHtml}
14
+
15
+ ${timelineHtml}
16
+
17
+ ${gapsListHtml}
18
+ </div>
19
+ `;
20
+ }
21
+ function buildGapStats(gapAnalysis) {
22
+ const riskColors = {
23
+ low: '#69db7c',
24
+ medium: '#ffd43b',
25
+ high: '#ff922b',
26
+ critical: '#ff6b6b',
27
+ };
28
+ const riskEmoji = {
29
+ low: '🟢',
30
+ medium: '🟡',
31
+ high: '🟠',
32
+ critical: '🔴',
33
+ };
34
+ return `
35
+ <div class="gap-stats">
36
+ <div class="gap-stat-card">
37
+ <div class="stat-icon">🕳️</div>
38
+ <div class="stat-info">
39
+ <div class="stat-label">Total Gaps</div>
40
+ <div class="stat-value">${gapAnalysis.gaps.length}</div>
41
+ <div class="stat-detail">${gapAnalysis.gapFrequency}/month avg</div>
42
+ </div>
43
+ </div>
44
+ <div class="gap-stat-card">
45
+ <div class="stat-icon">📅</div>
46
+ <div class="stat-info">
47
+ <div class="stat-label">Gap Days</div>
48
+ <div class="stat-value">${gapAnalysis.totalGapDays}</div>
49
+ <div class="stat-detail">${gapAnalysis.percentageOfPeriodInGaps}% of period</div>
50
+ </div>
51
+ </div>
52
+ <div class="gap-stat-card">
53
+ <div class="stat-icon">⏱️</div>
54
+ <div class="stat-info">
55
+ <div class="stat-label">Longest Gap</div>
56
+ <div class="stat-value">${gapAnalysis.longestGap?.durationDays || 0} days</div>
57
+ <div class="stat-detail">avg: ${gapAnalysis.averageGapLength} days</div>
58
+ </div>
59
+ </div>
60
+ <div class="gap-stat-card risk-card" style="--risk-color: ${riskColors[gapAnalysis.riskLevel]}">
61
+ <div class="stat-icon">${riskEmoji[gapAnalysis.riskLevel]}</div>
62
+ <div class="stat-info">
63
+ <div class="stat-label">Risk Level</div>
64
+ <div class="stat-value risk-${gapAnalysis.riskLevel}">${gapAnalysis.riskLevel.toUpperCase()}</div>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ ${gapAnalysis.riskFactors.length > 0 ? `
70
+ <div class="risk-factors">
71
+ <h4>Risk Factors</h4>
72
+ <ul>
73
+ ${gapAnalysis.riskFactors.map(f => `<li>${f}</li>`).join('')}
74
+ </ul>
75
+ </div>
76
+ ` : ''}
77
+ `;
78
+ }
79
+ function buildGapTimeline(gapAnalysis) {
80
+ if (gapAnalysis.gaps.length === 0) {
81
+ return `
82
+ <div class="gap-timeline-container">
83
+ <h3>Activity Timeline</h3>
84
+ <div class="no-gaps-message">
85
+ <span class="success-icon">✅</span>
86
+ <span>No significant activity gaps detected</span>
87
+ </div>
88
+ </div>
89
+ `;
90
+ }
91
+ // Find the full date range
92
+ const allDates = gapAnalysis.gaps.flatMap(g => [g.start, g.end]);
93
+ const minDate = new Date(Math.min(...allDates.map(d => d.getTime())));
94
+ const maxDate = new Date(Math.max(...allDates.map(d => d.getTime())));
95
+ const totalDays = Math.ceil((maxDate.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
96
+ if (totalDays <= 0) {
97
+ return '';
98
+ }
99
+ // Build gap segments
100
+ const segments = gapAnalysis.gaps.map(gap => {
101
+ const startOffset = Math.ceil((gap.start.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24));
102
+ const width = gap.durationDays;
103
+ const leftPercent = (startOffset / totalDays) * 100;
104
+ const widthPercent = (width / totalDays) * 100;
105
+ const typeClass = gap.possibleType === 'blocker' ? 'blocker' :
106
+ gap.possibleType === 'vacation' ? 'vacation' :
107
+ gap.possibleType === 'holiday' ? 'holiday' : 'unknown';
108
+ return `
109
+ <div class="gap-segment ${typeClass}"
110
+ style="left: ${leftPercent}%; width: ${widthPercent}%"
111
+ title="${gap.durationDays} days - ${gap.possibleType}">
112
+ </div>
113
+ `;
114
+ }).join('');
115
+ return `
116
+ <div class="gap-timeline-container">
117
+ <h3>Activity Timeline</h3>
118
+ <div class="gap-timeline">
119
+ <div class="timeline-track">
120
+ ${segments}
121
+ </div>
122
+ <div class="timeline-labels">
123
+ <span>${(0, date_fns_1.format)(minDate, 'MMM d, yyyy')}</span>
124
+ <span>${(0, date_fns_1.format)(maxDate, 'MMM d, yyyy')}</span>
125
+ </div>
126
+ </div>
127
+ <div class="timeline-legend">
128
+ <span class="legend-item"><span class="legend-color blocker"></span> Potential Blocker</span>
129
+ <span class="legend-item"><span class="legend-color vacation"></span> Vacation</span>
130
+ <span class="legend-item"><span class="legend-color holiday"></span> Holiday</span>
131
+ <span class="legend-item"><span class="legend-color unknown"></span> Unknown</span>
132
+ </div>
133
+ </div>
134
+ `;
135
+ }
136
+ function buildGapsList(gaps) {
137
+ if (gaps.length === 0) {
138
+ return '';
139
+ }
140
+ // Sort by duration descending, take top 5
141
+ const sortedGaps = [...gaps].sort((a, b) => b.durationDays - a.durationDays).slice(0, 5);
142
+ const gapItems = sortedGaps.map(gap => {
143
+ const typeEmoji = {
144
+ blocker: '🚧',
145
+ vacation: '🏖️',
146
+ holiday: '🎄',
147
+ weekend: '📅',
148
+ unknown: '❓',
149
+ };
150
+ return `
151
+ <div class="gap-item">
152
+ <div class="gap-item-header">
153
+ <span class="gap-type-icon">${typeEmoji[gap.possibleType]}</span>
154
+ <span class="gap-duration">${gap.durationDays} days</span>
155
+ <span class="gap-type-badge ${gap.possibleType}">${gap.possibleType}</span>
156
+ </div>
157
+ <div class="gap-dates">
158
+ ${(0, date_fns_1.format)(gap.start, 'MMM d')} - ${(0, date_fns_1.format)(gap.end, 'MMM d, yyyy')}
159
+ </div>
160
+ <div class="gap-context">
161
+ ${gap.commitBefore ? `
162
+ <div class="context-item before">
163
+ <span class="context-label">Before:</span>
164
+ <span class="context-message">"${truncate(gap.commitBefore.message, 50)}"</span>
165
+ </div>
166
+ ` : ''}
167
+ ${gap.commitAfter ? `
168
+ <div class="context-item after">
169
+ <span class="context-label">After:</span>
170
+ <span class="context-message">"${truncate(gap.commitAfter.message, 50)}"</span>
171
+ </div>
172
+ ` : ''}
173
+ </div>
174
+ </div>
175
+ `;
176
+ }).join('');
177
+ return `
178
+ <div class="gaps-list-section">
179
+ <h3>Top Gaps by Duration</h3>
180
+ <div class="gaps-list">
181
+ ${gapItems}
182
+ </div>
183
+ </div>
184
+ `;
185
+ }
186
+ function truncate(str, maxLength) {
187
+ if (str.length <= maxLength)
188
+ return str;
189
+ return str.substring(0, maxLength - 3) + '...';
190
+ }
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTeamSection = buildTeamSection;
4
+ /**
5
+ * Builds the HTML section for team analysis visualization
6
+ */
7
+ function buildTeamSection(analysis) {
8
+ if (!analysis || analysis.members.length === 0) {
9
+ return '<p class="no-data">No team data available</p>';
10
+ }
11
+ const { teamMetrics, memberBreakdown, loadDistribution } = analysis;
12
+ // Build summary cards
13
+ const summaryHtml = buildSummaryCards(teamMetrics, analysis.members.length);
14
+ // Build load distribution indicator
15
+ const loadDistHtml = buildLoadDistribution(loadDistribution);
16
+ // Build member breakdown
17
+ const membersHtml = buildMemberList(memberBreakdown);
18
+ // Build insights
19
+ const insightsHtml = buildInsights(loadDistribution.insights);
20
+ return `
21
+ <div class="team-section">
22
+ ${summaryHtml}
23
+ ${loadDistHtml}
24
+ ${membersHtml}
25
+ ${insightsHtml}
26
+ </div>
27
+ `;
28
+ }
29
+ function buildSummaryCards(metrics, memberCount) {
30
+ return `
31
+ <div class="team-summary">
32
+ <div class="team-stat-card">
33
+ <span class="stat-value">${metrics.totalCommits.toLocaleString()}</span>
34
+ <span class="stat-label">Total Commits</span>
35
+ </div>
36
+ <div class="team-stat-card">
37
+ <span class="stat-value">${memberCount}</span>
38
+ <span class="stat-label">Team Members</span>
39
+ </div>
40
+ <div class="team-stat-card">
41
+ <span class="stat-value">${metrics.commitsPerMember.toFixed(1)}</span>
42
+ <span class="stat-label">Commits/Member</span>
43
+ </div>
44
+ <div class="team-stat-card">
45
+ <span class="stat-value">${metrics.activeDays}</span>
46
+ <span class="stat-label">Active Days</span>
47
+ </div>
48
+ <div class="team-stat-card">
49
+ <span class="stat-value">${metrics.qualityScore.toFixed(1)}</span>
50
+ <span class="stat-label">Avg Quality</span>
51
+ </div>
52
+ </div>
53
+ `;
54
+ }
55
+ function buildLoadDistribution(loadDist) {
56
+ const gini = loadDist.giniCoefficient;
57
+ const percentage = Math.round(gini * 100);
58
+ // Color based on assessment
59
+ let colorClass = 'balanced';
60
+ let statusIcon = '✅';
61
+ if (loadDist.assessment === 'moderate-imbalance') {
62
+ colorClass = 'moderate';
63
+ statusIcon = '⚠️';
64
+ }
65
+ else if (loadDist.assessment === 'high-imbalance') {
66
+ colorClass = 'imbalanced';
67
+ statusIcon = '🚨';
68
+ }
69
+ const assessmentLabel = loadDist.assessment.replace('-', ' ').replace(/\b\w/g, c => c.toUpperCase());
70
+ return `
71
+ <div class="load-distribution">
72
+ <h3>Load Distribution</h3>
73
+ <div class="gini-container">
74
+ <div class="gini-bar-bg">
75
+ <div class="gini-bar ${colorClass}" style="width: ${percentage}%"></div>
76
+ </div>
77
+ <div class="gini-info">
78
+ <span class="gini-value">Gini: ${gini.toFixed(2)}</span>
79
+ <span class="gini-assessment ${colorClass}">${statusIcon} ${assessmentLabel}</span>
80
+ </div>
81
+ </div>
82
+ <p class="gini-explanation">
83
+ ${gini < 0.3 ? 'Work is evenly distributed across the team.' :
84
+ gini < 0.5 ? 'Some team members are contributing more than others.' :
85
+ 'Work is concentrated among a few contributors. Consider redistributing.'}
86
+ </p>
87
+ </div>
88
+ `;
89
+ }
90
+ function buildMemberList(members) {
91
+ // Sort by commits descending
92
+ const sorted = [...members].sort((a, b) => b.commits - a.commits);
93
+ const maxCommits = sorted[0]?.commits || 1;
94
+ const membersHtml = sorted.map((member, index) => {
95
+ const barWidth = (member.commits / maxCommits) * 100;
96
+ const rankBadge = index === 0 ? '👑' : index < 3 ? '⭐' : '';
97
+ return `
98
+ <div class="member-card">
99
+ <div class="member-rank">${index + 1}</div>
100
+ <div class="member-info">
101
+ <div class="member-name">${rankBadge} ${escapeHtml(member.author)}</div>
102
+ <div class="member-areas">${member.topAreas.slice(0, 3).map(a => `<span class="area-tag">${escapeHtml(a)}</span>`).join('')}</div>
103
+ </div>
104
+ <div class="member-bar-container">
105
+ <div class="member-bar" style="width: ${barWidth}%"></div>
106
+ </div>
107
+ <div class="member-stats">
108
+ <span class="member-commits">${member.commits}</span>
109
+ <span class="member-percentage">(${member.percentage.toFixed(1)}%)</span>
110
+ </div>
111
+ <div class="member-quality" title="Commit quality score">
112
+ <span class="quality-badge">${member.qualityScore.toFixed(1)}</span>
113
+ </div>
114
+ </div>
115
+ `;
116
+ }).join('');
117
+ return `
118
+ <div class="member-breakdown">
119
+ <h3>Team Members</h3>
120
+ <div class="member-list">
121
+ ${membersHtml}
122
+ </div>
123
+ </div>
124
+ `;
125
+ }
126
+ function buildInsights(insights) {
127
+ if (!insights || insights.length === 0) {
128
+ return '';
129
+ }
130
+ return `
131
+ <div class="team-insights">
132
+ <h3>Insights</h3>
133
+ <ul class="insights-list">
134
+ ${insights.map(insight => `<li class="insight-item">💡 ${escapeHtml(insight)}</li>`).join('')}
135
+ </ul>
136
+ </div>
137
+ `;
138
+ }
139
+ function escapeHtml(str) {
140
+ return str
141
+ .replace(/&/g, '&amp;')
142
+ .replace(/</g, '&lt;')
143
+ .replace(/>/g, '&gt;')
144
+ .replace(/"/g, '&quot;')
145
+ .replace(/'/g, '&#39;');
146
+ }