gitflow-analytics 1.0.1__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.
- gitflow_analytics/__init__.py +11 -11
- gitflow_analytics/_version.py +2 -2
- gitflow_analytics/classification/__init__.py +31 -0
- gitflow_analytics/classification/batch_classifier.py +752 -0
- gitflow_analytics/classification/classifier.py +464 -0
- gitflow_analytics/classification/feature_extractor.py +725 -0
- gitflow_analytics/classification/linguist_analyzer.py +574 -0
- gitflow_analytics/classification/model.py +455 -0
- gitflow_analytics/cli.py +4490 -378
- gitflow_analytics/cli_rich.py +503 -0
- gitflow_analytics/config/__init__.py +43 -0
- gitflow_analytics/config/errors.py +261 -0
- gitflow_analytics/config/loader.py +904 -0
- gitflow_analytics/config/profiles.py +264 -0
- gitflow_analytics/config/repository.py +124 -0
- gitflow_analytics/config/schema.py +441 -0
- gitflow_analytics/config/validator.py +154 -0
- gitflow_analytics/config.py +44 -398
- gitflow_analytics/core/analyzer.py +1320 -172
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +1554 -175
- gitflow_analytics/core/data_fetcher.py +1193 -0
- gitflow_analytics/core/identity.py +571 -185
- gitflow_analytics/core/metrics_storage.py +526 -0
- gitflow_analytics/core/progress.py +372 -0
- gitflow_analytics/core/schema_version.py +269 -0
- gitflow_analytics/extractors/base.py +13 -11
- gitflow_analytics/extractors/ml_tickets.py +1100 -0
- gitflow_analytics/extractors/story_points.py +77 -59
- gitflow_analytics/extractors/tickets.py +841 -89
- gitflow_analytics/identity_llm/__init__.py +6 -0
- gitflow_analytics/identity_llm/analysis_pass.py +231 -0
- gitflow_analytics/identity_llm/analyzer.py +464 -0
- gitflow_analytics/identity_llm/models.py +76 -0
- gitflow_analytics/integrations/github_integration.py +258 -87
- gitflow_analytics/integrations/jira_integration.py +572 -123
- gitflow_analytics/integrations/orchestrator.py +206 -82
- gitflow_analytics/metrics/activity_scoring.py +322 -0
- gitflow_analytics/metrics/branch_health.py +470 -0
- gitflow_analytics/metrics/dora.py +542 -179
- gitflow_analytics/models/database.py +986 -59
- gitflow_analytics/pm_framework/__init__.py +115 -0
- gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
- gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
- gitflow_analytics/pm_framework/base.py +406 -0
- gitflow_analytics/pm_framework/models.py +211 -0
- gitflow_analytics/pm_framework/orchestrator.py +652 -0
- gitflow_analytics/pm_framework/registry.py +333 -0
- gitflow_analytics/qualitative/__init__.py +29 -0
- gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
- gitflow_analytics/qualitative/classifiers/change_type.py +742 -0
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +506 -0
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +535 -0
- gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
- gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
- gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
- gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
- gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
- gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
- gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
- gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +438 -0
- gitflow_analytics/qualitative/core/__init__.py +13 -0
- gitflow_analytics/qualitative/core/llm_fallback.py +657 -0
- gitflow_analytics/qualitative/core/nlp_engine.py +382 -0
- gitflow_analytics/qualitative/core/pattern_cache.py +479 -0
- gitflow_analytics/qualitative/core/processor.py +673 -0
- gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
- gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
- gitflow_analytics/qualitative/models/__init__.py +25 -0
- gitflow_analytics/qualitative/models/schemas.py +306 -0
- gitflow_analytics/qualitative/utils/__init__.py +13 -0
- gitflow_analytics/qualitative/utils/batch_processor.py +339 -0
- gitflow_analytics/qualitative/utils/cost_tracker.py +345 -0
- gitflow_analytics/qualitative/utils/metrics.py +361 -0
- gitflow_analytics/qualitative/utils/text_processing.py +285 -0
- gitflow_analytics/reports/__init__.py +100 -0
- gitflow_analytics/reports/analytics_writer.py +550 -18
- gitflow_analytics/reports/base.py +648 -0
- gitflow_analytics/reports/branch_health_writer.py +322 -0
- gitflow_analytics/reports/classification_writer.py +924 -0
- gitflow_analytics/reports/cli_integration.py +427 -0
- gitflow_analytics/reports/csv_writer.py +1700 -216
- gitflow_analytics/reports/data_models.py +504 -0
- gitflow_analytics/reports/database_report_generator.py +427 -0
- gitflow_analytics/reports/example_usage.py +344 -0
- gitflow_analytics/reports/factory.py +499 -0
- gitflow_analytics/reports/formatters.py +698 -0
- gitflow_analytics/reports/html_generator.py +1116 -0
- gitflow_analytics/reports/interfaces.py +489 -0
- gitflow_analytics/reports/json_exporter.py +2770 -0
- gitflow_analytics/reports/narrative_writer.py +2289 -158
- gitflow_analytics/reports/story_point_correlation.py +1144 -0
- gitflow_analytics/reports/weekly_trends_writer.py +389 -0
- gitflow_analytics/training/__init__.py +5 -0
- gitflow_analytics/training/model_loader.py +377 -0
- gitflow_analytics/training/pipeline.py +550 -0
- gitflow_analytics/tui/__init__.py +5 -0
- gitflow_analytics/tui/app.py +724 -0
- gitflow_analytics/tui/screens/__init__.py +8 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +496 -0
- gitflow_analytics/tui/screens/configuration_screen.py +523 -0
- gitflow_analytics/tui/screens/loading_screen.py +348 -0
- gitflow_analytics/tui/screens/main_screen.py +321 -0
- gitflow_analytics/tui/screens/results_screen.py +722 -0
- gitflow_analytics/tui/widgets/__init__.py +7 -0
- gitflow_analytics/tui/widgets/data_table.py +255 -0
- gitflow_analytics/tui/widgets/export_modal.py +301 -0
- gitflow_analytics/tui/widgets/progress_widget.py +187 -0
- gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
- gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
- gitflow_analytics-1.0.1.dist-info/METADATA +0 -463
- gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.1.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)
|