Scripts/build-example-report.ps1
|
#Requires -Version 5.1 # Builds docs/beispiel-report.html from fictional samples via the real renderer. # pwsh -NoProfile -File ./Scripts/build-example-report.ps1 # powershell.exe -NoProfile -File .\Scripts\build-example-report.ps1 $ErrorActionPreference = 'Stop' $here = Split-Path -Parent $PSScriptRoot $manifest = Join-Path $here 'SMBeat.psd1' $outHtml = Join-Path $here 'docs/beispiel-report.html' Import-Module $manifest -Force $mod = Get-Module SMBeat if ($null -eq $mod) { throw 'Import-Module SMBeat failed.' } function New-SMBeatExampleRecord { param( [Parameter(Mandatory = $true)] [string]$Ts, [Parameter(Mandatory = $true)] [string]$Kind, [Parameter(Mandatory = $true)] [string]$Name, [int64]$Sent = 0, [int64]$Recv = 0, [string]$Client = '', [string]$User = '', [string]$Server = 'BEISPIEL-FS' ) [PSCustomObject]@{ ts = $Ts server = $Server kind = $Kind name = $Name client = $Client user = $User readBytesDelta = 0 writeBytesDelta = 0 sentBytesDelta = $Sent receivedBytesDelta = $Recv sampleIntervalSec = 60 } } function Add-SMBeatExampleHour { param( $List, [string]$Ts, [int64]$Sent, [int64]$Recv, [int64]$TcpRecv = -1, [string]$Share = '\\*\Daten', [string]$User = 'BEISPIEL\mueller', [string]$Client = '10.20.1.41' ) # NIC sits a bit above TCP 445 (Ethernet overhead + a little RDP/other). # SMB-Perf is the Gegencheck, almost equal. ETW share/user is most of TCP. # TcpRecv < Recv = ESTATA missed a short write; headline Geschrieben uses ETW. if ($TcpRecv -lt 0) { $TcpRecv = $Recv } $nicSent = [int64]($Sent * 1.025) $nicRecv = [int64]($Recv * 1.02) $nicOther = 250000 if ($nicRecv -lt ($Recv + $nicOther)) { $nicRecv = $Recv + $nicOther } $smbSent = [int64]($Sent * 0.998) $smbRecv = [int64]($Recv * 0.997) $etwSent = [int64]($Sent * 0.97) $List.Add((New-SMBeatExampleRecord -Ts $Ts -Kind 'tcp445' -Name '_listen445' -Sent $Sent -Recv $TcpRecv)) | Out-Null $List.Add((New-SMBeatExampleRecord -Ts $Ts -Kind 'tcp445' -Name $Client -Sent $Sent -Recv $TcpRecv -Client $Client)) | Out-Null $List.Add((New-SMBeatExampleRecord -Ts $Ts -Kind 'nic' -Name 'Ethernet' -Sent $nicSent -Recv $nicRecv)) | Out-Null $List.Add((New-SMBeatExampleRecord -Ts $Ts -Kind 'smbperf' -Name '_server' -Sent $smbSent -Recv $smbRecv)) | Out-Null $List.Add((New-SMBeatExampleRecord -Ts $Ts -Kind 'smbperf' -Name $Share -Sent $smbSent -Recv $smbRecv)) | Out-Null $List.Add((New-SMBeatExampleRecord -Ts $Ts -Kind 'share' -Name $Share -Sent $etwSent -Recv $Recv)) | Out-Null $List.Add((New-SMBeatExampleRecord -Ts $Ts -Kind 'user' -Name $User -Sent $etwSent -Recv $Recv -User $User)) | Out-Null $List.Add((New-SMBeatExampleRecord -Ts $Ts -Kind 'client' -Name $Client -Sent $etwSent -Recv $Recv -Client $Client)) | Out-Null } $records = New-Object System.Collections.Generic.List[object] # Covered hours in UTC (Europe/Berlin CEST = UTC+2). Window 24.08. 08:00 - 25.08. 18:00 local. # First sample 24.08. 12:05 -> before. Skip 16:00-17:00 -> gap. Last 25.08. 14:05 -> after. $hours = @( @{ Ts = '2026-08-24T10:05:00Z'; Sent = 420000000; Recv = 12000000; Share = '\\*\Daten'; User = '(unknown)'; Client = '10.20.1.41' } @{ Ts = '2026-08-24T11:05:00Z'; Sent = 880000000; Recv = 15000000; Share = '\\*\Daten'; User = '(unknown)'; Client = '10.20.1.41' } @{ Ts = '2026-08-24T12:05:00Z'; Sent = 2100000000; Recv = 18000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-24T13:05:00Z'; Sent = 7031000000; Recv = 22000000; Share = '\\*\Dokumentation'; User = 'BEISPIEL\schmidt'; Client = '10.20.1.55' } # gap 14:00Z / 15:00Z = 16:00-17:00 local @{ Ts = '2026-08-24T16:05:00Z'; Sent = 510000000; Recv = 9000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-24T17:05:00Z'; Sent = 330000000; Recv = 8000000; Share = '\\*\Privat'; User = 'BEISPIEL\becker'; Client = '10.20.1.18' } @{ Ts = '2026-08-24T18:05:00Z'; Sent = 140000000; Recv = 4000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-24T19:05:00Z'; Sent = 90000000; Recv = 2000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-24T20:05:00Z'; Sent = 40000000; Recv = 1000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-24T21:05:00Z'; Sent = 20000000; Recv = 500000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-24T22:05:00Z'; Sent = 8000000; Recv = 200000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-25T02:05:00Z'; Sent = 3000000; Recv = 22800000000; TcpRecv = 48000000; Share = '\\*\Backup'; User = 'BEISPIEL\backup'; Client = '10.20.1.9' } @{ Ts = '2026-08-25T03:05:00Z'; Sent = 2000000; Recv = 400000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-25T07:05:00Z'; Sent = 1900000000; Recv = 25000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-25T08:05:00Z'; Sent = 4257000000; Recv = 8000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-25T09:05:00Z'; Sent = 4297000000; Recv = 7000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-25T10:05:00Z'; Sent = 980000000; Recv = 114000000; Share = '\\*\Privat'; User = 'BEISPIEL\becker'; Client = '10.20.1.18' } @{ Ts = '2026-08-25T11:05:00Z'; Sent = 4373000000; Recv = 6000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } @{ Ts = '2026-08-25T12:05:00Z'; Sent = 2100000000; Recv = 5000000; Share = '\\*\Daten'; User = 'BEISPIEL\mueller'; Client = '10.20.1.41' } ) foreach ($h in $hours) { $hourStart = [datetime]::Parse($h.Ts, [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal).AddMinutes(-5) $n = 60 $tcpRecvTotal = [int64]$h.Recv if ($h.ContainsKey('TcpRecv')) { $tcpRecvTotal = [int64]$h.TcpRecv } $sentEach = [int64][math]::Floor($h.Sent / $n) $recvEach = [int64][math]::Floor($h.Recv / $n) $tcpRecvEach = [int64][math]::Floor($tcpRecvTotal / $n) $sentRem = [int64]($h.Sent - ($sentEach * ($n - 1))) $recvRem = [int64]($h.Recv - ($recvEach * ($n - 1))) $tcpRecvRem = [int64]($tcpRecvTotal - ($tcpRecvEach * ($n - 1))) for ($i = 0; $i -lt $n; $i++) { $when = $hourStart.AddMinutes($i) $stamp = $when.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $sent = $sentEach $recv = $recvEach $tcpRecv = $tcpRecvEach if ($i -eq ($n - 1)) { $sent = $sentRem $recv = $recvRem $tcpRecv = $tcpRecvRem } Add-SMBeatExampleHour -List $records -Ts $stamp -Sent $sent -Recv $recv -TcpRecv $tcpRecv -Share $h.Share -User $h.User -Client $h.Client } } $tz = $null foreach ($id in @('W. Europe Standard Time', 'Europe/Berlin', 'UTC')) { try { $tz = [TimeZoneInfo]::FindSystemTimeZoneById($id) break } catch { } } if ($null -eq $tz) { $tz = [TimeZoneInfo]::Utc } $startUtc = [datetime]::Parse('2026-08-24T06:00:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal) $endUtc = [datetime]::Parse('2026-08-25T16:00:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal) $generatedUtc = [datetime]::Parse('2026-08-25T16:12:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal) $events = @( [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-24T05:50:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'registered'; Message = 'Collector registered version=0.5.21 path=C:\ProgramData\SMBeat'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-24T05:51:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'started'; Message = 'Collector started pid=4242 version=0.5.21 mode=tcp445+etw'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-24T10:20:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'updated'; Message = '0.5.21 -> 0.5.22'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-24T10:21:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'started'; Message = 'Collector started pid=4310 version=0.5.22 mode=tcp445+etw'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-24T14:00:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'stopped'; Message = 'Collector stopped.'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-24T14:02:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'error'; Message = 'ESTATA read failed on 445 (fictional)'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-24T16:05:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'started'; Message = 'Collector started pid=4402 version=0.5.22 mode=tcp445+etw'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-25T04:16:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'reboot'; Message = 'LastBootUpTime 2026-08-25T04:16:00Z'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-25T04:20:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'started'; Message = 'Collector started pid=1888 version=0.5.22 mode=tcp445+etw'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-25T09:00:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'crash'; Message = 'LastBootUpTime 2026-08-25T09:00:00Z'; Host = 'BEISPIEL-FS' } [PSCustomObject]@{ Utc = [datetime]::Parse('2026-08-25T09:08:00Z', [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal); Kind = 'started'; Message = 'Collector started pid=2104 version=0.5.22 mode=tcp445+etw'; Host = 'BEISPIEL-FS' } ) $html = & $mod { param($Records, $StartUtc, $EndUtc, $GeneratedUtc, $TimeZone, $Events) $prev = [PSCustomObject]@{ RetrievedGB = 22.4 WrittenGB = 19.1 NicOutGB = 23.1 NicInGB = 19.6 SmbPerfSentGB = 22.3 SmbPerfReceivedGB = 19.0 } $report = New-SMBeatReportFromSamples -Records $Records -StartUtc $StartUtc -EndUtc $EndUtc -TimeZone $TimeZone -Granularity @('Hour', 'Day', 'Week') -IntervalSec 60 -PreviousTotals $prev -GeneratedUtc $GeneratedUtc Add-SMBeatReportSpans -Report $report -Records $Records -TimeZone $TimeZone -IntervalSec 60 | Out-Null $report | Add-Member -NotePropertyName Events -NotePropertyValue $Events -Force $tech = $report.Tech $tech.ComputerName = 'BEISPIEL-FS' $tech.Domain = 'BEISPIEL' $tech.OsCaption = 'Microsoft Windows Server 2019 Standard' $tech.OsVersion = '10.0.17763' $tech.OsBuild = '17763' $tech.Manufacturer = 'Beispiel GmbH' $tech.Model = 'FileServer 1' $tech.RamBytes = [int64]32GB $tech.LastBootUtc = '2026-08-25T04:16:00Z' $tech.CollectorVersion = '0.5.22' $tech.Mode = 'tcp445+etw' $tech.RetentionDays = 90 $tech.IncludeNics = $true $tech.IncludeSmbPerf = $true $tech.IncludeEtwShares = $true $tech.IncludeAdminShares = $false $tech.DataRoot = 'C:\ProgramData\SMBeat' ConvertTo-SMBeatHtmlReport -Report $report -Language de -Theme health } $records.ToArray() $startUtc $endUtc $generatedUtc $tz $events $css = @' .example-banner { margin: 0; padding: 1.25rem 1.6rem 1.4rem; background: var(--warn-bg); border-bottom: 5px solid var(--warn); text-align: center; } .example-banner .kicker { display: block; margin: 0 0 0.4rem; font-size: 0.82rem; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--warn); } .example-banner .title { margin: 0; font-size: 1.55rem; font-weight: 700; letter-spacing: -0.02em; color: var(--ink); line-height: 1.25; } .example-banner .body { margin: 0.55rem auto 0; max-width: 48rem; font-size: 1.05rem; line-height: 1.5; color: var(--ink); } .chip.example { background: var(--chip-warn-bg); border-color: var(--chip-warn-border); color: var(--chip-warn-ink); font-weight: 700; } '@ $banner = @' <aside class="example-banner" role="status"> <span class="kicker">Kein Kundensystem</span> <p class="title">Fiktives Beispiel fuer Schulung und Handbuch. Keine Kundendaten.</p> <p class="body">Enthaelt alle Ereignisarten: Registrierung, Start, Stopp, Fehler, Update, Neustart, Crash, Luecke, spaeterer Beginn, frueheres Ende.</p> </aside> '@ $html = $html.Replace(' </style>', ($css + ' </style>')) $html = $html.Replace('<body>', ('<body>' + [Environment]::NewLine + $banner.TrimEnd())) $html = $html.Replace('<h1>SMBeat Nutzungsbericht</h1>', '<h1>SMBeat Beispielbericht (fiktiv)</h1>') $html = $html.Replace('<div class="chips">', '<div class="chips">' + [Environment]::NewLine + ' <span class="chip example">BEISPIEL — keine Kundendaten</span>') $html = $html.Replace('<title>SMBeat Nutzungsbericht', '<title>SMBeat Beispielbericht (fiktiv)') Set-Content -LiteralPath $outHtml -Value $html -Encoding UTF8 Write-Host ('Wrote {0} ({1} chars, tz={2})' -f $outHtml, $html.Length, $tz.Id) |