Private/Report.ps1

<#
    Rendu HTML du rapport de migration.
 
    Page autonome (CSS inline, aucune ressource externe) : elle doit rester
    lisible transmise par courriel ou déposée sur un partage.
#>


function ConvertTo-SPMHtmlText {
    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][string]$Text)
    if ($null -eq $Text) { return '' }
    [System.Net.WebUtility]::HtmlEncode($Text)
}

function Write-SPMHtmlReport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][pscustomobject]$Report,
        [Parameter(Mandatory)][string]$Path
    )

    $statusColor = @{
        Completed = '#1a7f37'
        Failed = '#cf222e'
        Skipped = '#9a6700'
        Running = '#0969da'
        Submitted = '#8250df'
        Pending = '#57606a'
    }

    # Une valeur absente n'est pas zéro. Afficher « 0,0 s » sur une durée jamais
    # chronométrée fait conclure à un transfert instantané.
    $duree = {
        param($ms)
        if ($null -eq $ms) { 'non mesuré' } else { "$([math]::Round($ms / 1000, 1))&nbsp;s" }
    }

    # Le bandeau prime sur les cartes : c'est la première chose que doit lire
    # celui qui reçoit ce fichier par courriel.
    $alertes = @()
    if ($Report.PSObject.Properties['Alerts']) { $alertes = @($Report.Alerts) }

    $alertBlock = if ($alertes.Count) {
        $items = ($alertes | ForEach-Object { "<li>$(ConvertTo-SPMHtmlText $_)</li>" }) -join "`n"
        @"
  <div class="alert">
    <div class="alert-title">Ce rapport doit être lu avec ces réserves</div>
    <ul>
$items
    </ul>
  </div>
"@

    }
    else { '' }

    $rows = foreach ($s in $Report.StatusBreak) {
        $color = if ($statusColor.ContainsKey($s.Status)) { $statusColor[$s.Status] } else { '#57606a' }
        "<tr><td><span class='dot' style='background:$color'></span>$(ConvertTo-SPMHtmlText $s.Status)</td><td class='num'>$($s.Count)</td></tr>"
    }

    $failureRows = if ($Report.Failures.Count -gt 0) {
        foreach ($f in ($Report.Failures | Select-Object -First 200)) {
            "<tr><td class='mono'>$(ConvertTo-SPMHtmlText $f.Source)</td>" +
            "<td class='mono'>$(ConvertTo-SPMHtmlText $f.Target)</td>" +
            "<td class='err'>$(ConvertTo-SPMHtmlText $f.Error)</td></tr>"
        }
    }
    else {
        "<tr><td colspan='3' class='ok'>Aucun échec.</td></tr>"
    }

    $truncationNote = if ($Report.Failures.Count -gt 200) {
        "<p class='note'>Seuls les 200 premiers échecs sont affichés sur $($Report.Failures.Count).</p>"
    }
    else { '' }

    $throttleBlock = if ($Report.Throttling) {
        @"
      <div class="card">
        <div class="label">Throttling absorbé</div>
        <div class="value">$($Report.Throttling.Events)</div>
        <div class="sub">$($Report.Throttling.WaitedSec) s d'attente respectée</div>
      </div>
"@

    }
    else { '' }

    $rateColor = if ($null -eq $Report.SuccessRate) { '#57606a' }
    elseif ($Report.SuccessRate -ge 99) { '#1a7f37' }
    elseif ($Report.SuccessRate -ge 90) { '#9a6700' }
    else { '#cf222e' }

    $rateValue = if ($null -eq $Report.SuccessRate) { 'non mesuré' } else { "$($Report.SuccessRate)&nbsp;%" }

    # La carte des échecs ne devient verte que si quelque chose a réellement
    # abouti. « 0 échec » sur 0 unité terminée n'est pas un succès.
    $rien = $Report.PSObject.Properties['NothingTransferred'] -and $Report.NothingTransferred
    $enCours = $Report.PSObject.Properties['Running'] -and $Report.Running
    $failColor = if ($Report.Failed -or $rien -or $enCours) { '#cf222e' } else { '#1a7f37' }
    $failSub = if ($Report.Failed) { 'relançables avec -Resume' }
    elseif ($rien) { 'aucun — mais rien n''a abouti' }
    elseif ($enCours) { 'aucun — mais l''exécution est incomplète' }
    else { 'aucun' }

    $submittedBlock = if ($Report.PSObject.Properties['Submitted'] -and $Report.Submitted) {
        @"
      <div class="card">
        <div class="label">Soumises, non confirmées</div>
        <div class="value" style="color:#8250df">$($Report.Submitted)</div>
        <div class="sub">acceptées par le service, exécution non vérifiée</div>
      </div>
"@

    }
    else { '' }

    $html = @"
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rapport de migration SharePoint</title>
<style>
  :root { color-scheme: light dark; }
  body { font-family: -apple-system, "Segoe UI", system-ui, sans-serif; margin: 0; padding: 2rem;
         background: #f6f8fa; color: #1f2328; line-height: 1.5; }
  h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
  .meta { color: #57606a; font-size: .875rem; margin-bottom: 1.5rem; }
  .cards { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 2rem; }
  .card { background: #fff; border: 1px solid #d1d9e0; border-radius: 8px; padding: 1rem 1.25rem; min-width: 150px; }
  .label { font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: #57606a; }
  .value { font-size: 1.75rem; font-weight: 600; margin-top: .25rem; }
  .sub { font-size: .8125rem; color: #57606a; }
  table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid #d1d9e0;
          border-radius: 8px; overflow: hidden; margin-bottom: 1.5rem; }
  th, td { text-align: left; padding: .625rem .875rem; border-bottom: 1px solid #eaeef2; font-size: .875rem; }
  th { background: #f6f8fa; font-weight: 600; }
  tr:last-child td { border-bottom: none; }
  .num { text-align: right; font-variant-numeric: tabular-nums; }
  .mono { font-family: ui-monospace, "Cascadia Code", Menlo, monospace; font-size: .8125rem; word-break: break-all; }
  .err { color: #cf222e; font-size: .8125rem; }
  .ok { color: #1a7f37; }
  .note { color: #57606a; font-size: .8125rem; }
  .dot { display: inline-block; width: .625rem; height: .625rem; border-radius: 50%; margin-right: .5rem; }
  h2 { font-size: 1.125rem; margin: 1.5rem 0 .75rem; }
  .alert { background: #fff8f8; border: 1px solid #cf222e; border-left-width: 4px; border-radius: 8px;
           padding: .875rem 1.25rem; margin-bottom: 1.5rem; }
  .alert-title { font-weight: 600; color: #cf222e; margin-bottom: .375rem; }
  .alert ul { margin: 0; padding-left: 1.25rem; }
  .alert li { font-size: .875rem; }
  @media (prefers-color-scheme: dark) {
    body { background: #0d1117; color: #e6edf3; }
    .card, table { background: #161b22; border-color: #30363d; }
    th { background: #21262d; }
    th, td { border-bottom-color: #21262d; }
    .meta, .label, .sub, .note { color: #8b949e; }
    .alert { background: #2b1416; border-color: #f85149; }
    .alert-title { color: #f85149; }
  }
</style>
</head>
<body>
  <h1>Rapport de migration SharePoint</h1>
  <div class="meta">Généré le $(ConvertTo-SPMHtmlText $Report.GeneratedAt) &middot; état : <span class="mono">$(ConvertTo-SPMHtmlText $Report.StatePath)</span></div>
 
$alertBlock
  <div class="cards">
    <div class="card">
      <div class="label">Taux de réussite</div>
      <div class="value" style="color:$rateColor">$rateValue</div>
      <div class="sub">$($Report.Completed) / $($Report.TotalUnits) unités</div>
    </div>
    <div class="card">
      <div class="label">Échecs</div>
      <div class="value" style="color:$failColor">$($Report.Failed)</div>
      <div class="sub">$failSub</div>
    </div>
    <div class="card">
      <div class="label">Ignorées</div>
      <div class="value">$($Report.Skipped)</div>
      <div class="sub">déjà à jour</div>
    </div>
    <div class="card">
      <div class="label">Durée moyenne</div>
      <div class="value">$(& $duree $Report.AvgDurationMs)</div>
      <div class="sub">max $(& $duree $Report.MaxDurationMs)</div>
    </div>
$submittedBlock
$throttleBlock
  </div>
 
  <h2>Répartition par statut</h2>
  <table>
    <thead><tr><th>Statut</th><th class="num">Unités</th></tr></thead>
    <tbody>
$($rows -join "`n")
    </tbody>
  </table>
 
  <h2>Échecs</h2>
  <table>
    <thead><tr><th>Source</th><th>Destination</th><th>Erreur</th></tr></thead>
    <tbody>
$($failureRows -join "`n")
    </tbody>
  </table>
$truncationNote
</body>
</html>
"@


    Set-Content -LiteralPath $Path -Value $html -Encoding utf8
}