@@ -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 )) "" >← 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">← <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+
15081636function 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