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' Written = 'Geschrieben' WrittenHint = 'TCP 445, Client zum Server' Peak = 'Spitze' PeakOut = 'Spitze Abgerufen' PeakIn = 'Spitze Geschrieben' AverageHour = 'Schnitt' 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' VersusPrevious = 'gegenueber Vorperiode' ByServer = 'Server' ByShare = 'Freigaben' ByShareEmpty = 'Keine Freigaben-Bytes in diesem Fenster. Offene Sitzungen vor Collector-Start erscheinen als (unknown) bis zum Neuverbinden.' ByClient = 'Clients' ByClientHint = 'Remote-IP auf TCP 445' ByUser = 'Benutzer' ByUserEmpty = 'Keine Benutzer-Bytes in diesem Fenster. Offene Sitzungen vor Collector-Start erscheinen als (unknown) bis zum Neuverbinden.' ByUserHint = 'ETW Session, DOMAIN\Benutzer' Hourly = 'Stunden' Daily = 'Tage' Weekly = 'Wochen' Trend = 'Verlauf' Warnings = 'Hinweise' CoverageLow = 'Die Messung lief nicht die ganze Zeit im gewaehlten Zeitraum. Nach einem Neustart des Collectors ist das normal.' WarnEtwWritten = 'Geschrieben kommt von den Dateizugriffen (Benutzer und Freigaben). Die reine TCP-Zaehlung sieht kurze Uploads oft nicht - das ist kein Fehler.' WarnEstataRead = 'Abgerufen kommt von den Dateizugriffen, damit die Zahl zu Benutzer und Freigabe passt. Die reine TCP-Zaehlung lag hoeher als die Netzwerkkarte; solche Abweichungen gleichen wir an.' WarnShareLow = 'Die Freigaben zeigen deutlich weniger als die Gesamtsumme. Oft war die Verbindung schon offen, bevor die Messung startete - dann steht dort (unknown), bis der Client neu verbindet.' WarnNicOut = 'Die Netzwerkkarte zeigt deutlich mehr Ausgang als die Dateizugriffe. Abgerufen nutzt deshalb die Netzwerkkarte.' Name = 'Name' GB = 'GB' Empty = 'Keine Werte in diesem Fenster.' More = 'und {0} weitere' Foot = 'Abgerufen und Geschrieben sind die Nutzdaten auf Port 445. NIC Out/In ist die ganze Leitung (auch RDP, Backup, Overhead). Freigaben und Benutzer kommen aus den Dateizugriffen und muessen nicht genau die Summe ergeben.' 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' Written = 'Written' WrittenHint = 'TCP 445, client to server' Peak = 'Peak' PeakOut = 'Peak retrieved' PeakIn = 'Peak written' AverageHour = 'Average' 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' VersusPrevious = 'vs previous period' ByServer = 'Servers' ByShare = 'Shares' ByShareEmpty = 'No per-share bytes in this window. Sessions open before the collector started show as (unknown) until reconnect.' ByClient = 'Clients' ByClientHint = 'Remote IP on TCP 445' ByUser = 'Users' ByUserEmpty = 'No per-user bytes in this window. Sessions open before the collector started show as (unknown) until reconnect.' ByUserHint = 'ETW session, DOMAIN\user' Hourly = 'Hours' Daily = 'Days' Weekly = 'Weeks' Trend = 'Trend' Warnings = 'Notes' CoverageLow = 'The collector was not running for the full selected window. That is normal after a restart.' WarnEtwWritten = 'Written comes from file access (users and shares). Plain TCP often misses short uploads; that is expected.' WarnEstataRead = 'Retrieved comes from file access so it matches users and shares. Plain TCP was higher than the NIC; we align those figures.' WarnShareLow = 'Shares show much less than the total. The session was often already open before collection started; it stays (unknown) until the client reconnects.' WarnNicOut = 'The NIC shows much more outbound than file access. Retrieved therefore uses the NIC.' Name = 'Name' GB = 'GB' Empty = 'No values in this window.' More = 'and {0} more' Foot = 'Retrieved and Written are TCP payload on port 445 (no Ethernet headers). NIC Out/In are the wire (including RDP, backup, overhead). Shares and users come from ETW and will not match 445 one-to-one. 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 Get-SMBeatHtmlNumberCulture { param( [ValidateSet('de', 'en')] [string]$Language = 'en' ) if ($Language -eq 'de') { return [System.Globalization.CultureInfo]::GetCultureInfo('de-DE') } $script:SMBeatInvariant } function Format-SMBeatHtmlGB { param( $Value, [ValidateSet('de', 'en')] [string]$Language = 'en' ) $n = 0.0 if ($null -ne $Value) { $n = [double]$Value } $n.ToString('0.000', (Get-SMBeatHtmlNumberCulture -Language $Language)) } function Format-SMBeatHtmlNumber { param( $Value, [string]$Format = '0.#', [ValidateSet('de', 'en')] [string]$Language = 'en' ) $n = 0.0 if ($null -ne $Value) { $n = [double]$Value } $n.ToString($Format, (Get-SMBeatHtmlNumberCulture -Language $Language)) } function Format-SMBeatHtmlWhen { param( $Value, [TimeZoneInfo]$TimeZone, [ValidateSet('de', 'en')] [string]$Language = 'en' ) if ($null -eq $Value) { return '' } if (-not ($Value -is [datetime])) { return [string]$Value } $dt = $Value if ($null -eq $TimeZone) { $TimeZone = [TimeZoneInfo]::Local } if ($dt.Kind -eq [DateTimeKind]::Utc) { $dt = Convert-SMBeatUtcToTimeZone -Utc $dt -TimeZone $TimeZone } elseif ($dt.Kind -eq [DateTimeKind]::Local) { $dt = Convert-SMBeatUtcToTimeZone -Utc $dt.ToUniversalTime() -TimeZone $TimeZone } $pattern = 'yyyy-MM-dd HH:mm' if ($Language -eq 'de') { $pattern = 'dd.MM.yyyy HH:mm' } $dt.ToString($pattern, $script:SMBeatInvariant) } function Get-SMBeatHtmlBarPct { param( [double]$Value, [double]$Max, [double]$Floor = 0 ) if ($Max -le 0) { return '0' } $p = [math]::Round(($Value / $Max) * 100.0, 1) if ($p -lt 0) { $p = 0 } if ($p -gt 100) { $p = 100 } if ($Value -gt 0 -and $Floor -gt 0 -and $p -lt $Floor) { $p = $Floor } $p.ToString('0.#', $script:SMBeatInvariant) } function Select-SMBeatHtmlActiveRows { param( [object[]]$Rows ) if ($null -eq $Rows) { return @() } @($Rows | Where-Object { if (-not $_) { return $false } $sent = [int64]0 $recv = [int64]0 $write = [int64]0 if ($_.SentBytes) { $sent = [int64]$_.SentBytes } if ($_.ReceivedBytes) { $recv = [int64]$_.ReceivedBytes } if ($_.WriteBytes) { $write = [int64]$_.WriteBytes } ($sent -gt 0 -or $recv -gt 0 -or $write -gt 0) }) } function Get-SMBeatHtmlRowGB { param( $Row, [string]$Primary, [string]$Fallback ) if ($null -eq $Row) { return 0.0 } if ($Row.PSObject.Properties[$Primary] -and $null -ne $Row.$Primary) { return [double]$Row.$Primary } if ($Fallback -and $Row.PSObject.Properties[$Fallback] -and $null -ne $Row.$Fallback) { return [double]$Row.$Fallback } 0.0 } function ConvertTo-SMBeatHtmlDelta { param( $Current, $Previous, [string]$VersusText, [ValidateSet('de', 'en')] [string]$Language = 'en' ) if ($null -eq $Previous) { return '' } $prevGiB = [double]$Previous $curGiB = 0.0 if ($null -ne $Current) { $curGiB = [double]$Current } if ($prevGiB -le 0) { return '' } $chg = (($curGiB - $prevGiB) / $prevGiB) * 100.0 $cls = 'up' if ($chg -lt 0) { $cls = 'down' } elseif ($chg -eq 0) { $cls = 'flat' } $chgText = Format-SMBeatHtmlNumber -Value $chg -Format '+0.0;-0.0;0.0' -Language $Language '<div class="delta {0}">{1}% {2}</div>' -f ` $cls, ` (ConvertTo-SMBeatHtmlEncode -Text $chgText), ` (ConvertTo-SMBeatHtmlEncode -Text $VersusText) } function ConvertTo-SMBeatHtmlCard { param( [string]$Label, [string]$Hint, [string]$Value, [string]$CssClass, [string]$Extra, [switch]$RawHint ) if ([string]::IsNullOrWhiteSpace($CssClass)) { $CssClass = 'card' } $hintHtml = '' if (-not [string]::IsNullOrWhiteSpace($Hint)) { $hintText = $Hint if (-not $RawHint) { $hintText = ConvertTo-SMBeatHtmlEncode -Text $Hint } $hintHtml = '<div class="hint">{0}</div>' -f $hintText } $extraHtml = '' if (-not [string]::IsNullOrWhiteSpace($Extra)) { $extraHtml = $Extra } '<div class="{0}"><div class="label">{1}</div><div class="value">{2} <span class="unit">GB</span></div>{3}{4}</div>' -f ` $CssClass, ` (ConvertTo-SMBeatHtmlEncode -Text $Label), ` (ConvertTo-SMBeatHtmlEncode -Text $Value), ` $extraHtml, ` $hintHtml } function ConvertTo-SMBeatHtmlTable { param( [object[]]$Rows, [string]$NameHeader, [string]$OutHeader, [string]$InHeader, [string]$EmptyText, [string]$MoreText, [int]$Top = 15, [ValidateSet('de', 'en')] [string]$Language = 'en' ) $active = @(Select-SMBeatHtmlActiveRows -Rows $Rows) if ($active.Count -eq 0) { return ('<p class="empty">{0}</p>' -f (ConvertTo-SMBeatHtmlEncode -Text $EmptyText)) } $max = 0.0 foreach ($row in $active) { $outGiB = Get-SMBeatHtmlRowGB -Row $row -Primary 'RetrievedGB' -Fallback 'SentGB' $inGiB = Get-SMBeatHtmlRowGB -Row $row -Primary 'WrittenGB' -Fallback 'ReceivedGB' if ($outGiB -gt $max) { $max = $outGiB } if ($inGiB -gt $max) { $max = $inGiB } } $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('<table class="rank"><thead><tr>') [void]$sb.AppendLine(('<th class="rank">#</th><th>{0}</th><th class="num">{1}</th><th class="num">{2}</th></tr></thead><tbody>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $NameHeader), ` (ConvertTo-SMBeatHtmlEncode -Text $OutHeader), ` (ConvertTo-SMBeatHtmlEncode -Text $InHeader))) $i = 0 foreach ($row in $active) { if ($i -ge $Top) { break } $i++ $name = [string]$row.Name $nameClass = 'who' if ($name -eq '(unknown)') { $nameClass = 'who dim' } $outGiB = Get-SMBeatHtmlRowGB -Row $row -Primary 'RetrievedGB' -Fallback 'SentGB' $inGiB = Get-SMBeatHtmlRowGB -Row $row -Primary 'WrittenGB' -Fallback 'ReceivedGB' $outPct = Get-SMBeatHtmlBarPct -Value $outGiB -Max $max -Floor 2 $inPct = Get-SMBeatHtmlBarPct -Value $inGiB -Max $max -Floor 2 [void]$sb.AppendLine(('<tr><td class="rank">{0}</td><td><div class="{1}">{2}</div></td><td class="num"><div>{3}</div><div class="bar out"><i style="width:{4}%"></i></div></td><td class="num"><div>{5}</div><div class="bar in"><i style="width:{6}%"></i></div></td></tr>' -f ` $i, ` $nameClass, ` (ConvertTo-SMBeatHtmlEncode -Text $name), ` (Format-SMBeatHtmlGB -Value $outGiB -Language $Language), ` $outPct, ` (Format-SMBeatHtmlGB -Value $inGiB -Language $Language), ` $inPct)) } [void]$sb.AppendLine('</tbody></table>') if ($active.Count -gt $Top -and -not [string]::IsNullOrWhiteSpace($MoreText)) { $more = $active.Count - $Top [void]$sb.AppendLine(('<p class="more">{0}</p>' -f (ConvertTo-SMBeatHtmlEncode -Text ($MoreText -f $more)))) } $sb.ToString() } function Get-SMBeatHtmlNiceMax { param( [double]$Value ) if ($Value -le 0) { return 1.0 } $exp = [math]::Pow(10.0, [math]::Floor([math]::Log10($Value))) $n = $Value / $exp $nice = 10.0 if ($n -le 1.0) { $nice = 1.0 } elseif ($n -le 2.0) { $nice = 2.0 } elseif ($n -le 5.0) { $nice = 5.0 } $nice * $exp } function Format-SMBeatHtmlChartWhen { param( $Value, [TimeZoneInfo]$TimeZone, [ValidateSet('de', 'en')] [string]$Language = 'en' ) if ($null -eq $Value -or -not ($Value -is [datetime])) { return (Format-SMBeatHtmlWhen -Value $Value -TimeZone $TimeZone -Language $Language) } $dt = $Value if ($null -eq $TimeZone) { $TimeZone = [TimeZoneInfo]::Local } if ($dt.Kind -eq [DateTimeKind]::Utc) { $dt = Convert-SMBeatUtcToTimeZone -Utc $dt -TimeZone $TimeZone } elseif ($dt.Kind -eq [DateTimeKind]::Local) { $dt = Convert-SMBeatUtcToTimeZone -Utc $dt.ToUniversalTime() -TimeZone $TimeZone } $pattern = 'MM-dd HH:mm' if ($Language -eq 'de') { $pattern = 'dd.MM HH:mm' } $dt.ToString($pattern, $script:SMBeatInvariant) } function ConvertTo-SMBeatHtmlLineChart { param( [object[]]$Rows, [TimeZoneInfo]$TimeZone, [ValidateSet('de', 'en')] [string]$Language = 'en', [string]$RetrievedLabel = 'Retrieved', [string]$WrittenLabel = 'Written' ) if ($null -eq $Rows -or $Rows.Count -lt 1) { return '' } $dataMax = 0.0 $sumOut = 0.0 $sumIn = 0.0 $outs = New-Object 'System.Collections.Generic.List[double]' $ins = New-Object 'System.Collections.Generic.List[double]' foreach ($row in $Rows) { $outVal = Get-SMBeatHtmlRowGB -Row $row -Primary 'RetrievedGB' -Fallback '' $inVal = Get-SMBeatHtmlRowGB -Row $row -Primary 'WrittenGB' -Fallback 'ReceivedGB' [void]$outs.Add($outVal) [void]$ins.Add($inVal) $sumOut += $outVal $sumIn += $inVal if ($outVal -gt $dataMax) { $dataMax = $outVal } if ($inVal -gt $dataMax) { $dataMax = $inVal } } $scaleMax = Get-SMBeatHtmlNiceMax -Value $dataMax $vbW = 720.0 $vbH = 200.0 $padL = 54.0 $padR = 22.0 $padT = 28.0 $padB = 34.0 $innerW = $vbW - $padL - $padR $innerH = $vbH - $padT - $padB $n = $Rows.Count $seg = $n - 1 if ($seg -lt 1) { $seg = 1 } $step = $innerW / [double]$seg $plotRight = $padL + $innerW $plotBottom = $padT + $innerH $outPts = New-Object System.Text.StringBuilder $inPts = New-Object System.Text.StringBuilder $lastOutX = 0.0 $lastOutY = 0.0 $lastInX = 0.0 $lastInY = 0.0 for ($i = 0; $i -le $seg; $i++) { $src = $i if ($src -ge $n) { $src = $n - 1 } $x = $padL + ($i * $step) $outY = $plotBottom $inY = $plotBottom if ($scaleMax -gt 0) { $outY = $padT + ((1.0 - ($outs[$src] / $scaleMax)) * $innerH) $inY = $padT + ((1.0 - ($ins[$src] / $scaleMax)) * $innerH) } $xText = $x.ToString('0.##', $script:SMBeatInvariant) $outText = $outY.ToString('0.##', $script:SMBeatInvariant) $inText = $inY.ToString('0.##', $script:SMBeatInvariant) if ($i -gt 0) { [void]$outPts.Append(' ') [void]$inPts.Append(' ') } [void]$outPts.Append($xText) [void]$outPts.Append(',') [void]$outPts.Append($outText) [void]$inPts.Append($xText) [void]$inPts.Append(',') [void]$inPts.Append($inText) if ($i -eq $seg) { $lastOutX = $x $lastOutY = $outY $lastInX = $x $lastInY = $inY } } $title = '{0} - {1} Out {2} GB In {3} GB' -f ` (Format-SMBeatHtmlWhen -Value $Rows[0].PeriodStart -TimeZone $TimeZone -Language $Language), ` (Format-SMBeatHtmlWhen -Value $Rows[$n - 1].PeriodStart -TimeZone $TimeZone -Language $Language), ` (Format-SMBeatHtmlGB -Value $sumOut -Language $Language), ` (Format-SMBeatHtmlGB -Value $sumIn -Language $Language) $sb = New-Object System.Text.StringBuilder [void]$sb.Append('<div class="beat"><svg viewBox="0 0 720 200" width="100%" preserveAspectRatio="xMidYMin meet" role="img">') [void]$sb.Append(('<title>{0}</title>' -f (ConvertTo-SMBeatHtmlEncode -Text $title))) [void]$sb.Append('<rect x="1.6" y="1.6" width="716.8" height="196.8" rx="12" fill="none" stroke="#d8cbb3" stroke-width="1"/>') $yTicks = @(0.0, 0.25, 0.5, 0.75, 1.0) foreach ($frac in $yTicks) { $yVal = $scaleMax * $frac $y = $plotBottom - ($frac * $innerH) $yText = $y.ToString('0.##', $script:SMBeatInvariant) $x1 = $padL.ToString('0.##', $script:SMBeatInvariant) $x2 = $plotRight.ToString('0.##', $script:SMBeatInvariant) [void]$sb.Append(('<line x1="{0}" y1="{1}" x2="{2}" y2="{1}" stroke="#d8cbb3" stroke-width="0.7" opacity="0.9"/>' -f $x1, $yText, $x2)) $label = '{0} GB' -f (Format-SMBeatHtmlGB -Value $yVal -Language $Language) $lx = ($padL - 6.0).ToString('0.##', $script:SMBeatInvariant) [void]$sb.Append(('<text x="{0}" y="{1}" text-anchor="end" dominant-baseline="middle" fill="#5d685f" font-size="9" font-family="Segoe UI, Helvetica Neue, Arial, sans-serif">{2}</text>' -f ` $lx, $yText, (ConvertTo-SMBeatHtmlEncode -Text $label))) } $xIdx = New-Object 'System.Collections.Generic.List[int]' if ($seg -le 5) { for ($i = 0; $i -le $seg; $i++) { [void]$xIdx.Add($i) } } else { $wanted = @(0, [int][math]::Floor($seg / 4.0), [int][math]::Floor($seg / 2.0), [int][math]::Floor(($seg * 3.0) / 4.0), $seg) $seen = @{} foreach ($idx in $wanted) { if (-not $seen.ContainsKey($idx)) { $seen[$idx] = $true [void]$xIdx.Add($idx) } } } foreach ($idx in $xIdx) { $x = $padL + ($idx * $step) $xText = $x.ToString('0.##', $script:SMBeatInvariant) $tickY1 = $plotBottom.ToString('0.##', $script:SMBeatInvariant) $tickY2 = ($plotBottom + 4.0).ToString('0.##', $script:SMBeatInvariant) [void]$sb.Append(('<line x1="{0}" y1="{1}" x2="{0}" y2="{2}" stroke="#d8cbb3" stroke-width="0.8"/>' -f $xText, $tickY1, $tickY2)) $rowIdx = $idx if ($rowIdx -ge $n) { $rowIdx = $n - 1 } $when = Format-SMBeatHtmlChartWhen -Value $Rows[$rowIdx].PeriodStart -TimeZone $TimeZone -Language $Language $ly = ($plotBottom + 16.0).ToString('0.##', $script:SMBeatInvariant) $anchor = 'middle' if ($idx -eq 0) { $anchor = 'start' } elseif ($idx -eq $seg) { $anchor = 'end' } [void]$sb.Append(('<text x="{0}" y="{1}" text-anchor="{2}" fill="#5d685f" font-size="9" font-family="Segoe UI, Helvetica Neue, Arial, sans-serif">{3}</text>' -f ` $xText, $ly, $anchor, (ConvertTo-SMBeatHtmlEncode -Text $when))) } $legY = 16.0 [void]$sb.Append(('<line x1="{0}" y1="{1}" x2="{2}" y2="{1}" stroke="#2a6b45" stroke-width="2.2" stroke-linecap="round"/>' -f ` $padL.ToString('0.##', $script:SMBeatInvariant), ` $legY.ToString('0.##', $script:SMBeatInvariant), ` ($padL + 14.0).ToString('0.##', $script:SMBeatInvariant))) [void]$sb.Append(('<text x="{0}" y="{1}" fill="#5d685f" font-size="9" font-family="Segoe UI, Helvetica Neue, Arial, sans-serif">{2}</text>' -f ` ($padL + 18.0).ToString('0.##', $script:SMBeatInvariant), ` ($legY + 3.0).ToString('0.##', $script:SMBeatInvariant), ` (ConvertTo-SMBeatHtmlEncode -Text $RetrievedLabel))) [void]$sb.Append(('<line x1="{0}" y1="{1}" x2="{2}" y2="{1}" stroke="#c4a35a" stroke-width="2.2" stroke-linecap="round"/>' -f ` ($padL + 118.0).ToString('0.##', $script:SMBeatInvariant), ` $legY.ToString('0.##', $script:SMBeatInvariant), ` ($padL + 132.0).ToString('0.##', $script:SMBeatInvariant))) [void]$sb.Append(('<text x="{0}" y="{1}" fill="#5d685f" font-size="9" font-family="Segoe UI, Helvetica Neue, Arial, sans-serif">{2}</text>' -f ` ($padL + 136.0).ToString('0.##', $script:SMBeatInvariant), ` ($legY + 3.0).ToString('0.##', $script:SMBeatInvariant), ` (ConvertTo-SMBeatHtmlEncode -Text $WrittenLabel))) [void]$sb.Append(('<polyline fill="none" stroke="#c4a35a" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" points="{0}"/>' -f $inPts.ToString())) [void]$sb.Append(('<polyline fill="none" stroke="#2a6b45" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" points="{0}"/>' -f $outPts.ToString())) [void]$sb.Append(('<circle cx="{0}" cy="{1}" r="3.1" fill="#c4a35a"/>' -f ($lastInX.ToString('0.##', $script:SMBeatInvariant)), ($lastInY.ToString('0.##', $script:SMBeatInvariant)))) [void]$sb.Append(('<circle cx="{0}" cy="{1}" r="3.1" fill="#2a6b45"/>' -f ($lastOutX.ToString('0.##', $script:SMBeatInvariant)), ($lastOutY.ToString('0.##', $script:SMBeatInvariant)))) [void]$sb.Append('</svg></div>') $sb.ToString() } function ConvertTo-SMBeatHtmlBeatBlock { param( [string]$Title, [object[]]$Rows, [string]$EmptyText, [TimeZoneInfo]$TimeZone, [ValidateSet('de', 'en')] [string]$Language = 'en', [string]$RetrievedLabel, [string]$WrittenLabel ) $chart = ConvertTo-SMBeatHtmlLineChart -Rows $Rows -TimeZone $TimeZone -Language $Language -RetrievedLabel $RetrievedLabel -WrittenLabel $WrittenLabel if ([string]::IsNullOrWhiteSpace($chart)) { $chart = '<p class="empty">{0}</p>' -f (ConvertTo-SMBeatHtmlEncode -Text $EmptyText) } '<div class="beat-block"><h3>{0}</h3>{1}</div>' -f (ConvertTo-SMBeatHtmlEncode -Text $Title), $chart } function ConvertTo-SMBeatHtmlSeries { param( [object[]]$Rows, [string]$RetrievedHeader, [string]$WrittenHeader, [string]$NicOutHeader, [string]$NicInHeader, [string]$EmptyText, [TimeZoneInfo]$TimeZone, [ValidateSet('de', 'en')] [string]$Language = 'en' ) if ($null -eq $Rows -or $Rows.Count -eq 0) { return ('<p class="empty">{0}</p>' -f (ConvertTo-SMBeatHtmlEncode -Text $EmptyText)) } $maxOut = 0.0 $maxIn = 0.0 foreach ($row in $Rows) { $outGiB = Get-SMBeatHtmlRowGB -Row $row -Primary 'RetrievedGB' -Fallback '' $inGiB = Get-SMBeatHtmlRowGB -Row $row -Primary 'WrittenGB' -Fallback 'ReceivedGB' if ($outGiB -gt $maxOut) { $maxOut = $outGiB } if ($inGiB -gt $maxIn) { $maxIn = $inGiB } } $max = $maxOut if ($maxIn -gt $max) { $max = $maxIn } $sb = New-Object System.Text.StringBuilder $scroll = '' if ($Rows.Count -gt 14) { $scroll = ' scroll' } [void]$sb.AppendLine(('<div class="table-wrap{0}"><table class="series"><thead><tr><th>Start</th><th class="num">{1}</th><th class="num">{2}</th><th class="num">{3}</th><th class="num">{4}</th></tr></thead><tbody>' -f ` $scroll, ` (ConvertTo-SMBeatHtmlEncode -Text $RetrievedHeader), ` (ConvertTo-SMBeatHtmlEncode -Text $WrittenHeader), ` (ConvertTo-SMBeatHtmlEncode -Text $NicOutHeader), ` (ConvertTo-SMBeatHtmlEncode -Text $NicInHeader))) foreach ($row in $Rows) { $outGiB = Get-SMBeatHtmlRowGB -Row $row -Primary 'RetrievedGB' -Fallback '' $inGiB = Get-SMBeatHtmlRowGB -Row $row -Primary 'WrittenGB' -Fallback 'ReceivedGB' $outPct = Get-SMBeatHtmlBarPct -Value $outGiB -Max $max -Floor 2 $inPct = Get-SMBeatHtmlBarPct -Value $inGiB -Max $max -Floor 2 $hot = '' if (($maxOut -gt 0 -and $outGiB -ge $maxOut) -or ($maxIn -gt 0 -and $inGiB -ge $maxIn)) { $hot = ' class="hot"' } [void]$sb.AppendLine(('<tr{0}><td>{1}<div class="duo"><div class="bar slim out"><i style="width:{2}%"></i></div><div class="bar slim in"><i style="width:{3}%"></i></div></div></td><td class="num">{4}</td><td class="num">{5}</td><td class="num mute">{6}</td><td class="num mute">{7}</td></tr>' -f ` $hot, ` (ConvertTo-SMBeatHtmlEncode -Text (Format-SMBeatHtmlWhen -Value $row.PeriodStart -TimeZone $TimeZone -Language $Language)), ` $outPct, ` $inPct, ` (Format-SMBeatHtmlGB -Value $outGiB -Language $Language), ` (Format-SMBeatHtmlGB -Value $inGiB -Language $Language), ` (Format-SMBeatHtmlGB -Value $row.NicOutGB -Language $Language), ` (Format-SMBeatHtmlGB -Value $row.NicInGB -Language $Language))) } [void]$sb.AppendLine('</tbody></table></div>') $sb.ToString() } function ConvertTo-SMBeatHtmlPanel { param( [string]$Title, [string]$Hint, [string]$Body ) $hintHtml = '' if (-not [string]::IsNullOrWhiteSpace($Hint)) { $hintHtml = '<p class="lede">{0}</p>' -f (ConvertTo-SMBeatHtmlEncode -Text $Hint) } '<section class="panel"><div class="panel-head"><h2>{0}</h2></div>{1}{2}</section>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $Title), ` $hintHtml, ` $Body } 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 } $tz = Resolve-SMBeatTimeZone -TimeZone $Report.TimeZone $generatedText = Format-SMBeatHtmlWhen -Value $generated -TimeZone $tz -Language $Language $periodStart = Format-SMBeatHtmlWhen -Value $Report.StartUtc -TimeZone $tz -Language $Language $periodEnd = Format-SMBeatHtmlWhen -Value $Report.EndUtc -TimeZone $tz -Language $Language $tzLabel = $tz.Id $totals = $Report.Totals $retrievedText = Format-SMBeatHtmlGB -Value $totals.RetrievedGB -Language $Language $writtenText = Format-SMBeatHtmlGB -Value 0 -Language $Language if ($totals.PSObject.Properties['WrittenGB']) { $writtenText = Format-SMBeatHtmlGB -Value $totals.WrittenGB -Language $Language } $nicOutText = Format-SMBeatHtmlGB -Value $totals.NicOutGB -Language $Language $nicInText = Format-SMBeatHtmlGB -Value $totals.NicInGB -Language $Language $peakOutLabel = $t.Peak if ($t.ContainsKey('PeakOut')) { $peakOutLabel = $t.PeakOut } $peakInLabel = $t.Peak if ($t.ContainsKey('PeakIn')) { $peakInLabel = $t.PeakIn } $peakHtml = '' if ($Report.PSObject.Properties['PeakStart'] -and $Report.PeakStart) { $peakWhen = Format-SMBeatHtmlWhen -Value $Report.PeakStart -TimeZone $tz -Language $Language $peakGiB = Format-SMBeatHtmlGB -Value $Report.PeakRetrievedGB -Language $Language $avgGiB = Format-SMBeatHtmlGB -Value $Report.AverageRetrievedGBPerHour -Language $Language $peakHint = '{0}<br>{1} {2} GB/h' -f ` (ConvertTo-SMBeatHtmlEncode -Text $peakWhen), ` (ConvertTo-SMBeatHtmlEncode -Text $t.AverageHour), ` (ConvertTo-SMBeatHtmlEncode -Text $avgGiB) $peakHtml = ConvertTo-SMBeatHtmlCard -Label $peakOutLabel -Hint $peakHint -Value $peakGiB -CssClass 'card peak' -RawHint } $peakInHtml = '' if ($Report.PSObject.Properties['PeakWrittenStart'] -and $Report.PeakWrittenStart) { $peakInWhen = Format-SMBeatHtmlWhen -Value $Report.PeakWrittenStart -TimeZone $tz -Language $Language $peakInGiB = Format-SMBeatHtmlGB -Value $Report.PeakWrittenGB -Language $Language $avgInGiB = Format-SMBeatHtmlGB -Value $Report.AverageWrittenGBPerHour -Language $Language $peakInHint = '{0}<br>{1} {2} GB/h' -f ` (ConvertTo-SMBeatHtmlEncode -Text $peakInWhen), ` (ConvertTo-SMBeatHtmlEncode -Text $t.AverageHour), ` (ConvertTo-SMBeatHtmlEncode -Text $avgInGiB) $peakInHtml = ConvertTo-SMBeatHtmlCard -Label $peakInLabel -Hint $peakInHint -Value $peakInGiB -CssClass 'card peak in' -RawHint } $deltaOutHtml = '' $deltaInHtml = '' if ($Report.PreviousTotals) { $prevOut = $Report.PreviousTotals.RetrievedGB $prevIn = $null if ($Report.PreviousTotals.PSObject.Properties['WrittenGB']) { $prevIn = $Report.PreviousTotals.WrittenGB } elseif ($Report.PreviousTotals.PSObject.Properties['ReceivedGB']) { $prevIn = $Report.PreviousTotals.ReceivedGB } $writtenGiB = 0.0 if ($totals.PSObject.Properties['WrittenGB'] -and $null -ne $totals.WrittenGB) { $writtenGiB = [double]$totals.WrittenGB } $deltaOutHtml = ConvertTo-SMBeatHtmlDelta -Current $totals.RetrievedGB -Previous $prevOut -VersusText $t.VersusPrevious -Language $Language $deltaInHtml = ConvertTo-SMBeatHtmlDelta -Current $writtenGiB -Previous $prevIn -VersusText $t.VersusPrevious -Language $Language } $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 (-not $w -or ([string]$w -like 'Coverage *')) { continue } $text = [string]$w if ($text -like 'TCP 445 inbound is much smaller*') { $text = $t.WarnEtwWritten } elseif ($text -like 'TCP 445 outbound is larger*') { $text = $t.WarnEstataRead } elseif ($text -like 'Share bytes are much smaller*') { $text = $t.WarnShareLow } elseif ($text -like 'NIC Out is much larger*') { $text = $t.WarnNicOut } $notes.Add($text) | Out-Null } $warnHtml = '' if ($notes.Count -gt 0) { $items = ($notes | ForEach-Object { '<li>' + (ConvertTo-SMBeatHtmlEncode -Text $_) + '</li>' }) -join '' $warnHtml = '<section class="notes-banner"><h2>{0}</h2><ul>{1}</ul></section>' -f (ConvertTo-SMBeatHtmlEncode -Text $t.Warnings), $items } $covClass = 'ok' if ($coverage -lt 90) { $covClass = 'warn' } $coverageText = Format-SMBeatHtmlNumber -Value $coverage -Format '0.#' -Language $Language $more = $t.More $userBody = ConvertTo-SMBeatHtmlTable -Rows $Report.ByUser -NameHeader $t.Name -OutHeader $t.Retrieved -InHeader $t.Written -EmptyText $t.ByUserEmpty -MoreText $more -Language $Language $clientBody = ConvertTo-SMBeatHtmlTable -Rows $Report.ByClient -NameHeader $t.Name -OutHeader $t.Retrieved -InHeader $t.Written -EmptyText $t.Empty -MoreText $more -Language $Language $serverBody = ConvertTo-SMBeatHtmlTable -Rows $Report.ByServer -NameHeader $t.Name -OutHeader $t.Retrieved -InHeader $t.Written -EmptyText $t.Empty -MoreText $more -Language $Language $shareBody = ConvertTo-SMBeatHtmlTable -Rows $Report.ByShare -NameHeader $t.Name -OutHeader $t.Retrieved -InHeader $t.Written -EmptyText $t.ByShareEmpty -MoreText $more -Language $Language @" <!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> :root { --ink: #122018; --paper: #efe8d8; --panel: #fffaf0; --line: #d8cbb3; --mute: #5d685f; --forest: #163226; --leaf: #2a6b45; --brass: #c4a35a; --warn: #8a4310; --warn-bg: #f4e0c8; } * { box-sizing: border-box; } html { font-size: 16px; } body { margin: 0; color: var(--ink); background: radial-gradient(1200px 420px at 8% -10%, rgba(196,163,90,0.18), transparent 55%), radial-gradient(900px 380px at 110% 8%, rgba(42,107,69,0.12), transparent 50%), var(--paper); font-family: "Segoe UI Variable", "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif; line-height: 1.45; } header { background: linear-gradient(115deg, #0e1a14 0%, #163226 58%, #1d3d2c 100%); color: #f6f0e4; padding: 1.6rem 2rem 1.35rem; border-bottom: 3px solid var(--brass); } .brand { display: flex; align-items: flex-start; gap: 1rem; max-width: 72rem; margin: 0 auto; } .mark { width: 2.55rem; height: 2.55rem; flex: 0 0 auto; margin-top: 0.05rem; } .mark svg { display: block; width: 100%; height: 100%; } .eyebrow { font-size: 0.72rem; letter-spacing: 0.08em; color: #d7c38a; margin: 0 0 0.2rem; } header h1 { margin: 0; font-size: 1.55rem; font-weight: 600; letter-spacing: -0.02em; } .ver { margin-left: auto; font-size: 0.78rem; color: #c9d4cb; padding: 0.28rem 0.6rem; border: 1px solid rgba(196,163,90,0.35); border-radius: 999px; white-space: nowrap; } .chips { display: flex; flex-wrap: wrap; gap: 0.45rem; max-width: 72rem; margin: 1rem auto 0; } .chip { display: inline-flex; align-items: baseline; gap: 0.4rem; background: rgba(255,250,240,0.08); border: 1px solid rgba(246,240,228,0.14); color: #e8e2d4; border-radius: 999px; padding: 0.28rem 0.75rem; font-size: 0.8rem; } .chip strong { color: #d7c38a; font-weight: 600; } .chip.ok { background: rgba(80,160,110,0.18); border-color: rgba(140,210,160,0.28); } .chip.warn { background: rgba(196,120,50,0.22); border-color: rgba(232,176,110,0.35); color: #f3d7b0; } main { padding: 1.4rem 2rem 3.2rem; max-width: 72rem; margin: 0 auto; } h2 { font-size: 0.78rem; margin: 0; letter-spacing: 0.12em; text-transform: uppercase; color: #3d4a40; } .notes-banner { background: var(--warn-bg); border: 1px solid #e0c49a; border-left: 4px solid var(--warn); border-radius: 0.7rem; padding: 0.85rem 1rem; margin: 0 0 1.1rem; } .notes-banner h2 { color: var(--warn); margin-bottom: 0.35rem; } .notes-banner ul { margin: 0; padding-left: 1.15rem; } .cards { display: flex; flex-wrap: wrap; gap: 0.75rem; } .card { flex: 1 1 11.5rem; background: var(--panel); border: 1px solid var(--line); border-radius: 0.85rem; padding: 0.95rem 1.05rem 0.9rem; box-shadow: 0 1px 0 rgba(18,32,24,0.04); } .card.hero { flex: 2 1 16rem; background: linear-gradient(160deg, #163226 0%, #10241b 100%); border-color: #1f3d2d; color: #f6f0e4; box-shadow: 0 10px 28px rgba(16,36,27,0.18); } .card.hero.in { background: linear-gradient(160deg, #3a2a14 0%, #241910 100%); border-color: #4a3820; box-shadow: 0 10px 28px rgba(36,25,16,0.18); } .card.peak { border-color: #2a6b45; background: linear-gradient(180deg, #e8f3ea 0%, #fffaf0 70%); } .card.peak.in { border-color: #c4a35a; background: linear-gradient(180deg, #fff6e2 0%, #fffaf0 70%); } .card .label { font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--mute); font-weight: 600; } .card.hero .label { color: #d7c38a; } .card.hero.in .label { color: #e8c98a; } .card .value { font-size: 2.05rem; font-weight: 700; letter-spacing: -0.03em; margin-top: 0.15rem; font-variant-numeric: tabular-nums; line-height: 1.1; } .card.hero .value { color: #fff8ea; } .card .unit { font-size: 0.82rem; font-weight: 600; color: var(--mute); letter-spacing: 0.04em; } .card.hero .unit { color: #d7c38a; } .card.hero.in .unit { color: #e8c98a; } .card .hint { margin-top: 0.4rem; font-size: 0.78rem; color: var(--mute); } .card.hero .hint { color: #c9d4cb; } .card.hero.in .hint { color: #e4d4b4; } .delta { margin-top: 0.4rem; font-size: 0.78rem; font-weight: 600; } .delta.up { color: #8ee0a8; } .delta.down { color: #f0b4a0; } .delta.flat { color: #c9d4cb; } .pair { display: flex; flex-wrap: wrap; gap: 0.9rem; margin-top: 0.9rem; } .pair > .panel { flex: 1 1 22rem; min-width: 0; margin-top: 0; } .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 0.95rem; padding: 0.95rem 1.05rem 1rem; margin-top: 0.9rem; box-shadow: 0 1px 0 rgba(18,32,24,0.04); } .panel-head { margin-bottom: 0.55rem; } .lede, .empty, .more { margin: 0 0 0.65rem; font-size: 0.8rem; color: var(--mute); } .empty { background: #f3ece0; border-radius: 0.55rem; padding: 0.7rem 0.8rem; } table { border-collapse: collapse; width: 100%; } th, td { padding: 0.42rem 0.15rem 0.46rem; text-align: left; border-bottom: 1px solid #eee4d2; vertical-align: top; } th { font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--mute); font-weight: 600; } td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; padding-left: 0.8rem; } td.rank, th.rank { width: 1.6rem; color: var(--mute); font-variant-numeric: tabular-nums; font-size: 0.78rem; padding-right: 0.55rem; } .who { font-weight: 600; font-size: 0.92rem; } .who.dim { color: var(--mute); font-style: italic; font-weight: 500; } .bar { height: 5px; background: #efe4ce; border-radius: 99px; margin-top: 0.28rem; overflow: hidden; } .bar i { display: block; height: 100%; background: #2a6b45; border-radius: 99px; } .bar.in i { background: #c4a35a; } .bar.slim { height: 3px; margin-top: 0.22rem; } .duo { margin-top: 0.32rem; } tr.hot td { background: #fff4d8; } td.mute { color: var(--mute); } .table-wrap.scroll { max-height: 26rem; overflow: auto; border-radius: 0.4rem; } .table-wrap.scroll thead th { position: sticky; top: 0; background: var(--panel); box-shadow: 0 1px 0 #eee4d2; } .beat-block { margin: 0 0 1rem; } .beat-block:last-child { margin-bottom: 0.15rem; } .beat-block h3 { font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase; color: #3d4a40; margin: 0 0 0.4rem; } .beat { width: 100%; margin: 0.15rem 0 0.85rem; } .beat svg { display: block; width: 100%; height: auto; } footer { margin-top: 1.4rem; padding-top: 0.9rem; border-top: 1px solid var(--line); font-size: 0.78rem; color: var(--mute); max-width: 48rem; } @media (max-width: 720px) { header, main { padding-left: 1rem; padding-right: 1rem; } .card .value { font-size: 1.65rem; } .ver { display: none; } } @media print { body { background: #fff; } header { border-bottom-width: 2px; -webkit-print-color-adjust: exact; print-color-adjust: exact; } .beat svg { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .card, .panel { box-shadow: none; break-inside: avoid; } .table-wrap.scroll { max-height: none; overflow: visible; } } </style> </head> <body> <header> <div class="brand"> <div class="mark" aria-hidden="true"><svg viewBox="0 0 48 48" focusable="false"><rect x="1.8" y="1.8" width="44.4" height="44.4" rx="12" fill="#0e1a14" stroke="#c4a35a" stroke-width="1.35"/><rect x="5.1" y="5.1" width="37.8" height="37.8" rx="9.4" fill="none" stroke="#c4a35a" stroke-width="0.7" opacity="0.5"/><path d="M7.4 25.6 H14.2 L16.1 21.6 L17.8 25.6 H19.2 L21.4 9.2 L23.6 36.2 L26.2 25.6 L28.6 18.4 L31.2 25.6 L33.1 21.8 L34.8 25.6 H39.2" fill="none" stroke="#d7c38a" stroke-width="2.05" stroke-linecap="round" stroke-linejoin="round"/><circle cx="39.2" cy="25.6" r="2.15" fill="#2a6b45"/></svg></div> <div> <div class="eyebrow">$(ConvertTo-SMBeatHtmlEncode -Text $t.Company)</div> <h1>$(ConvertTo-SMBeatHtmlEncode -Text $t.Title)</h1> </div> <div class="ver">$(ConvertTo-SMBeatHtmlEncode -Text $t.Version) $(ConvertTo-SMBeatHtmlEncode -Text $version)</div> </div> <div class="chips"> <span class="chip"><strong>$(ConvertTo-SMBeatHtmlEncode -Text $t.Period):</strong> $(ConvertTo-SMBeatHtmlEncode -Text $periodStart) - $(ConvertTo-SMBeatHtmlEncode -Text $periodEnd) ($(ConvertTo-SMBeatHtmlEncode -Text $tzLabel))</span> <span class="chip $covClass"><strong>$(ConvertTo-SMBeatHtmlEncode -Text $t.Coverage):</strong> $coverageText%</span> <span class="chip"><strong>$(ConvertTo-SMBeatHtmlEncode -Text $t.Generated):</strong> $generatedText</span> </div> </header> <main> $warnHtml $(ConvertTo-SMBeatHtmlPanel -Title $t.Totals -Hint '' -Body ('<div class="cards">{0}{1}{2}{3}{4}{5}</div>' -f ` (ConvertTo-SMBeatHtmlCard -Label $t.Retrieved -Hint $t.RetrievedHint -Value $retrievedText -CssClass 'card hero' -Extra $deltaOutHtml), ` (ConvertTo-SMBeatHtmlCard -Label $t.Written -Hint $t.WrittenHint -Value $writtenText -CssClass 'card hero in' -Extra $deltaInHtml), ` $peakHtml, ` $peakInHtml, ` (ConvertTo-SMBeatHtmlCard -Label $t.NicOut -Hint $t.NicOutHint -Value $nicOutText -CssClass 'card'), ` (ConvertTo-SMBeatHtmlCard -Label $t.NicIn -Hint $t.NicInHint -Value $nicInText -CssClass 'card'))) $(ConvertTo-SMBeatHtmlPanel -Title $t.Trend -Hint '' -Body ( (ConvertTo-SMBeatHtmlBeatBlock -Title $t.Hourly -Rows $Report.Hourly -EmptyText $t.Empty -TimeZone $tz -Language $Language -RetrievedLabel $t.Retrieved -WrittenLabel $t.Written) + (ConvertTo-SMBeatHtmlBeatBlock -Title $t.Daily -Rows $Report.Daily -EmptyText $t.Empty -TimeZone $tz -Language $Language -RetrievedLabel $t.Retrieved -WrittenLabel $t.Written) + (ConvertTo-SMBeatHtmlBeatBlock -Title $t.Weekly -Rows $Report.Weekly -EmptyText $t.Empty -TimeZone $tz -Language $Language -RetrievedLabel $t.Retrieved -WrittenLabel $t.Written) )) <div class="pair"> $(ConvertTo-SMBeatHtmlPanel -Title $t.ByClient -Hint $t.ByClientHint -Body $clientBody) $(ConvertTo-SMBeatHtmlPanel -Title $t.ByUser -Hint $t.ByUserHint -Body $userBody) </div> <div class="pair"> $(ConvertTo-SMBeatHtmlPanel -Title $t.ByShare -Hint '' -Body $shareBody) $(ConvertTo-SMBeatHtmlPanel -Title $t.ByServer -Hint '' -Body $serverBody) </div> $(ConvertTo-SMBeatHtmlPanel -Title $t.Hourly -Hint '' -Body (ConvertTo-SMBeatHtmlSeries -Rows $Report.Hourly -RetrievedHeader $t.Retrieved -WrittenHeader $t.Written -NicOutHeader $t.NicOut -NicInHeader $t.NicIn -EmptyText $t.Empty -TimeZone $tz -Language $Language)) $(ConvertTo-SMBeatHtmlPanel -Title $t.Daily -Hint '' -Body (ConvertTo-SMBeatHtmlSeries -Rows $Report.Daily -RetrievedHeader $t.Retrieved -WrittenHeader $t.Written -NicOutHeader $t.NicOut -NicInHeader $t.NicIn -EmptyText $t.Empty -TimeZone $tz -Language $Language)) $(ConvertTo-SMBeatHtmlPanel -Title $t.Weekly -Hint '' -Body (ConvertTo-SMBeatHtmlSeries -Rows $Report.Weekly -RetrievedHeader $t.Retrieved -WrittenHeader $t.Written -NicOutHeader $t.NicOut -NicInHeader $t.NicIn -EmptyText $t.Empty -TimeZone $tz -Language $Language)) <footer>$(ConvertTo-SMBeatHtmlEncode -Text $t.Foot)</footer> </main> </body> </html> "@ } |