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)
"@
}
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
Status Check Detail
$($checkRows -join [Environment]::NewLine)
"@
$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"))
$(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
}
}