Public/New-TcReport.ps1
|
function New-TcReport { <# .SYNOPSIS Writes consolidation-report.html, a single file that holds the assessment, the plan and the solutions. .DESCRIPTION Works from the project and the snapshot alone, with no connection. The page needs nothing else: fonts, data and code are inside, its policy blocks every outbound connection, and it runs only its own script. Open it in a browser. Edge or Chrome can also write to the tracking file. The report shows what was not read as a gap, never as zero: Not collected and Not fully read stay apart from a service that is not used, a total that leaves out a tenant reads "at least", and a task the plan could not confirm stays with its reason. It shows Score and the score of what was read side by side. team-tracking.csv lives next to the report. It holds what the team records: statuses, assignees, notes, ticks and answers. It is created when it does not exist, and this command never writes to it after that, so generating again loses nothing. A row whose item left the plan stays in the file and shows in the report, and so does a row the report cannot read, which counts for nothing. Its columns are Type, Id, Label, Field, Value, Author, Date and Replaces, with commas, as UTF-8 with a byte order mark. The report holds personal data and the configuration of the tenants. Keep it out of synced folders. .PARAMETER Project The project file, or a project as read from it. .PARAMETER Snapshot The snapshot file, or a snapshot as read from it. Given as a file, the collection log beside it joins the report. .PARAMETER Plan The result of New-TcPlan on this snapshot. Without it, the plan is computed. .PARAMETER RuleSet The result of Invoke-TcRuleSet on this snapshot. Without it, the rules run first. .PARAMETER OutputPath A folder, or a file ending in .html. The default is the output folder of the project. .PARAMETER TrackingPath The tracking file. The default is team-tracking.csv next to the report. .PARAMETER IgnoreNameSuffix Words to remove from the end of a display name when looking for duplicates, as for New-TcPlan. .EXAMPLE New-TcReport -Project ./project.json -Snapshot ./output/20260115-090000-assessment/snapshot.json Writes the report in the output folder of the project, and returns its path. .EXAMPLE $plan = New-TcPlan -Project $project -Snapshot $snapshot New-TcReport -Project $project -Snapshot $snapshot -Plan $plan -OutputPath ./reports Writes the report from a plan that is already computed. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([pscustomobject])] param( [Parameter(Mandatory)][object]$Project, [Parameter(Mandatory)][object]$Snapshot, [System.Collections.IDictionary]$Plan, [System.Collections.IDictionary]$RuleSet, [string]$OutputPath, [string]$TrackingPath, [AllowEmptyCollection()][string[]]$IgnoreNameSuffix = @() ) $projectFolder = $null if ($Project -is [string]) { $projectPath = Resolve-TcPath -Path $Project $projectFolder = Split-Path -Path $projectPath -Parent $projectData = Read-TcProjectFile -Path $projectPath } else { $projectData = $Project } $log = @() if ($Snapshot -is [string]) { $snapshotPath = Resolve-TcPath -Path $Snapshot $snapshotData = Read-TcSnapshot -Path $snapshotPath $logPath = Join-Path (Split-Path -Path $snapshotPath -Parent) 'journal.log' if (Test-Path -LiteralPath $logPath) { $log = @([System.IO.File]::ReadAllLines($logPath) | Where-Object { $_.Trim() }) } } elseif ($Snapshot -is [System.Collections.IDictionary]) { $snapshotData = $Snapshot } else { throw 'Give -Snapshot as a path to snapshot.json, or as a snapshot read from it.' } $faults = @(Test-TcReportReference -Catalog (Get-TcProductCatalog)) if ($faults.Count) { throw "The product catalog points to what does not exist:$([Environment]::NewLine)$($faults -join [Environment]::NewLine)" } $roles = @(@(Get-TcProperty $projectData 'sources' @()) | ForEach-Object { Get-TcProperty $_ 'role' }) $strangers = @(@((Get-TcProperty $snapshotData 'tenants' @{}).Keys) | Where-Object { $_ -ne 'Target' -and $_ -notin $roles }) if ($strangers.Count) { throw "This snapshot has tenants that are not in the project: $($strangers -join ', '). Check that the snapshot belongs to this project." } if (-not $RuleSet) { $RuleSet = Invoke-TcRuleSet -Snapshot $snapshotData } if (-not $Plan) { $Plan = New-TcPlan -Project $projectData -Snapshot $snapshotData -RuleSet $RuleSet -IgnoreNameSuffix $IgnoreNameSuffix } if ($OutputPath) { $target = Resolve-TcPath -Path $OutputPath } else { $folder = [string](Get-TcProperty (Get-TcProperty $projectData 'output') 'path') if (-not [System.IO.Path]::IsPathRooted($folder)) { $folder = Join-Path $(if ($projectFolder) { $projectFolder } else { Resolve-TcPath -Path '.' }) $folder } $target = $folder } $reportPath = [System.IO.Path]::GetFullPath($(if ($target -match '\.html?$') { $target } else { Join-Path $target 'consolidation-report.html' })) $reportFolder = Split-Path -Path $reportPath -Parent $trackingFile = if ($TrackingPath) { [System.IO.Path]::GetFullPath((Resolve-TcPath -Path $TrackingPath)) } else { Join-Path $reportFolder 'team-tracking.csv' } if (-not $PSCmdlet.ShouldProcess($reportPath, 'Write the consolidation report')) { return } New-Item -ItemType Directory -Path $reportFolder -Force | Out-Null $tracking = Read-TcTrackingFile -Path $trackingFile -Create $readiness = @(Get-TcReadiness -Snapshot $snapshotData -RuleSet $RuleSet) $report = Get-TcReportData -Project $projectData -Snapshot $snapshotData -Plan $Plan -RuleSet $RuleSet -Readiness $readiness -Log $log -TrackingText $tracking.Text -TrackingName (Split-Path -Path $trackingFile -Leaf) -IgnoreNameSuffix $IgnoreNameSuffix $data = $report['Data'] # The merge of the plan decides what is an orphan. The page shows the same rows, live, from the file. $items = @( foreach ($item in @($data['actions']) + @($data['tasks']) + @($data['decisions'])) { [ordered]@{ id = $item['id'] } } foreach ($line in @($data['configurations'])) { [ordered]@{ id = '{0}|{1}' -f $line['role'], $line['setting'] } } ) $orphans = @(Merge-TcTrackingRow -Item $items -Tracking @($tracking.Rows) | Where-Object { $_['orphan'] } | ForEach-Object { $_['id'] }) $html = ConvertTo-TcReportHtml -Data $data -Tables $report['Tables'] $temporary = "$reportPath.tmp" [System.IO.File]::WriteAllText($temporary, $html, [System.Text.UTF8Encoding]::new($false)) Move-Item -LiteralPath $temporary -Destination $reportPath -Force if ($tracking.Locked) { Write-Warning "$trackingFile is open in another program, so the report holds no team record this time. Close it, then generate again." } elseif (-not $tracking.Readable) { Write-Warning "$trackingFile has lost its columns. It was left as it is: the first line must be Type,Id,Label,Field,Value,Author,Date,Replaces." } $service = Get-TcSyncRisk -Path $reportFolder if ($service) { Write-Warning "The folder $reportFolder is synced by $service. The report holds personal data: a local folder is safer." } Write-Warning 'This report holds personal data and the configuration of the tenants. Share it only with people who need it.' [pscustomobject]@{ Path = $reportPath TrackingPath = $trackingFile TrackingRows = @($tracking.Rows).Count OrphanRows = $orphans Actions = @($data['actions']).Count Tasks = @($data['tasks']).Count Findings = @($data['findings']).Count Products = @($data['solutions']['products']).Count SizeMB = [math]::Round((Get-Item -LiteralPath $reportPath).Length / 1MB, 1) } } |