Public/New-TechCareReport.ps1
|
function New-TechCareReport { <# .SYNOPSIS Genera el informe de la ejecucion en HTML, JSON y texto. .DESCRIPTION Construye un informe autocontenido (un solo fichero HTML, sin recursos externos ni peticiones de red) con el estado global, los hallazgos, las acciones realizadas, el espacio liberado y las recomendaciones manuales. Junto al HTML escribe un JSON con el mismo contenido para herramientas y un log de texto para pegar en el ticket. Es el unico cmdlet del modulo que funciona en cualquier sistema operativo: solo da forma a lo que ya esta en memoria, no consulta el equipo. Por eso la suite de pruebas puede verificarlo en Linux. .PARAMETER ReportPath Carpeta destino. Por defecto, una subcarpeta con el identificador de la ejecucion dentro de la carpeta temporal del usuario. .PARAMETER TechnicianName Nombre del tecnico, para la cabecera del informe. .PARAMETER CompanyName Nombre de la empresa, para la cabecera del informe. .PARAMETER ContactInfo Telefono o correo de contacto, para la cabecera del informe. .EXAMPLE New-TechCareReport Escribe el informe en la carpeta temporal y devuelve las tres rutas. .EXAMPLE New-TechCareReport -ReportPath C:\Reportes -TechnicianName 'Eduard Arbona' Informe con cabecera personalizada en una carpeta concreta. .EXAMPLE New-TechCareReport -ReportPath C:\Reportes -WhatIf Dice que ficheros escribiria sin crear la carpeta ni los ficheros. .OUTPUTS PSCustomObject .LINK https://github.com/earbona23/TechCare #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] [OutputType([pscustomobject])] param( [ValidateNotNullOrEmpty()] [string]$ReportPath, [AllowEmptyString()] [string]$TechnicianName = '', [AllowEmptyString()] [string]$CompanyName = '', [AllowEmptyString()] [string]$ContactInfo = '' ) if (-not $ReportPath) { if ($script:State.ReportPath) { # Invoke-TechCare ya fijo la carpeta de la pasada: el informe debe caer # junto al log incremental, no en otra carpeta distinta. $ReportPath = $script:State.ReportPath } else { # $env:TEMP no existe en Linux ni en macOS; GetTempPath si, en los tres. $ReportPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('TechCare\' + $script:State.RunId) } } $script:State.Branding.TechnicianName = $TechnicianName $script:State.Branding.CompanyName = $CompanyName $script:State.Branding.ContactInfo = $ContactInfo $computer = $env:COMPUTERNAME if ([string]::IsNullOrWhiteSpace($computer)) { $computer = [System.Environment]::MachineName } $results = @($script:State.Results.ToArray()) $overall = 'Gray' if ($results.Count -gt 0) { $overall = 'Green' foreach ($r in $results) { if ($r.Status -eq 'Red') { $overall = 'Red'; break } if ($r.Status -eq 'Yellow') { $overall = 'Yellow' } } } $totalFreed = [int64]0 foreach ($r in $results) { $totalFreed += $r.SpaceFreedBytes } $htmlPath = Join-Path -Path $ReportPath -ChildPath 'TechCare-Report.html' $jsonPath = Join-Path -Path $ReportPath -ChildPath 'TechCare-Report.json' $logPath = Join-Path -Path $ReportPath -ChildPath 'TechCare-Log.txt' $label = @{ Green = 'Correcto'; Yellow = 'Atencion'; Red = 'Critico'; Gray = 'Sin datos' } $color = @{ Green = '#1a7f37'; Yellow = '#9a6700'; Red = '#b42318'; Gray = '#57606a' } $mode = 'Vista previa' if ($script:State.Apply) { $mode = 'Aplicar' } $sb = New-Object -TypeName System.Text.StringBuilder [void]$sb.AppendLine('<!DOCTYPE html><html lang="es"><head><meta charset="utf-8">') [void]$sb.AppendLine('<meta name="viewport" content="width=device-width,initial-scale=1">') [void]$sb.AppendLine('<title>TechCare - Informe</title><style>') [void]$sb.AppendLine('body{font-family:Segoe UI,Arial,sans-serif;margin:0;background:#f6f8fa;color:#1f2328}') [void]$sb.AppendLine('.wrap{max-width:960px;margin:0 auto;padding:24px}') [void]$sb.AppendLine('.card{background:#fff;border:1px solid #d0d7de;border-radius:8px;padding:20px;margin:16px 0}') [void]$sb.AppendLine('h1{font-size:22px;margin:0 0 4px}h2{font-size:17px;margin:0 0 10px}') [void]$sb.AppendLine('.banner{border-radius:8px;padding:16px;color:#fff;font-weight:600;font-size:16px}') [void]$sb.AppendLine('ul{margin:6px 0 0 18px;padding:0}li{margin:2px 0;font-size:14px}') [void]$sb.AppendLine('.muted{color:#57606a;font-size:13px}') [void]$sb.AppendLine('.tag{display:inline-block;font-size:12px;padding:2px 8px;border-radius:10px;color:#fff;font-weight:600}') [void]$sb.AppendLine('@media print{body{background:#fff}.card{break-inside:avoid}}') [void]$sb.AppendLine('</style></head><body><div class="wrap">') [void]$sb.AppendLine('<div class="card"><h1>TechCare - Informe de mantenimiento</h1>') [void]$sb.AppendLine(('<p class="muted">Ejecucion {0} | {1} | Equipo {2} | Preset {3} | Modo: {4}</p>' -f (ConvertTo-TechCareHtml -Text $script:State.RunId), (ConvertTo-TechCareHtml -Text ((Get-Date).ToString('yyyy-MM-dd HH:mm'))), (ConvertTo-TechCareHtml -Text $computer), (ConvertTo-TechCareHtml -Text $script:State.Preset), (ConvertTo-TechCareHtml -Text $mode))) if ($TechnicianName -or $CompanyName -or $ContactInfo) { [void]$sb.AppendLine('<p class="muted">') if ($TechnicianName) { [void]$sb.AppendLine('Tecnico: ' + (ConvertTo-TechCareHtml -Text $TechnicianName) + '<br>') } if ($CompanyName) { [void]$sb.AppendLine('Empresa: ' + (ConvertTo-TechCareHtml -Text $CompanyName) + '<br>') } if ($ContactInfo) { [void]$sb.AppendLine('Contacto: ' + (ConvertTo-TechCareHtml -Text $ContactInfo)) } [void]$sb.AppendLine('</p>') } [void]$sb.AppendLine('</div>') [void]$sb.AppendLine(('<div class="banner" style="background:{0}">Estado general: {1}</div>' -f $color[$overall], $label[$overall])) if ($totalFreed -gt 0) { [void]$sb.AppendLine(('<div class="card"><h2>Espacio liberado</h2><p>{0}</p></div>' -f (ConvertTo-TechCareHtml -Text (Format-TechCareSize -Bytes $totalFreed)))) } foreach ($r in $results) { [void]$sb.AppendLine('<div class="card">') [void]$sb.AppendLine(('<h2>{0} <span class="tag" style="background:{1}">{2}</span></h2>' -f (ConvertTo-TechCareHtml -Text $r.Module), $color[$r.Status], $label[$r.Status])) foreach ($section in @( @{ Title = 'Hallazgos'; Items = $r.Findings }, @{ Title = 'Acciones realizadas'; Items = $r.Actions }, @{ Title = 'Recomendaciones manuales'; Items = $r.Recommendations } )) { $items = @($section.Items) if ($items.Count -eq 0) { continue } [void]$sb.AppendLine('<strong>' + $section.Title + '</strong><ul>') foreach ($i in $items) { [void]$sb.AppendLine('<li>' + (ConvertTo-TechCareHtml -Text $i) + '</li>') } [void]$sb.AppendLine('</ul>') } [void]$sb.AppendLine('</div>') } [void]$sb.AppendLine(('<div class="card muted">Generado por TechCare {0}. Sin telemetria: el informe no sale de este equipo.</div>' -f (ConvertTo-TechCareHtml -Text $script:ModuleVersion))) [void]$sb.AppendLine('</div></body></html>') $written = $false # Un solo gate para los tres ficheros y la carpeta: son una sola accion # logica, y asi -WhatIf no crea la carpeta que dice que no va a crear. if ($PSCmdlet.ShouldProcess($ReportPath, 'Escribir informe HTML, JSON y log de texto')) { if (-not (Test-Path -LiteralPath $ReportPath)) { [void](New-Item -ItemType Directory -Path $ReportPath -Force) } # Se escribe con UTF8Encoding($false) en vez de Set-Content -Encoding UTF8 # porque ese parametro NO significa lo mismo en las dos ediciones: # Windows PowerShell 5.1 antepone BOM y PowerShell 7 no. Un JSON con BOM # lo aceptan ConvertFrom-Json y Python, pero lo rechazan jq y varios # analizadores estrictos, asi que el mismo informe seria valido o no # segun la consola desde la que se genero. $sinBom = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false [System.IO.File]::WriteAllText($htmlPath, $sb.ToString(), $sinBom) $payload = [pscustomobject]@{ schemaVersion = 1 runId = $script:State.RunId moduleVersion = $script:ModuleVersion startedUtc = $script:State.StartedUtc.ToString('o') generatedUtc = (Get-Date).ToUniversalTime().ToString('o') computer = $computer preset = $script:State.Preset apply = $script:State.Apply overall = $overall spaceFreedBytes = $totalFreed results = $results } [System.IO.File]::WriteAllText($jsonPath, ($payload | ConvertTo-Json -Depth 6), $sinBom) $lines = foreach ($l in $script:State.Log) { $text = '{0} [{1}] [{2}] {3}' -f $l.TimeUtc, $l.Level, $l.Module, $l.Message if ($l.Error) { $text = $text + ' :: ' + $l.Error } $text } # Sin entradas el fichero queda vacio, no con un salto de linea suelto: # 0 bytes dice "no hubo log", 1 byte parece un fichero truncado. $textoLog = '' if (@($lines).Count -gt 0) { $textoLog = (@($lines) -join [System.Environment]::NewLine) + [System.Environment]::NewLine } [System.IO.File]::WriteAllText($logPath, $textoLog, $sinBom) $written = $true } return [pscustomobject]@{ HtmlPath = $htmlPath JsonPath = $jsonPath LogPath = $logPath Overall = $overall Written = $written } } |