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' 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' Warnings = 'Hinweise' CoverageLow = 'Niedrige Abdeckung: der Collector war nicht die volle Fensterlaenge aktiv. Nach einem frischen Start ist das normal.' Name = 'Name' GiB = 'GiB' Empty = 'Keine Werte in diesem Fenster.' More = 'und {0} weitere' Foot = 'Abgerufen und Geschrieben sind TCP-Payload auf Port 445 (ohne Ethernet-Header). NIC Out/In sind die Leitung (inkl. RDP, Backup, Overhead). Freigaben und Benutzer kommen aus ETW und entsprechen nicht 1:1 dem 445-Volumen. Alte SMB-Zaehler werden nicht als Headline genutzt.' Company = 'MERLIN Kommunikationstechnik' Utc = 'UTC' } } return @{ Title = 'SMBeat usage report' Period = 'Period' Coverage = 'Coverage' Generated = 'Generated' Version = 'SMBeat' Totals = 'Volume' Retrieved = 'Retrieved' RetrievedHint = 'TCP 445, server to client' Written = 'Written' WrittenHint = 'TCP 445, client to server' Peak = 'Peak' 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' Warnings = 'Notes' CoverageLow = 'Low coverage: the collector was not running for the full window. That is normal after a fresh start.' Name = 'Name' GiB = 'GiB' Empty = 'No values in this window.' 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 Format-SMBeatHtmlGiB { param( $Value ) $n = 0.0 if ($null -ne $Value) { $n = [double]$Value } $n.ToString('0.000', $script:SMBeatInvariant) } function Format-SMBeatHtmlWhen { param( $Value, [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 { $_ -and ([int64]$_.SentBytes -gt 0 -or [int64]$_.WriteBytes -gt 0) }) } function ConvertTo-SMBeatHtmlCard { param( [string]$Label, [string]$Hint, [string]$Value, [string]$CssClass, [string]$Extra ) if ([string]::IsNullOrWhiteSpace($CssClass)) { $CssClass = 'card' } $hintHtml = '' if (-not [string]::IsNullOrWhiteSpace($Hint)) { $hintHtml = '<div class="hint">{0}</div>' -f (ConvertTo-SMBeatHtmlEncode -Text $Hint) } $extraHtml = '' if (-not [string]::IsNullOrWhiteSpace($Extra)) { $extraHtml = $Extra } '<div class="{0}"><div class="label">{1}</div><div class="value">{2} <span class="unit">GiB</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]$ValueHeader, [string]$EmptyText, [string]$MoreText, [int]$Top = 15 ) $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) { $v = 0.0 if ($null -ne $row.SentGiB) { $v = [double]$row.SentGiB } if ($v -gt $max) { $max = $v } } $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></tr></thead><tbody>' -f ` (ConvertTo-SMBeatHtmlEncode -Text $NameHeader), ` (ConvertTo-SMBeatHtmlEncode -Text $ValueHeader))) $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' } $gib = 0.0 if ($null -ne $row.SentGiB) { $gib = [double]$row.SentGiB } $pct = Get-SMBeatHtmlBarPct -Value $gib -Max $max -Floor 2 [void]$sb.AppendLine(('<tr><td class="rank">{0}</td><td><div class="{1}">{2}</div><div class="bar"><i style="width:{3}%"></i></div></td><td class="num">{4}</td></tr>' -f ` $i, ` $nameClass, ` (ConvertTo-SMBeatHtmlEncode -Text $name), ` $pct, ` (Format-SMBeatHtmlGiB -Value $row.SentGiB))) } [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 ConvertTo-SMBeatHtmlSpark { param( [object[]]$Rows, [TimeZoneInfo]$TimeZone, [ValidateSet('de', 'en')] [string]$Language = 'en' ) if ($null -eq $Rows -or $Rows.Count -lt 2) { return '' } $max = 0.0 foreach ($row in $Rows) { $v = 0.0 if ($null -ne $row.RetrievedGiB) { $v = [double]$row.RetrievedGiB } if ($v -gt $max) { $max = $v } } $sb = New-Object System.Text.StringBuilder [void]$sb.Append('<div class="spark" aria-hidden="true">') foreach ($row in $Rows) { $v = 0.0 if ($null -ne $row.RetrievedGiB) { $v = [double]$row.RetrievedGiB } $h = Get-SMBeatHtmlBarPct -Value $v -Max $max -Floor 3 $title = '{0} {1} GiB' -f (Format-SMBeatHtmlWhen -Value $row.PeriodStart -TimeZone $TimeZone -Language $Language), (Format-SMBeatHtmlGiB -Value $v) $hot = '' if ($max -gt 0 -and $v -ge $max) { $hot = ' class="hot"' } [void]$sb.Append(('<span{0} title="{1}"><i style="height:{2}%"></i></span>' -f ` $hot, ` (ConvertTo-SMBeatHtmlEncode -Text $title), ` $h)) } [void]$sb.Append('</div>') $sb.ToString() } function ConvertTo-SMBeatHtmlSeries { param( [object[]]$Rows, [string]$RetrievedHeader, [string]$NicOutHeader, [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)) } $max = 0.0 foreach ($row in $Rows) { $v = 0.0 if ($null -ne $row.RetrievedGiB) { $v = [double]$row.RetrievedGiB } if ($v -gt $max) { $max = $v } } $sb = New-Object System.Text.StringBuilder $spark = ConvertTo-SMBeatHtmlSpark -Rows $Rows -TimeZone $TimeZone -Language $Language if (-not [string]::IsNullOrWhiteSpace($spark)) { [void]$sb.AppendLine($spark) } $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></tr></thead><tbody>' -f ` $scroll, ` (ConvertTo-SMBeatHtmlEncode -Text $RetrievedHeader), ` (ConvertTo-SMBeatHtmlEncode -Text $NicOutHeader))) foreach ($row in $Rows) { $v = 0.0 if ($null -ne $row.RetrievedGiB) { $v = [double]$row.RetrievedGiB } $pct = Get-SMBeatHtmlBarPct -Value $v -Max $max -Floor 2 $hot = '' if ($max -gt 0 -and $v -ge $max) { $hot = ' class="hot"' } [void]$sb.AppendLine(('<tr{0}><td>{1}<div class="bar slim"><i style="width:{2}%"></i></div></td><td class="num">{3}</td><td class="num mute">{4}</td></tr>' -f ` $hot, ` (ConvertTo-SMBeatHtmlEncode -Text (Format-SMBeatHtmlWhen -Value $row.PeriodStart -TimeZone $TimeZone -Language $Language)), ` $pct, ` (Format-SMBeatHtmlGiB -Value $row.RetrievedGiB), ` (Format-SMBeatHtmlGiB -Value $row.NicOutGiB))) } [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-SMBeatHtmlGiB -Value $totals.RetrievedGiB $writtenText = Format-SMBeatHtmlGiB -Value 0 if ($totals.PSObject.Properties['WrittenGiB']) { $writtenText = Format-SMBeatHtmlGiB -Value $totals.WrittenGiB } $nicOutText = Format-SMBeatHtmlGiB -Value $totals.NicOutGiB $nicInText = Format-SMBeatHtmlGiB -Value $totals.NicInGiB $peakHtml = '' if ($Report.PSObject.Properties['PeakStart'] -and $Report.PeakStart) { $peakWhen = Format-SMBeatHtmlWhen -Value $Report.PeakStart -TimeZone $tz -Language $Language $peakGiB = Format-SMBeatHtmlGiB -Value $Report.PeakRetrievedGiB $avgGiB = Format-SMBeatHtmlGiB -Value $Report.AverageRetrievedGiBPerHour $peakHint = '{0} / {1} {2} GiB/h' -f $peakWhen, $t.AverageHour, $avgGiB $peakHtml = ConvertTo-SMBeatHtmlCard -Label $t.Peak -Hint $peakHint -Value $peakGiB -CssClass 'card peak' } $prevHtml = '' $deltaHtml = '' if ($Report.PreviousTotals) { $prevValue = Format-SMBeatHtmlGiB -Value $Report.PreviousTotals.RetrievedGiB $prevHtml = ConvertTo-SMBeatHtmlCard -Label $t.Previous -Hint $t.PreviousHint -Value $prevValue -CssClass 'card' $prevGiB = 0.0 $curGiB = 0.0 if ($null -ne $Report.PreviousTotals.RetrievedGiB) { $prevGiB = [double]$Report.PreviousTotals.RetrievedGiB } if ($null -ne $totals.RetrievedGiB) { $curGiB = [double]$totals.RetrievedGiB } if ($prevGiB -gt 0) { $chg = (($curGiB - $prevGiB) / $prevGiB) * 100.0 $cls = 'up' if ($chg -lt 0) { $cls = 'down' } elseif ($chg -eq 0) { $cls = 'flat' } $chgText = $chg.ToString('+0.0;-0.0;0.0', $script:SMBeatInvariant) $deltaHtml = '<div class="delta {0}">{1}% {2}</div>' -f ` $cls, ` (ConvertTo-SMBeatHtmlEncode -Text $chgText), ` (ConvertTo-SMBeatHtmlEncode -Text $t.VersusPrevious) } } $notes = New-Object System.Collections.Generic.List[string] $coverage = 0.0 if ($null -ne $Report.CoveragePct) { $coverage = [double]$Report.CoveragePct } if ($coverage -lt 90) { $notes.Add($t.CoverageLow) | Out-Null } foreach ($w in @($Report.Warnings)) { if ($w -and ([string]$w -notlike 'Coverage *')) { $notes.Add([string]$w) | Out-Null } } $warnHtml = '' if ($notes.Count -gt 0) { $items = ($notes | ForEach-Object { '<li>' + (ConvertTo-SMBeatHtmlEncode -Text $_) + '</li>' }) -join '' $warnHtml = '<section 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 = $coverage.ToString('0.#', $script:SMBeatInvariant) $more = $t.More $userBody = ConvertTo-SMBeatHtmlTable -Rows $Report.ByUser -NameHeader $t.Name -ValueHeader $t.GiB -EmptyText $t.ByUserEmpty -MoreText $more $clientBody = ConvertTo-SMBeatHtmlTable -Rows $Report.ByClient -NameHeader $t.Name -ValueHeader $t.GiB -EmptyText $t.Empty -MoreText $more $serverBody = ConvertTo-SMBeatHtmlTable -Rows $Report.ByServer -NameHeader $t.Name -ValueHeader $t.GiB -EmptyText $t.Empty -MoreText $more $shareBody = ConvertTo-SMBeatHtmlTable -Rows $Report.ByShare -NameHeader $t.Name -ValueHeader $t.GiB -EmptyText $t.ByShareEmpty -MoreText $more @" <!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 { display: flex; align-items: flex-end; gap: 3px; width: 2.1rem; height: 2.1rem; padding: 0.28rem 0.32rem 0.24rem; border: 1.5px solid var(--brass); border-radius: 0.45rem; flex: 0 0 auto; margin-top: 0.15rem; } .mark span { display: block; flex: 1; background: var(--brass); border-radius: 1px; } .mark span:nth-child(1) { height: 42%; opacity: 0.55; } .mark span:nth-child(2) { height: 72%; opacity: 0.8; } .mark span:nth-child(3) { 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.peak { 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 .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 .hint { margin-top: 0.4rem; font-size: 0.78rem; color: var(--mute); } .card.hero .hint { color: #c9d4cb; } .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: linear-gradient(90deg, #2a6b45, #c4a35a); border-radius: 99px; } .bar.slim { height: 3px; 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; } .spark { display: flex; align-items: flex-end; gap: 1px; height: 4.4rem; margin: 0.15rem 0 0.85rem; padding: 0.2rem 0 0; } .spark span { flex: 1 1 0; min-width: 2px; height: 100%; display: flex; align-items: flex-end; } .spark i { display: block; width: 100%; background: #2a6b45; border-radius: 1px 1px 0 0; min-height: 1px; } .spark span.hot i { background: var(--brass); } 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; } .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"><span></span><span></span><span></span></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 $deltaHtml), ` (ConvertTo-SMBeatHtmlCard -Label $t.Written -Hint $t.WrittenHint -Value $writtenText -CssClass 'card'), ` $peakHtml, ` $prevHtml, ` (ConvertTo-SMBeatHtmlCard -Label $t.NicOut -Hint $t.NicOutHint -Value $nicOutText -CssClass 'card'), ` (ConvertTo-SMBeatHtmlCard -Label $t.NicIn -Hint $t.NicInHint -Value $nicInText -CssClass 'card'))) <div 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 -NicOutHeader $t.NicOut -EmptyText $t.Empty -TimeZone $tz -Language $Language)) $(ConvertTo-SMBeatHtmlPanel -Title $t.Daily -Hint '' -Body (ConvertTo-SMBeatHtmlSeries -Rows $Report.Daily -RetrievedHeader $t.Retrieved -NicOutHeader $t.NicOut -EmptyText $t.Empty -TimeZone $tz -Language $Language)) $(ConvertTo-SMBeatHtmlPanel -Title $t.Weekly -Hint '' -Body (ConvertTo-SMBeatHtmlSeries -Rows $Report.Weekly -RetrievedHeader $t.Retrieved -NicOutHeader $t.NicOut -EmptyText $t.Empty -TimeZone $tz -Language $Language)) <footer>$(ConvertTo-SMBeatHtmlEncode -Text $t.Foot)</footer> </main> </body> </html> "@ } |