Skip to content

Commit d22dfb4

Browse files
authored
Merge pull request #8 from microsoft:copilot/add-comparison-view-for-reports
Copilot/add comparison view for reports
2 parents 24dc86c + 2a32009 commit d22dfb4

6 files changed

Lines changed: 221 additions & 37 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
<p align="center">
2+
<img src="assets/adoqr_logo.png" alt="adoqr logo" width="400" />
3+
</p>
4+
15
# Azure DevOps Quick Review
26

37
Azure DevOps Quick Review (**adoqr**) is a PowerShell-based tool that analyzes

assets/adoqr_logo.png

343 KB
Loading

assets/exec_summary.png

109 KB
Loading

assets/remediation_steps.png

89.8 KB
Loading

docs/controls.html

Lines changed: 60 additions & 7 deletions
Large diffs are not rendered by default.

invoke-adoqr.ps1

Lines changed: 157 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,14 @@ function Write-RemediationHtmlReport {
13771377
$top5Issues = ($top5 | Measure-Object -Property Count -Sum).Sum
13781378
$top5Pct = if ($totalIssues -gt 0) { [math]::Round(($top5Issues / $totalIssues) * 100) } else { 0 }
13791379

1380+
# Branded header (shared with executive report + controls reference)
1381+
$issuesLabel = if ($totalIssues -eq 1) { '1 item to address' } else { "$([int]$totalIssues) items to address" }
1382+
$headerHtml = Get-AdoqrHeaderHtml -Eyebrow 'Remediation Plan' -Title $OrgName -MetaItems @(
1383+
"<a href=""$([System.Web.HttpUtility]::HtmlAttributeEncode($execFile))"">&larr; Back to Executive Summary</a>",
1384+
[System.Web.HttpUtility]::HtmlEncode($issuesLabel),
1385+
[System.Web.HttpUtility]::HtmlEncode($date)
1386+
)
1387+
13801388
$html = @"
13811389
<!DOCTYPE html>
13821390
<html lang="en">
@@ -1400,11 +1408,7 @@ function Write-RemediationHtmlReport {
14001408
a:hover { text-decoration: underline; }
14011409
.container { max-width: 1000px; margin: 0 auto; padding: 2rem 1.5rem; }
14021410
1403-
header { background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%); padding: 2.5rem 0; border-bottom: 1px solid var(--surface2); }
1404-
header h1 { margin: 0 0 .25rem; font-size: 1.75rem; font-weight: 700; }
1405-
header .subtitle { color: var(--text2); font-size: .95rem; }
1406-
.back-link { margin-top: .75rem; font-size: .9rem; }
1407-
1411+
$(Get-AdoqrHeaderCss)
14081412
.impact-box {
14091413
background: var(--surface); border-radius: var(--radius); padding: 1.5rem 2rem;
14101414
margin: 2rem 0; box-shadow: var(--shadow); display: flex; align-items: center;
@@ -1469,13 +1473,8 @@ function Write-RemediationHtmlReport {
14691473
</style>
14701474
</head>
14711475
<body>
1472-
<header>
1473-
<div class="container">
1474-
<h1>Remediation Plan</h1>
1475-
<p class="subtitle">Prioritized remediation actions for <strong>$([System.Web.HttpUtility]::HtmlEncode($OrgName))</strong></p>
1476-
<p class="back-link">&larr; <a href="$([System.Web.HttpUtility]::HtmlAttributeEncode($execFile))">Back to Executive Summary</a></p>
1477-
</div>
1478-
</header>
1476+
1477+
$headerHtml
14791478
14801479
<main class="container">
14811480
@@ -1505,6 +1504,135 @@ function Write-RemediationHtmlReport {
15051504
Write-Host " Remediation report saved: $FilePath" -ForegroundColor Green
15061505
}
15071506

1507+
# Returns the adoqr logo as a base64 data URI so HTML reports stay self-contained
1508+
# when shared without the assets folder. Cached after first read; returns '' on
1509+
# failure so callers can gracefully fall back to a text-only header.
1510+
function Get-AdoqrLogoDataUri {
1511+
if ($script:AdoqrLogoDataUri -is [string]) { return $script:AdoqrLogoDataUri }
1512+
$script:AdoqrLogoDataUri = ''
1513+
try {
1514+
# Prefer the script's own directory; fall back to caller invocation root,
1515+
# then current location. This lets the helper work both when invoke-adoqr.ps1
1516+
# runs normally and when the function is loaded standalone (e.g. tests).
1517+
$root = if ($PSScriptRoot) { $PSScriptRoot }
1518+
elseif ($MyInvocation.PSScriptRoot) { $MyInvocation.PSScriptRoot }
1519+
else { (Get-Location).Path }
1520+
$logoPath = Join-Path $root 'assets/adoqr_logo.png'
1521+
if (-not (Test-Path -LiteralPath $logoPath)) { return $script:AdoqrLogoDataUri }
1522+
$bytes = [System.IO.File]::ReadAllBytes($logoPath)
1523+
if ($bytes.Length -gt 0) {
1524+
$script:AdoqrLogoDataUri = 'data:image/png;base64,' + [Convert]::ToBase64String($bytes)
1525+
}
1526+
} catch {
1527+
Write-Verbose "Could not embed adoqr logo: $_"
1528+
}
1529+
return $script:AdoqrLogoDataUri
1530+
}
1531+
1532+
# Returns the shared CSS rules for the adoqr branded header. Used by every
1533+
# generated HTML report (executive summary, remediation plan) and kept in sync
1534+
# with docs/controls.html for a consistent look.
1535+
function Get-AdoqrHeaderCss {
1536+
return @'
1537+
header { background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%); padding: 2rem 0; border-bottom: 1px solid var(--surface2); }
1538+
.header-brand { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; margin: 0; }
1539+
.header-brand .header-logo {
1540+
display: block; height: 72px; width: auto; max-width: 100%;
1541+
flex: 0 0 auto;
1542+
filter: drop-shadow(0 4px 12px rgba(0,0,0,.35));
1543+
}
1544+
.header-brand .header-title-group { min-width: 0; }
1545+
.header-brand .header-logo + .header-title-group {
1546+
padding-left: 1.5rem;
1547+
border-left: 1px solid rgba(255,255,255,.12);
1548+
}
1549+
.header-brand h1 {
1550+
margin: 0; font-size: 1.5rem; font-weight: 700; line-height: 1.2;
1551+
display: flex; flex-direction: column; gap: .15rem;
1552+
}
1553+
.header-eyebrow {
1554+
font-size: .72rem; font-weight: 700; text-transform: uppercase;
1555+
letter-spacing: .14em; color: var(--text2);
1556+
}
1557+
.header-org { color: var(--text); }
1558+
.header-meta {
1559+
display: flex; flex-wrap: wrap; align-items: center;
1560+
gap: .35rem .85rem; margin: 1.25rem 0 0;
1561+
color: var(--text2); font-size: .82rem;
1562+
}
1563+
.header-meta > span { display: inline-flex; align-items: center; }
1564+
.header-meta > span + span::before {
1565+
content: ''; display: inline-block;
1566+
width: 3px; height: 3px; border-radius: 50%;
1567+
background: currentColor; opacity: .5;
1568+
margin-right: .85rem;
1569+
}
1570+
.header-meta a { color: inherit; text-decoration: none; border-bottom: 1px dotted rgba(148,163,184,.4); }
1571+
.header-meta a:hover { color: var(--text); border-bottom-color: var(--text); }
1572+
.visually-hidden {
1573+
position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
1574+
overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
1575+
}
1576+
@media (max-width: 640px) {
1577+
header { padding: 1.5rem 0; }
1578+
.header-brand { gap: 1rem; }
1579+
.header-brand .header-logo { height: 56px; }
1580+
.header-brand .header-logo + .header-title-group { padding-left: 1rem; }
1581+
.header-brand h1 { font-size: 1.25rem; }
1582+
.header-meta { margin-top: 1rem; font-size: .78rem; gap: .25rem .65rem; }
1583+
.header-meta > span + span::before { margin-right: .65rem; }
1584+
}
1585+
'@
1586+
}
1587+
1588+
# Returns the shared <header> HTML for all adoqr reports.
1589+
# - Eyebrow: small caps label (e.g. "Executive Summary", "Remediation Plan").
1590+
# - Title: prominent visible heading (usually the org name).
1591+
# - MetaItems: optional array of HTML snippets rendered as a bullet-separated
1592+
# meta row. Each item is already HTML-encoded by the caller.
1593+
function Get-AdoqrHeaderHtml {
1594+
param(
1595+
[Parameter(Mandatory)][string]$Eyebrow,
1596+
[Parameter(Mandatory)][string]$Title,
1597+
[string[]]$MetaItems = @()
1598+
)
1599+
1600+
$logoDataUri = Get-AdoqrLogoDataUri
1601+
$logoImgHtml = if ($logoDataUri) {
1602+
"<img class=""header-logo"" src=""$logoDataUri"" alt=""ADOQR — Azure DevOps Quick Review"" width=""288"" height=""72"" />"
1603+
} else { '' }
1604+
1605+
$eyebrowEnc = [System.Web.HttpUtility]::HtmlEncode($Eyebrow)
1606+
$titleEnc = [System.Web.HttpUtility]::HtmlEncode($Title)
1607+
1608+
$metaHtml = ''
1609+
if ($MetaItems -and $MetaItems.Count -gt 0) {
1610+
$spans = ($MetaItems | ForEach-Object { "<span>$_</span>" }) -join "`n "
1611+
$metaHtml = @"
1612+
<p class="header-meta" aria-label="Report metadata">
1613+
$spans
1614+
</p>
1615+
"@
1616+
}
1617+
1618+
return @"
1619+
<header>
1620+
<div class="container">
1621+
<div class="header-brand">
1622+
$logoImgHtml
1623+
<div class="header-title-group">
1624+
<h1>
1625+
<span class="header-eyebrow">$eyebrowEnc</span>
1626+
<span class="header-org">$titleEnc</span>
1627+
</h1>
1628+
</div>
1629+
</div>
1630+
$metaHtml
1631+
</div>
1632+
</header>
1633+
"@
1634+
}
1635+
15081636
function Write-ExecutiveHtmlReport {
15091637
param(
15101638
[string]$FilePath,
@@ -1583,6 +1711,18 @@ function Write-ExecutiveHtmlReport {
15831711
$orgMdFile = [System.IO.Path]::GetFileName($OrgSummary.ReportFile)
15841712
$notCheckedHtml = Build-NotCheckedSectionHtml -OrgSummary $OrgSummary -ProjectSummaries $ProjectSummaries
15851713

1714+
# Branded header (shared with remediation plan + controls reference)
1715+
$orgUrlDisplay = $OrgUrl -replace '^https?://', ''
1716+
$orgUrlAttr = [System.Web.HttpUtility]::HtmlAttributeEncode($OrgUrl)
1717+
$orgUrlDisplayEnc = [System.Web.HttpUtility]::HtmlEncode($orgUrlDisplay)
1718+
$projectsLabel = if ($totalProjects -eq 1) { '1 project' } else { "$totalProjects projects" }
1719+
$headerHtml = Get-AdoqrHeaderHtml -Eyebrow 'Executive Summary' -Title $OrgName -MetaItems @(
1720+
"<a href=""$orgUrlAttr"" target=""_blank"" rel=""noopener noreferrer"">$orgUrlDisplayEnc</a>",
1721+
[System.Web.HttpUtility]::HtmlEncode($projectsLabel),
1722+
[System.Web.HttpUtility]::HtmlEncode($ElapsedTime),
1723+
[System.Web.HttpUtility]::HtmlEncode($date)
1724+
)
1725+
15861726
$html = @"
15871727
<!DOCTYPE html>
15881728
<html lang="en">
@@ -1609,9 +1749,7 @@ function Write-ExecutiveHtmlReport {
16091749
16101750
.container { max-width: 1200px; margin: 0 auto; padding: 2rem 1.5rem; }
16111751
1612-
header { background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%); padding: 2.5rem 0; border-bottom: 1px solid var(--surface2); }
1613-
header h1 { margin: 0 0 .25rem; font-size: 1.75rem; font-weight: 700; }
1614-
header .subtitle { color: var(--text2); font-size: .95rem; }
1752+
$(Get-AdoqrHeaderCss)
16151753
.meta { display: flex; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
16161754
.meta-item { font-size: .85rem; color: var(--text2); }
16171755
.meta-item strong { color: var(--text); }
@@ -1912,27 +2050,16 @@ function Write-ExecutiveHtmlReport {
19122050
<body>
19132051
<a href="#main" class="skip-link">Skip to main content</a>
19142052
1915-
<header>
1916-
<div class="container">
1917-
<h1>Azure DevOps Quick Review</h1>
1918-
<p class="subtitle">Executive Summary for <strong>$([System.Web.HttpUtility]::HtmlEncode($OrgName))</strong></p>
1919-
<div class="meta" role="list">
1920-
<span class="meta-item" role="listitem"><strong>Date:</strong> $date</span>
1921-
<span class="meta-item" role="listitem"><strong>Organization:</strong> $([System.Web.HttpUtility]::HtmlEncode($OrgUrl))</span>
1922-
<span class="meta-item" role="listitem"><strong>Projects:</strong> $totalProjects</span>
1923-
<span class="meta-item" role="listitem"><strong>Duration:</strong> $ElapsedTime</span>
1924-
</div>
1925-
</div>
1926-
</header>
2053+
$headerHtml
19272054
19282055
<nav class="section-nav" aria-label="Section navigation">
19292056
<div class="section-nav-inner">
19302057
<a href="#adoption" data-target="adoption">Overview</a>
1931-
<a href="#not-checked-section" data-target="not-checked-section">Not Checked</a>
19322058
<a href="#top-remediations" data-target="top-remediations">Top Actions</a>
19332059
<a href="#hot-spots" data-target="hot-spots">Hot Spots</a>
19342060
<a href="#organization" data-target="organization">Organization</a>
19352061
<a href="#project-results" data-target="project-results">Projects</a>
2062+
<a href="#not-checked-section" data-target="not-checked-section">Not Checked</a>
19362063
<a href="#comparison-section" data-target="comparison-section">Run Comparison</a>
19372064
<span class="section-nav-resources">
19382065
<a class="nav-external" href="https://microsoft.github.io/adoqr/controls.html"
@@ -1989,8 +2116,6 @@ function Write-ExecutiveHtmlReport {
19892116
</div>
19902117
</section>
19912118
1992-
$notCheckedHtml
1993-
19942119
<!-- Priority Remediation Actions -->
19952120
$(if ($TopRemediations -and $TopRemediations.Count -gt 0) {
19962121
$totalRemedIssues = ($TopRemediations | Measure-Object -Property Count -Sum).Sum
@@ -2088,6 +2213,8 @@ function Write-ExecutiveHtmlReport {
20882213
</div>
20892214
</section>
20902215
2216+
$notCheckedHtml
2217+
20912218
$ComparisonHtml
20922219
20932220
</main>

0 commit comments

Comments
 (0)