Public/Export-MacRosettaAuditReport.ps1

function Export-MacRosettaAuditReport {
    <#
    .SYNOPSIS
        Exports MacRosettaAudit results to HTML, CSV, and/or JSON reports.

    .DESCRIPTION
        Takes pipeline output from Get-MacRosettaAudit and generates one or more report files.
        The HTML report is fully self-contained (inline CSS and JavaScript) and works completely offline.
        It includes a summary dashboard, filterable/sortable table, and a detail side-panel for each entry.
        Sorting only applies to the main result table, not to the detail inner tables.

    .PARAMETER InputObject
        Audit records from Get-MacRosettaAudit. Accepts pipeline input.

    .PARAMETER OutputPath
        Directory where report files are written.
        Defaults to ~/Desktop/MacRosettaAudit.

    .PARAMETER Format
        Report format to generate. Accepted values: Html, Csv, Json, All.
        Defaults to Html.

    .PARAMETER OpenReport
        Opens the generated HTML report in the default browser after export.
        Only applicable when Format is Html or All.

    .EXAMPLE
        Get-MacRosettaAudit | Export-MacRosettaAuditReport

        Full scan and export to HTML on the Desktop.

    .EXAMPLE
        Get-MacRosettaAudit | Export-MacRosettaAuditReport -Format All -OutputPath ~/audit

        Export HTML, CSV, and JSON to ~/audit.

    .EXAMPLE
        Get-MacRosettaAudit -IncludeDependencies | Export-MacRosettaAuditReport -Format Html -OpenReport

        Scan with dependency analysis, export HTML, and open it immediately.

    .NOTES
        Requires macOS with PowerShell 7+.
        The HTML report is fully offline-capable — no external CDN or internet connection required.
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [pscustomobject[]]$InputObject,

        [string]$OutputPath = (Join-Path $HOME 'Desktop/MacRosettaAudit'),

        [ValidateSet('Html', 'Csv', 'Json', 'All')]
        [string]$Format = 'Html',

        [switch]$OpenReport
    )

    begin {
        $allRecords = [System.Collections.Generic.List[object]]::new()
    }

    process {
        foreach ($item in $InputObject) {
            $allRecords.Add($item)
        }
    }

    end {
        if ($allRecords.Count -eq 0) {
            Write-Warning "No audit records received. Pipe Get-MacRosettaAudit output into Export-MacRosettaAuditReport."
            return
        }

        New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null

        $deduped = $allRecords |
            Sort-Object Category, DisplayName, ExecutablePath, ProcessId -Unique

        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        $hostname  = $env:COMPUTERNAME

        $csvPath  = Join-Path $OutputPath "rosetta-audit.csv"
        $jsonPath = Join-Path $OutputPath "rosetta-audit.json"
        $htmlPath = Join-Path $OutputPath "rosetta-audit.html"

        if ($Format -in 'Csv', 'All') {
            $deduped |
                Select-Object `
                    Category, DisplayName, Vendor, Version,
                    BundleDisplayName, BundleName, BundleId, BundleExecutable,
                    BundleShortVersion, BundleVersion, Status, RosettaNeeded,
                    CurrentlyUsingRosetta, ProcessArchitecture, RosettaRuntimeReason,
                    Architectures, Reason, Signed, SignatureId, TeamIdentifier, Authority,
                    DependencyCount, IntelDepCount, ExecutablePath, SourcePath,
                    BundlePath, PlistPath, LaunchLabel, ProcessId, ProcessUser |
                Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8

            Write-Host "CSV : $csvPath"
        }

        if ($Format -in 'Json', 'All') {
            $deduped | ConvertTo-Json -Depth 8 | Out-File -FilePath $jsonPath -Encoding UTF8
            Write-Host "JSON: $jsonPath"
        }

        if ($Format -in 'Html', 'All') {
            $total               = $deduped.Count
            $rosettaCount        = @($deduped | Where-Object { $_.RosettaNeeded }).Count
            $runtimeRosettaCount = @($deduped | Where-Object { $_.CurrentlyUsingRosetta }).Count
            $nativeCount         = @($deduped | Where-Object { $_.Status -eq "native" }).Count
            $universalCount      = @($deduped | Where-Object { $_.Status -eq "universal" }).Count
            $unknownCount        = @($deduped | Where-Object { $_.Status -eq "unknown" }).Count
            $intelDepCount       = @($deduped | Where-Object { $_.IntelDepCount -gt 0 }).Count

            $rows = ""

            foreach ($r in $deduped) {
                $statusLabel = switch ($r.Status) {
                    "rosetta"   { "Rosetta erforderlich" }
                    "native"    { "Native" }
                    "universal" { "Universal" }
                    "missing"   { "Fehlt" }
                    default     { "Unbekannt" }
                }

                $runtimeLabel = if ($r.CurrentlyUsingRosetta) { "Ja" } else { "Nein" }
                $runtimeClass = if ($r.CurrentlyUsingRosetta) { "runtimeYes" } else { "runtimeNo" }
                $runtimeData  = if ($r.CurrentlyUsingRosetta) { "true" } else { "false" }

                $depHtml = if ($r.Dependencies -and $r.Dependencies.Count -gt 0) {
                    ($r.Dependencies | ForEach-Object { "<li><code>$(ConvertTo-HtmlSafe $_)</code></li>" }) -join "`n"
                } else {
                    "<li>Keine Abhängigkeiten ausgewertet oder gefunden.</li>"
                }

                $intelDepHtml = if ($r.IntelDependencies -and $r.IntelDependencies.Count -gt 0) {
                    ($r.IntelDependencies | ForEach-Object { "<li><code>$(ConvertTo-HtmlSafe $_)</code></li>" }) -join "`n"
                } else {
                    "<li>Keine Intel-only Dependencies gefunden.</li>"
                }

                $rows += @"
<tr class="$($r.Status)" data-status="$($r.Status)" data-runtime="$runtimeData" data-category="$(ConvertTo-HtmlSafe $r.Category)">
<td><span class="badge $($r.Status)">$(ConvertTo-HtmlSafe $statusLabel)</span></td>
<td><strong>$(ConvertTo-HtmlSafe $r.DisplayName)</strong><br><small>$(ConvertTo-HtmlSafe $r.BundleId)</small></td>
<td>$(ConvertTo-HtmlSafe $r.Vendor)</td>
<td>$(ConvertTo-HtmlSafe $r.Version)</td>
<td>$(ConvertTo-HtmlSafe $r.Category)</td>
<td><code>$(ConvertTo-HtmlSafe $r.Architectures)</code></td>
<td><span class="badge $runtimeClass">$runtimeLabel</span></td>
<td><code>$(ConvertTo-HtmlSafe $r.ProcessArchitecture)</code></td>
<td>$($r.DependencyCount)</td>
<td>$($r.IntelDepCount)</td>
<td>
  <button class="detailButton" onclick="openDetails(this)">Details</button>
  <template class="detailTemplate">
    <div class="drawerContent">
      <h3>$(ConvertTo-HtmlSafe $r.DisplayName)</h3>
      <p class="drawerSubline">$(ConvertTo-HtmlSafe $r.Vendor) · $(ConvertTo-HtmlSafe $r.Version) · $(ConvertTo-HtmlSafe $statusLabel)</p>
      <h4>Allgemein</h4>
      <table class="inner">
        <tr><th>DisplayName</th><td>$(ConvertTo-HtmlSafe $r.DisplayName)</td></tr>
        <tr><th>Vendor</th><td>$(ConvertTo-HtmlSafe $r.Vendor)</td></tr>
        <tr><th>Version</th><td>$(ConvertTo-HtmlSafe $r.Version)</td></tr>
        <tr><th>Bundle Display Name</th><td>$(ConvertTo-HtmlSafe $r.BundleDisplayName)</td></tr>
        <tr><th>Bundle Name</th><td>$(ConvertTo-HtmlSafe $r.BundleName)</td></tr>
        <tr><th>Bundle ID</th><td><code>$(ConvertTo-HtmlSafe $r.BundleId)</code></td></tr>
        <tr><th>Bundle Executable</th><td><code>$(ConvertTo-HtmlSafe $r.BundleExecutable)</code></td></tr>
        <tr><th>Bundle Version</th><td>$(ConvertTo-HtmlSafe $r.BundleShortVersion) / $(ConvertTo-HtmlSafe $r.BundleVersion)</td></tr>
        <tr><th>Status</th><td>$(ConvertTo-HtmlSafe $statusLabel)</td></tr>
        <tr><th>Grund</th><td>$(ConvertTo-HtmlSafe $r.Reason)</td></tr>
      </table>
      <h4>Aktuelle Rosetta-Nutzung</h4>
      <table class="inner">
        <tr><th>Aktuell Rosetta</th><td>$(ConvertTo-HtmlSafe $runtimeLabel)</td></tr>
        <tr><th>Prozessarchitektur</th><td><code>$(ConvertTo-HtmlSafe $r.ProcessArchitecture)</code></td></tr>
        <tr><th>Prozess-ID</th><td>$(ConvertTo-HtmlSafe $r.ProcessId)</td></tr>
        <tr><th>Prozess-User</th><td>$(ConvertTo-HtmlSafe $r.ProcessUser)</td></tr>
        <tr><th>Bewertung</th><td>$(ConvertTo-HtmlSafe $r.RosettaRuntimeReason)</td></tr>
      </table>
      <h4>Pfade</h4>
      <table class="inner">
        <tr><th>Executable</th><td><code>$(ConvertTo-HtmlSafe $r.ExecutablePath)</code></td></tr>
        <tr><th>Quelle</th><td><code>$(ConvertTo-HtmlSafe $r.SourcePath)</code></td></tr>
        <tr><th>Bundle</th><td><code>$(ConvertTo-HtmlSafe $r.BundlePath)</code></td></tr>
        <tr><th>Plist</th><td><code>$(ConvertTo-HtmlSafe $r.PlistPath)</code></td></tr>
        <tr><th>Launch Label</th><td><code>$(ConvertTo-HtmlSafe $r.LaunchLabel)</code></td></tr>
      </table>
      <h4>Signatur</h4>
      <table class="inner">
        <tr><th>Signiert</th><td>$(ConvertTo-HtmlSafe $r.Signed)</td></tr>
        <tr><th>Identifier</th><td><code>$(ConvertTo-HtmlSafe $r.SignatureId)</code></td></tr>
        <tr><th>Team ID</th><td><code>$(ConvertTo-HtmlSafe $r.TeamIdentifier)</code></td></tr>
        <tr><th>Authority</th><td>$(ConvertTo-HtmlSafe $r.Authority)</td></tr>
      </table>
      <h4>Intel-only Dependencies</h4>
      <ul>$intelDepHtml</ul>
      <h4>Alle Dependencies</h4>
      <ul>$depHtml</ul>
    </div>
  </template>
</td>
</tr>
"@

            }

            $html = @"
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>MacRosettaAudit Report</title>
<style>
:root {
  --bg: #f5f7fb;
  --card: #ffffff;
  --text: #1f2937;
  --muted: #6b7280;
  --line: #e5e7eb;
  --bad: #fee2e2;
  --badText: #991b1b;
  --good: #dcfce7;
  --goodText: #166534;
  --warn: #fef3c7;
  --warnText: #92400e;
  --blue: #dbeafe;
  --blueText: #1e40af;
  --purple: #ede9fe;
  --purpleText: #5b21b6;
}
body { margin: 0; background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
header { padding: 28px 36px; background: #111827; color: white; }
header h1 { margin: 0 0 8px 0; font-size: 28px; }
header p { margin: 0; color: #d1d5db; }
main { padding: 24px 36px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 22px; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px; box-shadow: 0 3px 10px rgba(0,0,0,.04); }
.card .number { font-size: 28px; font-weight: 700; }
.card .label { color: var(--muted); font-size: 13px; }
.toolbar { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; align-items: center; }
input, select, button { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: white; font-size: 14px; }
input { min-width: 420px; }
button { cursor: pointer; }
table { width: 100%; border-collapse: collapse; background: white; border-radius: 14px; overflow: hidden; }
th { text-align: left; background: #f3f4f6; color: #374151; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; padding: 10px; border-bottom: 1px solid var(--line); cursor: pointer; }
td { padding: 10px; border-bottom: 1px solid var(--line); vertical-align: top; font-size: 13px; }
tr:hover { background: #f9fafb; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12px; word-break: break-all; }
.badge { display: inline-block; padding: 4px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; }
.badge.rosetta { background: var(--bad); color: var(--badText); }
.badge.native { background: var(--good); color: var(--goodText); }
.badge.universal { background: var(--blue); color: var(--blueText); }
.badge.unknown, .badge.missing { background: var(--warn); color: var(--warnText); }
.badge.runtimeYes { background: var(--purple); color: var(--purpleText); }
.badge.runtimeNo { background: #f3f4f6; color: #374151; }
.detailButton { white-space: nowrap; font-weight: 600; }
.inner { margin: 8px 0 18px 0; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
.inner th { width: 190px; text-transform: none; letter-spacing: 0; cursor: default; background: #f9fafb; pointer-events: none; }
.inner td, .inner th { font-size: 12px; padding: 8px; vertical-align: top; }
.hidden { display: none; }
.footer { margin-top: 18px; color: var(--muted); font-size: 12px; }
#drawerOverlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 1000; }
#drawerOverlay.open { display: block; }
#detailDrawer { position: fixed; top: 0; right: -760px; width: 720px; max-width: 92vw; height: 100vh; background: white; z-index: 1001; box-shadow: -10px 0 30px rgba(0,0,0,.18); transition: right .22s ease; overflow-y: auto; }
#detailDrawer.open { right: 0; }
.drawerHeader { position: sticky; top: 0; background: #111827; color: white; padding: 16px 18px; display: flex; justify-content: space-between; align-items: center; z-index: 1002; }
.drawerHeader h2 { margin: 0; font-size: 20px; }
.drawerHeader button { background: white; color: #111827; border: 0; font-weight: 700; }
.drawerContent { padding: 18px; text-align: left; }
.drawerContent h3 { margin: 0 0 4px 0; font-size: 22px; }
.drawerContent h4 { margin: 20px 0 8px 0; font-size: 15px; }
.drawerSubline { margin: 0 0 16px 0; color: var(--muted); }
.drawerContent ul { padding-left: 20px; }
.drawerContent li { margin-bottom: 6px; }
</style>
</head>
<body>
<header>
  <h1>MacRosettaAudit Report</h1>
  <p>Erstellt am $timestamp &nbsp;·&nbsp; Host: $hostname</p>
</header>
<main>
  <section class="cards">
    <div class="card"><div class="number">$total</div><div class="label">Gesamt</div></div>
    <div class="card"><div class="number">$runtimeRosettaCount</div><div class="label">Nutzen Rosetta aktuell</div></div>
    <div class="card"><div class="number">$rosettaCount</div><div class="label">Rosetta erforderlich</div></div>
    <div class="card"><div class="number">$nativeCount</div><div class="label">Native</div></div>
    <div class="card"><div class="number">$universalCount</div><div class="label">Universal</div></div>
    <div class="card"><div class="number">$intelDepCount</div><div class="label">Intel-only Dependencies</div></div>
    <div class="card"><div class="number">$unknownCount</div><div class="label">Unbekannt</div></div>
  </section>
  <section class="toolbar">
    <input id="search" placeholder="Suche nach DisplayName, Vendor, Version, Bundle ID, Pfad, Team ID..." />
    <select id="statusFilter">
      <option value="all">Alle Status</option>
      <option value="rosetta">Nur Rosetta erforderlich</option>
      <option value="native">Nur Native</option>
      <option value="universal">Nur Universal</option>
      <option value="unknown">Nur Unbekannt</option>
      <option value="missing">Nur Fehlend</option>
    </select>
    <select id="runtimeFilter">
      <option value="all">Aktuelle Rosetta-Nutzung: alle</option>
      <option value="true">Nur aktuell Rosetta</option>
      <option value="false">Nicht aktuell Rosetta</option>
    </select>
    <select id="categoryFilter">
      <option value="all">Alle Kategorien</option>
      <option value="Application">Applications</option>
      <option value="LaunchItem">LaunchItems</option>
      <option value="RunningProcess">RunningProcesses</option>
    </select>
  </section>
  <table id="resultTable">
    <thead>
      <tr>
        <th>Status</th>
        <th>DisplayName</th>
        <th>Vendor</th>
        <th>Version</th>
        <th>Kategorie</th>
        <th>Architektur</th>
        <th>Aktuell Rosetta</th>
        <th>Prozess-Arch</th>
        <th>Deps</th>
        <th>Intel Deps</th>
        <th>Details</th>
      </tr>
    </thead>
    <tbody>
      $rows
    </tbody>
  </table>
  <div class="footer">
    Hinweis: „Rosetta erforderlich" bedeutet Intel-only Binary. „Aktuell Rosetta" bedeutet, dass ein laufender Prozess aktuell als x86_64 erkannt wurde oder die laufende Binary Intel-only ist.
  </div>
</main>
<div id="drawerOverlay" onclick="closeDetails()"></div>
<aside id="detailDrawer">
  <div class="drawerHeader">
    <h2>Details</h2>
    <button onclick="closeDetails()">Schließen</button>
  </div>
  <div id="drawerBody"></div>
</aside>
<script>
const search = document.getElementById("search");
const statusFilter = document.getElementById("statusFilter");
const runtimeFilter = document.getElementById("runtimeFilter");
const categoryFilter = document.getElementById("categoryFilter");
const tableRows = Array.from(document.querySelectorAll("#resultTable > tbody > tr"));

function applyFilters() {
  const q = search.value.toLowerCase();
  const status = statusFilter.value;
  const runtime = runtimeFilter.value;
  const category = categoryFilter.value;
  tableRows.forEach(row => {
    const text = row.innerText.toLowerCase();
    const rowStatus = row.dataset.status || "";
    const rowRuntime = (row.dataset.runtime || "false").toLowerCase();
    const rowCategory = row.dataset.category || "";
    let visible = true;
    if (q && !text.includes(q)) visible = false;
    if (status !== "all" && rowStatus !== status) visible = false;
    if (runtime !== "all" && rowRuntime !== runtime) visible = false;
    if (category !== "all" && rowCategory !== category) visible = false;
    row.classList.toggle("hidden", !visible);
  });
}

function openDetails(button) {
  const template = button.parentElement.querySelector(".detailTemplate");
  const drawer = document.getElementById("detailDrawer");
  const overlay = document.getElementById("drawerOverlay");
  const body = document.getElementById("drawerBody");
  body.innerHTML = template.innerHTML;
  drawer.classList.add("open");
  overlay.classList.add("open");
}

function closeDetails() {
  document.getElementById("detailDrawer").classList.remove("open");
  document.getElementById("drawerOverlay").classList.remove("open");
  document.getElementById("drawerBody").innerHTML = "";
}

search.addEventListener("input", applyFilters);
statusFilter.addEventListener("change", applyFilters);
runtimeFilter.addEventListener("change", applyFilters);
categoryFilter.addEventListener("change", applyFilters);

document.querySelectorAll("#resultTable > thead > tr > th").forEach((th, index) => {
  th.addEventListener("click", () => {
    const tbody = document.querySelector("#resultTable > tbody");
    const sorted = tableRows.sort((a, b) => {
      const av = (a.children[index]?.innerText || "").trim().toLowerCase();
      const bv = (b.children[index]?.innerText || "").trim().toLowerCase();
      return av.localeCompare(bv, "de", { numeric: true });
    });
    sorted.forEach(row => tbody.appendChild(row));
    applyFilters();
  });
});

document.addEventListener("keydown", event => {
  if (event.key === "Escape") closeDetails();
});
</script>
</body>
</html>
"@

            $html | Out-File -FilePath $htmlPath -Encoding UTF8
            Write-Host "HTML: $htmlPath"
        }

        Write-Host ""
        Write-Host "Fertig. Ausgabeverzeichnis: $OutputPath" -ForegroundColor Green

        if ($OpenReport -and ($Format -in 'Html', 'All')) {
            /usr/bin/open "$htmlPath"
        }
    }
}