diff --git a/.claude/commands/pr-report.md b/.claude/commands/pr-report.md
index 02f83003f18..3a410442b6f 100644
--- a/.claude/commands/pr-report.md
+++ b/.claude/commands/pr-report.md
@@ -75,6 +75,7 @@ This command generates **two reports** (with two optional additions):
| `$OUTPUT_DIR/weekly_pr_report_fast.md` | Data-focused report with metrics and PR listings |
| `$OUTPUT_DIR/hypershift_pr_details_fast.json` | Raw PR data in JSON format |
| `$OUTPUT_DIR/hypershift_pr_summary.json` | Compact summary data for LLM analysis |
+| `$OUTPUT_DIR/weekly_pr_report.html` | Visual HTML dashboard with metrics, charts, and PR listings |
| `$OUTPUT_DIR/weekly_pr_report_impact.md` | LLM-generated impact analysis for contributors |
| `$OUTPUT_DIR/pr_scored.json` | (--score) Ranked PR list for deep analysis |
| `.work/pr_deep/*.json` | (--deep mode) Per-PR data with diffs for analysis |
@@ -148,6 +149,15 @@ Read `$OUTPUT_DIR/hypershift_pr_details_fast.json` and `$OUTPUT_DIR/jira_hierarc
significant changes, and overall project momentum. Write in a tone suitable for developers and
community members following the project.]
+## Highlights
+
+[3-5 bullet points with the most impactful changes this week. Each bullet should be one concise
+sentence focused on what shipped and why it matters. These are injected into the HTML dashboard.]
+
+- [Highlight 1]
+- [Highlight 2]
+- [Highlight 3]
+
## Strategic Initiatives Progress
[For each OCPSTRAT initiative with activity this week, provide:]
@@ -277,6 +287,25 @@ manual CI work). For each:]
| "GCP-216: feat(nodepool): add GCP platform support" | Adds foundational NodePool support for GCP platform, enabling cluster autoscaling and machine management. This is a key milestone for GCP HyperShift availability. |
| "CNTRLPLANE-2082: hypershift: run conformance directly on the root cluster" | Simplifies CI architecture by running conformance tests on the management cluster instead of nested clusters. Reduces resource requirements and test complexity. |
+**After writing the impact report**, inject the highlights into the HTML dashboard:
+
+1. Read `$OUTPUT_DIR/weekly_pr_report.html`
+2. Take the bullet points from the `## Highlights` section you just wrote
+3. Replace `
` with:
+
+```html
+
+
Week Highlights
+
+
First highlight bullet
+
Second highlight bullet
+ ...
+
+
+```
+
+4. Write the updated HTML back using the Edit tool
+
### Step 4: Deep Code Analysis (--deep mode only)
If `--deep` flag is specified, perform deep code analysis after the standard report:
@@ -698,6 +727,7 @@ After generating reports, provide the user with:
5. (--progress-report mode) Mention the progress report location
6. (--breaking-changes mode) Summarize breaking changes found and their severity
7. (--deep mode) Mention the collaboration report location
+8. Location of the HTML dashboard report (`weekly_pr_report.html`)
## Script Features
diff --git a/contrib/repo_metrics/weekly_pr_report.py b/contrib/repo_metrics/weekly_pr_report.py
index 9879802cc0b..e32d91082e8 100755
--- a/contrib/repo_metrics/weekly_pr_report.py
+++ b/contrib/repo_metrics/weekly_pr_report.py
@@ -7,6 +7,7 @@
import argparse
import asyncio
import base64
+import html as html_mod
import json
import os
import re
@@ -1194,6 +1195,272 @@ def generate_report(self, output_path: str):
print(f"Report written to {output_path}")
+ def generate_html_report(self, output_path: str):
+ """Generate a deterministic HTML dashboard report from PR data."""
+ e = html_mod.escape
+
+ # --- Compute stats ---
+ total = len(self.prs)
+ hs_count = len([p for p in self.prs if p['repo'] == 'openshift/hypershift'])
+ ai_count = len([p for p in self.prs if p['repo'] == 'openshift-eng/ai-helpers'])
+ en_count = len([p for p in self.prs if p['repo'] == 'openshift/enhancements'])
+ rl_count = len([p for p in self.prs if p['repo'] == 'openshift/release'])
+
+ authors = set(p['author'] for p in self.prs)
+ all_reviewers: Dict[str, int] = {}
+ for pr in self.prs:
+ for r in pr.get('reviewers', []):
+ all_reviewers[r] = all_reviewers.get(r, 0) + 1
+ top_reviewers = sorted(all_reviewers.items(), key=lambda x: x[1], reverse=True)[:5]
+ max_reviews = top_reviewers[0][1] if top_reviewers else 1
+
+ merge_times = [p['readyToMergeHours'] for p in self.prs if p.get('readyToMergeHours')]
+ avg_merge = f"{sum(merge_times)/len(merge_times):.1f}" if merge_times else "N/A"
+ median_merge = f"{sorted(merge_times)[len(merge_times)//2]:.1f}" if merge_times else "N/A"
+ fastest = min(self.prs, key=lambda p: p.get('readyToMergeHours') or float('inf'))
+ fastest_h = f"{fastest.get('readyToMergeHours', 0):.1f}"
+
+ merge_days: Dict[str, int] = {}
+ for pr in self.prs:
+ day = pr['mergedAt'].split('T')[0]
+ merge_days[day] = merge_days.get(day, 0) + 1
+ sorted_days = sorted(merge_days.items(), key=lambda x: x[1], reverse=True)[:7]
+ max_day_count = sorted_days[0][1] if sorted_days else 1
+
+ # Categorise PRs
+ bug_prs = []
+ ai_prs = []
+ enhancement_prs = []
+ feature_prs = []
+ for pr in self.prs:
+ if pr['repo'] == 'openshift-eng/ai-helpers':
+ ai_prs.append(pr)
+ elif pr['repo'] == 'openshift/enhancements':
+ enhancement_prs.append(pr)
+ elif any(t.startswith('OCPBUGS') for t in pr.get('jiraTickets', [])):
+ bug_prs.append(pr)
+ else:
+ feature_prs.append(pr)
+
+ bug_prs.sort(key=lambda p: p.get('readyToMergeHours') or 0)
+ enhancement_prs.sort(key=lambda p: p['mergedAt'], reverse=True)
+ ai_prs.sort(key=lambda p: p['mergedAt'], reverse=True)
+ feature_prs.sort(key=lambda p: p['mergedAt'], reverse=True)
+
+ generated = datetime.now(tz=__import__('datetime').timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
+
+ # --- Helper to build a PR table row ---
+ def _pr_row(pr: Dict, show_repo: bool = False) -> str:
+ title = e(pr['title'][:90])
+ merge_h = f"{pr.get('readyToMergeHours', 0):.1f}h"
+ repo_col = f"