Private/Html.ps1
|
#Requires -Version 5.1 function Get-SMBeatReportStrings { param( [ValidateSet('de', 'en')] [string]$Language ) if ($Language -eq 'de') { return @{ Title = 'SMBeat Nutzungsbericht' Period = 'Zeitraum' Coverage = 'Abdeckung' Generated = 'Erzeugt' Version = 'SMBeat' Totals = 'Volumen' Retrieved = 'Abgerufen' RetrievedHint = 'TCP 445, Server zum Client' NicOut = 'NIC Out' NicOutHint = 'Kontrolle: alles was den Host verlaesst' NicIn = 'NIC In' NicInHint = 'Kontrolle: alles was ankommt' Previous = 'Vorperiode' PreviousHint = 'gleich langes Fenster davor' ByServer = 'Server' ByShare = 'Freigaben' ByShareEmpty = 'Bytes je Freigabe werden noch nicht gemessen (ETW folgt).' ByClient = 'Clients' ByClientHint = 'Remote-IP auf TCP 445' ByUser = 'Benutzer' ByUserEmpty = 'Keine Benutzer-Zuordnung in diesem Fenster.' Hourly = 'Stunden' Daily = 'Tage' Weekly = 'Wochen' Warnings = 'Hinweise' CoverageLow = 'Niedrige Abdeckung: der Collector war nicht die volle Fensterlaenge aktiv. Nach einem frischen Start ist das normal.' Name = 'Name' GiB = 'GiB' Empty = 'Keine Werte in diesem Fenster.' Foot = 'Abgerufen ist TCP-Payload auf Port 445 (ohne Ethernet-Header). NIC Out/In sind die Leitung (inkl. RDP, Backup, Overhead). Freigaben je Share folgen spaeter per ETW. Alte SMB-Zaehler werden nicht als Headline genutzt.' Company = 'Merlin Kommunikationstechnik' Utc = 'UTC' } } return @{ Title = 'SMBeat usage report' Period = 'Period' Coverage = 'Coverage' Generated = 'Generated' Version = 'SMBeat' Totals = 'Volume' Retrieved = 'Retrieved' RetrievedHint = 'TCP 445, server to client' NicOut = 'NIC Out' NicOutHint = 'Control: all host egress' NicIn = 'NIC In' NicInHint = 'Control: all host ingress' Previous = 'Previous period' PreviousHint = 'window of equal length before this one' ByServer = 'Servers' ByShare = 'Shares' ByShareEmpty = 'Per-share bytes are not measured yet (ETW to follow).' ByClient = 'Clients' ByClientHint = 'Remote IP on TCP 445' ByUser = 'Users' ByUserEmpty = 'No user attribution in this window.' Hourly = 'Hours' Daily = 'Days' Weekly = 'Weeks' Warnings = 'Notes' CoverageLow = 'Low coverage: the collector was not running for the full window. That is normal after a fresh start.' Name = 'Name' GiB = 'GiB' Empty = 'No values in this window.' Foot = 'Retrieved is TCP payload on port 445 (no Ethernet headers). NIC Out/In are the wire (including RDP, backup, overhead). Per-share bytes will come from ETW later. Legacy SMB counters are not the headline.' Company = 'Merlin Kommunikationstechnik' Utc = 'UTC' } } function ConvertTo-SMBeatHtmlEncode { param( [string]$Text ) if ($null -eq $Text) { return '' } [System.Net.WebUtility]::HtmlEncode($Text) } function Format-SMBeatHtmlGiB { param( $Value ) $n = 0.0 if ($null -ne $Value) { $n = [double]$Value } $n.ToString('0.000', $script:SMBeatInvariant) } function Format-SMBeatHtmlWhen { param( $Value ) if ($null -eq $Value) { return '' } if ($Value -is [datetime]) { return $Value.ToString('yyyy-MM-dd HH:mm', $script:SMBeatInvariant) } return [string]$Value } function Select-SMBeatHtmlActiveRows { param( [object[]]$Rows ) if ($null -eq $Rows) { return @() } @($Rows | Where-Object { $_ -and ([int64]$_.SentBytes -gt 0 -or [int64]$_.WriteBytes -gt 0) }) } function ConvertTo-SMBeatHtmlCard { param( [string]$Label, [string]$Hint, [string]$Value, [string]$CssClass ) if ([string]::IsNullOrWhiteSpace($CssClass)) { $CssClass = 'card' } $hintHtml = '' if (-not [string]::IsNullOrWhiteSpace($Hint)) { $hintHtml = '<div class="hint">{0}</div>' -f (ConvertTo-SMBeatHtmlEncode -Text $Hint) } '<div class="{0}"><div class="label">{1}</div><div class="value">{2} <span class="unit">GiB</span></div>{3}</div>' -f ` $CssClass, ` (ConvertTo-SMBeatHtmlEncode -Text $Label), ` (ConvertTo-SMBeatHtmlEncode -Text $Value), ` $hintHtml } function ConvertTo-SMBeatHtmlTable { param( [object[]]$Rows, [string]$NameHeader, [string]$ValueHeader, [string]$EmptyText, [int]$Top = 15 ) $active = @(Select-SMBeatHtmlActiveRows -Rows $Rows) if ($active.Count -eq 0) { return ('<p class="muted">{0}</p>' -f (ConvertTo-SMBeatHtmlEncode -Text $EmptyText)) } $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('<table><thead><tr>') [void]$sb.AppendLine(('<th>{0}</th><th class="num">{1}</th></tr></thead><tbody>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $NameHeader), ` (ConvertTo-SMBeatHtmlEncode -Text $ValueHeader))) $i = 0 foreach ($row in $active) { if ($i -ge $Top) { break } $i++ [void]$sb.AppendLine(('<tr><td>{0}</td><td class="num">{1}</td></tr>' -f ` (ConvertTo-SMBeatHtmlEncode -Text ([string]$row.Name)), ` (Format-SMBeatHtmlGiB -Value $row.SentGiB))) } [void]$sb.AppendLine('</tbody></table>') $sb.ToString() } function ConvertTo-SMBeatHtmlSeries { param( [object[]]$Rows, [string]$RetrievedHeader, [string]$NicOutHeader, [string]$EmptyText ) if ($null -eq $Rows -or $Rows.Count -eq 0) { return ('<p class="muted">{0}</p>' -f (ConvertTo-SMBeatHtmlEncode -Text $EmptyText)) } $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine(('<table><thead><tr><th>Start</th><th class="num">{0}</th><th class="num">{1}</th></tr></thead><tbody>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $RetrievedHeader), ` (ConvertTo-SMBeatHtmlEncode -Text $NicOutHeader))) foreach ($row in $Rows) { [void]$sb.AppendLine(('<tr><td>{0}</td><td class="num">{1}</td><td class="num">{2}</td></tr>' -f ` (ConvertTo-SMBeatHtmlEncode -Text (Format-SMBeatHtmlWhen -Value $row.PeriodStart)), ` (Format-SMBeatHtmlGiB -Value $row.RetrievedGiB), ` (Format-SMBeatHtmlGiB -Value $row.NicOutGiB))) } [void]$sb.AppendLine('</tbody></table>') $sb.ToString() } function ConvertTo-SMBeatHtmlReport { param( [Parameter(Mandatory = $true)] $Report, [ValidateSet('de', 'en')] [string]$Language ) $t = Get-SMBeatReportStrings -Language $Language $version = Get-SMBeatModuleVersion if ($Report.PSObject.Properties['ModuleVersion'] -and $Report.ModuleVersion) { $version = [string]$Report.ModuleVersion } $generated = Get-SMBeatUtcNow if ($Report.PSObject.Properties['GeneratedUtc'] -and $Report.GeneratedUtc) { $generated = $Report.GeneratedUtc } $generatedText = (ConvertTo-SMBeatUtcString -DateTime $generated) $startUtc = $Report.StartUtc.ToString('yyyy-MM-dd HH:mm', $script:SMBeatInvariant) + 'Z' $endUtc = $Report.EndUtc.ToString('yyyy-MM-dd HH:mm', $script:SMBeatInvariant) + 'Z' $totals = $Report.Totals $retrievedText = Format-SMBeatHtmlGiB -Value $totals.RetrievedGiB $nicOutText = Format-SMBeatHtmlGiB -Value $totals.NicOutGiB $nicInText = Format-SMBeatHtmlGiB -Value $totals.NicInGiB $prevHtml = '' $prevValue = '0.000' if ($Report.PreviousTotals) { $prevValue = Format-SMBeatHtmlGiB -Value $Report.PreviousTotals.RetrievedGiB $prevHtml = ConvertTo-SMBeatHtmlCard -Label $t.Previous -Hint $t.PreviousHint -Value $prevValue -CssClass 'card' } $notes = New-Object System.Collections.Generic.List[string] $coverage = 0.0 if ($null -ne $Report.CoveragePct) { $coverage = [double]$Report.CoveragePct } if ($coverage -lt 90) { $notes.Add($t.CoverageLow) | Out-Null } foreach ($w in @($Report.Warnings)) { if ($w -and ([string]$w -notlike 'Coverage *')) { $notes.Add([string]$w) | Out-Null } } $warnHtml = '' if ($notes.Count -gt 0) { $items = ($notes | ForEach-Object { '<li>' + (ConvertTo-SMBeatHtmlEncode -Text $_) + '</li>' }) -join '' $warnHtml = '<section><h2>{0}</h2><ul class="notes">{1}</ul></section>' -f (ConvertTo-SMBeatHtmlEncode -Text $t.Warnings), $items } $userRows = @(Select-SMBeatHtmlActiveRows -Rows $Report.ByUser) $userSection = '' if ($userRows.Count -gt 0) { $userSection = '<section><h2>{0}</h2>{1}</section>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $t.ByUser), ` (ConvertTo-SMBeatHtmlTable -Rows $userRows -NameHeader $t.Name -ValueHeader $t.GiB -EmptyText $t.ByUserEmpty) } @" <!DOCTYPE html> <html lang="$Language"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>$(ConvertTo-SMBeatHtmlEncode -Text $t.Title) $version</title> <style> body { font-family: Segoe UI, Helvetica, Arial, sans-serif; margin: 0; color: #1a1a1a; background: #f4f4f2; } header { background: #1f2a22; color: #f4f4f2; padding: 1.4rem 2rem; } header h1 { margin: 0 0 0.25rem; font-size: 1.35rem; font-weight: 600; } header .meta { color: #c8d0c4; font-size: 0.9rem; } main { padding: 1.5rem 2rem 3rem; max-width: 56rem; } h2 { font-size: 1rem; margin: 1.8rem 0 0.6rem; color: #1f2a22; } .cards { display: flex; flex-wrap: wrap; gap: 0.8rem; } .card { flex: 1 1 11rem; background: #fff; border: 1px solid #d8d8d2; padding: 0.9rem 1rem; } .card.hero { flex: 2 1 16rem; background: #eef3ea; border-color: #c5d2bc; } .card .label { font-size: 0.75rem; letter-spacing: 0.04em; text-transform: uppercase; color: #5c655c; } .card .value { font-size: 1.85rem; font-weight: 600; margin-top: 0.2rem; } .card .unit { font-size: 0.85rem; font-weight: 500; color: #5c655c; } .card .hint { margin-top: 0.35rem; font-size: 0.8rem; color: #5c655c; } .facts { margin: 1rem 0 0; font-size: 0.92rem; color: #333; } .facts div { margin: 0.2rem 0; } table { border-collapse: collapse; width: 100%; background: #fff; margin: 0.4rem 0 0; } th, td { border: 1px solid #d8d8d2; padding: 0.4rem 0.6rem; text-align: left; } th { background: #eef3ea; font-weight: 600; } td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; } .muted { color: #666; } ul.notes { margin: 0.4rem 0 0 1.1rem; } footer { margin-top: 2rem; font-size: 0.82rem; color: #555; max-width: 46rem; } </style> </head> <body> <header> <h1>$(ConvertTo-SMBeatHtmlEncode -Text $t.Title)</h1> <div class="meta">$(ConvertTo-SMBeatHtmlEncode -Text $t.Company) · $(ConvertTo-SMBeatHtmlEncode -Text $t.Version) $(ConvertTo-SMBeatHtmlEncode -Text $version)</div> </header> <main> <div class="facts"> <div><strong>$(ConvertTo-SMBeatHtmlEncode -Text $t.Period):</strong> $startUtc - $endUtc ($(ConvertTo-SMBeatHtmlEncode -Text ([string]$Report.TimeZone)))</div> <div><strong>$(ConvertTo-SMBeatHtmlEncode -Text $t.Coverage):</strong> $($Report.CoveragePct)%</div> <div><strong>$(ConvertTo-SMBeatHtmlEncode -Text $t.Generated):</strong> $generatedText</div> </div> $warnHtml <section> <h2>$(ConvertTo-SMBeatHtmlEncode -Text $t.Totals)</h2> <div class="cards"> $(ConvertTo-SMBeatHtmlCard -Label $t.Retrieved -Hint $t.RetrievedHint -Value $retrievedText -CssClass 'card hero') $prevHtml $(ConvertTo-SMBeatHtmlCard -Label $t.NicOut -Hint $t.NicOutHint -Value $nicOutText -CssClass 'card') $(ConvertTo-SMBeatHtmlCard -Label $t.NicIn -Hint $t.NicInHint -Value $nicInText -CssClass 'card') </div> </section> <section> <h2>$(ConvertTo-SMBeatHtmlEncode -Text $t.ByClient)</h2> <p class="muted">$(ConvertTo-SMBeatHtmlEncode -Text $t.ByClientHint)</p> $(ConvertTo-SMBeatHtmlTable -Rows $Report.ByClient -NameHeader $t.Name -ValueHeader $t.GiB -EmptyText $t.Empty) </section> <section> <h2>$(ConvertTo-SMBeatHtmlEncode -Text $t.ByServer)</h2> $(ConvertTo-SMBeatHtmlTable -Rows $Report.ByServer -NameHeader $t.Name -ValueHeader $t.GiB -EmptyText $t.Empty) </section> <section> <h2>$(ConvertTo-SMBeatHtmlEncode -Text $t.ByShare)</h2> $(ConvertTo-SMBeatHtmlTable -Rows $Report.ByShare -NameHeader $t.Name -ValueHeader $t.GiB -EmptyText $t.ByShareEmpty) </section> $userSection <section> <h2>$(ConvertTo-SMBeatHtmlEncode -Text $t.Hourly)</h2> $(ConvertTo-SMBeatHtmlSeries -Rows $Report.Hourly -RetrievedHeader $t.Retrieved -NicOutHeader $t.NicOut -EmptyText $t.Empty) </section> <section> <h2>$(ConvertTo-SMBeatHtmlEncode -Text $t.Daily)</h2> $(ConvertTo-SMBeatHtmlSeries -Rows $Report.Daily -RetrievedHeader $t.Retrieved -NicOutHeader $t.NicOut -EmptyText $t.Empty) </section> <section> <h2>$(ConvertTo-SMBeatHtmlEncode -Text $t.Weekly)</h2> $(ConvertTo-SMBeatHtmlSeries -Rows $Report.Weekly -RetrievedHeader $t.Retrieved -NicOutHeader $t.NicOut -EmptyText $t.Empty) </section> <footer>$(ConvertTo-SMBeatHtmlEncode -Text $t.Foot)</footer> </main> </body> </html> "@ } |