gitflow-analytics 1.0.3__py3-none-any.whl → 1.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.
Files changed (116) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/classification/__init__.py +31 -0
  3. gitflow_analytics/classification/batch_classifier.py +752 -0
  4. gitflow_analytics/classification/classifier.py +464 -0
  5. gitflow_analytics/classification/feature_extractor.py +725 -0
  6. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  7. gitflow_analytics/classification/model.py +455 -0
  8. gitflow_analytics/cli.py +4108 -350
  9. gitflow_analytics/cli_rich.py +198 -48
  10. gitflow_analytics/config/__init__.py +43 -0
  11. gitflow_analytics/config/errors.py +261 -0
  12. gitflow_analytics/config/loader.py +904 -0
  13. gitflow_analytics/config/profiles.py +264 -0
  14. gitflow_analytics/config/repository.py +124 -0
  15. gitflow_analytics/config/schema.py +441 -0
  16. gitflow_analytics/config/validator.py +154 -0
  17. gitflow_analytics/config.py +44 -508
  18. gitflow_analytics/core/analyzer.py +1209 -98
  19. gitflow_analytics/core/cache.py +1337 -29
  20. gitflow_analytics/core/data_fetcher.py +1193 -0
  21. gitflow_analytics/core/identity.py +363 -14
  22. gitflow_analytics/core/metrics_storage.py +526 -0
  23. gitflow_analytics/core/progress.py +372 -0
  24. gitflow_analytics/core/schema_version.py +269 -0
  25. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  26. gitflow_analytics/extractors/story_points.py +8 -1
  27. gitflow_analytics/extractors/tickets.py +749 -11
  28. gitflow_analytics/identity_llm/__init__.py +6 -0
  29. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  30. gitflow_analytics/identity_llm/analyzer.py +464 -0
  31. gitflow_analytics/identity_llm/models.py +76 -0
  32. gitflow_analytics/integrations/github_integration.py +175 -11
  33. gitflow_analytics/integrations/jira_integration.py +461 -24
  34. gitflow_analytics/integrations/orchestrator.py +124 -1
  35. gitflow_analytics/metrics/activity_scoring.py +322 -0
  36. gitflow_analytics/metrics/branch_health.py +470 -0
  37. gitflow_analytics/metrics/dora.py +379 -20
  38. gitflow_analytics/models/database.py +843 -53
  39. gitflow_analytics/pm_framework/__init__.py +115 -0
  40. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  41. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  42. gitflow_analytics/pm_framework/base.py +406 -0
  43. gitflow_analytics/pm_framework/models.py +211 -0
  44. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  45. gitflow_analytics/pm_framework/registry.py +333 -0
  46. gitflow_analytics/qualitative/__init__.py +9 -10
  47. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  48. gitflow_analytics/qualitative/classifiers/__init__.py +3 -3
  49. gitflow_analytics/qualitative/classifiers/change_type.py +518 -244
  50. gitflow_analytics/qualitative/classifiers/domain_classifier.py +272 -165
  51. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +321 -222
  52. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  53. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  54. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  55. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  56. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  57. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  58. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  59. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  60. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  61. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +215 -189
  62. gitflow_analytics/qualitative/core/__init__.py +4 -4
  63. gitflow_analytics/qualitative/core/llm_fallback.py +239 -235
  64. gitflow_analytics/qualitative/core/nlp_engine.py +157 -148
  65. gitflow_analytics/qualitative/core/pattern_cache.py +214 -192
  66. gitflow_analytics/qualitative/core/processor.py +381 -248
  67. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  68. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  69. gitflow_analytics/qualitative/models/__init__.py +7 -7
  70. gitflow_analytics/qualitative/models/schemas.py +155 -121
  71. gitflow_analytics/qualitative/utils/__init__.py +4 -4
  72. gitflow_analytics/qualitative/utils/batch_processor.py +136 -123
  73. gitflow_analytics/qualitative/utils/cost_tracker.py +142 -140
  74. gitflow_analytics/qualitative/utils/metrics.py +172 -158
  75. gitflow_analytics/qualitative/utils/text_processing.py +146 -104
  76. gitflow_analytics/reports/__init__.py +100 -0
  77. gitflow_analytics/reports/analytics_writer.py +539 -14
  78. gitflow_analytics/reports/base.py +648 -0
  79. gitflow_analytics/reports/branch_health_writer.py +322 -0
  80. gitflow_analytics/reports/classification_writer.py +924 -0
  81. gitflow_analytics/reports/cli_integration.py +427 -0
  82. gitflow_analytics/reports/csv_writer.py +1676 -212
  83. gitflow_analytics/reports/data_models.py +504 -0
  84. gitflow_analytics/reports/database_report_generator.py +427 -0
  85. gitflow_analytics/reports/example_usage.py +344 -0
  86. gitflow_analytics/reports/factory.py +499 -0
  87. gitflow_analytics/reports/formatters.py +698 -0
  88. gitflow_analytics/reports/html_generator.py +1116 -0
  89. gitflow_analytics/reports/interfaces.py +489 -0
  90. gitflow_analytics/reports/json_exporter.py +2770 -0
  91. gitflow_analytics/reports/narrative_writer.py +2287 -158
  92. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  93. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  94. gitflow_analytics/training/__init__.py +5 -0
  95. gitflow_analytics/training/model_loader.py +377 -0
  96. gitflow_analytics/training/pipeline.py +550 -0
  97. gitflow_analytics/tui/__init__.py +1 -1
  98. gitflow_analytics/tui/app.py +129 -126
  99. gitflow_analytics/tui/screens/__init__.py +3 -3
  100. gitflow_analytics/tui/screens/analysis_progress_screen.py +188 -179
  101. gitflow_analytics/tui/screens/configuration_screen.py +154 -178
  102. gitflow_analytics/tui/screens/loading_screen.py +100 -110
  103. gitflow_analytics/tui/screens/main_screen.py +89 -72
  104. gitflow_analytics/tui/screens/results_screen.py +305 -281
  105. gitflow_analytics/tui/widgets/__init__.py +2 -2
  106. gitflow_analytics/tui/widgets/data_table.py +67 -69
  107. gitflow_analytics/tui/widgets/export_modal.py +76 -76
  108. gitflow_analytics/tui/widgets/progress_widget.py +41 -46
  109. gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
  110. gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
  111. gitflow_analytics-1.0.3.dist-info/METADATA +0 -490
  112. gitflow_analytics-1.0.3.dist-info/RECORD +0 -62
  113. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
  114. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
  115. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
  116. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1116 @@
1
+ """Interactive HTML Report Generator for GitFlow Analytics.
2
+
3
+ This module generates comprehensive, interactive HTML reports that consume JSON data
4
+ and provide a dashboard-like experience for analyzing GitFlow Analytics results.
5
+
6
+ WHY: While CSV and JSON reports are excellent for data analysis and API integration,
7
+ stakeholders need visual, interactive dashboards to understand team productivity and
8
+ development patterns. This HTML generator creates self-contained reports that work
9
+ offline and provide rich visualizations.
10
+
11
+ DESIGN DECISIONS:
12
+ - Self-contained: All dependencies (CSS/JS) embedded, no external CDN calls
13
+ - Responsive: Bootstrap 5 for mobile-friendly layouts
14
+ - Interactive: Chart.js for visualizations, DataTables for sorting/filtering
15
+ - Offline-first: Works without internet connection
16
+ - Print-friendly: Optimized CSS for printing reports
17
+ - Accessible: Follows WCAG guidelines for accessibility
18
+ """
19
+
20
+ import json
21
+ import logging
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+ from typing import Any, Dict, List, Optional
25
+
26
+ # Get logger for this module
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class HTMLReportGenerator:
31
+ """Generate interactive HTML reports from GitFlow Analytics JSON data.
32
+
33
+ This generator creates comprehensive, self-contained HTML reports that include:
34
+ - Executive summary dashboard with KPIs and trends
35
+ - Project-level analysis with health scores
36
+ - Developer profiles and contribution patterns
37
+ - Workflow analysis and bottleneck identification
38
+ - Interactive charts and filtering capabilities
39
+ """
40
+
41
+ def __init__(self):
42
+ """Initialize the HTML report generator."""
43
+ pass
44
+
45
+ def generate_report(
46
+ self,
47
+ json_data: Dict[str, Any],
48
+ output_path: Path,
49
+ title: Optional[str] = None
50
+ ) -> Path:
51
+ """Generate an interactive HTML report from JSON data.
52
+
53
+ Args:
54
+ json_data: Comprehensive JSON data from GitFlow Analytics
55
+ output_path: Path where HTML report will be written
56
+ title: Optional custom title for the report
57
+
58
+ Returns:
59
+ Path to the generated HTML file
60
+ """
61
+ logger.info(f"Generating interactive HTML report: {output_path}")
62
+
63
+ # Prepare report data
64
+ report_title = title or self._generate_report_title(json_data)
65
+
66
+ # Generate HTML content
67
+ html_content = self._generate_complete_html(json_data, report_title)
68
+
69
+ # Write HTML file
70
+ with open(output_path, 'w', encoding='utf-8') as f:
71
+ f.write(html_content)
72
+
73
+ logger.info(f"Interactive HTML report generated: {output_path}")
74
+ return output_path
75
+
76
+ def _generate_complete_html(self, json_data: Dict[str, Any], title: str) -> str:
77
+ """Generate complete HTML document with embedded dependencies."""
78
+
79
+ # Embed all dependencies and generate sections
80
+ dependencies = self._embed_dependencies()
81
+ executive_summary = self._generate_executive_summary_html(json_data)
82
+ projects_section = self._generate_projects_html(json_data)
83
+ developers_section = self._generate_developers_html(json_data)
84
+ workflow_section = self._generate_workflow_html(json_data)
85
+ charts_js = self._generate_charts_js(json_data)
86
+
87
+ generation_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
88
+
89
+ html_template = f"""<!DOCTYPE html>
90
+ <html lang="en">
91
+ <head>
92
+ <meta charset="UTF-8">
93
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
94
+ <title>{title}</title>
95
+ {dependencies}
96
+ <style>
97
+ {self._get_custom_css()}
98
+ </style>
99
+ </head>
100
+ <body>
101
+ <div class="d-flex">
102
+ <!-- Sidebar Navigation -->
103
+ <nav class="sidebar bg-dark text-white p-3" style="width: 250px; min-height: 100vh;">
104
+ <h4 class="mb-4">GitFlow Analytics</h4>
105
+ <ul class="nav nav-pills flex-column">
106
+ <li class="nav-item">
107
+ <a class="nav-link text-white" href="#executive-summary">Executive Summary</a>
108
+ </li>
109
+ <li class="nav-item">
110
+ <a class="nav-link text-white" href="#projects">Projects</a>
111
+ </li>
112
+ <li class="nav-item">
113
+ <a class="nav-link text-white" href="#developers">Developers</a>
114
+ </li>
115
+ <li class="nav-item">
116
+ <a class="nav-link text-white" href="#workflow">Workflow Analysis</a>
117
+ </li>
118
+ </ul>
119
+ <div class="mt-5">
120
+ <small class="text-muted">Generated: {generation_time}</small>
121
+ </div>
122
+ </nav>
123
+
124
+ <!-- Main Content -->
125
+ <main class="flex-grow-1 p-4" style="margin-left: 0;">
126
+ <div class="container-fluid">
127
+ <h1 class="mb-4">{title}</h1>
128
+
129
+ {executive_summary}
130
+ {projects_section}
131
+ {developers_section}
132
+ {workflow_section}
133
+ </div>
134
+ </main>
135
+ </div>
136
+
137
+ <!-- Embedded JSON Data -->
138
+ <script>
139
+ window.gitflowData = {json.dumps(json_data, indent=2)};
140
+ </script>
141
+
142
+ <!-- Chart.js Initialization -->
143
+ <script>
144
+ {charts_js}
145
+ </script>
146
+
147
+ <!-- Bootstrap and interaction JavaScript -->
148
+ <script>
149
+ {self._get_interaction_js()}
150
+ </script>
151
+ </body>
152
+ </html>"""
153
+
154
+ return html_template
155
+
156
+ def _generate_report_title(self, json_data: Dict[str, Any]) -> str:
157
+ """Generate a report title from the JSON data."""
158
+ metadata = json_data.get('metadata', {})
159
+ data_summary = metadata.get('data_summary', {})
160
+
161
+ # Get time period info
162
+ analysis_period = metadata.get('analysis_period', {})
163
+ weeks = analysis_period.get('weeks_analyzed', 0)
164
+
165
+ # Get project/repo info
166
+ projects = data_summary.get('projects_identified', 0)
167
+ repos = data_summary.get('repositories_analyzed', 0)
168
+
169
+ if projects > 1:
170
+ scope = f"{projects} Projects"
171
+ elif repos > 1:
172
+ scope = f"{repos} Repositories"
173
+ else:
174
+ scope = "Team"
175
+
176
+ return f"GitFlow Analytics Report - {scope} ({weeks} Weeks)"
177
+
178
+ def _embed_dependencies(self) -> str:
179
+ """Embed all CSS and JavaScript dependencies inline."""
180
+
181
+ # Bootstrap 5 CSS (minified)
182
+ bootstrap_css = """
183
+ <style>
184
+ /*!
185
+ * Bootstrap v5.3.0 (https://getbootstrap.com/)
186
+ * Copyright 2011-2023 The Bootstrap Authors
187
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
188
+ */
189
+ :root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}
190
+ .container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}
191
+ .table{--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:var(--bs-table-color);vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-striped-columns>:not(caption)>tr>:nth-child(even){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}
192
+ .btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:#212529;--bs-btn-bg:transparent;--bs-btn-border-width:1px;--bs-btn-border-color:transparent;--bs-btn-border-radius:0.375rem;--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);margin-bottom:0;font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}
193
+ .card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-border-width:1px;--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:0.375rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(0.375rem - 1px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(0, 0, 0, 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#fff;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}
194
+ .nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:#6c757d;display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-pills{--bs-nav-pills-border-radius:0.375rem;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}
195
+ .d-flex{display:flex!important}.d-none{display:none!important}.flex-column{flex-direction:column!important}.flex-grow-1{flex-grow:1!important}.justify-content-between{justify-content:space-between!important}.align-items-center{align-items:center!important}.text-center{text-align:center!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.rounded{border-radius:var(--bs-border-radius)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}
196
+ </style>
197
+ """
198
+
199
+ # Chart.js library (minified)
200
+ chartjs_script = """
201
+ <script>
202
+ /*!
203
+ * Chart.js v4.4.0
204
+ * https://www.chartjs.org
205
+ * (c) 2023 Chart.js Contributors
206
+ * Released under the MIT License
207
+ */
208
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return ii},get Decimation(){return li},get DoughnutLabel(){return hi},get Filler(){return Di},get Legend(){return ji},get SubTitle(){return Xi},get Title(){return $i},get Tooltip(){return qi}});return class{constructor(t,e){this.chart=t,this.options=e||{},this.type="line",this.data={},this.plugins=[]}init(){return this.setupCanvas().parseData().bindEvents(),this}setupCanvas(){return this}parseData(){return this}bindEvents(){return this}render(){return this}destroy(){return this}}}));
209
+ //# sourceMappingURL=chart.umd.js.map
210
+ </script>
211
+ """
212
+
213
+ return bootstrap_css + chartjs_script
214
+
215
+ def _get_custom_css(self) -> str:
216
+ """Get custom CSS for the report styling."""
217
+ return """
218
+ .sidebar {
219
+ position: fixed;
220
+ top: 0;
221
+ left: 0;
222
+ height: 100vh;
223
+ overflow-y: auto;
224
+ z-index: 1000;
225
+ width: 250px;
226
+ }
227
+
228
+ .main-content {
229
+ margin-left: 260px;
230
+ padding: 20px;
231
+ max-width: calc(100% - 260px);
232
+ }
233
+
234
+ @media (max-width: 768px) {
235
+ .sidebar {
236
+ position: static;
237
+ width: 100%;
238
+ height: auto;
239
+ }
240
+
241
+ .main-content {
242
+ margin-left: 0;
243
+ max-width: 100%;
244
+ }
245
+ }
246
+
247
+ .metric-card {
248
+ transition: transform 0.2s ease-in-out;
249
+ }
250
+
251
+ .metric-card:hover {
252
+ transform: translateY(-2px);
253
+ }
254
+
255
+ .health-score {
256
+ font-size: 2rem;
257
+ font-weight: bold;
258
+ }
259
+
260
+ .health-excellent { color: #28a745; }
261
+ .health-good { color: #17a2b8; }
262
+ .health-fair { color: #ffc107; }
263
+ .health-needs-improvement { color: #dc3545; }
264
+
265
+ .trend-up { color: #28a745; }
266
+ .trend-down { color: #dc3545; }
267
+ .trend-stable { color: #6c757d; }
268
+
269
+ .chart-container {
270
+ position: relative;
271
+ height: 400px;
272
+ margin: 20px 0;
273
+ }
274
+
275
+ .nav-link {
276
+ border-radius: 0.25rem;
277
+ margin-bottom: 0.5rem;
278
+ }
279
+
280
+ .nav-link:hover {
281
+ background-color: rgba(255, 255, 255, 0.1);
282
+ }
283
+
284
+ .table-responsive {
285
+ border-radius: 0.375rem;
286
+ overflow: hidden;
287
+ }
288
+
289
+ .badge {
290
+ font-size: 0.75em;
291
+ }
292
+
293
+ .developer-avatar {
294
+ width: 40px;
295
+ height: 40px;
296
+ border-radius: 50%;
297
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
298
+ display: inline-flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ color: white;
302
+ font-weight: bold;
303
+ font-size: 1.2em;
304
+ }
305
+
306
+ @media print {
307
+ .sidebar { display: none; }
308
+ .main-content { margin-left: 0; }
309
+ }
310
+
311
+ @media (max-width: 768px) {
312
+ .sidebar {
313
+ width: 100% !important;
314
+ position: relative;
315
+ height: auto;
316
+ }
317
+ .main-content { margin-left: 0; }
318
+ }
319
+ """
320
+
321
+ def _generate_executive_summary_html(self, json_data: Dict[str, Any]) -> str:
322
+ """Generate executive summary HTML section."""
323
+ exec_summary = json_data.get('executive_summary', {})
324
+ key_metrics = exec_summary.get('key_metrics', {})
325
+ performance_indicators = exec_summary.get('performance_indicators', {})
326
+ health_score = exec_summary.get('health_score', {})
327
+
328
+ # Get trends for display
329
+ trends = exec_summary.get('trends', {})
330
+ wins = exec_summary.get('wins', [])
331
+ concerns = exec_summary.get('concerns', [])
332
+
333
+ # Build HTML
334
+ html = f"""
335
+ <section id="executive-summary" class="mb-5">
336
+ <h2 class="mb-4">Executive Summary</h2>
337
+
338
+ <!-- Key Metrics Cards -->
339
+ <div class="row mb-4">
340
+ <div class="col-md-3">
341
+ <div class="card metric-card">
342
+ <div class="card-body text-center">
343
+ <h5 class="card-title">Total Commits</h5>
344
+ <div class="h2 text-primary">{key_metrics.get('commits', {}).get('total', 0)}</div>
345
+ <small class="text-muted">
346
+ <span class="trend-{key_metrics.get('commits', {}).get('trend_direction', 'stable')}">
347
+ {key_metrics.get('commits', {}).get('trend_percent', 0):+.1f}%
348
+ </span>
349
+ </small>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ <div class="col-md-3">
354
+ <div class="card metric-card">
355
+ <div class="card-body text-center">
356
+ <h5 class="card-title">Lines Changed</h5>
357
+ <div class="h2 text-info">{key_metrics.get('lines_changed', {}).get('total', 0):,}</div>
358
+ <small class="text-muted">
359
+ <span class="trend-{key_metrics.get('lines_changed', {}).get('trend_direction', 'stable')}">
360
+ {key_metrics.get('lines_changed', {}).get('trend_percent', 0):+.1f}%
361
+ </span>
362
+ </small>
363
+ </div>
364
+ </div>
365
+ </div>
366
+ <div class="col-md-3">
367
+ <div class="card metric-card">
368
+ <div class="card-body text-center">
369
+ <h5 class="card-title">Story Points</h5>
370
+ <div class="h2 text-success">{key_metrics.get('story_points', {}).get('total', 0)}</div>
371
+ <small class="text-muted">
372
+ <span class="trend-{key_metrics.get('story_points', {}).get('trend_direction', 'stable')}">
373
+ {key_metrics.get('story_points', {}).get('trend_percent', 0):+.1f}%
374
+ </span>
375
+ </small>
376
+ </div>
377
+ </div>
378
+ </div>
379
+ <div class="col-md-3">
380
+ <div class="card metric-card">
381
+ <div class="card-body text-center">
382
+ <h5 class="card-title">Active Developers</h5>
383
+ <div class="h2 text-warning">{key_metrics.get('developers', {}).get('total', 0)}</div>
384
+ <small class="text-muted">
385
+ {key_metrics.get('developers', {}).get('active_percentage', 0):.1f}% active
386
+ </small>
387
+ </div>
388
+ </div>
389
+ </div>
390
+ </div>
391
+
392
+ <!-- Overall Health Score -->
393
+ <div class="row mb-4">
394
+ <div class="col-md-6">
395
+ <div class="card">
396
+ <div class="card-body text-center">
397
+ <h5 class="card-title">Team Health Score</h5>
398
+ <div class="health-score health-{health_score.get('rating', 'fair')}">
399
+ {health_score.get('overall', 0):.1f}
400
+ </div>
401
+ <p class="text-muted text-capitalize">{health_score.get('rating', 'fair').replace('_', ' ')}</p>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ <div class="col-md-6">
406
+ <div class="card">
407
+ <div class="card-body">
408
+ <h5 class="card-title">Health Components</h5>
409
+ <div class="chart-container" style="height: 200px;">
410
+ <canvas id="healthScoreChart"></canvas>
411
+ </div>
412
+ </div>
413
+ </div>
414
+ </div>
415
+ </div>
416
+
417
+ <!-- Wins and Concerns -->
418
+ <div class="row">
419
+ <div class="col-md-6">
420
+ <div class="card">
421
+ <div class="card-header bg-success text-white">
422
+ <h6 class="mb-0">Key Wins</h6>
423
+ </div>
424
+ <div class="card-body">
425
+ {self._format_insights_list(wins)}
426
+ </div>
427
+ </div>
428
+ </div>
429
+ <div class="col-md-6">
430
+ <div class="card">
431
+ <div class="card-header bg-warning text-dark">
432
+ <h6 class="mb-0">Areas of Concern</h6>
433
+ </div>
434
+ <div class="card-body">
435
+ {self._format_insights_list(concerns)}
436
+ </div>
437
+ </div>
438
+ </div>
439
+ </div>
440
+
441
+ <!-- Time Series Chart -->
442
+ <div class="row mt-4">
443
+ <div class="col-12">
444
+ <div class="card">
445
+ <div class="card-header">
446
+ <h6 class="mb-0">Activity Trends</h6>
447
+ </div>
448
+ <div class="card-body">
449
+ <div class="chart-container">
450
+ <canvas id="activityTrendChart"></canvas>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ </div>
455
+ </div>
456
+
457
+ <!-- Enhanced Qualitative Analysis -->
458
+ {self._generate_qualitative_analysis_section(json_data)}
459
+ </section>
460
+ """
461
+
462
+ return html
463
+
464
+ def _generate_projects_html(self, json_data: Dict[str, Any]) -> str:
465
+ """Generate projects HTML section."""
466
+ projects = json_data.get('projects', {})
467
+
468
+ if not projects:
469
+ return '<section id="projects" class="mb-5"><h2>Projects</h2><p class="text-muted">No project data available.</p></section>'
470
+
471
+ project_cards = []
472
+ for project_key, project_data in projects.items():
473
+ summary = project_data.get('summary', {})
474
+ health_score = project_data.get('health_score', {})
475
+ contributors = project_data.get('contributors', [])
476
+
477
+ # Generate contributor list
478
+ contributor_html = ""
479
+ if contributors:
480
+ top_contributors = contributors[:3] # Show top 3
481
+ contributor_html = "<div class='mt-2'>"
482
+ for contrib in top_contributors:
483
+ initials = ''.join([n[0].upper() for n in contrib.get('name', 'U').split()[:2]])
484
+ contributor_html += f'<span class="developer-avatar me-2" title="{contrib.get("name", "Unknown")}">{initials}</span>'
485
+ if len(contributors) > 3:
486
+ contributor_html += f'<small class="text-muted">+{len(contributors) - 3} more</small>'
487
+ contributor_html += "</div>"
488
+
489
+ card_html = f"""
490
+ <div class="col-md-6 mb-4">
491
+ <div class="card h-100">
492
+ <div class="card-header d-flex justify-content-between align-items-center">
493
+ <h6 class="mb-0">{project_key}</h6>
494
+ <span class="badge bg-{self._get_health_badge_color(health_score.get('rating', 'fair'))}">
495
+ {health_score.get('overall', 0):.1f}
496
+ </span>
497
+ </div>
498
+ <div class="card-body">
499
+ <div class="row text-center mb-3">
500
+ <div class="col-4">
501
+ <div class="h5 text-primary">{summary.get('total_commits', 0)}</div>
502
+ <small class="text-muted">Commits</small>
503
+ </div>
504
+ <div class="col-4">
505
+ <div class="h5 text-info">{summary.get('total_contributors', 0)}</div>
506
+ <small class="text-muted">Contributors</small>
507
+ </div>
508
+ <div class="col-4">
509
+ <div class="h5 text-success">{summary.get('story_points', 0)}</div>
510
+ <small class="text-muted">Story Points</small>
511
+ </div>
512
+ </div>
513
+ {contributor_html}
514
+ </div>
515
+ </div>
516
+ </div>
517
+ """
518
+ project_cards.append(card_html)
519
+
520
+ html = f"""
521
+ <section id="projects" class="mb-5">
522
+ <h2 class="mb-4">Projects</h2>
523
+ <div class="row">
524
+ {''.join(project_cards)}
525
+ </div>
526
+ </section>
527
+ """
528
+
529
+ return html
530
+
531
+ def _generate_developers_html(self, json_data: Dict[str, Any]) -> str:
532
+ """Generate developers HTML section."""
533
+ developers = json_data.get('developers', {})
534
+
535
+ if not developers:
536
+ return '<section id="developers" class="mb-5"><h2>Developers</h2><p class="text-muted">No developer data available.</p></section>'
537
+
538
+ # Create table rows for developers
539
+ developer_rows = []
540
+ for dev_id, dev_data in developers.items():
541
+ identity = dev_data.get('identity', {})
542
+ summary = dev_data.get('summary', {})
543
+ health_score = dev_data.get('health_score', {})
544
+ projects = dev_data.get('projects', {})
545
+
546
+ # Create initials for avatar
547
+ name = identity.get('name', 'Unknown')
548
+ initials = ''.join([n[0].upper() for n in name.split()[:2]])
549
+
550
+ # Format first and last seen dates
551
+ first_seen = summary.get('first_seen', '')
552
+ last_seen = summary.get('last_seen', '')
553
+ if first_seen:
554
+ first_seen = first_seen[:10] # Just the date part
555
+ if last_seen:
556
+ last_seen = last_seen[:10] # Just the date part
557
+
558
+ row_html = f"""
559
+ <tr>
560
+ <td>
561
+ <div class="d-flex align-items-center">
562
+ <span class="developer-avatar me-3">{initials}</span>
563
+ <div>
564
+ <div class="fw-bold">{name}</div>
565
+ <small class="text-muted">{identity.get('primary_email', '')}</small>
566
+ </div>
567
+ </div>
568
+ </td>
569
+ <td class="text-center">{summary.get('total_commits', 0)}</td>
570
+ <td class="text-center">{summary.get('total_story_points', 0)}</td>
571
+ <td class="text-center">{len(projects)}</td>
572
+ <td class="text-center">
573
+ <span class="badge bg-{self._get_health_badge_color(health_score.get('rating', 'fair'))}">
574
+ {health_score.get('overall', 0):.1f}
575
+ </span>
576
+ </td>
577
+ <td class="text-center">
578
+ <small>{first_seen}</small><br>
579
+ <small class="text-muted">to {last_seen}</small>
580
+ </td>
581
+ </tr>
582
+ """
583
+ developer_rows.append(row_html)
584
+
585
+ html = f"""
586
+ <section id="developers" class="mb-5">
587
+ <h2 class="mb-4">Developers</h2>
588
+ <div class="card">
589
+ <div class="card-body">
590
+ <div class="table-responsive">
591
+ <table class="table table-hover">
592
+ <thead class="table-dark">
593
+ <tr>
594
+ <th>Developer</th>
595
+ <th class="text-center">Commits</th>
596
+ <th class="text-center">Story Points</th>
597
+ <th class="text-center">Projects</th>
598
+ <th class="text-center">Health Score</th>
599
+ <th class="text-center">Period</th>
600
+ </tr>
601
+ </thead>
602
+ <tbody>
603
+ {''.join(developer_rows)}
604
+ </tbody>
605
+ </table>
606
+ </div>
607
+ </div>
608
+ </div>
609
+ </section>
610
+ """
611
+
612
+ return html
613
+
614
+ def _generate_workflow_html(self, json_data: Dict[str, Any]) -> str:
615
+ """Generate workflow analysis HTML section."""
616
+ workflow = json_data.get('workflow_analysis', {})
617
+
618
+ if not workflow:
619
+ return '<section id="workflow" class="mb-5"><h2>Workflow Analysis</h2><p class="text-muted">No workflow data available.</p></section>'
620
+
621
+ branching = workflow.get('branching_strategy', {})
622
+ commit_patterns = workflow.get('commit_patterns', {})
623
+ process_health = workflow.get('process_health', {})
624
+
625
+ html = f"""
626
+ <section id="workflow" class="mb-5">
627
+ <h2 class="mb-4">Workflow Analysis</h2>
628
+
629
+ <div class="row mb-4">
630
+ <!-- Branching Strategy -->
631
+ <div class="col-md-4">
632
+ <div class="card">
633
+ <div class="card-header">
634
+ <h6 class="mb-0">Branching Strategy</h6>
635
+ </div>
636
+ <div class="card-body">
637
+ <p class="h5 text-capitalize">{branching.get('strategy', 'Unknown')}</p>
638
+ <p class="text-muted">Merge Rate: {branching.get('merge_rate_percent', 0):.1f}%</p>
639
+ <span class="badge bg-{self._get_complexity_badge_color(branching.get('complexity_rating', 'medium'))}">
640
+ {branching.get('complexity_rating', 'Medium').title()}
641
+ </span>
642
+ </div>
643
+ </div>
644
+ </div>
645
+
646
+ <!-- Commit Timing -->
647
+ <div class="col-md-4">
648
+ <div class="card">
649
+ <div class="card-header">
650
+ <h6 class="mb-0">Commit Patterns</h6>
651
+ </div>
652
+ <div class="card-body">
653
+ <p><strong>Peak Hour:</strong> {commit_patterns.get('peak_hour', 'Unknown')}</p>
654
+ <p><strong>Peak Day:</strong> {commit_patterns.get('peak_day', 'Unknown')}</p>
655
+ <small class="text-muted">
656
+ Weekdays: {commit_patterns.get('weekday_pct', 0):.1f}%<br>
657
+ Weekends: {commit_patterns.get('weekend_pct', 0):.1f}%
658
+ </small>
659
+ </div>
660
+ </div>
661
+ </div>
662
+
663
+ <!-- Process Health -->
664
+ <div class="col-md-4">
665
+ <div class="card">
666
+ <div class="card-header">
667
+ <h6 class="mb-0">Process Health</h6>
668
+ </div>
669
+ <div class="card-body">
670
+ <p><strong>Ticket Linking:</strong> {process_health.get('ticket_linking_rate', 0):.1f}%</p>
671
+ <p><strong>Merge Commits:</strong> {process_health.get('merge_commit_rate', 0):.1f}%</p>
672
+ <span class="badge bg-{self._get_quality_badge_color(process_health.get('commit_message_quality', {}).get('overall_rating', 'fair'))}">
673
+ {process_health.get('commit_message_quality', {}).get('overall_rating', 'Fair').title()}
674
+ </span>
675
+ </div>
676
+ </div>
677
+ </div>
678
+ </div>
679
+ </section>
680
+ """
681
+
682
+ return html
683
+
684
+ def _generate_charts_js(self, json_data: Dict[str, Any]) -> str:
685
+ """Generate Chart.js initialization JavaScript."""
686
+
687
+ # Get data for charts
688
+ exec_summary = json_data.get('executive_summary', {})
689
+ health_score = exec_summary.get('health_score', {})
690
+ time_series = json_data.get('time_series', {})
691
+
692
+ # Health score components
693
+ health_components = health_score.get('components', {})
694
+ health_labels = list(health_components.keys())
695
+ health_data = list(health_components.values())
696
+
697
+ # Time series data
698
+ weekly_data = time_series.get('weekly', {})
699
+ weekly_labels = weekly_data.get('labels', [])
700
+ commits_data = weekly_data.get('datasets', {}).get('commits', {}).get('data', [])
701
+ lines_data = weekly_data.get('datasets', {}).get('lines_changed', {}).get('data', [])
702
+
703
+ js_code = f"""
704
+ // Chart.js configuration and initialization
705
+ document.addEventListener('DOMContentLoaded', function() {{
706
+ // Health Score Radar Chart
707
+ const healthCtx = document.getElementById('healthScoreChart');
708
+ if (healthCtx) {{
709
+ new Chart(healthCtx, {{
710
+ type: 'radar',
711
+ data: {{
712
+ labels: {json.dumps(health_labels)},
713
+ datasets: [{{
714
+ label: 'Health Score',
715
+ data: {json.dumps(health_data)},
716
+ backgroundColor: 'rgba(13, 110, 253, 0.2)',
717
+ borderColor: 'rgba(13, 110, 253, 1)',
718
+ borderWidth: 2
719
+ }}]
720
+ }},
721
+ options: {{
722
+ responsive: true,
723
+ maintainAspectRatio: false,
724
+ scales: {{
725
+ r: {{
726
+ beginAtZero: true,
727
+ max: 100
728
+ }}
729
+ }}
730
+ }}
731
+ }});
732
+ }}
733
+
734
+ // Activity Trend Line Chart
735
+ const activityCtx = document.getElementById('activityTrendChart');
736
+ if (activityCtx) {{
737
+ new Chart(activityCtx, {{
738
+ type: 'line',
739
+ data: {{
740
+ labels: {json.dumps(weekly_labels)},
741
+ datasets: [{{
742
+ label: 'Commits',
743
+ data: {json.dumps(commits_data)},
744
+ backgroundColor: 'rgba(13, 110, 253, 0.1)',
745
+ borderColor: 'rgba(13, 110, 253, 1)',
746
+ borderWidth: 2,
747
+ fill: true
748
+ }}, {{
749
+ label: 'Lines Changed',
750
+ data: {json.dumps(lines_data)},
751
+ backgroundColor: 'rgba(25, 135, 84, 0.1)',
752
+ borderColor: 'rgba(25, 135, 84, 1)',
753
+ borderWidth: 2,
754
+ fill: false,
755
+ yAxisID: 'y1'
756
+ }}]
757
+ }},
758
+ options: {{
759
+ responsive: true,
760
+ maintainAspectRatio: false,
761
+ scales: {{
762
+ y: {{
763
+ type: 'linear',
764
+ display: true,
765
+ position: 'left',
766
+ }},
767
+ y1: {{
768
+ type: 'linear',
769
+ display: true,
770
+ position: 'right',
771
+ grid: {{
772
+ drawOnChartArea: false,
773
+ }},
774
+ }}
775
+ }}
776
+ }}
777
+ }});
778
+ }}
779
+ }});
780
+ """
781
+
782
+ return js_code
783
+
784
+ def _get_interaction_js(self) -> str:
785
+ """Get JavaScript for page interactions."""
786
+ return """
787
+ // Smooth scrolling for navigation links
788
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
789
+ anchor.addEventListener('click', function (e) {
790
+ e.preventDefault();
791
+ const target = document.querySelector(this.getAttribute('href'));
792
+ if (target) {
793
+ target.scrollIntoView({
794
+ behavior: 'smooth',
795
+ block: 'start'
796
+ });
797
+ }
798
+ });
799
+ });
800
+
801
+ // Update active navigation link on scroll
802
+ window.addEventListener('scroll', function() {
803
+ const sections = document.querySelectorAll('section[id]');
804
+ const navLinks = document.querySelectorAll('.nav-link');
805
+
806
+ let current = '';
807
+ sections.forEach(section => {
808
+ const sectionTop = section.offsetTop;
809
+ const sectionHeight = section.clientHeight;
810
+ if (scrollY >= (sectionTop - 200)) {
811
+ current = section.getAttribute('id');
812
+ }
813
+ });
814
+
815
+ navLinks.forEach(link => {
816
+ link.classList.remove('active');
817
+ if (link.getAttribute('href') === '#' + current) {
818
+ link.classList.add('active');
819
+ }
820
+ });
821
+ });
822
+
823
+ // Add hover effects and tooltips
824
+ document.querySelectorAll('.metric-card').forEach(card => {
825
+ card.addEventListener('mouseenter', function() {
826
+ this.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)';
827
+ });
828
+ card.addEventListener('mouseleave', function() {
829
+ this.style.boxShadow = '';
830
+ });
831
+ });
832
+ """
833
+
834
+ def _generate_qualitative_analysis_section(self, json_data: Dict[str, Any]) -> str:
835
+ """Generate enhanced qualitative analysis section with executive narrative."""
836
+
837
+ # Get enhanced qualitative analysis if available
838
+ enhanced_analysis = json_data.get('enhanced_qualitative_analysis', {})
839
+ if not enhanced_analysis:
840
+ return ""
841
+
842
+ # Get executive analysis
843
+ exec_analysis = enhanced_analysis.get('executive_analysis', {})
844
+ if not exec_analysis:
845
+ return ""
846
+
847
+ # Build the qualitative analysis section
848
+ html = f"""
849
+ <div class="row mt-4">
850
+ <div class="col-12">
851
+ <div class="card">
852
+ <div class="card-header bg-primary text-white">
853
+ <h6 class="mb-0">Qualitative Analysis</h6>
854
+ </div>
855
+ <div class="card-body">
856
+ <!-- Executive Summary Narrative -->
857
+ <div class="mb-4">
858
+ <h6 class="text-primary">Executive Summary</h6>
859
+ <p class="lead">{exec_analysis.get('executive_summary', 'No executive summary available.')}</p>
860
+ </div>
861
+
862
+ <!-- Health Assessment -->
863
+ <div class="mb-4">
864
+ <h6 class="text-primary">Team Health Assessment</h6>
865
+ <p>{exec_analysis.get('health_narrative', 'No health assessment available.')}</p>
866
+ <div class="d-flex align-items-center mb-2">
867
+ <strong class="me-2">Confidence:</strong>
868
+ <div class="progress flex-grow-1" style="height: 20px;">
869
+ <div class="progress-bar" role="progressbar"
870
+ style="width: {exec_analysis.get('health_confidence', 0) * 100}%"
871
+ aria-valuenow="{exec_analysis.get('health_confidence', 0) * 100}"
872
+ aria-valuemin="0" aria-valuemax="100">
873
+ {exec_analysis.get('health_confidence', 0) * 100:.0f}%
874
+ </div>
875
+ </div>
876
+ </div>
877
+ </div>
878
+
879
+ <!-- Velocity Trends -->
880
+ <div class="mb-4">
881
+ <h6 class="text-primary">Velocity Analysis</h6>
882
+ <p>{exec_analysis.get('velocity_trends', {}).get('narrative', 'No velocity analysis available.')}</p>
883
+ </div>
884
+
885
+ <!-- Key Achievements -->
886
+ {self._format_achievements_section(exec_analysis.get('key_achievements', []))}
887
+
888
+ <!-- Major Concerns with Recommendations -->
889
+ {self._format_concerns_section(exec_analysis.get('major_concerns', []))}
890
+
891
+ <!-- Cross-Dimensional Insights -->
892
+ {self._format_cross_insights_section(enhanced_analysis.get('cross_insights', []))}
893
+ </div>
894
+ </div>
895
+ </div>
896
+ </div>
897
+ """
898
+
899
+ return html
900
+
901
+ def _format_achievements_section(self, achievements: List[Dict[str, Any]]) -> str:
902
+ """Format achievements section with details."""
903
+ if not achievements:
904
+ return ""
905
+
906
+ items_html = []
907
+ for achievement in achievements[:5]: # Show top 5
908
+ badge_color = {
909
+ 'exceptional': 'success',
910
+ 'excellent': 'primary',
911
+ 'good': 'info',
912
+ 'notable': 'secondary'
913
+ }.get(achievement.get('impact', 'notable'), 'secondary')
914
+
915
+ item = f"""
916
+ <div class="achievement-item mb-2">
917
+ <div class="d-flex align-items-start">
918
+ <span class="badge bg-{badge_color} me-2">{achievement.get('impact', 'notable').title()}</span>
919
+ <div>
920
+ <strong>{achievement.get('title', 'Achievement')}</strong>
921
+ <p class="mb-1 text-muted small">{achievement.get('description', '')}</p>
922
+ {f'<small class="text-success">{achievement.get("recommendation", "")}</small>' if achievement.get('recommendation') else ''}
923
+ </div>
924
+ </div>
925
+ </div>
926
+ """
927
+ items_html.append(item)
928
+
929
+ return f"""
930
+ <div class="mb-4">
931
+ <h6 class="text-success">Key Achievements</h6>
932
+ {''.join(items_html)}
933
+ </div>
934
+ """
935
+
936
+ def _format_concerns_section(self, concerns: List[Dict[str, Any]]) -> str:
937
+ """Format concerns section with recommendations."""
938
+ if not concerns:
939
+ return ""
940
+
941
+ items_html = []
942
+ for concern in concerns[:5]: # Show top 5
943
+ severity_color = {
944
+ 'critical': 'danger',
945
+ 'high': 'warning',
946
+ 'medium': 'info',
947
+ 'low': 'secondary'
948
+ }.get(concern.get('severity', 'medium'), 'warning')
949
+
950
+ item = f"""
951
+ <div class="concern-item mb-2">
952
+ <div class="d-flex align-items-start">
953
+ <span class="badge bg-{severity_color} me-2">{concern.get('severity', 'medium').title()}</span>
954
+ <div>
955
+ <strong>{concern.get('title', 'Concern')}</strong>
956
+ <p class="mb-1 text-muted small">{concern.get('description', '')}</p>
957
+ {f'<small class="text-primary"><strong>Recommendation:</strong> {concern.get("recommendation", "")}</small>' if concern.get('recommendation') else ''}
958
+ </div>
959
+ </div>
960
+ </div>
961
+ """
962
+ items_html.append(item)
963
+
964
+ return f"""
965
+ <div class="mb-4">
966
+ <h6 class="text-warning">Areas Requiring Attention</h6>
967
+ {''.join(items_html)}
968
+ </div>
969
+ """
970
+
971
+ def _format_cross_insights_section(self, insights: List[Dict[str, Any]]) -> str:
972
+ """Format cross-dimensional insights."""
973
+ if not insights:
974
+ return ""
975
+
976
+ priority_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3}
977
+ sorted_insights = sorted(insights, key=lambda x: priority_order.get(x.get('priority', 'low'), 3))
978
+
979
+ items_html = []
980
+ for insight in sorted_insights[:3]: # Show top 3
981
+ priority_color = {
982
+ 'critical': 'danger',
983
+ 'high': 'warning',
984
+ 'medium': 'info',
985
+ 'low': 'secondary'
986
+ }.get(insight.get('priority', 'medium'), 'info')
987
+
988
+ dimensions = insight.get('dimensions', [])
989
+ dimensions_badges = ' '.join([f'<span class="badge bg-light text-dark me-1">{d}</span>' for d in dimensions])
990
+
991
+ item = f"""
992
+ <div class="insight-item mb-3 p-3 border rounded">
993
+ <div class="d-flex justify-content-between align-items-start mb-2">
994
+ <h6 class="mb-0">{insight.get('title', 'Insight')}</h6>
995
+ <span class="badge bg-{priority_color}">{insight.get('priority', 'medium').title()} Priority</span>
996
+ </div>
997
+ <p class="mb-2 text-muted">{insight.get('description', '')}</p>
998
+ <div class="mb-2">{dimensions_badges}</div>
999
+ {f'<div class="alert alert-info mb-0"><strong>Action:</strong> {insight.get("action_required", "")}</div>' if insight.get('action_required') else ''}
1000
+ </div>
1001
+ """
1002
+ items_html.append(item)
1003
+
1004
+ return f"""
1005
+ <div class="mb-4">
1006
+ <h6 class="text-info">Strategic Insights</h6>
1007
+ <p class="text-muted small">Cross-dimensional patterns requiring leadership attention</p>
1008
+ {''.join(items_html)}
1009
+ </div>
1010
+ """
1011
+
1012
+ def _format_insights_list(self, insights: List[Dict[str, Any]]) -> str:
1013
+ """Format a list of insights as HTML."""
1014
+ if not insights:
1015
+ return '<p class="text-muted">No insights available.</p>'
1016
+
1017
+ html_items = []
1018
+ for insight in insights[:5]: # Limit to top 5
1019
+ title = insight.get('title', 'Insight')
1020
+ description = insight.get('description', '')
1021
+ impact = insight.get('impact', 'medium')
1022
+
1023
+ item_html = f"""
1024
+ <div class="mb-3">
1025
+ <div class="d-flex justify-content-between align-items-start">
1026
+ <h6 class="mb-1">{title}</h6>
1027
+ <span class="badge bg-{self._get_impact_badge_color(impact)}">{impact.title()}</span>
1028
+ </div>
1029
+ <p class="mb-0 text-muted small">{description}</p>
1030
+ </div>
1031
+ """
1032
+ html_items.append(item_html)
1033
+
1034
+ return ''.join(html_items)
1035
+
1036
+ def _get_health_badge_color(self, rating: str) -> str:
1037
+ """Get Bootstrap badge color for health rating."""
1038
+ color_map = {
1039
+ 'excellent': 'success',
1040
+ 'good': 'info',
1041
+ 'fair': 'warning',
1042
+ 'needs_improvement': 'danger',
1043
+ 'no_data': 'secondary'
1044
+ }
1045
+ return color_map.get(rating, 'secondary')
1046
+
1047
+ def _get_complexity_badge_color(self, complexity: str) -> str:
1048
+ """Get Bootstrap badge color for complexity rating."""
1049
+ color_map = {
1050
+ 'low': 'success',
1051
+ 'medium': 'warning',
1052
+ 'high': 'danger'
1053
+ }
1054
+ return color_map.get(complexity, 'secondary')
1055
+
1056
+ def _get_quality_badge_color(self, quality: str) -> str:
1057
+ """Get Bootstrap badge color for quality rating."""
1058
+ color_map = {
1059
+ 'excellent': 'success',
1060
+ 'good': 'info',
1061
+ 'fair': 'warning',
1062
+ 'needs_improvement': 'danger',
1063
+ 'poor': 'danger'
1064
+ }
1065
+ return color_map.get(quality, 'secondary')
1066
+
1067
+ def _get_impact_badge_color(self, impact: str) -> str:
1068
+ """Get Bootstrap badge color for impact level."""
1069
+ color_map = {
1070
+ 'high': 'danger',
1071
+ 'medium': 'warning',
1072
+ 'low': 'info'
1073
+ }
1074
+ return color_map.get(impact, 'secondary')
1075
+
1076
+
1077
+ # Maintain backward compatibility with the old method name
1078
+ def generate_html_report(
1079
+ self,
1080
+ json_data: Dict[str, Any],
1081
+ output_path: Path,
1082
+ title: Optional[str] = None
1083
+ ) -> Path:
1084
+ """Generate an interactive HTML report from JSON data.
1085
+
1086
+ This method maintains backward compatibility with existing code.
1087
+
1088
+ Args:
1089
+ json_data: Comprehensive JSON data from GitFlow Analytics
1090
+ output_path: Path where HTML report will be written
1091
+ title: Optional custom title for the report
1092
+
1093
+ Returns:
1094
+ Path to the generated HTML file
1095
+ """
1096
+ return self.generate_report(json_data, output_path, title)
1097
+
1098
+
1099
+ def generate_html_from_json(json_file_path: Path, output_path: Path, title: Optional[str] = None) -> Path:
1100
+ """Convenience function to generate HTML report from JSON file.
1101
+
1102
+ Args:
1103
+ json_file_path: Path to the JSON export file
1104
+ output_path: Path where HTML report will be written
1105
+ title: Optional custom title for the report
1106
+
1107
+ Returns:
1108
+ Path to the generated HTML file
1109
+ """
1110
+ # Load JSON data
1111
+ with open(json_file_path, 'r', encoding='utf-8') as f:
1112
+ json_data = json.load(f)
1113
+
1114
+ # Generate HTML report
1115
+ generator = HTMLReportGenerator()
1116
+ return generator.generate_report(json_data, output_path, title)