src/report/renderers/Export-React.ps1

#Requires -Version 7.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

<#
.SYNOPSIS
    Render a self-contained, interactive HTML report (client-side filter/sort/
    search dashboard + optional cross-run drift panel) from scored findings.

.DESCRIPTION
    Same renderer contract as Export-Html/Export-Pptx/Export-Excel: consumes the
    scored Findings object produced by Get-Score (GeneratedOn/Frameworks/Areas/
    Gaps/Manual/Errors/Findings) plus the raw Collect object (only its optional
    _meta.scope / _meta.managementGroupId are read, mirroring Export-Pptx).

    Everything is inlined into one report-react.html file — CSS, JS, and the
    findings/drift data as a JSON blob in a <script> tag. No CDN fetch, no
    external <script src="http...">/<link href="http..."> references, so the
    report opens and works fully offline (same offline-artifact rule the PPTX
    renderer follows for the OpenXML SDK).

    The interactive UI (Dashboard / Findings / Gaps / Manual review / Drift
    tabs) is hand-written vanilla JS reading window.__SCOUT_DATA__ — no React
    UMD bundle, no build step, per the "vanilla JS component model" option in
    the design brief for this renderer.

.PARAMETER Findings
    The scored Findings object from Get-Score.

.PARAMETER Collect
    The raw Collect object (only _meta.scope / _meta.managementGroupId read).

.PARAMETER OutputPath
    Folder to write report-react.html into. Created if missing.

.PARAMETER Drift
    Optional. The drift object from Get-ScoutDrift (see
    src/report/Get-ScoutDrift.ps1). When supplied, the report embeds it and
    renders a Drift tab (New/Resolved/Regressed/Unchanged + per-finding
    changes + overall score delta). When omitted/$null, the Drift tab is not
    shown — this keeps Export-React callable with the exact same 3-argument
    shape as every other Export-* renderer for callers that don't have drift
    data yet (e.g. a first CollectOnly/FromCollect run), while still letting
    Invoke-ScoutAssessment pass -Drift once Get-ScoutDrift has run.

.OUTPUTS
    [string] the full path to the written report-react.html file.

.NOTES
    Tracks ADO Story AB#5053.
#>

function Export-React {
    param(
        $Findings,
        $Collect,
        [string] $OutputPath,
        $Drift = $null
    )

    if (-not (Test-Path $OutputPath)) {
        New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
    }

    # Safe property access — $Collect (and, defensively, $Findings) may be a
    # plain deserialized PSCustomObject missing keys we don't require, and
    # Set-StrictMode -Version Latest throws on a missing-property dot-access.
    $metaSrc = $null
    if ($null -ne $Collect) {
        $metaProp = $Collect.PSObject.Properties['_meta']
        if ($metaProp) { $metaSrc = $metaProp.Value }
    }
    $scope = $null
    $mgId  = $null
    if ($null -ne $metaSrc) {
        $scopeProp = $metaSrc.PSObject.Properties['scope']
        if ($scopeProp) { $scope = $scopeProp.Value }
        $mgIdProp = $metaSrc.PSObject.Properties['managementGroupId']
        if ($mgIdProp) { $mgId = $mgIdProp.Value }
    }
    $meta = [pscustomobject]@{ Scope = $scope; ManagementGroupId = $mgId }

    $payload = [pscustomobject]@{
        Findings = $Findings
        Drift    = $Drift
        Meta     = $meta
    }

    # </script> inside embedded JSON would otherwise close the <script> tag early.
    $json = ($payload | ConvertTo-Json -Depth 100) -replace '</', '<\/'

    $tplPath = "$PSScriptRoot/../templates/report-react.html.template"
    $tpl = Get-Content $tplPath -Raw
    $html = $tpl.Replace('/*__SCOUT_DATA__*/', $json)

    $outFile = Join-Path $OutputPath 'report-react.html'
    $html | Out-File $outFile -Encoding utf8

    return $outFile
}