metripy 0.2.7__py3-none-any.whl → 0.3.6__py3-none-any.whl

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.

Potentially problematic release.


This version of metripy might be problematic. Click here for more details.

Files changed (67) hide show
  1. metripy/Application/Analyzer.py +23 -3
  2. metripy/Application/Application.py +16 -2
  3. metripy/Application/Config/Config.py +34 -0
  4. metripy/Application/Config/File/ConfigFileReaderFactory.py +6 -5
  5. metripy/Application/Config/File/ConfigFileReaderInterface.py +70 -3
  6. metripy/Application/Config/File/JsonConfigFileReader.py +5 -70
  7. metripy/Application/Config/File/YamlConfigFileReader.py +17 -0
  8. metripy/Application/Config/Parser.py +24 -11
  9. metripy/Application/Config/ProjectConfig.py +64 -0
  10. metripy/Application/Info.py +61 -0
  11. metripy/Dependency/Dependency.py +17 -1
  12. metripy/Dependency/Pip/Pip.py +21 -31
  13. metripy/Dependency/Pip/PyPi.py +1 -0
  14. metripy/Git/GitAnalyzer.py +0 -3
  15. metripy/Import/Json/JsonImporter.py +17 -0
  16. metripy/LangAnalyzer/AbstractLangAnalyzer.py +4 -3
  17. metripy/LangAnalyzer/Php/PhpAnalyzer.py +2 -1
  18. metripy/LangAnalyzer/Python/PythonAnalyzer.py +31 -9
  19. metripy/LangAnalyzer/Python/PythonHalSteadAnalyzer.py +55 -0
  20. metripy/LangAnalyzer/Typescript/TypescriptAnalyzer.py +12 -9
  21. metripy/LangAnalyzer/Typescript/TypescriptAstParser.py +1 -1
  22. metripy/Metric/Code/AggregatedMetrics.py +12 -5
  23. metripy/Metric/Code/FileMetrics.py +32 -1
  24. metripy/Metric/Code/ModuleMetrics.py +5 -5
  25. metripy/Metric/Code/SegmentedMetrics.py +72 -36
  26. metripy/Metric/Code/Segmentor.py +44 -0
  27. metripy/Metric/FileTree/FileTreeParser.py +0 -4
  28. metripy/Metric/Git/GitMetrics.py +1 -1
  29. metripy/Metric/ProjectMetrics.py +29 -0
  30. metripy/Metric/Trend/AggregatedTrendMetric.py +101 -0
  31. metripy/Metric/Trend/ClassTrendMetric.py +20 -0
  32. metripy/Metric/Trend/FileTrendMetric.py +46 -0
  33. metripy/Metric/Trend/FunctionTrendMetric.py +28 -0
  34. metripy/Metric/Trend/SegmentedTrendMetric.py +29 -0
  35. metripy/Report/Html/DependencyPageRenderer.py +21 -0
  36. metripy/Report/Html/FilesPageRenderer.py +28 -0
  37. metripy/Report/Html/GitAnalysisPageRenderer.py +55 -0
  38. metripy/Report/Html/IndexPageRenderer.py +47 -0
  39. metripy/Report/Html/PageRenderer.py +43 -0
  40. metripy/Report/Html/PageRendererFactory.py +37 -0
  41. metripy/Report/Html/Reporter.py +78 -137
  42. metripy/Report/Html/TopOffendersPageRenderer.py +84 -0
  43. metripy/Report/Html/TrendsPageRenderer.py +137 -0
  44. metripy/Report/Json/GitJsonReporter.py +3 -0
  45. metripy/Report/Json/JsonReporter.py +6 -2
  46. metripy/Report/ReporterFactory.py +6 -3
  47. metripy/Tree/ClassNode.py +21 -0
  48. metripy/Tree/FunctionNode.py +66 -1
  49. metripy/Trend/TrendAnalyzer.py +150 -0
  50. metripy/templates/html_report/css/styles.css +1386 -0
  51. metripy/templates/html_report/dependencies.html +411 -0
  52. metripy/templates/html_report/files.html +1080 -0
  53. metripy/templates/html_report/git_analysis.html +325 -0
  54. metripy/templates/html_report/images/logo.svg +31 -0
  55. metripy/templates/html_report/index.html +374 -0
  56. metripy/templates/html_report/js/charts.js +313 -0
  57. metripy/templates/html_report/js/dashboard.js +546 -0
  58. metripy/templates/html_report/js/git_analysis.js +383 -0
  59. metripy/templates/html_report/top_offenders.html +267 -0
  60. metripy/templates/html_report/trends.html +468 -0
  61. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/METADATA +27 -9
  62. metripy-0.3.6.dist-info/RECORD +96 -0
  63. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/licenses/LICENSE +1 -1
  64. metripy-0.2.7.dist-info/RECORD +0 -66
  65. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/WHEEL +0 -0
  66. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/entry_points.txt +0 -0
  67. {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Git Analysis JavaScript
3
+ * Handles charts and interactivity for the Git Analysis page
4
+ */
5
+
6
+ class GitAnalysis {
7
+ constructor() {
8
+ this.data = {};
9
+ this.init();
10
+ }
11
+
12
+ init() {
13
+ this.loadData();
14
+ this.setupCharts();
15
+ this.setupInteractivity();
16
+ }
17
+
18
+ loadData() {
19
+ try {
20
+ // Load git statistics from embedded data
21
+ if (window.GIT_STATS_DATA) {
22
+ this.data.gitStats = this.formatGitCommitsData(window.GIT_STATS_DATA);
23
+ }
24
+
25
+ // Load git churn data from embedded data
26
+ if (window.GIT_CHURN_DATA) {
27
+ this.data.gitChurn = window.GIT_CHURN_DATA;
28
+ }
29
+
30
+ // Load comprehensive git analysis data
31
+ if (window.GIT_ANALYSIS_DATA) {
32
+ this.data.analysis = window.GIT_ANALYSIS_DATA;
33
+ this.populateAnalysisData();
34
+ }
35
+
36
+ console.log('Git analysis data loaded successfully');
37
+ } catch (error) {
38
+ console.error('Error loading git analysis data:', error);
39
+ }
40
+ }
41
+
42
+ formatGitCommitsData(gitData) {
43
+ // Convert git statistics data to chart format
44
+ if (gitData && typeof gitData === 'object' && !Array.isArray(gitData)) {
45
+ return Object.entries(gitData).map(([date, count]) => ({
46
+ month: this.formatMonthName(date),
47
+ commits: count,
48
+ date: new Date(date + '-01').toISOString()
49
+ })).sort((a, b) => new Date(a.date) - new Date(b.date));
50
+ }
51
+ return [];
52
+ }
53
+
54
+ formatMonthName(monthString) {
55
+ try {
56
+ let date;
57
+ if (monthString.includes('-')) {
58
+ date = new Date(monthString + (monthString.split('-').length === 2 ? '-01' : ''));
59
+ } else {
60
+ date = new Date(monthString);
61
+ }
62
+
63
+ return date.toLocaleDateString('en-US', {
64
+ month: 'short',
65
+ year: 'numeric'
66
+ });
67
+ } catch (error) {
68
+ return monthString; // Return original if parsing fails
69
+ }
70
+ }
71
+
72
+ setupCharts() {
73
+ this.setupCommitChart();
74
+ this.setupChurnChart();
75
+ }
76
+
77
+ setupCommitChart() {
78
+ const canvas = document.getElementById('gitCommitsChart');
79
+ if (!canvas || !this.data.gitStats || this.data.gitStats.length === 0) {
80
+ return;
81
+ }
82
+
83
+ const ctx = canvas.getContext('2d');
84
+
85
+ new Chart(ctx, {
86
+ type: 'line',
87
+ data: {
88
+ labels: this.data.gitStats.map(item => item.month),
89
+ datasets: [{
90
+ label: 'Commits',
91
+ data: this.data.gitStats.map(item => item.commits),
92
+ borderColor: '#3b82f6',
93
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
94
+ borderWidth: 2,
95
+ fill: true,
96
+ tension: 0.4,
97
+ pointBackgroundColor: '#3b82f6',
98
+ pointBorderColor: '#ffffff',
99
+ pointBorderWidth: 2,
100
+ pointRadius: 4,
101
+ pointHoverRadius: 6
102
+ }]
103
+ },
104
+ options: {
105
+ responsive: true,
106
+ maintainAspectRatio: false,
107
+ scales: {
108
+ y: {
109
+ beginAtZero: true,
110
+ grid: {
111
+ color: 'rgba(0, 0, 0, 0.05)'
112
+ },
113
+ ticks: {
114
+ color: '#64748b'
115
+ }
116
+ },
117
+ x: {
118
+ grid: {
119
+ color: 'rgba(0, 0, 0, 0.05)'
120
+ },
121
+ ticks: {
122
+ color: '#64748b'
123
+ }
124
+ }
125
+ },
126
+ plugins: {
127
+ legend: {
128
+ display: false
129
+ },
130
+ tooltip: {
131
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
132
+ titleColor: '#ffffff',
133
+ bodyColor: '#ffffff',
134
+ borderColor: '#3b82f6',
135
+ borderWidth: 1,
136
+ cornerRadius: 8,
137
+ displayColors: false,
138
+ callbacks: {
139
+ title: (context) => `${context[0].label}`,
140
+ label: (context) => `${context.parsed.y} commits`
141
+ }
142
+ }
143
+ },
144
+ interaction: {
145
+ intersect: false,
146
+ mode: 'index'
147
+ }
148
+ }
149
+ });
150
+ }
151
+
152
+ setupChurnChart() {
153
+ const canvas = document.getElementById('gitChurnChart');
154
+ if (!canvas || !this.data.gitChurn) {
155
+ return;
156
+ }
157
+
158
+ const ctx = canvas.getContext('2d');
159
+
160
+ // Sample churn data if not provided
161
+ const churnData = this.data.gitChurn || this.generateSampleChurnData();
162
+
163
+ const labels = Object.keys(churnData);
164
+ const added = labels.map(label => churnData[label].added);
165
+ const removed = labels.map(label => -churnData[label].removed);
166
+ new Chart(ctx, {
167
+ type: 'bar',
168
+ data: {
169
+ labels: labels,
170
+ datasets: [
171
+ {
172
+ label: 'Lines Removed',
173
+ data: removed, // Goes downward
174
+ backgroundColor: 'rgba(255, 99, 132, 0.6)'
175
+ },
176
+ {
177
+ label: 'Lines Added',
178
+ data: added, // Goes upward
179
+ backgroundColor: 'rgba(75, 192, 192, 0.6)'
180
+ }
181
+ ]
182
+ },
183
+ options: {
184
+ responsive: true,
185
+ plugins: {
186
+ tooltip: {
187
+ callbacks: {
188
+ label: function(context) {
189
+ return `${context.dataset.label}: ${Math.abs(context.raw)}`;
190
+ }
191
+ }
192
+ }
193
+ },
194
+ scales: {
195
+ y: {
196
+ stacked: true,
197
+ ticks: {
198
+ callback: function(value) {
199
+ return Math.abs(value);
200
+ }
201
+ },
202
+ },
203
+ x: {
204
+ stacked: true
205
+ }
206
+
207
+ }
208
+ }
209
+ });
210
+ }
211
+
212
+ generateSampleChurnData() {
213
+ // Generate sample data if churn data is not provided
214
+ const months = [];
215
+ const added = [];
216
+ const removed = [];
217
+
218
+ // Get last 6 months
219
+ for (let i = 5; i >= 0; i--) {
220
+ const date = new Date();
221
+ date.setMonth(date.getMonth() - i);
222
+ months.push(date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }));
223
+
224
+ // Generate random but realistic data
225
+ added.push(Math.floor(Math.random() * 2000) + 500);
226
+ removed.push(Math.floor(Math.random() * 1500) + 200);
227
+ }
228
+
229
+ return {
230
+ labels: months,
231
+ added: added,
232
+ removed: removed
233
+ };
234
+ }
235
+
236
+ populateAnalysisData() {
237
+ if (!this.data.analysis) return;
238
+
239
+ try {
240
+ this.populateActiveDays();
241
+ this.populateCommitQuality();
242
+ this.populateRecommendations();
243
+
244
+ // Setup table interactivity after populating tables
245
+ this.setupTableInteractivity();
246
+ } catch (error) {
247
+ console.error('Error populating analysis data:', error);
248
+ }
249
+ }
250
+
251
+ populateActiveDays() {
252
+ const container = document.getElementById('active-days-content');
253
+ if (!container || !this.data.analysis.active_days) return;
254
+
255
+ container.innerHTML = '';
256
+
257
+ this.data.analysis.active_days.forEach(day => {
258
+ const p = document.createElement('p');
259
+ const note = day.is_deploy_day ? ` (${day.note})` : '';
260
+ p.innerHTML = `<strong>${day.day_name}:</strong> ${day.percentage}% of commits${note}`;
261
+ container.appendChild(p);
262
+ });
263
+ }
264
+
265
+ populateCommitQuality() {
266
+ const container = document.getElementById('commit-quality-content');
267
+ if (!container || !this.data.analysis) return;
268
+
269
+ container.innerHTML = `
270
+ <p><strong>Small commits:</strong> ${this.data.analysis.small_commits_percentage}% (&lt;50 lines)</p>
271
+ <p><strong>Message quality:</strong> ${this.data.analysis.good_message_percentage}% have good descriptions</p>
272
+ <p><strong>Fix commits:</strong> ${this.data.analysis.fix_commits_percentage}% (indicates stability)</p>
273
+ `;
274
+ }
275
+
276
+ populateRecommendations() {
277
+ const container = document.getElementById('recommendations-content');
278
+ if (!container || !this.data.analysis.recommendations) return;
279
+
280
+ container.innerHTML = '';
281
+
282
+ this.data.analysis.recommendations.forEach(rec => {
283
+ const p = document.createElement('p');
284
+ p.textContent = `• ${rec.recommendation}`;
285
+ container.appendChild(p);
286
+ });
287
+ }
288
+
289
+ setupInteractivity() {
290
+ // Setup navigation highlight
291
+ this.setupNavigationHighlight();
292
+ // Table interactivity is set up after populating tables in populateAnalysisData()
293
+ }
294
+
295
+ setupTableInteractivity() {
296
+ const tables = document.querySelectorAll('.hotspot-table tbody tr, .contributor-table tbody tr');
297
+ tables.forEach(row => {
298
+ row.style.cursor = 'pointer';
299
+ row.addEventListener('click', function() {
300
+ // Highlight clicked row
301
+ this.style.backgroundColor = 'var(--bg-tertiary)';
302
+ setTimeout(() => {
303
+ this.style.backgroundColor = '';
304
+ }, 300);
305
+ });
306
+
307
+ row.addEventListener('mouseenter', function() {
308
+ this.style.backgroundColor = 'rgba(59, 130, 246, 0.05)';
309
+ });
310
+
311
+ row.addEventListener('mouseleave', function() {
312
+ this.style.backgroundColor = '';
313
+ });
314
+ });
315
+ }
316
+
317
+ setupNavigationHighlight() {
318
+ // Ensure git analysis nav item is highlighted
319
+ const navItems = document.querySelectorAll('.nav-item');
320
+ navItems.forEach(item => {
321
+ const link = item.querySelector('.nav-link');
322
+ if (link && link.getAttribute('href') === 'git_analysis.html') {
323
+ item.classList.add('active');
324
+ } else {
325
+ item.classList.remove('active');
326
+ }
327
+ });
328
+ }
329
+
330
+ // Utility function to show notifications
331
+ showNotification(message, type = 'info') {
332
+ const notification = document.createElement('div');
333
+ notification.className = `notification notification-${type}`;
334
+ notification.style.cssText = `
335
+ position: fixed;
336
+ top: 20px;
337
+ right: 20px;
338
+ padding: 1rem 1.5rem;
339
+ border-radius: 0.5rem;
340
+ color: white;
341
+ font-weight: 500;
342
+ z-index: 1000;
343
+ transform: translateX(100%);
344
+ transition: transform 0.3s ease;
345
+ `;
346
+
347
+ // Set background color based on type
348
+ const colors = {
349
+ success: '#10b981',
350
+ error: '#ef4444',
351
+ warning: '#f59e0b',
352
+ info: '#3b82f6'
353
+ };
354
+ notification.style.backgroundColor = colors[type] || colors.info;
355
+ notification.textContent = message;
356
+
357
+ // Add to DOM
358
+ document.body.appendChild(notification);
359
+
360
+ // Animate in
361
+ setTimeout(() => {
362
+ notification.style.transform = 'translateX(0)';
363
+ }, 100);
364
+
365
+ // Remove after delay
366
+ setTimeout(() => {
367
+ notification.style.transform = 'translateX(100%)';
368
+ setTimeout(() => {
369
+ if (document.body.contains(notification)) {
370
+ document.body.removeChild(notification);
371
+ }
372
+ }, 300);
373
+ }, 3000);
374
+ }
375
+ }
376
+
377
+ // Initialize git analysis when DOM is loaded
378
+ document.addEventListener('DOMContentLoaded', () => {
379
+ window.gitAnalysis = new GitAnalysis();
380
+ });
381
+
382
+ // Export for global access
383
+ window.GitAnalysis = GitAnalysis;
@@ -0,0 +1,267 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Git Analysis - Code Metrics Dashboard</title>
7
+ <link rel="stylesheet" href="css/styles.css">
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <style>.cc-offenders {
11
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
12
+ background-color: #f9f9f9;
13
+ padding: 1.5rem;
14
+ border-radius: 8px;
15
+ max-width: 600px;
16
+ margin: 2rem auto;
17
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
18
+ }
19
+
20
+ .cc-offenders h2 {
21
+ text-align: center;
22
+ color: #333;
23
+ margin-bottom: 1rem;
24
+ }
25
+
26
+ .offender-list {
27
+ list-style: none;
28
+ padding: 0;
29
+ margin: 0;
30
+ }
31
+
32
+ .offender {
33
+ background-color: #fff;
34
+ border: 1px solid #ddd;
35
+ padding: 1rem;
36
+ margin-bottom: 1rem;
37
+ border-radius: 4px;
38
+ border-left-width: 6px;
39
+ border-left-style: solid;
40
+ transition: box-shadow 0.2s ease;
41
+ }
42
+
43
+ .offender:hover {
44
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
45
+ }
46
+
47
+
48
+ .offender-name {
49
+ font-weight: bold;
50
+ font-size: 1.1rem;
51
+ margin-bottom: 0.5rem;
52
+ color: #222;
53
+ word-break: break-word; /* Ensures long names wrap */
54
+ white-space: normal; /* Allows wrapping */
55
+ }
56
+
57
+
58
+ .offender-details {
59
+ font-size: 0.95rem;
60
+ color: #555;
61
+ }
62
+
63
+ .label {
64
+ font-weight: 600;
65
+ margin-right: 0.5rem;
66
+ }
67
+
68
+ /* Status-based border colors */
69
+ .status-good {
70
+ border-left-color: #10b981; /* Green */
71
+ }
72
+
73
+ .status-ok {
74
+ border-left-color: #f59e0b; /* Yellow/Amber */
75
+ }
76
+
77
+ .status-warning {
78
+ border-left-color: #f97316; /* Orange */
79
+ }
80
+
81
+ .status-critical {
82
+ border-left-color: #ef4444; /* Red */
83
+ }
84
+
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <div class="dashboard">
89
+ <!-- Sidebar -->
90
+ <aside class="sidebar">
91
+ <div class="sidebar-header">
92
+ <div class="logo">
93
+ <img src="images/logo.svg" alt="Metripy Logo" class="logo-icon">
94
+ <h2>Metripy</h2>
95
+ </div>
96
+ </div>
97
+
98
+ <nav class="sidebar-nav">
99
+ <ul>
100
+ <li class="nav-item">
101
+ <a href="index.html" class="nav-link">
102
+ <i class="fas fa-tachometer-alt"></i>
103
+ <span>Overview</span>
104
+ </a>
105
+ </li>
106
+ <li class="nav-item">
107
+ <a href="files.html" class="nav-link">
108
+ <i class="fas fa-file-code"></i>
109
+ <span>Files</span>
110
+ </a>
111
+ </li>
112
+ <li class="nav-item active">
113
+ <a href="top_offenders.html" class="nav-link">
114
+ <i class="fa-solid fa-stethoscope"></i>
115
+ <span>Top Offenders</span>
116
+ </a>
117
+ </li>
118
+ <li class="nav-item">
119
+ <a href="git_analysis.html" class="nav-link">
120
+ <i class="fab fa-git-alt"></i>
121
+ <span>Git Analysis</span>
122
+ </a>
123
+ </li>
124
+ <li class="nav-item">
125
+ <a href="dependencies.html" class="nav-link">
126
+ <i class="fas fa-cubes"></i>
127
+ <span>Dependencies</span>
128
+ </a>
129
+ </li>
130
+ <li class="nav-item">
131
+ <a href="trends.html" class="nav-link">
132
+ <i class="fas fa-chart-line"></i>
133
+ <span>Trends</span>
134
+ </a>
135
+ </li>
136
+ </ul>
137
+ </nav>
138
+
139
+ <div class="sidebar-footer">
140
+ <div class="project-info">
141
+ <h4>Project: {{project_name}}</h4>
142
+ <p>Last updated: {{last_updated}}</p>
143
+ </div>
144
+ </div>
145
+ </aside>
146
+
147
+ <!-- Main Content -->
148
+ <main class="main-content">
149
+ <header class="page-header">
150
+ <div class="header-content">
151
+ <h1>Top Offenders</h1>
152
+ <p class="subtitle">View top offenders of different criteria on file and function level</p>
153
+ </div>
154
+ </header>
155
+
156
+ <section class="two-column-section">
157
+ <div class="info-card">
158
+ <h4>File Lines of code</h4>
159
+
160
+ <ul class="offender-list">
161
+ {{#EACH file_loc_offenders AS offender}}
162
+ <li class="offender status-{{offender.status}}">
163
+ <div class="offender-name">{{offender.full_name}}</div>
164
+ <div class="offender-details">
165
+ <span class="label">Lines of code:</span> {{offender.loc}}
166
+ </div>
167
+ </li>
168
+ {{/EACH}}
169
+ </ul>
170
+
171
+ </div>
172
+ <div class="info-card">
173
+ <h4>Function size</h4>
174
+
175
+ <ul class="offender-list">
176
+ {{#EACH function_size_offenders AS offender}}
177
+ <li class="offender status-{{offender.status}}">
178
+ <div class="offender-name">{{offender.full_name}}</div>
179
+ <div class="offender-details">
180
+ <span class="label">Size:</span> {{offender.loc}} Lines
181
+ </div>
182
+ </li>
183
+ {{/EACH}}
184
+ </ul>
185
+
186
+ </div>
187
+ </section>
188
+
189
+ <div class="section-divider"></div>
190
+
191
+ <section class="two-column-section">
192
+ <div class="info-card">
193
+ <h4>File Cyclomatic complexity</h4>
194
+
195
+ <ul class="offender-list">
196
+ {{#EACH file_cc_offenders AS offender}}
197
+ <li class="offender status-{{offender.status}}">
198
+ <div class="offender-name">{{offender.full_name}}</div>
199
+ <div class="offender-details">
200
+ <span class="label">Complexity:</span> {{offender.totalCc}}
201
+ </div>
202
+ </li>
203
+ {{/EACH}}
204
+ </ul>
205
+
206
+ </div>
207
+ <div class="info-card">
208
+ <h4>Function Cyclomatic complexity</h4>
209
+
210
+ <ul class="offender-list">
211
+ {{#EACH function_cc_offenders AS offender}}
212
+ <li class="offender status-{{offender.status}}">
213
+ <div class="offender-name">{{offender.full_name}}</div>
214
+ <div class="offender-details">
215
+ <span class="label">Complexity:</span> {{offender.complexity}}
216
+ </div>
217
+ </li>
218
+ {{/EACH}}
219
+ </ul>
220
+
221
+ </div>
222
+ </section>
223
+
224
+ <div class="section-divider"></div>
225
+
226
+ <section class="two-column-section">
227
+ <div class="info-card">
228
+ <h4>File Maintainability Index</h4>
229
+
230
+ <ul class="offender-list">
231
+ {{#EACH file_mi_offenders AS offender}}
232
+ <li class="offender status-{{offender.status}}">
233
+ <div class="offender-name">{{offender.full_name}}</div>
234
+ <div class="offender-details">
235
+ <span class="label">Maintainability Index:</span> {{offender.maintainabilityIndex}}
236
+ </div>
237
+ </li>
238
+ {{/EACH}}
239
+ </ul>
240
+
241
+ </div>
242
+ <div class="info-card">
243
+ <h4>Function Maintainability Index</h4>
244
+
245
+ <ul class="offender-list">
246
+ {{#EACH function_mi_offenders AS offender}}
247
+ <li class="offender status-{{offender.status}}">
248
+ <div class="offender-name">{{offender.full_name}}</div>
249
+ <div class="offender-details">
250
+ <span class="label">Maintainability Index:</span> {{offender.maintainability_index}}
251
+ </div>
252
+ </li>
253
+ {{/EACH}}
254
+ </ul>
255
+
256
+ </div>
257
+ </section>
258
+ </main>
259
+ </div>
260
+
261
+ <!-- Scripts -->
262
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
263
+ <script>
264
+ // Embed data directly from template engine
265
+ </script>
266
+ </body>
267
+ </html>