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 &mdash; 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)