function Get-VoidBotDashboardProperty { param( [Parameter(Mandatory = $false)] [AllowNull()] $InputObject, [Parameter(Mandatory = $true)] [string] $PropertyName ) if ($null -eq $InputObject) { return $null } $property = $InputObject.PSObject.Properties[$PropertyName] if ($null -eq $property) { return $null } return $property.Value } function Read-VoidBotDashboardJson { param( [Parameter(Mandatory = $true)] [string] $Path ) if (-not (Test-Path -LiteralPath $Path)) { return [PSCustomObject]@{ _path = $Path _missing = $true } } try { $content = Get-Content -LiteralPath $Path -Raw if ([string]::IsNullOrWhiteSpace($content)) { return [PSCustomObject]@{ _path = $Path _missing = $true } } $parsed = $content | ConvertFrom-Json Add-Member -InputObject $parsed -NotePropertyName "_path" -NotePropertyValue $Path -Force return $parsed } catch { return [PSCustomObject]@{ _path = $Path _error = $_.Exception.Message } } } function ConvertTo-VoidBotDashboardText { param( [Parameter(Mandatory = $false)] [AllowNull()] $Value ) if ($null -eq $Value) { return "missing" } if ($Value -is [bool]) { return $(if ($Value) { "yes" } else { "no" }) } if ($Value -is [DateTime]) { return $Value.ToString("yyyy-MM-dd HH:mm:ss") } if ($Value -is [DateTimeOffset]) { return $Value.ToString("yyyy-MM-dd HH:mm:ss zzz") } if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) { $items = @($Value) if ($items.Count -eq 0) { return "none" } if ($items | Where-Object { $_ -is [string] -or $_ -is [ValueType] }) { return ($items | ForEach-Object { [string]$_ }) -join ", " } return ($Value | ConvertTo-Json -Depth 6 -Compress) } return [string]$Value } function Escape-VoidBotDashboardHtml { param( [Parameter(Mandatory = $false)] [AllowNull()] $Value ) return [System.Net.WebUtility]::HtmlEncode((ConvertTo-VoidBotDashboardText -Value $Value)) } function ConvertTo-VoidBotDashboardFileUri { param( [Parameter(Mandatory = $true)] [string] $Path ) return ([System.Uri]::new($Path)).AbsoluteUri } function New-VoidBotDashboardFileLink { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $false)] [string] $Label ) $text = if ([string]::IsNullOrWhiteSpace($Label)) { $Path } else { $Label } return "" + (Escape-VoidBotDashboardHtml $text) + "" } function Get-VoidBotDashboardTone { param( [Parameter(Mandatory = $false)] [AllowNull()] [string] $State ) if ([string]::IsNullOrWhiteSpace($State)) { return "neutral" } switch ($State.ToLowerInvariant()) { "healthy" { return "good" } "passed" { return "good" } "completed" { return "good" } "ready" { return "good" } "enabled" { return "good" } "ok" { return "good" } "warning" { return "warn" } "running" { return "info" } "starting" { return "info" } "compressing" { return "info" } "uploading" { return "info" } "pruning" { return "info" } "bootstrapping" { return "info" } "build" { return "info" } "bot_start" { return "info" } "worker_start" { return "info" } "already_running" { return "warn" } "failed" { return "bad" } "error" { return "bad" } "disabled" { return "bad" } "stalled" { return "bad" } "missing" { return "bad" } default { return "neutral" } } } function New-VoidBotDashboardBadge { param( [Parameter(Mandatory = $false)] [AllowNull()] [string] $State, [Parameter(Mandatory = $false)] [string] $Label ) $text = if ([string]::IsNullOrWhiteSpace($Label)) { if ([string]::IsNullOrWhiteSpace($State)) { "unknown" } else { $State } } else { $Label } $tone = Get-VoidBotDashboardTone -State $State return "" + (Escape-VoidBotDashboardHtml $text) + "" } function Get-VoidBotDashboardRelativeTime { param( [Parameter(Mandatory = $false)] [AllowNull()] [string] $Timestamp ) if ([string]::IsNullOrWhiteSpace($Timestamp)) { return "unknown" } try { $target = [DateTimeOffset]::Parse($Timestamp) $delta = [DateTimeOffset]::Now - $target if ($delta.TotalSeconds -lt 60) { return "{0}s ago" -f [Math]::Max([int][Math]::Round($delta.TotalSeconds), 0) } if ($delta.TotalMinutes -lt 60) { return "{0}m ago" -f [int][Math]::Round($delta.TotalMinutes) } if ($delta.TotalHours -lt 48) { return "{0}h ago" -f [int][Math]::Round($delta.TotalHours) } return "{0}d ago" -f [int][Math]::Round($delta.TotalDays) } catch { return "unknown" } } function Get-VoidBotDashboardElapsedTime { param( [Parameter(Mandatory = $false)] [AllowNull()] [string] $StartedAt ) if ([string]::IsNullOrWhiteSpace($StartedAt)) { return "unknown" } try { $delta = [DateTimeOffset]::Now - [DateTimeOffset]::Parse($StartedAt) if ($delta.TotalMinutes -lt 1) { return "{0}s" -f [Math]::Max([int][Math]::Round($delta.TotalSeconds), 0) } if ($delta.TotalHours -lt 1) { return "{0}m" -f [int][Math]::Round($delta.TotalMinutes) } return "{0}h {1}m" -f [int][Math]::Floor($delta.TotalHours), $delta.Minutes } catch { return "unknown" } } function New-VoidBotDashboardFactRow { param( [Parameter(Mandatory = $true)] [string] $Label, [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $ValueHtml ) return "" + (Escape-VoidBotDashboardHtml $Label) + "" + $ValueHtml + "" } function New-VoidBotDashboardFactSection { param( [Parameter(Mandatory = $true)] [string] $Title, [Parameter(Mandatory = $true)] [string[]] $Rows ) $body = if ($Rows.Count -gt 0) { ($Rows -join [Environment]::NewLine) } else { "Nothing here yet." } return @"

$(Escape-VoidBotDashboardHtml $Title)

$body
"@ } function New-VoidBotDashboardCard { param( [Parameter(Mandatory = $true)] [string] $Title, [Parameter(Mandatory = $false)] [AllowNull()] [string] $State, [Parameter(Mandatory = $true)] [string] $Subtitle, [Parameter(Mandatory = $false)] [string] $Meta = "" ) $metaHtml = if ([string]::IsNullOrWhiteSpace($Meta)) { "" } else { "
" + (Escape-VoidBotDashboardHtml $Meta) + "
" } return @"

$(Escape-VoidBotDashboardHtml $Title)

$(New-VoidBotDashboardBadge -State $State)

$(Escape-VoidBotDashboardHtml $Subtitle)

$metaHtml
"@ } function Update-VoidBotOperationsDashboard { param( [Parameter(Mandatory = $true)] [string] $RepoRoot, [Parameter(Mandatory = $true)] [string] $StorageRoot ) $statusDir = Join-Path $StorageRoot "status" $logsDir = Join-Path $StorageRoot "logs" $dashboardPath = Join-Path $statusDir "operations-dashboard.html" $snapshotPath = Join-Path $statusDir "operations-dashboard.json" New-Item -ItemType Directory -Force -Path $statusDir | Out-Null $runtimePath = Join-Path $statusDir "runtime-stack.json" $operationsPath = Join-Path $statusDir "operations-health.json" $watchdogPath = Join-Path $statusDir "operations-watchdog.json" $offsitePath = Join-Path $statusDir "offsite-backup.json" $runtime = Read-VoidBotDashboardJson -Path $runtimePath $operations = Read-VoidBotDashboardJson -Path $operationsPath $watchdog = Read-VoidBotDashboardJson -Path $watchdogPath $offsite = Read-VoidBotDashboardJson -Path $offsitePath $operationsState = [string](Get-VoidBotDashboardProperty -InputObject $operations -PropertyName "status") if ([string]::IsNullOrWhiteSpace($operationsState)) { $operationsState = if (Get-VoidBotDashboardProperty -InputObject $operations -PropertyName "_missing") { "missing" } else { "unknown" } } $runtimeState = if (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "_missing") { "missing" } elseif ([bool](Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "ready")) { "ready" } else { $stageValue = [string](Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "stage") if ([string]::IsNullOrWhiteSpace($stageValue)) { "starting" } else { $stageValue } } $watchdogRunStatus = [string](Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "runStatus") $watchdogStartedAt = [string](Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "startedAt") $watchdogStep = [string](Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "currentStep") $watchdogLimitMinutes = Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "executionTimeLimitMinutes" $watchdogReportStatus = [string](Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "reportStatus") $watchdogState = if ([string]::IsNullOrWhiteSpace($watchdogRunStatus)) { if (Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "_missing") { "missing" } else { $operationsState } } elseif ($watchdogRunStatus -eq "running" -and -not [string]::IsNullOrWhiteSpace($watchdogStartedAt)) { try { $elapsedMinutes = ([DateTimeOffset]::Now - [DateTimeOffset]::Parse($watchdogStartedAt)).TotalMinutes $limitValue = if ($watchdogLimitMinutes -is [ValueType]) { [double]$watchdogLimitMinutes } else { 15.0 } if ($elapsedMinutes -gt $limitValue) { "stalled" } else { "running" } } catch { "running" } } elseif ($watchdogRunStatus -eq "completed" -and -not [string]::IsNullOrWhiteSpace($watchdogReportStatus)) { $watchdogReportStatus } else { $watchdogRunStatus } $offsiteState = [string](Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "status") if ([string]::IsNullOrWhiteSpace($offsiteState)) { $offsiteState = if (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "_missing") { "missing" } else { "unknown" } } $operationsSummary = Get-VoidBotDashboardProperty -InputObject $operations -PropertyName "summary" $operationsSubtitle = if ($null -ne $operationsSummary) { "{0} failed, {1} warning, {2} passed" -f ` (Get-VoidBotDashboardProperty -InputObject $operationsSummary -PropertyName "failed"), ` (Get-VoidBotDashboardProperty -InputObject $operationsSummary -PropertyName "warning"), ` (Get-VoidBotDashboardProperty -InputObject $operationsSummary -PropertyName "passed") } else { "No health report yet." } $watchdogSubtitle = if ($watchdogRunStatus -eq "running") { "Current step: {0}" -f $(if ([string]::IsNullOrWhiteSpace($watchdogStep)) { "working" } else { $watchdogStep }) } elseif ($watchdogState -eq "stalled") { "Still marked running after {0}" -f (Get-VoidBotDashboardElapsedTime -StartedAt $watchdogStartedAt) } elseif ($watchdogRunStatus -eq "failed") { "Watchdog run crashed before finishing." } elseif ($watchdogState -eq "healthy") { "Watchdog run completed cleanly." } elseif ($watchdogState -eq "warning") { "Watchdog run finished with warnings." } elseif ($watchdogState -eq "failed") { "Watchdog run finished with failed checks." } else { "No active watchdog run." } $runtimeSubtitle = if ($runtimeState -eq "ready") { "Bot and worker reported ready." } elseif ($runtimeState -eq "missing") { "Runtime stack has not written status yet." } else { "Current stage: {0}" -f $runtimeState } $offsiteSubtitle = if ($offsiteState -eq "completed") { "Latest offsite sync finished." } elseif ($offsiteState -eq "missing") { "No offsite sync status yet." } else { "Current phase: {0}" -f $offsiteState } $cardsHtml = @( New-VoidBotDashboardCard -Title "Operations Health" -State $operationsState -Subtitle $operationsSubtitle -Meta ("Checked " + (Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $operations -PropertyName "checkedAt")))) New-VoidBotDashboardCard -Title "Watchdog Run" -State $watchdogState -Subtitle $watchdogSubtitle -Meta ("Started " + (Get-VoidBotDashboardRelativeTime -Timestamp $watchdogStartedAt)) New-VoidBotDashboardCard -Title "Runtime Stack" -State $runtimeState -Subtitle $runtimeSubtitle -Meta ("Updated " + (Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "completedAt")))) New-VoidBotDashboardCard -Title "Offsite Backup" -State $offsiteState -Subtitle $offsiteSubtitle -Meta ("Updated " + (Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "completedAt")))) ) -join [Environment]::NewLine $runtimeRows = @( New-VoidBotDashboardFactRow -Label "Status file" -ValueHtml (New-VoidBotDashboardFileLink -Path $runtimePath) New-VoidBotDashboardFactRow -Label "Ready" -ValueHtml (New-VoidBotDashboardBadge -State $runtimeState -Label ([string](Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "ready"))) New-VoidBotDashboardFactRow -Label "Stage" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "stage")) + "") New-VoidBotDashboardFactRow -Label "Bot PID" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "bot") -PropertyName "pid")) + "") New-VoidBotDashboardFactRow -Label "Bot log" -ValueHtml $(if (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "bot") -PropertyName "log") { New-VoidBotDashboardFileLink -Path ([string](Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "bot") -PropertyName "log")) } else { "missing" }) New-VoidBotDashboardFactRow -Label "Worker PID" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "worker") -PropertyName "pid")) + "") New-VoidBotDashboardFactRow -Label "Worker log" -ValueHtml $(if (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "worker") -PropertyName "log") { New-VoidBotDashboardFileLink -Path ([string](Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "worker") -PropertyName "log")) } else { "missing" }) New-VoidBotDashboardFactRow -Label "Completed" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "completedAt")) ) $watchdogRows = @( New-VoidBotDashboardFactRow -Label "Status file" -ValueHtml (New-VoidBotDashboardFileLink -Path $watchdogPath) New-VoidBotDashboardFactRow -Label "Run status" -ValueHtml (New-VoidBotDashboardBadge -State $watchdogState -Label $watchdogState) New-VoidBotDashboardFactRow -Label "Current step" -ValueHtml ("" + (Escape-VoidBotDashboardHtml $watchdogStep) + "") New-VoidBotDashboardFactRow -Label "Started" -ValueHtml (Escape-VoidBotDashboardHtml $watchdogStartedAt) New-VoidBotDashboardFactRow -Label "Elapsed" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardElapsedTime -StartedAt $watchdogStartedAt)) New-VoidBotDashboardFactRow -Label "Completed" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "completedAt")) New-VoidBotDashboardFactRow -Label "Last report status" -ValueHtml (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "reportStatus")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "reportStatus"))) New-VoidBotDashboardFactRow -Label "Notification" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "lastNotificationReason")) New-VoidBotDashboardFactRow -Label "Last run error" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "lastRunError")) New-VoidBotDashboardFactRow -Label "Watchdog log" -ValueHtml $(if (Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "logPath") { New-VoidBotDashboardFileLink -Path ([string](Get-VoidBotDashboardProperty -InputObject $watchdog -PropertyName "logPath")) } else { "missing" }) ) $offsiteRows = @( New-VoidBotDashboardFactRow -Label "Status file" -ValueHtml (New-VoidBotDashboardFileLink -Path $offsitePath) New-VoidBotDashboardFactRow -Label "Status" -ValueHtml (New-VoidBotDashboardBadge -State $offsiteState -Label $offsiteState) New-VoidBotDashboardFactRow -Label "Started" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "startedAt")) New-VoidBotDashboardFactRow -Label "Completed" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "completedAt")) New-VoidBotDashboardFactRow -Label "Backup directory" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "backupDirectory")) New-VoidBotDashboardFactRow -Label "Archive path" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "archivePath")) New-VoidBotDashboardFactRow -Label "Remote file" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "remoteFile") -PropertyName "path")) New-VoidBotDashboardFactRow -Label "Error" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "error")) New-VoidBotDashboardFactRow -Label "Offsite log" -ValueHtml $(if (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "logPath") { New-VoidBotDashboardFileLink -Path ([string](Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "logPath")) } else { New-VoidBotDashboardFileLink -Path (Join-Path $logsDir "offsite-backup.log") }) ) $fileRows = @( New-VoidBotDashboardFactRow -Label "Dashboard HTML" -ValueHtml (New-VoidBotDashboardFileLink -Path $dashboardPath) New-VoidBotDashboardFactRow -Label "Dashboard snapshot" -ValueHtml (New-VoidBotDashboardFileLink -Path $snapshotPath) New-VoidBotDashboardFactRow -Label "Operations report" -ValueHtml (New-VoidBotDashboardFileLink -Path $operationsPath) New-VoidBotDashboardFactRow -Label "Operations log" -ValueHtml (New-VoidBotDashboardFileLink -Path (Join-Path $logsDir "operations-watchdog.log")) New-VoidBotDashboardFactRow -Label "Repo root" -ValueHtml ("" + (Escape-VoidBotDashboardHtml $RepoRoot) + "") New-VoidBotDashboardFactRow -Label "Storage root" -ValueHtml ("" + (Escape-VoidBotDashboardHtml $StorageRoot) + "") ) $checkRows = @() foreach ($check in @((Get-VoidBotDashboardProperty -InputObject $operations -PropertyName "checks"))) { $checkRows += "" + (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $check -PropertyName "status")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $check -PropertyName "status"))) + "" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $check -PropertyName "name")) + "" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $check -PropertyName "detail")) + "" } if ($checkRows.Count -eq 0) { $checkRows = @("No watchdog checks have been written yet.") } $checksSection = @"

Watchdog Checks

$($checkRows -join [Environment]::NewLine)
StatusCheckDetail
"@ $snapshot = [ordered]@{ generatedAt = (Get-Date).ToString("o") dashboardPath = $dashboardPath repoRoot = $RepoRoot storageRoot = $StorageRoot states = [ordered]@{ operations = $operationsState runtime = $runtimeState watchdog = $watchdogState offsite = $offsiteState } files = [ordered]@{ runtime = $runtimePath operations = $operationsPath watchdog = $watchdogPath offsite = $offsitePath operationsLog = Join-Path $logsDir "operations-watchdog.log" offsiteLog = Join-Path $logsDir "offsite-backup.log" } } $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $snapshotPath -Encoding utf8 $html = @" VoidBot Operations Dashboard
VoidBot Operations Dashboard

Status files, but with fewer lies.

This page is regenerated from the live JSON status files under $(Escape-VoidBotDashboardHtml $statusDir) and refreshes itself every 10 seconds. If a hidden task hangs, it should stay frozen here with the last step it managed to confess.

Generated $(Escape-VoidBotDashboardHtml (Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz"))
$cardsHtml
$(New-VoidBotDashboardFactSection -Title "Runtime Stack" -Rows $runtimeRows) $(New-VoidBotDashboardFactSection -Title "Watchdog Run" -Rows $watchdogRows) $(New-VoidBotDashboardFactSection -Title "Offsite Backup" -Rows $offsiteRows) $(New-VoidBotDashboardFactSection -Title "Useful Files" -Rows $fileRows)
$checksSection
"@ Set-Content -LiteralPath $dashboardPath -Value $html -Encoding utf8 return [PSCustomObject]@{ DashboardPath = $dashboardPath SnapshotPath = $snapshotPath } }