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
- 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.
- The helper file should expose a small stable public contract: run-context creation, run completion, result-row creation, summary creation, and artifact export.
- Keep the row model generic from day one. Build around target type and target name instead of assuming every report is host-only.
- Use a status taxonomy that remains reusable across future reports: Pass, Warning, Fail, Error, Skipped, NotAssessed, and Unreachable.
- Export HTML, CSV, JSON, and a plain log from the helper so future report starters can remain consistent without rewriting basic artifact plumbing.
- 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.