← zurück zur Toolbox

Active-Directory-Group-Policy-Html-Report.ps1 · Quelltext

64237 Bytes · SHA-256: 89c5091359370286e144022cb3aaf3ddb300b2b15622f77b6ea0803bfaeb64f8

⤓ Download
# TOOLBOX-TILE: {"title": "Active Directory Group Policy Html Report", "desc": "Richtlinienergebnissatz (RSoP) als moderner HTML-Bericht — GPOs mit Filtergrund, wirksame Einstellungen je Erweiterung, Sicherheitsgruppen, Ereignisprotokoll; erzeugt HTML + XML. Windows PowerShell 5.1, keine Abhängigkeiten.", "icon": "📋", "type": "download", "os": ["windows"], "order": 28}
# ============================================================================
# Active-Directory-Group-Policy-Html-Report.ps1
# Moderner HTML-Bericht des Richtlinienergebnissatzes (RSoP) — erzeugt HTML + XML.
#
# Schnellstart:
#
#   # 0) Skript ausführbar machen — einmalig, je nach Situation:
#   Unblock-File .\Active-Directory-Group-Policy-Html-Report.ps1        # falls aus dem Internet geladen
#   Set-ExecutionPolicy -Scope CurrentUser RemoteSigned                 # dauerhaft für den Benutzer
#   powershell -ExecutionPolicy Bypass -File .\Active-Directory-Group-Policy-Html-Report.ps1   # oder nur für diesen Aufruf
#
#   # 1) RSoP erheben und Bericht öffnen (HTML + XML landen in $HOME):
#   .\Active-Directory-Group-Policy-Html-Report.ps1 -Show
#
#   # 2) Bericht aus einer vorhandenen XML-Datei rendern:
#   .\Active-Directory-Group-Policy-Html-Report.ps1 -InputXml .\XMLReport.xml -Path .\Bericht.html
#
#   # 3) Ablageorte für HTML und XML selbst bestimmen:
#   .\Active-Directory-Group-Policy-Html-Report.ps1 -Path C:\Temp\RSoP.html -XmlPath C:\Temp\RSoP.xml
#
#   # 4) Nur HTML, ohne XML-Rohdaten:
#   .\Active-Directory-Group-Policy-Html-Report.ps1 -NoXml
#
#   # Hinweis: Computerrichtlinien erfordern eine als Administrator gestartete
#   # PowerShell — ohne Adminrechte enthält der Bericht nur den Benutzerbereich.
# ============================================================================

<#
.SYNOPSIS
    Erzeugt einen modernen, eigenständigen HTML-Bericht des Richtlinienergebnissatzes (RSoP).

.DESCRIPTION
    Sammelt die RSoP-Daten als XML (Get-GPResultantSetOfPolicy, sonst gpresult /X)
    oder liest eine vorhandene XML-Datei ein und rendert daraus einen HTML-Bericht:
    dunkle Navigations-Leiste, helle Lesefläche in voller Breite, Hell/Dunkel-Modus,
    Inline-SVG-Icons, druckbar — alles in einer Datei, ohne externe Abhängigkeiten.

    Benötigt nur Windows PowerShell 5.1 (Bordmittel). Das GroupPolicy-Modul
    (z. B. auf einem Domänencontroller) wird genutzt, wenn vorhanden — sonst
    fällt das Skript auf gpresult.exe zurück.

.PARAMETER InputXml
    Vorhandene RSoP-XML-Datei (aus "gpresult /X" oder
    "Get-GPResultantSetOfPolicy -ReportType Xml"). Ohne Angabe werden die
    Daten des lokalen Rechners/angemeldeten Benutzers frisch erhoben.

.PARAMETER Path
    Zieldatei des HTML-Berichts. Standard: $HOME\GPResult-Report.html

.PARAMETER Show
    Öffnet den fertigen Bericht im Standardbrowser.

.PARAMETER XmlPath
    Ablageort für die erhobenen RSoP-Rohdaten (XML). Standard: wie -Path, nur
    mit Endung .xml — wie beim klassischen gpresult entstehen also HTML und XML.
    Wirkt nur, wenn das Skript die Daten selbst erhebt (ohne -InputXml).

.PARAMETER NoXml
    Verwirft die erhobenen XML-Rohdaten nach dem Rendern (nur das HTML bleibt).

.EXAMPLE
    .\Active-Directory-Group-Policy-Html-Report.ps1 -Show
    Erhebt die RSoP-Daten des lokalen Systems, schreibt HTML + XML und öffnet den Bericht.

.EXAMPLE
    .\Active-Directory-Group-Policy-Html-Report.ps1 -InputXml .\XMLReport.xml -Path .\GPResult-Report.html
    Rendert einen Bericht aus einer vorhandenen XML-Datei.

.NOTES
    Ohne Administratorrechte enthält der Ergebnissatz nur den Benutzerbereich;
    der Computerbereich erfordert eine erhöhte Sitzung.
#>
[CmdletBinding()]
param(
    [string]$InputXml,
    [string]$Path = (Join-Path $HOME 'GPResult-Report.html'),
    [switch]$Show,
    [string]$XmlPath,
    [switch]$NoXml
)

$ErrorActionPreference = 'Stop'

# ============================================================================
#region Hilfsfunktionen
# ============================================================================

function esc([string]$s) {
    if ($null -eq $s) { return '' }
    return [System.Net.WebUtility]::HtmlEncode($s)
}

function txt($node) {
    # Liefert den Text eines XML-Knotens — egal ob PowerShell ihn als String
    # oder (bei Attributen/Namespaces) als XmlElement adaptiert hat.
    if ($null -eq $node) { return '' }
    if ($node -is [System.Xml.XmlElement]) { return $node.InnerText }
    return [string]$node
}

function AsArray($x) {
    if ($null -eq $x) { return @() }
    return @($x)
}

function boolDe([string]$v) {
    switch -Regex ($v) {
        '^(?i)true$'  { 'Ja'; break }
        '^(?i)false$' { 'Nein'; break }
        default       { $v }
    }
}

function ParseIsoDate([string]$s) {
    if ([string]::IsNullOrWhiteSpace($s)) { return $null }
    try {
        return [datetime]::Parse($s, [Globalization.CultureInfo]::InvariantCulture,
            [Globalization.DateTimeStyles]::RoundtripKind)
    } catch { return $null }
}

function FmtDate($dt) {
    if ($null -eq $dt) { return '—' }
    if ($dt.Kind -eq [System.DateTimeKind]::Utc) { $dt = $dt.ToLocalTime() }
    return $dt.ToString('dd.MM.yyyy HH:mm:ss')
}

function badge([string]$kind, [string]$label) {
    return '<span class="badge b-' + $kind + '">' + (esc $label) + '</span>'
}

#endregion

# ============================================================================
#region Icons (Inline-SVG, erben die Textfarbe via currentColor)
# ============================================================================

$Icons = @{
    user    = '<circle cx="9" cy="6" r="3.1"/><path d="M2.6 16a6.4 6.4 0 0 1 12.8 0"/>'
    users   = '<circle cx="6.5" cy="6" r="2.9"/><path d="M1.2 15.6a5.4 5.4 0 0 1 10.6 0"/><path d="M12 3.4a2.9 2.9 0 0 1 0 5.4M13.2 15.6a5.4 5.4 0 0 0-2.9-4.5"/>'
    pc      = '<rect x="2" y="3" width="14" height="9.5" rx="1.4"/><path d="M6.5 16h5M9 12.5V16"/>'
    shield  = '<path d="M9 1.6l6 2.2v5c0 4.4-3 6.9-6 8.5-3-1.6-6-4.1-6-8.5v-5z"/><path d="M6.2 9.2l2 2 3.6-4"/>'
    layers  = '<path d="M9 2l7 3.5L9 9 2 5.5z"/><path d="M2 9l7 3.5L16 9"/><path d="M2 12.5L9 16l7-3.5"/>'
    target  = '<circle cx="9" cy="9" r="7"/><circle cx="9" cy="9" r="3"/>'
    gear    = '<circle cx="9" cy="9" r="2.9"/><path d="M9 1.6v2M9 14.4v2M1.6 9h2M14.4 9h2M3.8 3.8l1.4 1.4M12.8 12.8l1.4 1.4M14.2 3.8l-1.4 1.4M5.2 12.8l-1.4 1.4"/>'
    clock   = '<circle cx="9" cy="9" r="7"/><path d="M9 5v4l2.7 1.6"/>'
    list    = '<path d="M4 2h7l3 3v11H4z"/><path d="M11 2v3h3M6.5 9h5M6.5 12h5"/>'
    globe   = '<circle cx="9" cy="9" r="7"/><path d="M2 9h14M9 2c2.4 2 2.4 12 0 14M9 2c-2.4 2-2.4 12 0 14"/>'
    bolt    = '<path d="M10 1.5L3.5 10.5h4l-1 6L13 7.5H9z"/>'
    sliders = '<path d="M3 5h12M3 9h12M3 13h12"/><circle cx="6.5" cy="5" r="1.6"/><circle cx="11.5" cy="9" r="1.6"/><circle cx="7.5" cy="13" r="1.6"/>'
    home    = '<path d="M3 8.5L9 3l6 5.5"/><path d="M4.6 7.6V15h8.8V7.6"/>'
    alert   = '<path d="M9 2.2L16.6 15H1.4z"/><path d="M9 7v4M9 13.3v.2"/>'
    printer = '<rect x="3.5" y="7" width="11" height="6" rx="1"/><path d="M5.5 7V3h7v4M5.5 12.5V16h7v-3.5"/>'
    sun     = '<circle cx="9" cy="9" r="3.2"/><path d="M9 1.6v1.8M9 14.6v1.8M1.6 9h1.8M14.6 9h1.8M3.7 3.7l1.3 1.3M13 13l1.3 1.3M14.3 3.7L13 5M5 13l-1.3 1.3"/>'
    link    = '<path d="M7.2 10.8a3.4 3.4 0 0 1 0-4.8l2-2a3.4 3.4 0 0 1 4.8 4.8l-1 1"/><path d="M10.8 7.2a3.4 3.4 0 0 1 0 4.8l-2 2a3.4 3.4 0 0 1-4.8-4.8l1-1"/>'
}

function ic([string]$name) {
    $body = $Icons[$name]
    if (-not $body) { $body = $Icons['list'] }
    return '<svg class="i" viewBox="0 0 18 18" aria-hidden="true">' + $body + '</svg>'
}

#endregion

# ============================================================================
#region Statisches CSS und JS  (Design „Boards & Ice“ — volle Breite)
# ============================================================================

$Css = @'
:root{
 --bg:#EAF1F6; --rail:#0C1E2E; --rail-2:#0A1826; --rail-ink:#DEE9F1; --rail-muted:#90A7B9; --rail-line:#1E3346;
 --pane:#F3F7FA; --surface:#FFFFFF; --surface-2:#EEF4F8; --ink:#122A3C; --ink-2:#3A5468; --muted:#6E869A;
 --line:#D4E0E9; --accent:#0E76AC; --accent-strong:#0B5D89; --accent-tint:#E2EFF7; --accent-ink:#FFFFFF;
 --red:#B23A32; --red-t:#F6E6E4; --amber:#9B6A1E; --amber-t:#F5ECDA; --green:#2A7D57; --green-t:#E2F0EA;
 --shadow:0 1px 2px rgba(16,40,60,.06), 0 10px 30px rgba(16,40,60,.07);
 --sans:'Segoe UI',system-ui,-apple-system,'Inter',Roboto,'Helvetica Neue',Arial,sans-serif;
 --mono:ui-monospace,'Cascadia Code','JetBrains Mono','SF Mono',Menlo,Consolas,monospace;
}
:root[data-theme="dark"]{
 --bg:#060E17; --rail:#07121D; --rail-2:#050D16; --rail-ink:#CFDEE9; --rail-muted:#7E95A7; --rail-line:#172A39;
 --pane:#0C1D2B; --surface:#122A3B; --surface-2:#0F2333; --ink:#DCE7EF; --ink-2:#A7BDCC; --muted:#7E96A8;
 --line:#20384A; --accent:#41A9DD; --accent-strong:#79C4EA; --accent-tint:#123044; --accent-ink:#06131D;
 --red:#E5837A; --red-t:#2C1917; --amber:#D5A24E; --amber-t:#292112; --green:#5FBB8D; --green-t:#112619;
 --shadow:0 1px 2px rgba(0,0,0,.45), 0 12px 34px rgba(0,0,0,.4);
}
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{font-family:var(--sans);background:var(--bg);color:var(--ink);
 -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
a{color:inherit}
svg.i{width:1em;height:1em;stroke:currentColor;fill:none;stroke-width:1.6;
 stroke-linecap:round;stroke-linejoin:round;vertical-align:-.13em}
.shell{display:grid;grid-template-columns:308px 1fr;height:100vh;overflow:hidden}

/* ---------- Rail (Navigation) ---------- */
.rail{background:linear-gradient(180deg,var(--rail),var(--rail-2));color:var(--rail-ink);
 border-right:1px solid var(--rail-line);overflow-y:auto;overflow-x:hidden;padding:0 0 40px}
.rail::-webkit-scrollbar{width:9px}.rail::-webkit-scrollbar-thumb{background:var(--rail-line);border-radius:9px}
.brand{display:flex;align-items:center;gap:11px;padding:22px 22px 18px;position:sticky;top:0;
 background:linear-gradient(180deg,var(--rail),rgba(12,30,46,.86));backdrop-filter:blur(6px);
 border-bottom:1px solid var(--rail-line);cursor:pointer;z-index:2}
.logo{width:26px;height:26px;border-radius:6px;flex:0 0 auto;position:relative;
 background:radial-gradient(circle at 50% 42%,var(--accent),var(--accent-strong));box-shadow:0 0 0 3px rgba(65,169,221,.15)}
.logo::after{content:"";position:absolute;inset:8px;border:1.6px solid rgba(255,255,255,.85);border-radius:50%}
.brand b{font-size:.94rem;letter-spacing:.2px;line-height:1.1}
.brand small{display:block;font-family:var(--mono);font-size:.62rem;letter-spacing:.16em;
 text-transform:uppercase;color:var(--rail-muted);margin-bottom:2px}
.overview{display:flex;gap:10px;align-items:center;margin:14px 14px 4px;padding:10px 12px;border-radius:9px;
 font-family:var(--mono);font-size:.72rem;letter-spacing:.06em;text-transform:uppercase;color:var(--rail-muted);
 cursor:pointer;border:1px solid transparent;text-decoration:none}
.overview:hover{background:rgba(255,255,255,.04);color:var(--rail-ink)}
.overview.active{color:var(--accent-strong);border-color:var(--rail-line);background:rgba(65,169,221,.08)}
.cat{display:flex;align-items:center;gap:8px;margin:20px 22px 8px;font-family:var(--mono);font-size:.63rem;
 letter-spacing:.16em;text-transform:uppercase;color:var(--rail-muted)}
.tick{width:8px;height:8px;border-radius:2px;flex:0 0 auto}
.tick.t-user{background:var(--accent)}
.tick.t-comp{background:var(--green)}
.navitem{display:flex;gap:12px;align-items:flex-start;margin:2px 12px;padding:9px 12px;border-radius:9px;
 text-decoration:none;color:var(--rail-ink);border-left:2px solid transparent;transition:background .15s}
.navitem:hover{background:rgba(255,255,255,.05)}
.navitem .code{font-family:var(--mono);font-size:.74rem;color:var(--rail-muted);padding-top:1px;min-width:1.7em}
.ntitle{display:block;font-size:.9rem;font-weight:600;line-height:1.28}
.ntag{display:block;font-size:.73rem;color:var(--rail-muted);margin-top:1px}
.navitem.active{background:rgba(65,169,221,.12);border-left-color:var(--accent)}
.navitem.active .code,.navitem.active .ntag{color:var(--accent-strong)}
.railfoot{margin:22px 14px 0;padding-top:16px;border-top:1px solid var(--rail-line);display:flex;flex-direction:column;gap:8px}
.rlink{display:flex;align-items:flex-start;gap:10px;padding:9px 12px;border-radius:8px;
 color:var(--rail-muted);font-size:.78rem;border:1px solid var(--rail-line);background:rgba(255,255,255,.02);line-height:1.4}
.rlink .g{flex:0 0 auto;padding-top:1px;color:var(--rail-muted)}
.rlink .rt{flex:1;min-width:0;overflow-wrap:anywhere}
.railctl{display:flex;gap:8px;margin:10px 14px 0}
.railctl button{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;
 font-family:var(--mono);font-size:.72rem;letter-spacing:.04em;color:var(--rail-muted);
 background:rgba(255,255,255,.03);border:1px solid var(--rail-line);border-radius:8px;padding:9px;cursor:pointer}
.railctl button:hover{color:var(--rail-ink);border-color:var(--accent)}
.raillinks{display:flex;gap:8px;margin:8px 14px 0}
.raillinks a{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;
 font-family:var(--mono);font-size:.68rem;letter-spacing:.03em;color:var(--rail-muted);
 border:1px solid var(--rail-line);border-radius:8px;padding:8px 6px;text-decoration:none;
 background:rgba(255,255,255,.02);white-space:nowrap}
.raillinks a:hover{color:var(--rail-ink);border-color:var(--accent)}

/* ---------- Lesefläche ---------- */
.reading{background:var(--pane);overflow-y:auto;overflow-x:hidden;position:relative;scroll-behavior:smooth}
.progress{position:sticky;top:0;height:3px;background:transparent;z-index:5}
.progress i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--accent),var(--accent-strong))}
.reading-inner{max-width:none;margin:0;padding:clamp(24px,3vw,48px) clamp(18px,3vw,52px) 130px}
.sec{scroll-margin-top:24px}
.sec h2{display:flex;align-items:center;gap:11px;font-size:1.42rem;font-weight:750;letter-spacing:-.01em;
 margin:2.2em 0 .35em;padding-top:.8em;border-top:1px solid var(--line);color:var(--ink)}
.sec h2 .sic{display:inline-flex;color:var(--accent);font-size:1.15em}
.sec h2 .count{font-family:var(--mono);font-size:.62em;font-weight:600;color:var(--accent-strong);
 background:var(--accent-tint);border:1px solid color-mix(in srgb,var(--accent) 30%,var(--line));
 border-radius:999px;padding:2px 10px;margin-left:2px}
.sec .sub{font-size:.92rem;color:var(--muted);margin:0 0 1.2em}
.sec h3{font-size:1.08rem;font-weight:700;margin:1.7em 0 .6em;color:var(--ink)}
.sec p{font-size:1rem;line-height:1.66;color:var(--ink-2)}
code{font-family:var(--mono);font-size:.85em;background:var(--surface-2);border:1px solid var(--line);
 border-radius:5px;padding:.08em .4em;color:var(--ink)}

/* Kennzahl-Kacheln */
.kvgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(215px,1fr));gap:12px;margin:14px 0}
.kv{display:flex;gap:12px;align-items:flex-start;padding:14px 16px;border-radius:12px;
 background:var(--surface);border:1px solid var(--line);box-shadow:var(--shadow)}
.kv .kic{color:var(--accent);font-size:1.25rem;padding-top:2px}
.kv>span:last-child{flex:1;min-width:0}
.kv .kl{display:block;font-family:var(--mono);font-size:.64rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:3px}
.kv .kw{display:block;font-size:.98rem;font-weight:650;color:var(--ink);line-height:1.35;overflow-wrap:anywhere}
.kv .kw small{display:block;font-weight:400;font-size:.78rem;color:var(--muted);margin-top:1px}

/* Badges */
.badge{display:inline-block;font-family:var(--mono);font-size:.68rem;letter-spacing:.05em;
 padding:3px 9px;border-radius:999px;border:1px solid var(--line);white-space:nowrap}
.b-ok{color:var(--green);background:var(--green-t);border-color:color-mix(in srgb,var(--green) 34%,var(--line))}
.b-warn{color:var(--amber);background:var(--amber-t);border-color:color-mix(in srgb,var(--amber) 38%,var(--line))}
.b-err{color:var(--red);background:var(--red-t);border-color:color-mix(in srgb,var(--red) 38%,var(--line))}
.b-mut{color:var(--muted);background:var(--surface-2)}

/* Tabellen */
.tablewrap{overflow-x:auto;margin:1.1em 0 1.6em;border:1px solid var(--line);border-radius:12px;
 box-shadow:var(--shadow);background:var(--surface)}
table{border-collapse:collapse;width:100%;font-size:.85rem;background:var(--surface)}
thead th{background:var(--surface-2);text-align:left;font-weight:700;color:var(--ink);
 font-size:.78rem;letter-spacing:.01em;padding:9px 12px;border-bottom:1px solid var(--line);white-space:nowrap}
tbody td{padding:9px 12px;border-bottom:1px solid var(--line);color:var(--ink-2);vertical-align:top;
 line-height:1.5;overflow-wrap:anywhere}
tbody tr:last-child td{border-bottom:0}
tbody tr:nth-child(even) td{background:color-mix(in srgb,var(--surface-2) 55%,transparent)}
td.strong{color:var(--ink);font-weight:600}
td.num{font-family:var(--mono);white-space:nowrap}
td .mono,.mono{font-family:var(--mono);font-size:.86em;color:var(--muted)}
td .sett{display:block;color:var(--ink);font-weight:600}
td .vline{display:block;margin:1px 0}
td .vline b{color:var(--muted);font-weight:500;font-family:var(--mono);font-size:.82em}

/* Hinweis-Box */
.note{margin:1.3em 0;padding:14px 20px;background:var(--accent-tint);display:flex;gap:12px;align-items:flex-start;
 border:1px solid color-mix(in srgb,var(--accent) 28%,var(--line));border-left:4px solid var(--accent);
 border-radius:0 12px 12px 0;box-shadow:var(--shadow);color:var(--ink);line-height:1.55}
.note .nic{color:var(--accent);font-size:1.15rem;padding-top:2px}
.note.warn{background:var(--amber-t);border-color:color-mix(in srgb,var(--amber) 36%,var(--line));border-left-color:var(--amber)}
.note.warn .nic{color:var(--amber)}

/* Ereignis-Akkordeon */
details.evt{margin:1.1em 0;border:1px solid var(--line);border-radius:12px;background:var(--surface);box-shadow:var(--shadow)}
details.evt>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:10px;
 padding:13px 18px;font-weight:650;color:var(--ink);border-radius:12px}
details.evt>summary::-webkit-details-marker{display:none}
details.evt>summary .chev{margin-left:auto;font-family:var(--mono);color:var(--muted);transition:transform .2s}
details.evt[open]>summary .chev{transform:rotate(90deg)}
details.evt>summary:hover{background:color-mix(in srgb,var(--surface-2) 60%,transparent)}
details.evt .tablewrap{margin:0;border:0;border-top:1px solid var(--line);border-radius:0 0 12px 12px;box-shadow:none}
td.desc{white-space:pre-line}

/* ---------- Deckblatt ---------- */
.cover{position:relative;padding-top:8px}
.cover-motif{position:absolute;top:-40px;right:-60px;width:520px;height:520px;pointer-events:none;opacity:.5;
 background:
  radial-gradient(circle,transparent 0 148px,color-mix(in srgb,var(--accent) 32%,transparent) 148px 150px,transparent 151px),
  radial-gradient(circle,transparent 0 92px,color-mix(in srgb,var(--accent) 22%,transparent) 92px 93px,transparent 94px),
  radial-gradient(circle,color-mix(in srgb,var(--accent) 16%,transparent) 0 5px,transparent 6px);
 -webkit-mask-image:linear-gradient(200deg,#000,transparent 72%);mask-image:linear-gradient(200deg,#000,transparent 72%)}
.eyebrow{font-family:var(--mono);font-size:.74rem;letter-spacing:.14em;text-transform:uppercase;color:var(--accent-strong);margin:6px 0 18px}
.cover-h1{font-size:clamp(2.1rem,4.6vw,3.4rem);font-weight:820;letter-spacing:-.03em;line-height:1.04;margin:0 0 .34em;color:var(--ink)}
.thesis{font-size:clamp(1.02rem,1.7vw,1.22rem);line-height:1.55;color:var(--ink-2);max-width:52em;margin:0 0 34px;font-weight:450}
.thesis b{color:var(--ink)}
.docmeta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin:0 0 26px}
.dc{font-family:var(--mono);font-size:.72rem;letter-spacing:.1em;color:var(--muted)}
.dcat{font-family:var(--mono);font-size:.66rem;letter-spacing:.09em;text-transform:uppercase;
 padding:3px 9px;border-radius:999px;border:1px solid var(--line);color:var(--accent-strong)}
.cardgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(225px,1fr));gap:14px;margin:8px 0 34px}
.card{position:relative;display:flex;flex-direction:column;gap:2px;padding:18px 18px 20px;border-radius:14px;
 background:var(--surface);border:1px solid var(--line);text-decoration:none;box-shadow:var(--shadow);
 transition:transform .16s,border-color .16s;overflow:hidden}
.card::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--muted)}
.card.cat-user::before{background:var(--accent)}
.card.cat-comp::before{background:var(--green)}
.card.cat-proc::before{background:var(--amber)}
.card:hover{transform:translateY(-3px);border-color:color-mix(in srgb,var(--accent) 45%,var(--line))}
.card-ic{position:absolute;top:16px;right:16px;color:var(--muted);opacity:.6;font-size:1.3rem}
.card-code{font-family:var(--mono);font-size:1.5rem;font-weight:600;color:var(--ink);letter-spacing:-.01em}
.card-cat{font-family:var(--mono);font-size:.62rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin:2px 0 8px}
.card-title{font-size:1.02rem;font-weight:700;color:var(--ink);line-height:1.25}
.card-tag{font-size:.83rem;color:var(--ink-2);margin-top:3px;overflow-wrap:anywhere}
.disclaimer{font-size:.82rem;color:var(--muted);line-height:1.55;border-top:1px solid var(--line);padding-top:18px;max-width:60em}
.foot{margin-top:64px;padding-top:22px;border-top:1px solid var(--line);font-size:.8rem;color:var(--muted);
 font-family:var(--mono);line-height:1.6}

/* ---------- Topbar (mobil) ---------- */
.topbar{display:none;align-items:center;gap:12px;padding:11px 14px;background:var(--rail);color:var(--rail-ink);
 border-bottom:1px solid var(--rail-line);position:sticky;top:0;z-index:20}
.burger{width:38px;height:34px;border-radius:8px;border:1px solid var(--rail-line);background:transparent;
 color:var(--rail-ink);cursor:pointer;font-size:1.1rem}
.topbar b{font-size:.9rem}
.scrim{display:none}

@media (max-width:900px){
 .shell{grid-template-columns:1fr;height:calc(100vh - 57px)}
 .topbar{display:flex}
 .rail{position:fixed;inset:0 auto 0 0;width:300px;z-index:40;transform:translateX(-102%);transition:transform .26s ease}
 .shell.open .rail{transform:none}
 .shell.open .scrim{display:block;position:fixed;inset:0;background:rgba(4,10,16,.5);z-index:35}
}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;scroll-behavior:auto!important}}

/* ---------- Druck ---------- */
@media print{
 .rail,.topbar,.progress,.railctl,.scrim{display:none!important}
 .shell{display:block;height:auto}.reading{overflow:visible;height:auto;background:#fff}
 .reading-inner{max-width:none;padding:0}
 details.evt{display:none}
 .tablewrap,.kv,.card,.note{box-shadow:none;break-inside:avoid}
 .sec h2{break-after:avoid}
 body{background:#fff}
}
'@

$Js = @'
(function(){
 var shell=document.getElementById('shell');
 var reading=document.getElementById('reading');
 var bar=document.getElementById('bar');
 var over=document.getElementById('overview');
 var links=[].slice.call(document.querySelectorAll('.navitem'));
 var secs=links.map(function(a){return document.getElementById(a.getAttribute('href').slice(1));}).filter(Boolean);
 function onScroll(){
   var st=reading.scrollTop,h=reading.scrollHeight-reading.clientHeight;
   if(bar)bar.style.width=(h>0?(st/h*100):0)+'%';
   var cur=null;
   for(var i=0;i<secs.length;i++){if(secs[i].offsetTop-100<=st)cur=secs[i];}
   links.forEach(function(a){a.classList.toggle('active',cur&&a.getAttribute('href')==='#'+cur.id);});
   if(over)over.classList.toggle('active',!cur);
 }
 reading.addEventListener('scroll',onScroll);onScroll();
 function goTo(id){var t=document.getElementById(id);if(t){reading.scrollTo({top:(id==='cover'?0:t.offsetTop-14),behavior:'smooth'});}
   if(shell.classList.contains('open'))shell.classList.remove('open');}
 links.concat(over?[over]:[]).forEach(function(a){a.addEventListener('click',function(e){
   e.preventDefault();goTo(a.getAttribute('href').slice(1));});});
 var brand=document.getElementById('brand');
 if(brand)brand.addEventListener('click',function(){goTo('cover');});
 var tb=document.getElementById('themeBtn');
 if(tb)tb.addEventListener('click',function(){var r=document.documentElement;
   var t=r.getAttribute('data-theme')==='dark'?'light':'dark';r.setAttribute('data-theme',t);
   try{localStorage.setItem('gp-theme',t);}catch(e){}});
 var pb=document.getElementById('printBtn');
 if(pb)pb.addEventListener('click',function(){window.print();});
 var bg=document.getElementById('burger');
 if(bg)bg.addEventListener('click',function(){shell.classList.toggle('open');});
 var sc=document.getElementById('scrim');
 if(sc)sc.addEventListener('click',function(){shell.classList.remove('open');});
})();
'@

$ThemeInit = '(function(){try{var t=localStorage.getItem("gp-theme");if(!t)t=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.setAttribute("data-theme",t);}catch(e){}})();'

#endregion

# ============================================================================
#region RSoP-Daten beschaffen und laden
# ============================================================================

$ScriptName = Split-Path -Leaf $MyInvocation.MyCommand.Path
$outFull = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)

$tempXml = $null
$savedXml = $null
if (-not $InputXml) {
    # Wie das klassische gpresult entstehen HTML UND XML — die Rohdaten landen
    # standardmäßig neben dem Bericht (abschaltbar mit -NoXml).
    if ($NoXml) {
        $tempXml = Join-Path $env:TEMP ("RSoP_{0:yyyyMMdd_HHmmss}.xml" -f (Get-Date))
        $collectTarget = $tempXml
    } else {
        if (-not $XmlPath) { $XmlPath = [System.IO.Path]::ChangeExtension($outFull, 'xml') }
        $savedXml = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($XmlPath)
        $collectTarget = $savedXml
    }
    Write-Host 'Erhebe RSoP-Daten des lokalen Systems ...' -ForegroundColor Cyan
    if (Get-Command -Name Get-GPResultantSetOfPolicy -ErrorAction SilentlyContinue) {
        $null = Get-GPResultantSetOfPolicy -ReportType Xml -Path $collectTarget
    } else {
        & "$env:windir\system32\gpresult.exe" /X $collectTarget /F | Out-Null
        if ($LASTEXITCODE -ne 0 -or -not (Test-Path $collectTarget)) {
            throw "gpresult /X ist fehlgeschlagen (Exitcode $LASTEXITCODE)."
        }
    }
    $InputXml = $collectTarget
}

$xmlFull = (Resolve-Path -LiteralPath $InputXml).ProviderPath
$doc = New-Object System.Xml.XmlDocument
$doc.Load($xmlFull)

if ($doc.DocumentElement.LocalName -ne 'Rsop') {
    throw "Unerwartetes XML-Format: Wurzelelement '$($doc.DocumentElement.LocalName)' statt 'Rsop'."
}

$rsop     = $doc.Rsop
$readTime = ParseIsoDate (txt $rsop.ReadTime)
$dataType = txt $rsop.DataType

# GUID -> GPO-Anzeigename (für die Quellenauflösung in den Einstellungen)
$script:GpoNames = @{}

#endregion

# ============================================================================
#region Scope-Daten extrahieren (Benutzer / Computer)
# ============================================================================

function Get-ScopeInfo($node, [string]$prefix, [string]$label) {
    if ($null -eq $node) { return $null }

    $gpos = @()
    foreach ($g in (AsArray $node.GPO)) {
        $guid = txt $g.Path.Identifier
        # Ein GPO kann an mehreren Containern verknüpft sein -> alle Links auswerten
        $links = AsArray $g.Link
        $appliedOrder = 0
        $linkEnabled = ''; $noOverride = ''; $somPaths = @(); $linkOrder = ''
        foreach ($l in $links) {
            $v = 0; [void][int]::TryParse((txt $l.AppliedOrder), [ref]$v)
            if ($v -gt $appliedOrder) { $appliedOrder = $v }
            $somPaths += (txt $l.SOMPath)
            if (-not $linkOrder)   { $linkOrder   = txt $l.LinkOrder }
            if (-not $linkEnabled) { $linkEnabled = txt $l.Enabled }
            if ((txt $l.NoOverride) -match '(?i)true') { $noOverride = 'true' }
        }
        $o = [pscustomobject]@{
            Name         = txt $g.Name
            Guid         = $guid
            VerAD        = txt $g.VersionDirectory
            VerSysvol    = txt $g.VersionSysvol
            Enabled      = txt $g.Enabled
            IsValid      = txt $g.IsValid
            FilterAllowed= txt $g.FilterAllowed
            AccessDenied = txt $g.AccessDenied
            SOMPath      = ($somPaths -join '; ')
            LinkOrder    = $linkOrder
            AppliedOrder = $appliedOrder
            LinkEnabled  = $linkEnabled
            NoOverride   = $noOverride
            SecFilter    = ((AsArray $g.SecurityFilter) | ForEach-Object { txt $_ }) -join '; '
            Extensions   = ((AsArray $g.ExtensionName)  | ForEach-Object { txt $_ }) -join ', '
        }
        # Anzeigename je GUID merken — ein aufgeloester Name schlaegt eine blosse GUID
        if ($guid) {
            $existing = $script:GpoNames[$guid]
            if (-not $existing -or $existing -eq $guid) { $script:GpoNames[$guid] = $o.Name }
        }
        $gpos += $o
    }

    $groups = @()
    foreach ($sg in (AsArray $node.SecurityGroup)) {
        $groups += [pscustomobject]@{ Name = txt $sg.Name; SID = txt $sg.SID }
    }

    $soms = @()
    foreach ($s in (AsArray $node.SearchedSOM)) {
        $soms += [pscustomobject]@{
            Path   = txt $s.Path
            Type   = txt $s.Type
            Order  = [int]('0' + (txt $s.Order))
            Blocks = txt $s.BlocksInheritance
            Blocked= txt $s.Blocked
            Reason = txt $s.Reason
        }
    }

    $extStatus = @()
    foreach ($e in (AsArray $node.ExtensionStatus)) {
        $extStatus += [pscustomobject]@{
            Name   = txt $e.Name
            Guid   = txt $e.Identifier
            Begin  = ParseIsoDate (txt $e.BeginTime)
            End    = ParseIsoDate (txt $e.EndTime)
            Status = txt $e.LoggingStatus
            Error  = txt $e.Error
        }
    }

    # Ereignisdetails (ein SinglePass pro Erhebung)
    $spe = $null
    foreach ($ed in (AsArray $node.EventsDetails)) {
        foreach ($p in (AsArray $ed.SinglePassEventsDetails)) { if ($null -eq $spe) { $spe = $p } }
    }

    $events = @()
    $extTimes = @()
    $attrs = @{}
    if ($spe) {
        foreach ($a in $spe.Attributes) { $attrs[$a.Name] = $a.Value }
        foreach ($er in (AsArray $spe.EventRecord)) {
            $raw  = txt $er.EventXml
            $id   = ''
            $time = $null
            $m = [regex]::Match($raw, '<EventID>(\d+)</EventID>')
            if ($m.Success) { $id = $m.Groups[1].Value }
            $m = [regex]::Match($raw, "SystemTime='([^']+)'")
            if ($m.Success) { $time = ParseIsoDate $m.Groups[1].Value }
            $events += [pscustomobject]@{
                Id   = $id
                Time = $time
                Desc = (txt $er.EventDescription).Trim()
            }
        }
        foreach ($et in (AsArray $spe.ExtensionProcessingTime)) {
            $extTimes += [pscustomobject]@{
                Name = txt $et.ExtensionName
                Guid = txt $et.ExtensionGuid
                Ms   = txt $et.ElapsedTimeInMilliseconds
                At   = ParseIsoDate (txt $et.ProcessedTimeStamp)
            }
        }
    }

    $extData = @()
    foreach ($ed in (AsArray $node.ExtensionData)) {
        $extEl = $null; $extName = ''
        foreach ($c in $ed.ChildNodes) {
            if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
            if ($c.LocalName -eq 'Extension') { $extEl = $c }
            elseif ($c.LocalName -eq 'Name') { $extName = $c.InnerText }
        }
        if ($extEl) { $extData += [pscustomobject]@{ El = $extEl; Title = $extName } }
    }

    $applied = @($gpos | Where-Object { $_.AppliedOrder -gt 0 } | Sort-Object AppliedOrder)
    $denied  = @($gpos | Where-Object { $_.AppliedOrder -le 0 })

    return [pscustomobject]@{
        Prefix    = $prefix
        Label     = $label
        Name      = txt $node.Name
        Domain    = txt $node.Domain
        Som       = txt $node.SOM
        Site      = txt $node.Site
        SlowLink  = txt $node.SlowLink
        Version   = txt $node.Version
        Gpos      = $gpos
        Applied   = $applied
        Denied    = $denied
        Groups    = $groups
        Soms      = @($soms | Sort-Object Order)
        ExtStatus = $extStatus
        ExtTimes  = $extTimes
        Events    = $events
        Attrs     = $attrs
        ExtData   = $extData
    }
}

$uInfo = Get-ScopeInfo $rsop.UserResults     'u' 'Benutzer'
$cInfo = Get-ScopeInfo $rsop.ComputerResults 'c' 'Computer'

if (-not $uInfo -and -not $cInfo) {
    throw 'Die XML-Datei enthält weder Benutzer- noch Computerergebnisse.'
}

# Nachtrag: GPOs, deren Name nur die GUID ist, bekommen den in einem anderen
# Bereich aufgelösten Anzeigenamen (z. B. kennt der Computerbereich den Namen,
# der Benutzerbereich nur die GUID).
foreach ($s in @($uInfo, $cInfo)) {
    if (-not $s) { continue }
    foreach ($g in $s.Gpos) {
        if ($g.Name -match '^\{[0-9A-Fa-f-]+\}$' -and $g.Guid) {
            $better = $script:GpoNames[$g.Guid]
            if ($better -and $better -ne $g.Name) { $g.Name = $better }
        }
    }
}

#endregion

# ============================================================================
#region HTML-Bausteine
# ============================================================================

function New-Kv([string]$icon, [string]$label, [string]$value, [string]$sub) {
    $s = ''
    if ($sub) { $s = '<small>' + (esc $sub) + '</small>' }
    return '<div class="kv"><span class="kic">' + (ic $icon) + '</span><span><span class="kl">' +
        (esc $label) + '</span><span class="kw">' + (esc $value) + $s + '</span></span></div>'
}

function Get-DeniedReason($g) {
    if ($g.AccessDenied -match '(?i)true') { return (badge 'err' 'Zugriff verweigert') }
    if ($g.IsValid -match '(?i)false')     { return (badge 'mut' 'Nicht angewendet (leer)') }
    if ($g.FilterAllowed -match '(?i)false') { return (badge 'warn' 'Durch Filter verweigert') }
    if ($g.LinkEnabled -match '(?i)false') { return (badge 'warn' 'Verknüpfung deaktiviert') }
    if ($g.Enabled -match '(?i)false')     { return (badge 'warn' 'GPO deaktiviert') }
    return (badge 'mut' 'Nicht angewendet (leer)')
}

function New-GpoRows($list, [bool]$appliedView) {
    $rows = ''
    foreach ($g in $list) {
        $nameCell = '<span class="sett">' + (esc $g.Name) + '</span>'
        if ($g.Guid -and $g.Guid -ne $g.Name -and $g.Guid -ne 'LocalGPO') {
            $nameCell += '<span class="mono">' + (esc $g.Guid) + '</span>'
        }
        if ($g.SecFilter) {
            $nameCell += '<span class="vline"><b>Sicherheitsfilter:</b> ' + (esc $g.SecFilter) + '</span>'
        }
        if ($appliedView -and $g.Extensions) {
            $nameCell += '<span class="vline"><b>Erweiterungen:</b> ' + (esc $g.Extensions) + '</span>'
        }
        $ver = (esc $g.VerAD) + ' / ' + (esc $g.VerSysvol)
        $enforced = if ($g.NoOverride -match '(?i)true') { badge 'warn' 'Erzwungen' } else { 'Nein' }
        if ($appliedView) {
            $status = badge 'ok' 'Angewendet'
            $rows += '<tr><td class="num">' + $g.AppliedOrder + '</td><td>' + $nameCell + '</td><td>' +
                (esc $g.SOMPath) + '</td><td class="num">' + $ver + '</td><td>' + $enforced +
                '</td><td>' + $status + '</td></tr>'
        } else {
            $rows += '<tr><td>' + $nameCell + '</td><td>' + (esc $g.SOMPath) + '</td><td class="num">' +
                $ver + '</td><td>' + (Get-DeniedReason $g) + '</td></tr>'
        }
    }
    return $rows
}

function clip([string]$s) {
    # Kürzt Monsterwerte (z. B. Zertifikats-Blobs), damit die Tabellen lesbar bleiben.
    if ($null -eq $s) { return '' }
    if ($s.Length -gt 96) { return $s.Substring(0, 96) + ' …' }
    return $s
}

function Get-LeafLines($el, $skipNode) {
    # Sammelt lesbare "Name: Wert"-Zeilen aus einem Einstellungs-Element —
    # generisch, damit alle Erweiterungstypen (Security, Registry, ...) funktionieren.
    $lines = @()
    foreach ($c in $el.ChildNodes) {
        if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
        if ($c.LocalName -in @('GPO','Precedence')) { continue }
        if ($skipNode -and [object]::ReferenceEquals($c, $skipNode)) { continue }
        $hasChildEl = $false
        foreach ($cc in $c.ChildNodes) {
            if ($cc.NodeType -eq [System.Xml.XmlNodeType]::Element) { $hasChildEl = $true; break }
        }
        if (-not $hasChildEl) {
            $lines += ('<span class="vline"><b>' + (esc $c.LocalName) + ':</b> ' + (esc (clip (boolDe $c.InnerText))) + '</span>')
        } else {
            $parts = @()
            foreach ($d in $c.SelectNodes('.//*')) {
                $leaf = $true
                foreach ($dd in $d.ChildNodes) {
                    if ($dd.NodeType -eq [System.Xml.XmlNodeType]::Element) { $leaf = $false; break }
                }
                if ($leaf -and $d.InnerText -ne '') { $parts += (esc (clip (boolDe $d.InnerText))) }
            }
            $lines += ('<span class="vline"><b>' + (esc $c.LocalName) + ':</b> ' + ($parts -join ' · ') + '</span>')
        }
    }
    return ($lines -join '')
}

function Get-SettingName($el) {
    foreach ($c in $el.ChildNodes) {
        if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.LocalName -eq 'Name') {
            $hasChildEl = $false
            foreach ($cc in $c.ChildNodes) {
                if ($cc.NodeType -eq [System.Xml.XmlNodeType]::Element) { $hasChildEl = $true; break }
            }
            if (-not $hasChildEl) { return @{ Text = $c.InnerText; Node = $c } }
        }
    }
    foreach ($cand in @('Display/Name','KeyName','Path')) {
        $n = $el.SelectSingleNode(('./*[local-name()="{0}"]' -f ($cand -replace '/.*','')))
        if ($cand -eq 'Display/Name') {
            $disp = $null
            foreach ($c in $el.ChildNodes) {
                if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.LocalName -eq 'Display') { $disp = $c; break }
            }
            if ($disp) {
                foreach ($c in $disp.ChildNodes) {
                    if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.LocalName -eq 'Name' -and $c.InnerText) {
                        return @{ Text = $c.InnerText; Node = $null }
                    }
                }
            }
        } elseif ($n -and $n.InnerText) {
            return @{ Text = $n.InnerText; Node = $n }
        }
    }
    return @{ Text = ''; Node = $null }
}

$ExtTypeNames = @{
    'SecuritySettings'             = 'Sicherheitseinstellungen'
    'RegistrySettings'             = 'Registrierung / Administrative Vorlagen'
    'ScriptsSettings'              = 'Skripte'
    'PublicKeySettings'            = 'Richtlinien für öffentliche Schlüssel'
    'AuditSettings'                = 'Erweiterte Überwachungsrichtlinie'
    'FolderRedirectionSettings'    = 'Ordnerumleitung'
    'SoftwareInstallationSettings' = 'Softwareinstallation'
    'DriveMapSettings'             = 'Laufwerkzuordnungen'
    'EFSSettings'                  = 'EFS-Einstellungen'
    'WLanSvcSettings'              = 'WLAN-Richtlinien'
}
$GroupNames = @{
    'Account'              = 'Kontorichtlinien'
    'SecurityOptions'      = 'Sicherheitsoptionen'
    'UserRightsAssignment' = 'Zuweisen von Benutzerrechten'
    'EventLog'             = 'Ereignisprotokoll'
    'RestrictedGroups'     = 'Eingeschränkte Gruppen'
    'SystemServices'       = 'Systemdienste'
    'Registry'             = 'Registrierungs-ACLs'
    'File'                 = 'Dateisystem-ACLs'
    'Policy'               = 'Richtlinien'
    'RegistrySetting'      = 'Registrierungseinstellungen'
    'Auditing'             = 'Überwachung'
    'Script'               = 'Skripte'
}

function New-SettingsHtml($info) {
    if (-not $info.ExtData -or $info.ExtData.Count -eq 0) {
        return '<div class="note"><span class="nic">' + (ic 'sliders') + '</span><span>' +
            'Für diesen Bereich enthält der Ergebnissatz keine Detaileinstellungen.</span></div>'
    }
    $h = ''
    foreach ($item in $info.ExtData) {
        $ext = $item.El
        $typeRaw = $ext.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
        $type = ($typeRaw -replace '^.*:', '')
        $typeName = $ExtTypeNames[$type]
        if (-not $typeName) { $typeName = $type }
        $foreignAuthority = ($item.Title -match 'different authority')

        # Einstellungen nach Elementnamen gruppieren; "Blocked" ist Meta-Information
        $groups = [ordered]@{}
        $blocked = ''
        foreach ($c in $ext.ChildNodes) {
            if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
            if ($c.LocalName -eq 'Blocked') { $blocked = $c.InnerText; continue }
            if (-not $groups.Contains($c.LocalName)) { $groups[$c.LocalName] = New-Object System.Collections.ArrayList }
            $null = $groups[$c.LocalName].Add($c)
        }
        if ($groups.Count -eq 0) {
            # Kein Inhalt: nur erwähnen, wenn der Bereich anderweitig gesteuert wird
            if ($foreignAuthority -or $blocked -match '(?i)true') {
                $h += '<div class="note warn"><span class="nic">' + (ic 'alert') + '</span><span><b>' +
                    (esc $typeName) + ':</b> Dieser Bereich wird von einer anderen Autorität gesteuert ' +
                    '(z. B. MDM/Intune) — klassische GPO-Einstellungen sind hier blockiert.</span></div>'
            }
            continue
        }

        $blockedBadge = ''
        if ($blocked -match '(?i)true') { $blockedBadge = ' ' + (badge 'warn' 'Blockiert') }
        $h += '<h3>' + (ic 'sliders') + ' ' + (esc $typeName) + $blockedBadge + '</h3>'
        foreach ($gName in $groups.Keys) {
            $friendly = $GroupNames[$gName]
            if (-not $friendly) { $friendly = $gName }
            $rows = ''
            foreach ($setting in $groups[$gName]) {
                $nameInfo = Get-SettingName $setting
                $sName = $nameInfo.Text
                if (-not $sName) { $sName = $gName }
                $vals = Get-LeafLines $setting $nameInfo.Node
                if (-not $vals) { $vals = '<span class="mono">—</span>' }
                $srcGuid = ''
                foreach ($c in $setting.ChildNodes) {
                    if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.LocalName -eq 'GPO') {
                        $srcGuid = $c.InnerText.Trim(); break
                    }
                }
                # GPO-Kind enthält Identifier+Domain hintereinander — Identifier isolieren
                $mm = [regex]::Match($srcGuid, '\{[0-9A-Fa-f-]+\}|LocalGPO')
                if ($mm.Success) { $srcGuid = $mm.Value }
                $srcName = $script:GpoNames[$srcGuid]
                if (-not $srcName) { $srcName = $srcGuid }
                if (-not $srcName) { $srcName = '—' }
                $prec = ''
                foreach ($c in $setting.ChildNodes) {
                    if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.LocalName -eq 'Precedence') {
                        $prec = $c.InnerText; break
                    }
                }
                if (-not $prec) { $prec = '—' }
                $rows += '<tr><td class="strong">' + (esc $sName) + '</td><td>' + $vals + '</td><td>' +
                    (esc $srcName) + '</td><td class="num">' + (esc $prec) + '</td></tr>'
            }
            $h += '<div class="tablewrap"><table><thead><tr><th>' + (esc $friendly) +
                '</th><th>Wert</th><th>Quell-GPO</th><th>Rang</th></tr></thead><tbody>' +
                $rows + '</tbody></table></div>'
        }
    }
    if (-not $h) {
        $h = '<div class="note"><span class="nic">' + (ic 'sliders') + '</span><span>' +
            'Für diesen Bereich enthält der Ergebnissatz keine Detaileinstellungen.</span></div>'
    }
    return $h
}

function New-Section([string]$id, [string]$icon, [string]$title, [string]$countBadge, [string]$sub, [string]$body) {
    $cb = ''
    if ($countBadge) { $cb = '<span class="count">' + (esc $countBadge) + '</span>' }
    $sb = ''
    if ($sub) { $sb = '<p class="sub">' + (esc $sub) + '</p>' }
    return '<section class="sec" id="' + $id + '"><h2><span class="sic">' + (ic $icon) + '</span>' +
        (esc $title) + $cb + '</h2>' + $sb + $body + '</section>'
}

function New-ScopeHtml($info) {
    if ($null -eq $info) { return @{ Html = ''; Nav = @() } }
    $p = $info.Prefix
    $nav = @()
    $H = New-Object System.Text.StringBuilder

    # --- Zusammenfassung -----------------------------------------------------
    $slow = if ($info.SlowLink -match '(?i)true') { 'Ja (langsame Verbindung)' } else { 'Nein' }
    $kv = '<div class="kvgrid">'
    $kv += New-Kv 'user'   'Konto'            $info.Name ''
    $kv += New-Kv 'globe'  'Domäne'          $info.Domain ''
    $kv += New-Kv 'target' 'Container (SOM)'  $info.Som ''
    $kv += New-Kv 'home'   'AD-Standort'      $info.Site ''
    $kv += New-Kv 'bolt'   'Langsame Verbindung' $slow ''
    if ($info.Attrs['DomainControllerName']) {
        $kv += New-Kv 'pc' 'Domänencontroller' $info.Attrs['DomainControllerName'] $info.Attrs['DomainControllerIPAddress']
    }
    if ($info.Attrs['PolicyElapsedTimeInMilliseconds']) {
        $kv += New-Kv 'clock' 'Verarbeitungsdauer' ($info.Attrs['PolicyElapsedTimeInMilliseconds'] + ' ms') $info.Attrs['ProcessingTrigger']
    }
    $errW = ''
    if ($info.Attrs.ContainsKey('ErrorCount')) {
        $errW = $info.Attrs['ErrorCount'] + ' Fehler · ' + $info.Attrs['WarningCount'] + ' Warnungen'
        $kv += New-Kv 'alert' 'Fehler / Warnungen' $errW ''
    }
    $kv += '</div>'
    $null = $H.Append((New-Section "$p-sum" ($(if ($p -eq 'u') { 'user' } else { 'pc' })) "$($info.Label) — Zusammenfassung" '' 'Identität, Kontext und Verarbeitungskennzahlen dieses Bereichs.' $kv))
    $nav += @{ Code = "$($p.ToUpper())1"; Title = 'Zusammenfassung'; Tag = $info.Name; Id = "$p-sum" }

    # --- GPOs ----------------------------------------------------------------
    $body = ''
    if ($info.Applied.Count -gt 0) {
        $body += '<h3>' + (ic 'shield') + ' Angewendet (' + $info.Applied.Count + ')</h3>'
        $body += '<div class="tablewrap"><table><thead><tr><th>Rang</th><th>Gruppenrichtlinienobjekt</th><th>Verknüpft mit</th><th>Version (AD/Sysvol)</th><th>Erzwungen</th><th>Status</th></tr></thead><tbody>'
        $body += New-GpoRows $info.Applied $true
        $body += '</tbody></table></div>'
    } else {
        $body += '<div class="note"><span class="nic">' + (ic 'layers') + '</span><span>Es wurden keine Gruppenrichtlinienobjekte angewendet.</span></div>'
    }
    if ($info.Denied.Count -gt 0) {
        $body += '<h3>' + (ic 'alert') + ' Nicht angewendet (' + $info.Denied.Count + ')</h3>'
        $body += '<div class="tablewrap"><table><thead><tr><th>Gruppenrichtlinienobjekt</th><th>Verknüpft mit</th><th>Version (AD/Sysvol)</th><th>Grund</th></tr></thead><tbody>'
        $body += New-GpoRows $info.Denied $false
        $body += '</tbody></table></div>'
    }
    $null = $H.Append((New-Section "$p-gpo" 'layers' 'Gruppenrichtlinienobjekte' ("{0} / {1}" -f $info.Applied.Count, $info.Gpos.Count) 'Angewendete und verworfene GPOs samt Verknüpfungsort, Version und Filterstatus.' $body))
    $nav += @{ Code = "$($p.ToUpper())2"; Title = 'Richtlinienobjekte'; Tag = ("{0} angewendet · {1} verworfen" -f $info.Applied.Count, $info.Denied.Count); Id = "$p-gpo" }

    # --- Einstellungen -------------------------------------------------------
    $null = $H.Append((New-Section "$p-set" 'sliders' 'Richtlinieneinstellungen' '' 'Die tatsächlich wirksamen Einstellungen je Client-Erweiterung, mit Quell-GPO und Rang.' (New-SettingsHtml $info)))
    $nav += @{ Code = "$($p.ToUpper())3"; Title = 'Einstellungen'; Tag = 'wirksame Werte je Erweiterung'; Id = "$p-set" }

    # --- Sicherheitsgruppen --------------------------------------------------
    $rows = ''
    $i = 0
    foreach ($g in $info.Groups) {
        $i++
        $rows += '<tr><td class="num">' + $i + '</td><td class="strong">' + (esc $g.Name) +
            '</td><td><span class="mono">' + (esc $g.SID) + '</span></td></tr>'
    }
    $body = '<div class="tablewrap"><table><thead><tr><th>#</th><th>Gruppe</th><th>SID</th></tr></thead><tbody>' + $rows + '</tbody></table></div>'
    $null = $H.Append((New-Section "$p-grp" 'users' 'Sicherheitsgruppen' ([string]$info.Groups.Count) 'Gruppenmitgliedschaften, die bei der Richtlinienauswertung wirksam waren.' $body))
    $nav += @{ Code = "$($p.ToUpper())4"; Title = 'Sicherheitsgruppen'; Tag = ("{0} Mitgliedschaften" -f $info.Groups.Count); Id = "$p-grp" }

    # --- Durchsuchte Container ----------------------------------------------
    $rows = ''
    foreach ($s in $info.Soms) {
        $blocks  = if ($s.Blocks  -match '(?i)true') { badge 'warn' 'Ja' } else { 'Nein' }
        $blocked = if ($s.Blocked -match '(?i)true') { badge 'err' 'Ja' } else { 'Nein' }
        $rows += '<tr><td class="num">' + $s.Order + '</td><td class="strong">' + (esc $s.Path) +
            '</td><td>' + (esc $s.Type) + '</td><td>' + $blocks + '</td><td>' + $blocked +
            '</td><td>' + (esc $s.Reason) + '</td></tr>'
    }
    $body = '<div class="tablewrap"><table><thead><tr><th>Reihenfolge</th><th>Pfad</th><th>Typ</th><th>Blockiert Vererbung</th><th>Blockiert</th><th>Modus</th></tr></thead><tbody>' + $rows + '</tbody></table></div>'
    $null = $H.Append((New-Section "$p-som" 'target' 'Durchsuchte Container (SOM)' ([string]$info.Soms.Count) 'Verwaltungsbereiche (Lokal, Standort, Domäne, OU), die nach verknüpften GPOs durchsucht wurden.' $body))
    $nav += @{ Code = "$($p.ToUpper())5"; Title = 'Container (SOM)'; Tag = ("{0} durchsucht" -f $info.Soms.Count); Id = "$p-som" }

    # --- Komponenten / Verarbeitung -----------------------------------------
    $body = ''
    $rows = ''
    foreach ($e in $info.ExtStatus) {
        $span = FmtDate $e.Begin
        if ($e.End) {
            if ($e.Begin -and $e.End.Date -eq $e.Begin.Date) { $span += ' → ' + $e.End.ToString('HH:mm:ss') }
            else { $span += ' → ' + (FmtDate $e.End) }
        }
        $st = if ($e.Status -eq 'Complete') { badge 'ok' 'Vollständig' } else { badge 'warn' $e.Status }
        $err = if ($e.Error -eq '0') { badge 'ok' 'Kein Fehler' } else { badge 'err' ("Fehler " + $e.Error) }
        $rows += '<tr><td class="strong">' + (esc $e.Name) + '<span class="mono">' + (esc $e.Guid) +
            '</span></td><td class="num">' + $span + '</td><td>' + $st + '</td><td>' + $err + '</td></tr>'
    }
    if ($rows) {
        $body += '<h3>' + (ic 'gear') + ' Client-Erweiterungen (Status)</h3>'
        $body += '<div class="tablewrap"><table><thead><tr><th>Erweiterung</th><th>Zeitraum</th><th>Protokollstatus</th><th>Ergebnis</th></tr></thead><tbody>' + $rows + '</tbody></table></div>'
    }
    $rows = ''
    foreach ($t in $info.ExtTimes) {
        $rows += '<tr><td class="strong">' + (esc $t.Name) + '<span class="mono">' + (esc $t.Guid) +
            '</span></td><td class="num">' + (esc $t.Ms) + ' ms</td><td class="num">' + (FmtDate $t.At) + '</td></tr>'
    }
    if ($rows) {
        $body += '<h3>' + (ic 'clock') + ' Verarbeitungszeiten (letzter Durchlauf)</h3>'
        $body += '<div class="tablewrap"><table><thead><tr><th>Erweiterung</th><th>Dauer</th><th>Verarbeitet am</th></tr></thead><tbody>' + $rows + '</tbody></table></div>'
    }
    if ($info.Attrs.Count -gt 0) {
        $a = $info.Attrs
        $kv = '<h3>' + (ic 'bolt') + ' Letzter Verarbeitungslauf</h3><div class="kvgrid">'
        if ($a['ProcessingTrigger'])  { $kv += New-Kv 'bolt'  'Auslöser' $a['ProcessingTrigger'] $a['ProcessingAppMode'] }
        if ($a['LinkSpeedInKbps'])    { $kv += New-Kv 'globe' 'Verbindung' ($a['LinkSpeedInKbps'] + ' KBit/s') ('Schwellenwert ' + $a['SlowLinkThresholdInKbps'] + ' KBit/s') }
        if ($a['PolicyProcessingMode']) { $kv += New-Kv 'gear' 'Loopback-Modus' $a['PolicyProcessingMode'] '' }
        if ($a.ContainsKey('ErrorCount')) { $kv += New-Kv 'alert' 'Fehler / Warnungen' ($a['ErrorCount'] + ' / ' + $a['WarningCount']) '' }
        $kv += '</div>'
        $body = $kv + $body
    }
    if (-not $body) {
        $body = '<div class="note"><span class="nic">' + (ic 'gear') + '</span><span>Keine Verarbeitungsdaten im Ergebnissatz enthalten.</span></div>'
    }
    $null = $H.Append((New-Section "$p-proc" 'gear' 'Verarbeitung & Komponenten' '' 'Status und Laufzeiten der Gruppenrichtlinien-Clienterweiterungen.' $body))
    $nav += @{ Code = "$($p.ToUpper())6"; Title = 'Verarbeitung'; Tag = ("{0} Erweiterungen" -f $info.ExtStatus.Count); Id = "$p-proc" }

    # --- Ereignisse ----------------------------------------------------------
    $body = ''
    if ($info.Events.Count -gt 0) {
        $rows = ''
        $i = 0
        foreach ($ev in $info.Events) {
            $i++
            $t = if ($ev.Time) { $ev.Time.ToLocalTime().ToString('HH:mm:ss.fff') } else { '—' }
            $rows += '<tr><td class="num">' + $i + '</td><td class="num">' + $t + '</td><td class="num">' +
                (esc $ev.Id) + '</td><td class="desc">' + (esc $ev.Desc) + '</td></tr>'
        }
        $body = '<details class="evt"><summary>' + (ic 'list') + ' ' + $info.Events.Count +
            ' Ereignisse des Verarbeitungslaufs anzeigen<span class="chev">›</span></summary>' +
            '<div class="tablewrap"><table><thead><tr><th>#</th><th>Zeit</th><th>Ereignis-ID</th><th>Beschreibung</th></tr></thead><tbody>' +
            $rows + '</tbody></table></div></details>'
    } else {
        $body = '<div class="note"><span class="nic">' + (ic 'list') + '</span><span>Keine Ereignisse im Ergebnissatz enthalten.</span></div>'
    }
    $null = $H.Append((New-Section "$p-evt" 'list' 'Ereignisprotokoll' ([string]$info.Events.Count) 'Operatives Protokoll (Microsoft-Windows-GroupPolicy) des letzten Verarbeitungslaufs.' $body))
    $nav += @{ Code = "$($p.ToUpper())7"; Title = 'Ereignisse'; Tag = ("{0} Einträge" -f $info.Events.Count); Id = "$p-evt" }

    return @{ Html = $H.ToString(); Nav = $nav }
}

#endregion

# ============================================================================
#region Seite zusammensetzen
# ============================================================================

$uPart = New-ScopeHtml $uInfo
$cPart = New-ScopeHtml $cInfo

$hostName = ''
if ($cInfo) { $hostName = ($cInfo.Name -replace '\$$','' -replace '^.*\\','') }
elseif ($uInfo -and $uInfo.Attrs['DomainControllerName']) { $hostName = $uInfo.Attrs['DomainControllerName'] }
if (-not $hostName -and $env:COMPUTERNAME) { $hostName = $env:COMPUTERNAME }
$domain = if ($cInfo) { $cInfo.Domain } elseif ($uInfo) { $uInfo.Domain } else { '' }

function NavItems($items) {
    $h = ''
    foreach ($n in $items) {
        $h += '<a class="navitem" href="#' + $n.Id + '"><span class="code">' + $n.Code +
            '</span><span><span class="ntitle">' + (esc $n.Title) + '</span><span class="ntag">' +
            (esc $n.Tag) + '</span></span></a>'
    }
    return $h
}

$railNav = ''
if ($uInfo) {
    $railNav += '<div class="cat"><span class="tick t-user"></span>Benutzer</div>' + (NavItems $uPart.Nav)
}
if ($cInfo) {
    $railNav += '<div class="cat"><span class="tick t-comp"></span>Computer</div>' + (NavItems $cPart.Nav)
}

# --- Deckblatt-Kacheln
$cards = ''
if ($uInfo) {
    $cards += '<a class="card cat-user" href="#u-gpo"><span class="card-ic">' + (ic 'layers') + '</span>' +
        '<span class="card-code">' + $uInfo.Applied.Count + '</span><span class="card-cat">Benutzer</span>' +
        '<span class="card-title">Angewendete GPOs</span><span class="card-tag">von ' + $uInfo.Gpos.Count +
        ' verknüpften · ' + (esc $uInfo.Name) + '</span></a>'
}
if ($cInfo) {
    $cards += '<a class="card cat-comp" href="#c-gpo"><span class="card-ic">' + (ic 'layers') + '</span>' +
        '<span class="card-code">' + $cInfo.Applied.Count + '</span><span class="card-cat">Computer</span>' +
        '<span class="card-title">Angewendete GPOs</span><span class="card-tag">von ' + $cInfo.Gpos.Count +
        ' verknüpften · ' + (esc $cInfo.Name) + '</span></a>'
}
$errSum = 0; $warnSum = 0; $msSum = 0
foreach ($s in @($uInfo, $cInfo)) {
    if ($s -and $s.Attrs.ContainsKey('ErrorCount'))   { $errSum  += [int]$s.Attrs['ErrorCount'] }
    if ($s -and $s.Attrs.ContainsKey('WarningCount')) { $warnSum += [int]$s.Attrs['WarningCount'] }
    if ($s -and $s.Attrs.ContainsKey('PolicyElapsedTimeInMilliseconds')) { $msSum += [int]$s.Attrs['PolicyElapsedTimeInMilliseconds'] }
}
$procTarget = if ($uInfo) { '#u-proc' } else { '#c-proc' }
$cards += '<a class="card cat-proc" href="' + $procTarget + '"><span class="card-ic">' + (ic 'alert') + '</span>' +
    '<span class="card-code">' + $errSum + ' / ' + $warnSum + '</span><span class="card-cat">Verarbeitung</span>' +
    '<span class="card-title">Fehler / Warnungen</span><span class="card-tag">über alle Verarbeitungsläufe</span></a>'
$cards += '<a class="card cat-proc" href="' + $procTarget + '"><span class="card-ic">' + (ic 'clock') + '</span>' +
    '<span class="card-code">' + $msSum + ' ms</span><span class="card-cat">Verarbeitung</span>' +
    '<span class="card-title">Gesamtdauer</span><span class="card-tag">Benutzer- und Computerrichtlinien</span></a>'
$dcName = ''
foreach ($s in @($cInfo, $uInfo)) { if (-not $dcName -and $s -and $s.Attrs['DomainControllerName']) { $dcName = $s.Attrs['DomainControllerName'] } }
if ($dcName) {
    $dcShort = ($dcName -split '\.')[0]
    $grpTarget = if ($uInfo) { '#u-grp' } else { '#c-grp' }
    $cards += '<a class="card cat-comp" href="#c-sum"><span class="card-ic">' + (ic 'globe') + '</span>' +
        '<span class="card-code">' + (esc $dcShort) + '</span><span class="card-cat">Infrastruktur</span>' +
        '<span class="card-title">Domänencontroller</span><span class="card-tag">' + (esc $dcName) + '</span></a>'
    $gCounts = @()
    if ($uInfo) { $gCounts += [string]$uInfo.Groups.Count }
    if ($cInfo) { $gCounts += [string]$cInfo.Groups.Count }
    $cards += '<a class="card cat-user" href="' + $grpTarget + '"><span class="card-ic">' + (ic 'users') + '</span>' +
        '<span class="card-code">' + ($gCounts -join ' / ') + '</span><span class="card-cat">Sicherheit</span>' +
        '<span class="card-title">Gruppenmitgliedschaften</span><span class="card-tag">Benutzer / Computer</span></a>'
}

$who = @()
if ($uInfo) { $who += 'Benutzer <b>' + (esc $uInfo.Name) + '</b>' }
if ($cInfo) { $who += 'Computer <b>' + (esc $cInfo.Name) + '</b>' }
$isLocal = (-not $domain -or $domain -eq 'Local')
$whereText = if ($isLocal) { ' (eigenständiges System, lokale Richtlinien)' }
             else { ' in der Domäne <b>' + (esc $domain) + '</b>' }
$thesis = 'Richtlinienergebnissatz für ' + ($who -join ' und ') + $whereText +
    ' — erhoben am <b>' + (FmtDate $readTime) + '</b>.'

$missing = ''
if (-not $cInfo) {
    $missing = '<div class="note warn"><span class="nic">' + (ic 'alert') + '</span><span>' +
        'Der Ergebnissatz enthält keine <b>Computerrichtlinien</b> — dafür muss die Erhebung ' +
        'in einer erhöhten Sitzung (Administrator) erfolgen.</span></div>'
} elseif (-not $uInfo) {
    $missing = '<div class="note warn"><span class="nic">' + (ic 'alert') + '</span><span>' +
        'Der Ergebnissatz enthält keine <b>Benutzerrichtlinien</b>.</span></div>'
}

$xmlLeaf = Split-Path -Leaf $xmlFull
$genStamp = (Get-Date).ToString('dd.MM.yyyy HH:mm:ss')

$cover = '<section class="sec cover" id="cover"><div class="cover-motif"></div>' +
    '<p class="eyebrow">Active Directory · Gruppenrichtlinien · gpresult</p>' +
    '<h1 class="cover-h1">Richtlinien&shy;ergebnissatz</h1>' +
    '<p class="thesis">' + $thesis + '</p>' +
    '<div class="docmeta"><span class="dcat">RSoP · ' + (esc $dataType) + '</span>' +
    '<span class="dc">' + (esc $xmlLeaf) + '</span><span class="dc">' + (esc $hostName) + '</span></div>' +
    '<div class="cardgrid">' + $cards + '</div>' + $missing +
    '<p class="disclaimer">Dieser Bericht wurde automatisch aus dem Richtlinienergebnissatz (Resultant Set of ' +
    'Policy) erzeugt. Er zeigt die zum Erhebungszeitpunkt tatsächlich wirksamen Gruppenrichtlinien — ' +
    'einschließlich verworfener Objekte samt Grund, Sicherheitsgruppen, durchsuchter Container und des ' +
    'operativen Ereignisprotokolls.</p></section>'

$title = 'RSoP-Bericht — ' + $hostName + ' · ' + $(if ($isLocal) { 'Lokal' } else { $domain })

$html = '<!DOCTYPE html>' + "`n" +
    '<html lang="de" data-theme="light">' + "`n" +
    '<head>' + "`n" +
    '<meta charset="utf-8">' + "`n" +
    '<meta name="viewport" content="width=device-width, initial-scale=1">' + "`n" +
    '<title>' + (esc $title) + '</title>' + "`n" +
    '<script>' + $ThemeInit + '</script>' + "`n" +
    '<style>' + "`n" + $Css + "`n" + '</style>' + "`n" +
    '</head>' + "`n" +
    '<body>' + "`n" +
    '<div class="topbar"><button class="burger" id="burger">&#9776;</button><b>' + (esc $title) + '</b></div>' + "`n" +
    '<div class="shell" id="shell">' + "`n" +
    '<aside class="rail">' +
    '<div class="brand" id="brand"><span class="logo"></span><span><small>gpresult · RSoP</small><b>' +
    (esc $hostName) + ' · ' + (esc $(if ($isLocal) { 'Lokal' } else { $domain })) + '</b></span></div>' +
    '<a class="overview" id="overview" href="#cover">' + (ic 'home') + ' Übersicht</a>' +
    $railNav +
    '<div class="railfoot">' +
    '<div class="rlink"><span class="g">' + (ic 'list') + '</span><span class="rt">Quelle: ' + (esc $xmlLeaf) +
    ' · ' + (esc $dataType) + '</span></div>' +
    '<div class="rlink"><span class="g">' + (ic 'clock') + '</span><span class="rt">Erhoben: ' + (FmtDate $readTime) +
    '<br>Bericht: ' + $genStamp + '</span></div>' +
    '</div>' +
    '<div class="railctl"><button id="themeBtn">' + (ic 'sun') + ' Thema</button>' +
    '<button id="printBtn">' + (ic 'printer') + ' Drucken</button></div>' +
    '<div class="raillinks">' +
    '<a href="https://toolbox.jozapf.de" target="_blank" rel="noopener">' + (ic 'link') + ' toolbox.jozapf.de</a>' +
    '<a href="https://jozapf.de" target="_blank" rel="noopener">' + (ic 'globe') + ' jozapf.de</a>' +
    '</div>' +
    '</aside>' + "`n" +
    '<main class="reading" id="reading"><div class="progress"><i id="bar"></i></div>' +
    '<div class="reading-inner">' +
    $cover + $uPart.Html + $cPart.Html +
    '<footer class="foot">Erzeugt am ' + $genStamp + ' mit ' + (esc $ScriptName) + ' · Datenquelle: ' +
    (esc $xmlLeaf) + ' (' + (esc $dataType) + ') · Erhoben am ' + (FmtDate $readTime) + '</footer>' +
    '</div></main>' + "`n" +
    '<div class="scrim" id="scrim"></div>' + "`n" +
    '</div>' + "`n" +
    '<script>' + "`n" + $Js + "`n" + '</script>' + "`n" +
    '</body>' + "`n" + '</html>'

#endregion

# ============================================================================
#region Schreiben und abschließen
# ============================================================================

$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($outFull, $html, $enc)

$sizeKb = [math]::Round((Get-Item -LiteralPath $outFull).Length / 1KB)
Write-Host ''
Write-Host "Bericht erstellt: $outFull ($sizeKb KB)" -ForegroundColor Green
if ($savedXml) { Write-Host "RSoP-XML:         $savedXml" -ForegroundColor Green }
if ($uInfo) { Write-Host ("  Benutzer:  {0} — {1} von {2} GPOs angewendet" -f $uInfo.Name, $uInfo.Applied.Count, $uInfo.Gpos.Count) }
if ($cInfo) { Write-Host ("  Computer:  {0} — {1} von {2} GPOs angewendet" -f $cInfo.Name, $cInfo.Applied.Count, $cInfo.Gpos.Count) }

if ($tempXml) {
    Remove-Item -LiteralPath $tempXml -ErrorAction SilentlyContinue
}

if ($Show) { Start-Process $outFull }

#endregion