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' Span = 'Spanne' Spans = 'Letzte Stunde, Tag, Woche' SpanHint = 'Rueckblick bis Erzeugt: letzte Stunde, letzte 24 Stunden, letzte 7 Tage.' SpanHour = '1 Stunde' SpanDay = '1 Tag' SpanWeek = '1 Woche' Coverage = 'Abdeckung' Generated = 'Erzeugt' CollectedSince = 'Daten seit' 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' SmbPerfOut = 'SMB-Perf Out' SmbPerfOutHint = 'Gegencheck: SMB-Server gesendet, nicht Headline' SmbPerfIn = 'SMB-Perf In' SmbPerfInHint = 'Gegencheck: SMB-Server empfangen' 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.' BySmbPerf = 'SMB-Perf Freigaben' BySmbPerfHint = 'Windows SMB-Zaehler je Freigabe, Gegencheck' BySmbPerfEmpty = 'Keine SMB-Perf-Freigaben in diesem Fenster. Alte Samples ohne kind=smbperf lassen die Tabelle leer.' 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' Events = 'Ereignisse' EventsHint = 'Registrierung, Start, Stopp, Modul-Update und Server-Neustart (geplant oder ungeplant).|Eine Luecke bedeutet fehlende Daten, nicht null Verbrauch.' EventWhen = 'Zeit' EventKind = 'Ereignis' EventGap = 'Luecke' EventBefore = 'Aufzeichnung begann spaeter' EventAfter = 'Aufzeichnung endete frueher' EventRegistered = 'Collector registriert' EventStarted = 'Aufzeichnung gestartet' EventStopped = 'Aufzeichnung gestoppt' EventError = 'Fehler' EventReboot = 'Server neu gestartet' EventCrash = 'Server ungeplant neu gestartet' EventUpdated = 'Modul aktualisiert' EventHour = '1 Stunde' EventHours = '{0} Stunden' Warnings = 'Hinweise' WarningsIntro = 'Die folgenden Texte erklaeren auffaellige Zahlen. Es sind Erlaeuterungen, keine Stoerungen.' CoverageLowTitle = 'Zeitraum nicht vollstaendig gemessen' CoverageLow = 'Die Messung hat nicht den ganzen gewaehlten Zeitraum erfasst (Abdeckung unter 90 Prozent). Das ist nach einem Neustart des Servers oder einem spaeteren Start der Messung normal. Die fehlenden Stunden sind unbekannt, nicht null. Die Summen gelten nur fuer die Zeit, die wirklich gemessen wurde.' WarnEtwWrittenTitle = 'Geschrieben kommt von den Dateizugriffen' WarnEtwWritten = 'Die grosse Zahl Geschrieben zaehlt, welcher Benutzer auf welche Freigabe geschrieben hat. Die reine TCP-Zaehlung sieht kurze Uploads oft nicht: die Verbindung ist schon wieder zu, bevor der Zaehler sie vollstaendig erfasst. Benutzer, Freigabe und die Headline nutzen deshalb dieselbe Zahl. Das ist erwartet, kein Fehler.' WarnEstataReadTitle = 'Abgerufen an Benutzer und Freigabe angeglichen' WarnEstataRead = 'Die grosse Zahl Abgerufen kommt hier von den Dateizugriffen (welcher Benutzer welche Freigabe gelesen hat), nicht von der reinen TCP-Leitung. Die TCP-Zaehlung lag hoeher als das, was die Netzwerkkarte insgesamt verlassen hat. Das kommt bei langen Lesevorgaengen vor. Wir nehmen dann die Dateizugriffe, damit Gesamtsumme, Benutzer und Freigabe zusammenpassen. Das ist eine Angleichung, kein Messfehler.' WarnShareLowTitle = 'Freigaben kleiner als die Gesamtsumme' WarnShareLow = 'Die Freigaben zusammen zeigen deutlich weniger als Abgerufen. Oft war die Verbindung schon offen, bevor die Messung startete. Dann fehlt der Freigabenname und es steht (unknown), bis der Client sich neu verbindet. Die Gesamtsumme oben bleibt trotzdem gueltig.' WarnNicOutTitle = 'Netzwerkkarte groesser als die Dateizugriffe' WarnNicOut = 'Die Netzwerkkarte zaehlt alles, was den Server verlaesst: Dateien, Remote-Desktop, Backup, Updates. Wenn sie deutlich ueber den Dateizugriffen liegt, nutzt Abgerufen die Netzwerkkarte, damit die Headline nicht zu klein wird.' 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). SMB-Perf ist der Gegencheck aus den Windows-SMB-Zaehlern, nicht die Headline. 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' Span = 'Span' Spans = 'Last hour, day, week' SpanHint = 'Lookback to Generated: last hour, last 24 hours, last 7 days.' SpanHour = '1 hour' SpanDay = '1 day' SpanWeek = '1 week' Coverage = 'Coverage' Generated = 'Generated' CollectedSince = 'Data since' 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' SmbPerfOut = 'SMB-Perf Out' SmbPerfOutHint = 'Control: SMB Server sent, not the headline' SmbPerfIn = 'SMB-Perf In' SmbPerfInHint = 'Control: SMB Server received' 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.' BySmbPerf = 'SMB-Perf shares' BySmbPerfHint = 'Windows SMB counters per share, control only' BySmbPerfEmpty = 'No SMB-Perf share rows in this window. Older samples without kind=smbperf leave this table empty.' 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' Events = 'Events' EventsHint = 'Registration, start, stop, module update, and server reboot (planned or unexpected).|A gap means missing data, not zero usage.' EventWhen = 'When' EventKind = 'Event' EventGap = 'Gap' EventBefore = 'Recording started later' EventAfter = 'Recording ended earlier' EventRegistered = 'Collector registered' EventStarted = 'Recording started' EventStopped = 'Recording stopped' EventError = 'Error' EventReboot = 'Server rebooted' EventCrash = 'Server restarted unexpectedly' EventUpdated = 'Module updated' EventHour = '1 hour' EventHours = '{0} hours' Warnings = 'Notes' WarningsIntro = 'The texts below explain numbers that look inconsistent. They are explanations, not faults.' CoverageLowTitle = 'The window was not measured in full' CoverageLow = 'Collection did not cover the whole selected period (coverage under 90 percent). That is normal after a server restart or if collection started later. Missing hours are unknown, not zero. Totals apply only to the time that was actually measured.' WarnEtwWrittenTitle = 'Written comes from file access' WarnEtwWritten = 'The headline Written figure counts which user wrote to which share. Plain TCP often misses short uploads because the connection is gone before the counter can record them fully. Users, shares, and the headline therefore use the same file-access number. That is expected, not an error.' WarnEstataReadTitle = 'Retrieved was aligned with users and shares' WarnEstataRead = 'The headline Retrieved figure comes from file access (which user read which share), not from the raw TCP count. TCP was higher than everything the network card sent. That happens on long reads. We then use file access so the total matches users and shares. That is an alignment, not a measurement fault.' WarnShareLowTitle = 'Shares are smaller than the total' WarnShareLow = 'Shares together show much less than Retrieved. The session was often already open before collection started. Then the share name is missing and the row stays (unknown) until the client reconnects. The headline total above is still valid.' WarnNicOutTitle = 'The NIC is larger than file access' WarnNicOut = 'The network card counts everything leaving the host: files, remote desktop, backup, updates. When it is much larger than file access, Retrieved uses the NIC so the headline is not too small.' 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). SMB-Perf is the Windows SMB counter check, not the headline. Shares and users come from ETW and will not match 445 one-to-one.' 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-SMBeatHtmlSpan { param( [datetime]$StartUtc, [datetime]$EndUtc, [ValidateSet('de', 'en')] [string]$Language = 'en' ) $span = $EndUtc - $StartUtc if ($span.Ticks -lt 0) { $span = $span.Negate() } $days = [int]$span.Days $hours = [int]$span.Hours $minutes = [int]$span.Minutes $parts = New-Object System.Collections.Generic.List[string] if ($Language -eq 'de') { if ($days -eq 1) { $parts.Add('1 Tag') | Out-Null } elseif ($days -gt 1) { $parts.Add(('{0} Tage' -f $days)) | Out-Null } if ($hours -eq 1) { $parts.Add('1 Stunde') | Out-Null } elseif ($hours -gt 1) { $parts.Add(('{0} Stunden' -f $hours)) | Out-Null } if ($minutes -eq 1) { $parts.Add('1 Minute') | Out-Null } elseif ($minutes -gt 1) { $parts.Add(('{0} Minuten' -f $minutes)) | Out-Null } if ($parts.Count -eq 0) { return 'unter 1 Minute' } return ($parts -join ' ') } if ($days -eq 1) { $parts.Add('1 day') | Out-Null } elseif ($days -gt 1) { $parts.Add(('{0} days' -f $days)) | Out-Null } if ($hours -eq 1) { $parts.Add('1 hour') | Out-Null } elseif ($hours -gt 1) { $parts.Add(('{0} hours' -f $hours)) | Out-Null } if ($minutes -eq 1) { $parts.Add('1 minute') | Out-Null } elseif ($minutes -gt 1) { $parts.Add(('{0} minutes' -f $minutes)) | Out-Null } if ($parts.Count -eq 0) { return 'under 1 minute' } $parts -join ' ' } 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 Get-SMBeatHtmlSpanTotals { param( $Span, [ValidateSet('de', 'en')] [string]$Language ) $outGiB = 0.0 $inGiB = 0.0 if ($Span -and $Span.Totals) { if ($Span.Totals.PSObject.Properties['RetrievedGB'] -and $null -ne $Span.Totals.RetrievedGB) { $outGiB = [double]$Span.Totals.RetrievedGB } if ($Span.Totals.PSObject.Properties['WrittenGB'] -and $null -ne $Span.Totals.WrittenGB) { $inGiB = [double]$Span.Totals.WrittenGB } } [PSCustomObject]@{ RetrievedGB = $outGiB WrittenGB = $inGiB RetrievedText = (Format-SMBeatHtmlGB -Value $outGiB -Language $Language) WrittenText = (Format-SMBeatHtmlGB -Value $inGiB -Language $Language) } } function ConvertTo-SMBeatHtmlSpanCompare { param( $Hour, $Day, $Week, [string]$HourLabel, [string]$DayLabel, [string]$WeekLabel, [string]$RetrievedLabel, [string]$WrittenLabel, [ValidateSet('de', 'en')] [string]$Language ) $hourT = Get-SMBeatHtmlSpanTotals -Span $Hour -Language $Language $dayT = Get-SMBeatHtmlSpanTotals -Span $Day -Language $Language $weekT = Get-SMBeatHtmlSpanTotals -Span $Week -Language $Language $max = 0.0 foreach ($n in @($hourT.RetrievedGB, $hourT.WrittenGB, $dayT.RetrievedGB, $dayT.WrittenGB, $weekT.RetrievedGB, $weekT.WrittenGB)) { if ($n -gt $max) { $max = $n } } $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('<table class="spans"><thead><tr>') [void]$sb.AppendLine(('<th></th><th class="num">{0}</th><th class="num">{1}</th><th class="num">{2}</th></tr></thead><tbody>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $HourLabel), ` (ConvertTo-SMBeatHtmlEncode -Text $DayLabel), ` (ConvertTo-SMBeatHtmlEncode -Text $WeekLabel))) [void]$sb.AppendLine(('<tr><td class="who">{0}</td>{1}{2}{3}</tr>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $RetrievedLabel), ` (ConvertTo-SMBeatHtmlSpanCell -Value $hourT.RetrievedGB -Text $hourT.RetrievedText -Kind 'out' -Max $max), ` (ConvertTo-SMBeatHtmlSpanCell -Value $dayT.RetrievedGB -Text $dayT.RetrievedText -Kind 'out' -Max $max), ` (ConvertTo-SMBeatHtmlSpanCell -Value $weekT.RetrievedGB -Text $weekT.RetrievedText -Kind 'out' -Max $max))) [void]$sb.AppendLine(('<tr><td class="who">{0}</td>{1}{2}{3}</tr>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $WrittenLabel), ` (ConvertTo-SMBeatHtmlSpanCell -Value $hourT.WrittenGB -Text $hourT.WrittenText -Kind 'in' -Max $max), ` (ConvertTo-SMBeatHtmlSpanCell -Value $dayT.WrittenGB -Text $dayT.WrittenText -Kind 'in' -Max $max), ` (ConvertTo-SMBeatHtmlSpanCell -Value $weekT.WrittenGB -Text $weekT.WrittenText -Kind 'in' -Max $max))) [void]$sb.AppendLine('</tbody></table>') $sb.ToString() } function ConvertTo-SMBeatHtmlSpanCell { param( [double]$Value, [string]$Text, [string]$Kind, [double]$Max ) $pct = Get-SMBeatHtmlBarPct -Value $Value -Max $Max -Floor 2 '<td class="num"><div>{0}</div><div class="bar {1}"><i style="width:{2}%"></i></div></td>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $Text), ` $Kind, ` $pct } 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="var(--line)" 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="var(--line)" 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="var(--mute)" 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="var(--line)" 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="var(--mute)" 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="var(--accent-out)" 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="var(--mute)" 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="var(--accent-in)" 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="var(--mute)" 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="var(--accent-in)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" points="{0}"/>' -f $inPts.ToString())) [void]$sb.Append(('<polyline fill="none" stroke="var(--accent-out)" 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="var(--accent-in)"/>' -f ($lastInX.ToString('0.##', $script:SMBeatInvariant)), ($lastInY.ToString('0.##', $script:SMBeatInvariant)))) [void]$sb.Append(('<circle cx="{0}" cy="{1}" r="3.1" fill="var(--accent-out)"/>' -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 Get-SMBeatHtmlEventLabel { param( [string]$Kind, $Strings ) switch ($Kind) { 'before' { return [string]$Strings['EventBefore'] } 'after' { return [string]$Strings['EventAfter'] } 'gap' { return [string]$Strings['EventGap'] } 'registered' { return [string]$Strings['EventRegistered'] } 'started' { return [string]$Strings['EventStarted'] } 'stopped' { return [string]$Strings['EventStopped'] } 'error' { return [string]$Strings['EventError'] } 'reboot' { return [string]$Strings['EventReboot'] } 'crash' { return [string]$Strings['EventCrash'] } 'updated' { return [string]$Strings['EventUpdated'] } default { return $Kind } } } function ConvertTo-SMBeatHtmlTimeline { param( $Report, [TimeZoneInfo]$TimeZone, [ValidateSet('de', 'en')] [string]$Language, $Strings ) $rows = New-Object System.Collections.Generic.List[object] if ($Report.PSObject.Properties['Gaps']) { foreach ($g in @($Report.Gaps)) { if ($null -eq $g) { continue } $rows.Add([PSCustomObject]@{ Utc = [datetime]$g.StartUtc EndUtc = [datetime]$g.EndUtc Kind = [string]$g.Kind Hours = [int]$g.Hours Message = '' }) | Out-Null } } if ($Report.PSObject.Properties['Events']) { foreach ($e in @($Report.Events)) { if ($null -eq $e) { continue } $rows.Add([PSCustomObject]@{ Utc = [datetime]$e.Utc EndUtc = $null Kind = [string]$e.Kind Hours = 0 Message = [string]$e.Message }) | Out-Null } } if ($rows.Count -eq 0) { return '' } $ordered = @($rows | Sort-Object Utc) $sb = New-Object System.Text.StringBuilder $thWhen = ConvertTo-SMBeatHtmlEncode -Text ([string]$Strings['EventWhen']) $thKind = ConvertTo-SMBeatHtmlEncode -Text ([string]$Strings['EventKind']) [void]$sb.AppendLine("<div class=`"table-wrap`"><table class=`"events`"><thead><tr><th>$thWhen</th><th>$thKind</th></tr></thead><tbody>") foreach ($row in $ordered) { $when = Format-SMBeatHtmlWhen -Value $row.Utc -TimeZone $TimeZone -Language $Language if ($null -ne $row.EndUtc -and $row.EndUtc -is [datetime] -and $row.EndUtc -ne [datetime]::MinValue) { $when = $when + ' - ' + (Format-SMBeatHtmlWhen -Value $row.EndUtc -TimeZone $TimeZone -Language $Language) } $label = Get-SMBeatHtmlEventLabel -Kind $row.Kind -Strings $Strings if ($row.Hours -eq 1) { $label = $label + ' (' + [string]$Strings['EventHour'] + ')' } elseif ($row.Hours -gt 1) { $hourText = [string]$Strings['EventHours'] if ($hourText.Contains('{0}')) { $hourText = $hourText.Replace('{0}', [string]$row.Hours) } else { $hourText = [string]$row.Hours } $label = $label + ' (' + $hourText + ')' } if (-not [string]::IsNullOrWhiteSpace($row.Message) -and ($row.Kind -eq 'error' -or $row.Kind -eq 'updated')) { $label = '{0}: {1}' -f $label, $row.Message } $css = 'ev-ok' if ($row.Kind -eq 'gap' -or $row.Kind -eq 'before' -or $row.Kind -eq 'after' -or $row.Kind -eq 'stopped' -or $row.Kind -eq 'error' -or $row.Kind -eq 'reboot' -or $row.Kind -eq 'crash') { $css = 'ev-warn' } [void]$sb.AppendLine(('<tr class="{0}"><td>{1}</td><td>{2}</td></tr>' -f ` $css, ` (ConvertTo-SMBeatHtmlEncode -Text $when), ` (ConvertTo-SMBeatHtmlEncode -Text $label))) } [void]$sb.AppendLine('</tbody></table></div>') ConvertTo-SMBeatHtmlPanel -Title $Strings.Events -Hint $Strings.EventsHint -Body $sb.ToString() } function ConvertTo-SMBeatHtmlPanel { param( [string]$Title, [string]$Hint, [string]$Body ) $hintHtml = '' if (-not [string]::IsNullOrWhiteSpace($Hint)) { $parts = New-Object System.Collections.Generic.List[string] foreach ($chunk in @($Hint -split '\|')) { $text = $chunk.Trim() if (-not [string]::IsNullOrWhiteSpace($text)) { [void]$parts.Add((ConvertTo-SMBeatHtmlEncode -Text $text)) } } $hintHtml = '<p class="lede">{0}</p>' -f ($parts.ToArray() -join '<br />') } '<section class="panel"><div class="panel-head"><h2>{0}</h2></div>{1}{2}</section>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $Title), ` $hintHtml, ` $Body } function ConvertTo-SMBeatHtmlEmbeddedRows { param( [object[]]$Rows ) $list = New-Object System.Collections.Generic.List[object] foreach ($row in @($Rows)) { if ($null -eq $row) { continue } $name = '' if ($row.PSObject.Properties['Name']) { $name = [string]$row.Name } $sent = [int64]0 $recv = [int64]0 if ($row.PSObject.Properties['SentBytes'] -and $row.SentBytes) { $sent = [int64]$row.SentBytes } if ($row.PSObject.Properties['ReceivedBytes'] -and $row.ReceivedBytes) { $recv = [int64]$row.ReceivedBytes } [void]$list.Add([PSCustomObject]@{ Name = $name SentBytes = $sent ReceivedBytes = $recv }) } return ,$list } function ConvertTo-SMBeatHtmlEmbeddedSeries { param( [object[]]$Rows ) $list = New-Object System.Collections.Generic.List[object] foreach ($row in @($Rows)) { if ($null -eq $row) { continue } $start = '' if ($row.PSObject.Properties['PeriodStart'] -and $row.PeriodStart) { $start = ([datetime]$row.PeriodStart).ToString('yyyy-MM-ddTHH:mm:ss', $script:SMBeatInvariant) } $retrieved = [int64]0 $written = [int64]0 $nicOut = [int64]0 $nicIn = [int64]0 if ($row.PSObject.Properties['RetrievedBytes'] -and $row.RetrievedBytes) { $retrieved = [int64]$row.RetrievedBytes } if ($row.PSObject.Properties['WrittenBytes'] -and $row.WrittenBytes) { $written = [int64]$row.WrittenBytes } if ($row.PSObject.Properties['NicBytesOut'] -and $row.NicBytesOut) { $nicOut = [int64]$row.NicBytesOut } if ($row.PSObject.Properties['NicBytesIn'] -and $row.NicBytesIn) { $nicIn = [int64]$row.NicBytesIn } [void]$list.Add([PSCustomObject]@{ PeriodStart = $start RetrievedBytes = $retrieved WrittenBytes = $written NicBytesOut = $nicOut NicBytesIn = $nicIn }) } return ,$list } function ConvertTo-SMBeatHtmlEmbeddedData { param( [Parameter(Mandatory = $true)] $Report ) $totals = $Report.Totals $smbSent = [int64]0 $smbRecv = [int64]0 if ($totals -and $totals.PSObject.Properties['SmbPerfSentBytes'] -and $totals.SmbPerfSentBytes) { $smbSent = [int64]$totals.SmbPerfSentBytes } if ($totals -and $totals.PSObject.Properties['SmbPerfReceivedBytes'] -and $totals.SmbPerfReceivedBytes) { $smbRecv = [int64]$totals.SmbPerfReceivedBytes } $bySmbPerf = New-Object System.Collections.Generic.List[object] if ($Report.PSObject.Properties['BySmbPerf']) { $bySmbPerf = ConvertTo-SMBeatHtmlEmbeddedRows -Rows $Report.BySmbPerf } $collectedSinceText = $null if ($Report.PSObject.Properties['CollectedSinceUtc'] -and $null -ne $Report.CollectedSinceUtc -and $Report.CollectedSinceUtc -ne [datetime]::MinValue) { $collectedSinceText = ConvertTo-SMBeatUtcString -DateTime ([datetime]$Report.CollectedSinceUtc) } $gapEmbed = New-Object System.Collections.Generic.List[object] if ($Report.PSObject.Properties['Gaps']) { foreach ($g in @($Report.Gaps)) { if ($null -eq $g) { continue } [void]$gapEmbed.Add([PSCustomObject]@{ Kind = [string]$g.Kind StartUtc = (ConvertTo-SMBeatUtcString -DateTime ([datetime]$g.StartUtc)) EndUtc = (ConvertTo-SMBeatUtcString -DateTime ([datetime]$g.EndUtc)) Hours = [int]$g.Hours }) } } $eventEmbed = New-Object System.Collections.Generic.List[object] if ($Report.PSObject.Properties['Events']) { foreach ($e in @($Report.Events)) { if ($null -eq $e) { continue } if (-not $e.PSObject.Properties['Utc'] -or $null -eq $e.Utc) { continue } $hostName = '' if ($e.PSObject.Properties['Host'] -and $e.Host) { $hostName = [string]$e.Host } [void]$eventEmbed.Add([PSCustomObject]@{ Kind = [string]$e.Kind Utc = (ConvertTo-SMBeatUtcString -DateTime ([datetime]$e.Utc)) Message = [string]$e.Message Host = $hostName }) } } $payload = [PSCustomObject]@{ Format = 'SMBeat-Report/1' ModuleVersion = [string]$Report.ModuleVersion StartUtc = (ConvertTo-SMBeatUtcString -DateTime $Report.StartUtc) EndUtc = (ConvertTo-SMBeatUtcString -DateTime $Report.EndUtc) TimeZone = [string]$Report.TimeZone CoveragePct = $Report.CoveragePct SampleCount = $Report.SampleCount CollectedSinceUtc = $collectedSinceText Totals = [PSCustomObject]@{ RetrievedBytes = [int64]$totals.RetrievedBytes WrittenBytes = [int64]$totals.WrittenBytes NicBytesOut = [int64]$totals.NicBytesOut NicBytesIn = [int64]$totals.NicBytesIn SmbPerfSentBytes = $smbSent SmbPerfReceivedBytes = $smbRecv } ByShare = (ConvertTo-SMBeatHtmlEmbeddedRows -Rows $Report.ByShare) BySmbPerf = $bySmbPerf ByUser = (ConvertTo-SMBeatHtmlEmbeddedRows -Rows $Report.ByUser) ByClient = (ConvertTo-SMBeatHtmlEmbeddedRows -Rows $Report.ByClient) ByServer = (ConvertTo-SMBeatHtmlEmbeddedRows -Rows $Report.ByServer) Hourly = (ConvertTo-SMBeatHtmlEmbeddedSeries -Rows $Report.Hourly) Daily = (ConvertTo-SMBeatHtmlEmbeddedSeries -Rows $Report.Daily) Weekly = (ConvertTo-SMBeatHtmlEmbeddedSeries -Rows $Report.Weekly) Warnings = @($Report.Warnings) Gaps = $gapEmbed.ToArray() CollectorEvents = $eventEmbed.ToArray() } $json = ConvertTo-Json -InputObject $payload -Depth 6 -Compress $json.Replace('</', '\u003c/') } function Get-SMBeatHtmlTheme { param( [ValidateSet('health', 'pine', 'dark')] [string]$Theme = 'health' ) if ($Theme -eq 'dark') { return [ordered]@{ name = 'dark' ink = '#e6edf3' paper = '#12151a' panel = '#1a1f27' line = '#2a3340' mute = '#8b96a3' heading = '#c5d0dc' 'accent-out' = '#5aa8d4' 'accent-in' = '#e07a72' 'header-from' = '#0d1117' 'header-mid' = '#151b24' 'header-to' = '#1a222c' 'header-ink' = '#e6edf3' 'header-accent' = '#9ec9e6' 'header-mute' = '#9aa7b4' 'header-border' = '#2f3d4d' 'wash-a' = 'rgba(90,168,212,0.10)' 'wash-b' = 'rgba(224,122,114,0.08)' 'chip-bg' = 'rgba(230,237,243,0.06)' 'chip-border' = 'rgba(230,237,243,0.14)' 'chip-ink' = '#d5dde6' 'chip-strong' = '#9ec9e6' 'chip-ok-bg' = 'rgba(90,168,212,0.18)' 'chip-ok-border' = 'rgba(90,168,212,0.32)' 'chip-warn-bg' = 'rgba(224,122,114,0.20)' 'chip-warn-border' = 'rgba(224,122,114,0.38)' 'chip-warn-ink' = '#f0c4c0' 'ver-border' = 'rgba(158,201,230,0.28)' warn = '#f0b4a0' 'warn-bg' = '#2a1c1a' 'warn-line' = '#5a3834' 'card-shadow' = 'rgba(0,0,0,0.28)' 'hero-out-from' = '#1a3a50' 'hero-out-to' = '#142c3e' 'hero-out-border' = '#2a5474' 'hero-out-shadow' = 'rgba(0,0,0,0.28)' 'hero-in-from' = '#4a2220' 'hero-in-to' = '#351816' 'hero-in-border' = '#7a322c' 'hero-in-shadow' = 'rgba(0,0,0,0.28)' 'hero-ink' = '#e6edf3' 'hero-value' = '#ffffff' 'hero-label' = '#9ec9e6' 'hero-label-in' = '#f0c4c0' 'hero-hint' = '#9aa7b4' 'hero-hint-in' = '#c8b0ac' 'peak-out-border' = '#5aa8d4' 'peak-out-from' = '#1a2834' 'peak-out-to' = '#1a1f27' 'peak-in-border' = '#e07a72' 'peak-in-from' = '#2a1c1a' 'peak-in-to' = '#1a1f27' 'delta-up' = '#7ec8ea' 'delta-down' = '#f0a8a0' 'delta-flat' = '#8b96a3' 'empty-bg' = '#1e242c' 'row-line' = '#2a3340' 'bar-track' = '#2a3340' hot = '#243040' 'mark-fill' = '#0d1117' 'mark-stroke' = '#9ec9e6' 'mark-path' = '#e6edf3' 'mark-dot' = '#e07a72' 'print-paper' = '#fff' } } if ($Theme -eq 'pine') { return [ordered]@{ name = 'pine' ink = '#122018' paper = '#efe8d8' panel = '#fffaf0' line = '#d8cbb3' mute = '#5d685f' heading = '#3d4a40' 'accent-out' = '#2a6b45' 'accent-in' = '#c4a35a' 'header-from' = '#0e1a14' 'header-mid' = '#163226' 'header-to' = '#1d3d2c' 'header-ink' = '#f6f0e4' 'header-accent' = '#d7c38a' 'header-mute' = '#c9d4cb' 'header-border' = '#c4a35a' 'wash-a' = 'rgba(196,163,90,0.18)' 'wash-b' = 'rgba(42,107,69,0.12)' 'chip-bg' = 'rgba(255,250,240,0.08)' 'chip-border' = 'rgba(246,240,228,0.14)' 'chip-ink' = '#e8e2d4' 'chip-strong' = '#d7c38a' 'chip-ok-bg' = 'rgba(80,160,110,0.18)' 'chip-ok-border' = 'rgba(140,210,160,0.28)' 'chip-warn-bg' = 'rgba(196,120,50,0.22)' 'chip-warn-border' = 'rgba(232,176,110,0.35)' 'chip-warn-ink' = '#f3d7b0' 'ver-border' = 'rgba(196,163,90,0.35)' warn = '#8a4310' 'warn-bg' = '#f4e0c8' 'warn-line' = '#e0c49a' 'card-shadow' = 'rgba(18,32,24,0.04)' 'hero-out-from' = '#163226' 'hero-out-to' = '#10241b' 'hero-out-border' = '#1f3d2d' 'hero-out-shadow' = 'rgba(16,36,27,0.18)' 'hero-in-from' = '#3a2a14' 'hero-in-to' = '#241910' 'hero-in-border' = '#4a3820' 'hero-in-shadow' = 'rgba(36,25,16,0.18)' 'hero-ink' = '#f6f0e4' 'hero-value' = '#fff8ea' 'hero-label' = '#d7c38a' 'hero-label-in' = '#e8c98a' 'hero-hint' = '#c9d4cb' 'hero-hint-in' = '#e4d4b4' 'peak-out-border' = '#2a6b45' 'peak-out-from' = '#e8f3ea' 'peak-out-to' = '#fffaf0' 'peak-in-border' = '#c4a35a' 'peak-in-from' = '#fff6e2' 'peak-in-to' = '#fffaf0' 'delta-up' = '#8ee0a8' 'delta-down' = '#f0b4a0' 'delta-flat' = '#c9d4cb' 'empty-bg' = '#f3ece0' 'row-line' = '#eee4d2' 'bar-track' = '#efe4ce' hot = '#fff4d8' 'mark-fill' = '#0e1a14' 'mark-stroke' = '#c4a35a' 'mark-path' = '#d7c38a' 'mark-dot' = '#2a6b45' 'print-paper' = '#fff' } } return [ordered]@{ name = 'health' ink = '#1b2430' paper = '#e8eef3' panel = '#f7fafc' line = '#c5d0db' mute = '#5b6774' heading = '#3e4d5c' 'accent-out' = '#3d7eab' 'accent-in' = '#c2473d' 'header-from' = '#4a7fa3' 'header-mid' = '#5a8fb4' 'header-to' = '#6b9dc0' 'header-ink' = '#f4f8fb' 'header-accent' = '#d9ebf6' 'header-mute' = '#d0e0ec' 'header-border' = '#8eb4cc' 'wash-a' = 'rgba(90,155,196,0.16)' 'wash-b' = 'rgba(160,170,180,0.14)' 'chip-bg' = 'rgba(247,250,252,0.10)' 'chip-border' = 'rgba(244,248,251,0.16)' 'chip-ink' = '#eef4f8' 'chip-strong' = '#d9ebf6' 'chip-ok-bg' = 'rgba(80,150,190,0.22)' 'chip-ok-border' = 'rgba(160,210,230,0.32)' 'chip-warn-bg' = 'rgba(194,71,61,0.22)' 'chip-warn-border' = 'rgba(232,140,130,0.35)' 'chip-warn-ink' = '#f6d4d0' 'ver-border' = 'rgba(217,235,246,0.35)' warn = '#a33b32' 'warn-bg' = '#f6e9e7' 'warn-line' = '#e0b8b4' 'card-shadow' = 'rgba(27,36,48,0.05)' 'hero-out-from' = '#2d5f82' 'hero-out-to' = '#214a66' 'hero-out-border' = '#2a5474' 'hero-out-shadow' = 'rgba(33,74,102,0.20)' 'hero-in-from' = '#8f3a34' 'hero-in-to' = '#5e2420' 'hero-in-border' = '#7a322c' 'hero-in-shadow' = 'rgba(94,36,32,0.18)' 'hero-ink' = '#f4f8fb' 'hero-value' = '#ffffff' 'hero-label' = '#c5dcec' 'hero-label-in' = '#f0c8c4' 'hero-hint' = '#c8d8e4' 'hero-hint-in' = '#e8d0cc' 'peak-out-border' = '#3d7eab' 'peak-out-from' = '#e3eef6' 'peak-out-to' = '#f7fafc' 'peak-in-border' = '#c2473d' 'peak-in-from' = '#f8eaea' 'peak-in-to' = '#f7fafc' 'delta-up' = '#4a9bc7' 'delta-down' = '#e08a82' 'delta-flat' = '#c8d4de' 'empty-bg' = '#e4ebf1' 'row-line' = '#e2e8ee' 'bar-track' = '#dce4ec' hot = '#f3e6e5' 'mark-fill' = '#2d5f82' 'mark-stroke' = '#d9ebf6' 'mark-path' = '#e8f2f8' 'mark-dot' = '#c2473d' 'print-paper' = '#fff' } } function ConvertTo-SMBeatHtmlThemeCss { param( [Parameter(Mandatory = $true)] $Theme ) $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine(' :root {') foreach ($entry in $Theme.GetEnumerator()) { if ($entry.Key -eq 'name') { continue } [void]$sb.Append(' --') [void]$sb.Append([string]$entry.Key) [void]$sb.Append(': ') [void]$sb.Append([string]$entry.Value) [void]$sb.AppendLine(';') } [void]$sb.Append(' }') $sb.ToString() } function ConvertTo-SMBeatHtmlReport { param( [Parameter(Mandatory = $true)] $Report, [ValidateSet('de', 'en')] [string]$Language, [ValidateSet('health', 'pine', 'dark')] [string]$Theme = 'health' ) $themeMap = Get-SMBeatHtmlTheme -Theme $Theme $themeName = [string]$themeMap['name'] $themeCss = ConvertTo-SMBeatHtmlThemeCss -Theme $themeMap $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 $spanText = Format-SMBeatHtmlSpan -StartUtc $Report.StartUtc -EndUtc $Report.EndUtc -Language $Language $tzLabel = $tz.Id $sinceChip = '' if ($Report.PSObject.Properties['CollectedSinceUtc'] -and $null -ne $Report.CollectedSinceUtc -and $Report.CollectedSinceUtc -ne [datetime]::MinValue) { $sinceText = Format-SMBeatHtmlWhen -Value $Report.CollectedSinceUtc -TimeZone $tz -Language $Language $sinceChip = '<span class="chip"><strong>{0}:</strong> {1}</span>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $t.CollectedSince), ` (ConvertTo-SMBeatHtmlEncode -Text $sinceText) } $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 $smbPerfOutText = Format-SMBeatHtmlGB -Value 0 -Language $Language $smbPerfInText = Format-SMBeatHtmlGB -Value 0 -Language $Language if ($totals.PSObject.Properties['SmbPerfSentGB']) { $smbPerfOutText = Format-SMBeatHtmlGB -Value $totals.SmbPerfSentGB -Language $Language } if ($totals.PSObject.Properties['SmbPerfReceivedGB']) { $smbPerfInText = Format-SMBeatHtmlGB -Value $totals.SmbPerfReceivedGB -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 } $spanHour = $null $spanDay = $null $spanWeek = $null if ($Report.PSObject.Properties['SpanHour']) { $spanHour = $Report.SpanHour } if ($Report.PSObject.Properties['SpanDay']) { $spanDay = $Report.SpanDay } if ($Report.PSObject.Properties['SpanWeek']) { $spanWeek = $Report.SpanWeek } $spansHtml = ConvertTo-SMBeatHtmlPanel -Title $t.Spans -Hint $t.SpanHint -Body ( ConvertTo-SMBeatHtmlSpanCompare -Hour $spanHour -Day $spanDay -Week $spanWeek -HourLabel $t.SpanHour -DayLabel $t.SpanDay -WeekLabel $t.SpanWeek -RetrievedLabel $t.Retrieved -WrittenLabel $t.Written -Language $Language ) $timelineHtml = ConvertTo-SMBeatHtmlTimeline -Report $Report -TimeZone $tz -Language $Language -Strings $t $notes = New-Object System.Collections.Generic.List[object] $coverage = 0.0 if ($null -ne $Report.CoveragePct) { $coverage = [double]$Report.CoveragePct } if ($coverage -lt 90) { $notes.Add([PSCustomObject]@{ Title = $t.CoverageLowTitle; Body = $t.CoverageLow }) | Out-Null } foreach ($w in @($Report.Warnings)) { if (-not $w -or ([string]$w -like 'Coverage *')) { continue } $title = '' $text = [string]$w if ($text -like 'TCP 445 inbound is much smaller*') { $title = $t.WarnEtwWrittenTitle $text = $t.WarnEtwWritten } elseif ($text -like 'TCP 445 outbound is larger*') { $title = $t.WarnEstataReadTitle $text = $t.WarnEstataRead } elseif ($text -like 'Share bytes are much smaller*') { $title = $t.WarnShareLowTitle $text = $t.WarnShareLow } elseif ($text -like 'NIC Out is much larger*') { $title = $t.WarnNicOutTitle $text = $t.WarnNicOut } $notes.Add([PSCustomObject]@{ Title = $title; Body = $text }) | Out-Null } $warnHtml = '' if ($notes.Count -gt 0) { $intro = '' if ($t.ContainsKey('WarningsIntro') -and -not [string]::IsNullOrWhiteSpace($t.WarningsIntro)) { $intro = '<p class="lede">{0}</p>' -f (ConvertTo-SMBeatHtmlEncode -Text $t.WarningsIntro) } $items = New-Object System.Text.StringBuilder foreach ($note in $notes) { $head = '' if (-not [string]::IsNullOrWhiteSpace([string]$note.Title)) { $head = '<h3>{0}</h3>' -f (ConvertTo-SMBeatHtmlEncode -Text $note.Title) } [void]$items.Append(('<div class="note">{0}<p>{1}</p></div>' -f $head, (ConvertTo-SMBeatHtmlEncode -Text $note.Body))) } $warnHtml = '<section class="notes-banner"><h2>{0}</h2>{1}{2}</section>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $t.Warnings), ` $intro, ` $items.ToString() } $covClass = 'ok' if ($coverage -lt 90) { $covClass = 'warn' } $coverageText = Format-SMBeatHtmlNumber -Value $coverage -Format '0.#' -Language $Language $embeddedJson = ConvertTo-SMBeatHtmlEmbeddedData -Report $Report $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 $smbPerfRows = @() if ($Report.PSObject.Properties['BySmbPerf']) { $smbPerfRows = @($Report.BySmbPerf) } $smbPerfBody = ConvertTo-SMBeatHtmlTable -Rows $smbPerfRows -NameHeader $t.Name -OutHeader $t.SmbPerfOut -InHeader $t.SmbPerfIn -EmptyText $t.BySmbPerfEmpty -MoreText $more -Language $Language @" <!DOCTYPE html> <html lang="$Language" class="theme-$themeName"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>$(ConvertTo-SMBeatHtmlEncode -Text $t.Title) $version</title> <style> $themeCss html.theme-dark { color-scheme: dark; } * { box-sizing: border-box; } html { font-size: 16px; } body { margin: 0; color: var(--ink); background: radial-gradient(1200px 420px at 8% -10%, var(--wash-a), transparent 55%), radial-gradient(900px 380px at 110% 8%, var(--wash-b), 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, var(--header-from) 0%, var(--header-mid) 58%, var(--header-to) 100%); color: var(--header-ink); padding: 1.6rem 2rem 1.35rem; border-bottom: 3px solid var(--header-border); } .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: var(--header-accent); 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: var(--header-mute); padding: 0.28rem 0.6rem; border: 1px solid var(--ver-border); border-radius: 999px; white-space: nowrap; } .chips { display: flex; flex-wrap: nowrap; align-items: center; justify-content: center; gap: 0.35rem; max-width: none; margin: 1rem 0 0; overflow-x: auto; } .chip { display: inline-flex; flex: 0 0 auto; align-items: baseline; gap: 0.35rem; background: var(--chip-bg); border: 1px solid var(--chip-border); color: var(--chip-ink); border-radius: 999px; padding: 0.22rem 0.55rem; font-size: 0.74rem; white-space: nowrap; } .chip strong { color: var(--chip-strong); font-weight: 600; } .chip.ok { background: var(--chip-ok-bg); border-color: var(--chip-ok-border); } .chip.warn { background: var(--chip-warn-bg); border-color: var(--chip-warn-border); color: var(--chip-warn-ink); } 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: var(--heading); } .notes-banner { background: var(--warn-bg); border: 1px solid var(--warn-line); border-left: 4px solid var(--warn); border-radius: 0.7rem; padding: 0.95rem 1.1rem 1.1rem; margin: 1.4rem 0 0; } .notes-banner h2 { color: var(--warn); margin-bottom: 0.35rem; } .notes-banner .lede { margin: 0 0 0.85rem; } .notes-banner .note + .note { margin-top: 0.95rem; padding-top: 0.85rem; border-top: 1px solid var(--warn-line); } .notes-banner h3 { margin: 0 0 0.3rem; font-size: 0.95rem; letter-spacing: 0; text-transform: none; color: var(--ink); } .notes-banner .note p { margin: 0; font-size: 0.86rem; color: var(--ink); max-width: 48rem; } table.events { width: 100%; border-collapse: collapse; font-size: 0.86rem; } table.events th { text-align: left; color: var(--mute); font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.2rem 0.4rem 0.45rem 0; } table.events td { padding: 0.35rem 0.5rem 0.35rem 0; border-top: 1px solid var(--line); vertical-align: top; } table.events tr.ev-warn td { color: var(--warn); background: var(--warn-bg); } .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 var(--card-shadow); } .card.hero { flex: 2 1 16rem; background: linear-gradient(160deg, var(--hero-out-from) 0%, var(--hero-out-to) 100%); border-color: var(--hero-out-border); color: var(--hero-ink); box-shadow: 0 10px 28px var(--hero-out-shadow); } .card.hero.in { background: linear-gradient(160deg, var(--hero-in-from) 0%, var(--hero-in-to) 100%); border-color: var(--hero-in-border); box-shadow: 0 10px 28px var(--hero-in-shadow); } .card.peak { border-color: var(--peak-out-border); background: linear-gradient(180deg, var(--peak-out-from) 0%, var(--peak-out-to) 70%); } .card.peak.in { border-color: var(--peak-in-border); background: linear-gradient(180deg, var(--peak-in-from) 0%, var(--peak-in-to) 70%); } .card .label { font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--mute); font-weight: 600; } .card.hero .label { color: var(--hero-label); } .card.hero.in .label { color: var(--hero-label-in); } .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: var(--hero-value); } .card .unit { font-size: 0.82rem; font-weight: 600; color: var(--mute); letter-spacing: 0.04em; } .card.hero .unit { color: var(--hero-label); } .card.hero.in .unit { color: var(--hero-label-in); } .card .hint { margin-top: 0.4rem; font-size: 0.78rem; color: var(--mute); } .card.hero .hint { color: var(--hero-hint); } .card.hero.in .hint { color: var(--hero-hint-in); } .delta { margin-top: 0.4rem; font-size: 0.78rem; font-weight: 600; } .delta.up { color: var(--delta-up); } .delta.down { color: var(--delta-down); } .delta.flat { color: var(--delta-flat); } .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 var(--card-shadow); } .panel-head { margin-bottom: 0.55rem; } .lede, .empty, .more { margin: 0 0 0.85rem; font-size: 0.8rem; line-height: 1.45; color: var(--mute); } .empty { background: var(--empty-bg); 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 var(--row-line); 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; } table.spans th.num, table.spans td.num { width: 22%; } table.spans td.num { font-size: 1.05rem; font-weight: 600; } .who { font-weight: 600; font-size: 0.92rem; } .who.dim { color: var(--mute); font-style: italic; font-weight: 500; } .bar { height: 5px; background: var(--bar-track); border-radius: 99px; margin-top: 0.28rem; overflow: hidden; } .bar i { display: block; height: 100%; background: var(--accent-out); border-radius: 99px; } .bar.in i { background: var(--accent-in); } .bar.slim { height: 3px; margin-top: 0.22rem; } .duo { margin-top: 0.32rem; } tr.hot td { background: var(--hot); } 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 var(--row-line); } .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: var(--heading); 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: var(--print-paper); } 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="var(--mark-fill)" stroke="var(--mark-stroke)" stroke-width="1.35"/><rect x="5.1" y="5.1" width="37.8" height="37.8" rx="9.4" fill="none" stroke="var(--mark-stroke)" 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="var(--mark-path)" stroke-width="2.05" stroke-linecap="round" stroke-linejoin="round"/><circle cx="39.2" cy="25.6" r="2.15" fill="var(--mark-dot)"/></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"><strong>$(ConvertTo-SMBeatHtmlEncode -Text $t.Span):</strong> $(ConvertTo-SMBeatHtmlEncode -Text $spanText)</span> $sinceChip <span class="chip"><strong>$(ConvertTo-SMBeatHtmlEncode -Text $t.Generated):</strong> $generatedText</span> <span class="chip $covClass"><strong>$(ConvertTo-SMBeatHtmlEncode -Text $t.Coverage):</strong> $coverageText%</span> </div> </header> <main> $(ConvertTo-SMBeatHtmlPanel -Title $t.Totals -Hint '' -Body ('<div class="cards">{0}{1}{2}{3}{4}{5}{6}{7}</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-SMBeatHtmlCard -Label $t.SmbPerfOut -Hint $t.SmbPerfOutHint -Value $smbPerfOutText -CssClass 'card'), ` (ConvertTo-SMBeatHtmlCard -Label $t.SmbPerfIn -Hint $t.SmbPerfInHint -Value $smbPerfInText -CssClass 'card'))) $spansHtml $timelineHtml $(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.BySmbPerf -Hint $t.BySmbPerfHint -Body $smbPerfBody) </div> $(ConvertTo-SMBeatHtmlPanel -Title $t.ByServer -Hint '' -Body $serverBody) $(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)) $warnHtml <footer>$(ConvertTo-SMBeatHtmlEncode -Text $t.Foot)</footer> </main> <script type="application/json" id="smbeat-data">$embeddedJson</script> </body> </html> "@ } |