Build the Ops reporting foundation helper

Build the real local PowerShell helper used by Ops Stack reporting pages, including the file to create, the functions it must expose, and a usable starter implementation that exports HTML, CSV, JSON, and log artifacts.

Good For

  • Creating the local reporting helper from scratch
  • Understanding what the foundation script must contain
  • Verifying the helper before building report packs
  • Turning loose examples into a reusable local reporting layer
  • Preparing for future module-style packaging

How to Use It

  1. Create the helper as a real local file first. The reporting foundation page expects `.\templates\reporting\OpsReporting.Foundation.ps1` to exist before it loads the reusable functions.
  2. The helper file should expose a small stable public contract: run-context creation, run completion, result-row creation, summary creation, and artifact export.
  3. Keep the row model generic from day one. Build around target type and target name instead of assuming every report is host-only.
  4. Use a status taxonomy that remains reusable across future reports: Pass, Warning, Fail, Error, Skipped, NotAssessed, and Unreachable.
  5. Export HTML, CSV, JSON, and a plain log from the helper so future report starters can remain consistent without rewriting basic artifact plumbing.
  6. Treat this `.ps1` helper as the starter implementation. If it becomes the shared layer for paid packs, promote it into a versioned PowerShell module with tests and comment-based help.

Execution Modes

  • local-authoring
  • local-validation

Inputs and Outputs

Inputs

  • Local folder path for the helper
  • Function contract names
  • Starter schema decisions
  • Optional sample rows for validation

Outputs

  • html-report
  • csv
  • json
  • log-file
  • operator-notes

Command Starter

Changes system state: review before running

# ---------------------------------------------------------------------
# Create the reporting helper file expected by the foundation page
# ---------------------------------------------------------------------
$FoundationRoot = '.\templates\reporting'
New-Item -ItemType Directory -Path $FoundationRoot -Force | Out-Null
$FoundationPath = Join-Path $FoundationRoot 'OpsReporting.Foundation.ps1'

@'

# ---------------------------------------------------------------------
# OpsReporting.Foundation.ps1
# Minimal reusable reporting helper for Ops Stack report starters.
# ---------------------------------------------------------------------

function New-OpsReportRunContext {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$CheckName,
        [string]$Environment = 'unknown',
        [string]$ExecutionMode = 'local',
        [string]$InputSource = 'manual',
        [int]$RequestedTargetCount = 0
    )

    [pscustomobject]@{
        RunId = Get-Date -Format 'yyyyMMdd-HHmmss'
        CheckName = $CheckName
        Environment = $Environment
        ExecutionMode = $ExecutionMode
        InputSource = $InputSource
        RequestedTargetCount = $RequestedTargetCount
        StartedUtc = (Get-Date).ToUniversalTime()
        CompletedUtc = $null
        DurationMs = $null
    }
}

function Complete-OpsReportRunContext {
    [CmdletBinding()]
    param([Parameter(Mandatory)]$RunContext)

    $CompletedUtc = (Get-Date).ToUniversalTime()
    $RunContext.CompletedUtc = $CompletedUtc
    $RunContext.DurationMs = [math]::Round(($CompletedUtc - $RunContext.StartedUtc).TotalMilliseconds, 0)
    $RunContext
}

function New-OpsReportResultRow {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$RunId,
        [Parameter(Mandatory)][string]$TargetType,
        [Parameter(Mandatory)][string]$TargetName,
        [string]$TargetId = '',
        [string]$Host = '',
        [Parameter(Mandatory)][string]$CheckGroup,
        [Parameter(Mandatory)][string]$CheckName,
        [ValidateSet('Pass','Warning','Fail','Error','Skipped','NotAssessed','Unreachable')]
        [Parameter(Mandatory)][string]$Status,
        [string]$Finding = '',
        [string]$ExpectedValue = '',
        [string]$ActualValue = '',
        [string]$ErrorText = '',
        [string]$ErrorCategory = '',
        [int]$DurationMs = 0
    )

    [pscustomobject]@{
        RunId = $RunId
        TargetType = $TargetType
        TargetName = $TargetName
        TargetId = $TargetId
        Host = $Host
        CheckGroup = $CheckGroup
        CheckName = $CheckName
        Status = $Status
        Finding = $Finding
        ExpectedValue = $ExpectedValue
        ActualValue = $ActualValue
        ErrorText = $ErrorText
        ErrorCategory = $ErrorCategory
        DurationMs = $DurationMs
        CollectedUtc = (Get-Date).ToUniversalTime()
    }
}

function New-OpsReportSummary {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]$RunContext,
        [Parameter(Mandatory)][object[]]$Results
    )

    $Rows = @($Results)
    $TargetGroups = $Rows | Group-Object TargetType, TargetName
    $TargetsWithWarnings = @($TargetGroups | Where-Object { $_.Group.Status -contains 'Warning' }).Count
    $TargetsWithFailures = @($TargetGroups | Where-Object { $_.Group.Status -contains 'Fail' -or $_.Group.Status -contains 'Error' }).Count
    $TargetsUnreachable = @($TargetGroups | Where-Object { $_.Group.Status -contains 'Unreachable' }).Count

    [pscustomobject]@{
        RunId = $RunContext.RunId
        CheckName = $RunContext.CheckName
        ResultRowsTotal = $Rows.Count
        PassRows = @($Rows | Where-Object Status -eq 'Pass').Count
        WarningRows = @($Rows | Where-Object Status -eq 'Warning').Count
        FailRows = @($Rows | Where-Object Status -eq 'Fail').Count
        ErrorRows = @($Rows | Where-Object Status -eq 'Error').Count
        SkippedRows = @($Rows | Where-Object Status -eq 'Skipped').Count
        NotAssessedRows = @($Rows | Where-Object Status -eq 'NotAssessed').Count
        UnreachableRows = @($Rows | Where-Object Status -eq 'Unreachable').Count
        TargetsRequested = $RunContext.RequestedTargetCount
        TargetsProcessed = $TargetGroups.Count
        TargetsWithWarnings = $TargetsWithWarnings
        TargetsWithFailures = $TargetsWithFailures
        TargetsUnreachable = $TargetsUnreachable
        OverallStatus = if ($TargetsWithFailures -gt 0) { 'Fail' } elseif ($TargetsWithWarnings -gt 0) { 'Warning' } else { 'Pass' }
    }
}

function Export-OpsReportArtifacts {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$OutputRoot,
        [Parameter(Mandatory)]$RunContext,
        [Parameter(Mandatory)]$Summary,
        [Parameter(Mandatory)][object[]]$Results
    )

    New-Item -ItemType Directory -Path $OutputRoot -Force | Out-Null
    $BaseName = Join-Path $OutputRoot $RunContext.RunId
    $CsvPath = "$BaseName-results.csv"
    $JsonPath = "$BaseName-report.json"
    $HtmlPath = "$BaseName-summary.html"
    $LogPath = "$BaseName-execution.log"

    $Results | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8
    [pscustomobject]@{ RunContext = $RunContext; Summary = $Summary; Results = $Results } |
        ConvertTo-Json -Depth 8 |
        Set-Content -Path $JsonPath -Encoding UTF8

    $HtmlBody = @()
    $HtmlBody += '<h1>Ops Report Summary</h1>'
    $HtmlBody += ($Summary | ConvertTo-Html -Fragment)
    $HtmlBody += '<h2>Result Rows</h2>'
    $HtmlBody += ($Results | ConvertTo-Html -Fragment)
    $HtmlDocument = ConvertTo-Html -Title $RunContext.CheckName -Body ($HtmlBody -join [Environment]::NewLine)
    $HtmlDocument | Set-Content -Path $HtmlPath -Encoding UTF8

    "[$((Get-Date).ToUniversalTime().ToString('o'))] Exported report artifacts for RunId $($RunContext.RunId)." |
        Set-Content -Path $LogPath -Encoding UTF8

    [pscustomobject]@{
        OutputFolder = (Resolve-Path $OutputRoot).Path
        HtmlPath = (Resolve-Path $HtmlPath).Path
        CsvPath = (Resolve-Path $CsvPath).Path
        JsonPath = (Resolve-Path $JsonPath).Path
        LogPath = (Resolve-Path $LogPath).Path
        ArtifactCount = 4
    }
}
'@ | Set-Content -Path $FoundationPath -Encoding UTF8

# Load the helper and confirm that the public contract functions exist.
. $FoundationPath
'New-OpsReportRunContext','Complete-OpsReportRunContext','New-OpsReportResultRow','New-OpsReportSummary','Export-OpsReportArtifacts' |
    ForEach-Object { Get-Command $_ -ErrorAction Stop | Select-Object Name, CommandType }

Validation

  • The helper file exists at the documented path and can be dot-sourced without parse errors.
  • All expected contract functions load into the current PowerShell session after the helper is sourced.
  • A simple sample run can create a run context, at least one result row, a summary object, and a placeholder artifact export without rewriting the function names.

Reporting

  • Defines the minimum helper contract behind the public reporting foundation page.
  • Gives later health, patching, and evidence packs a stable starting point instead of hidden repo assumptions.
  • Acts as the bridge between public Toolchest examples and future module-style packaging.

Safety Notes

  • This page creates local folders and a local helper file. Review the path before running it.
  • If you are testing the helper layout, remove the generated folder tree and helper file afterward so the run can be cleanly undone.
  • Use that folder and file cleanup as the explicit undo path for test runs before you promote the helper into a shared location.
  • The helper exports local artifacts only; it does not modify remote systems.
  • Do not store credentials, tokens, or secret-bearing arguments inside generated reports or logs.