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 Write-VoidBotDashboardUtf8File { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Content ) $directory = Split-Path -Parent $Path if (-not [string]::IsNullOrWhiteSpace($directory)) { New-Item -ItemType Directory -Force -Path $directory | Out-Null } $encoding = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::WriteAllText($Path, $Content, $encoding) } 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 Read-VoidBotDashboardDotEnv { param( [Parameter(Mandatory = $true)] [string] $Path ) $values = @{} if (-not (Test-Path -LiteralPath $Path)) { return $values } foreach ($line in Get-Content -LiteralPath $Path) { if ([string]::IsNullOrWhiteSpace($line)) { continue } if ($line.TrimStart().StartsWith("#")) { continue } $separatorIndex = $line.IndexOf("=") if ($separatorIndex -lt 1) { continue } $key = $line.Substring(0, $separatorIndex).Trim() $value = $line.Substring($separatorIndex + 1) $values[$key] = $value } return $values } function Resolve-VoidBotDashboardConfigPath { param( [Parameter(Mandatory = $true)] [string] $RepoRoot, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $Value, [Parameter(Mandatory = $true)] [string] $Fallback ) $raw = if ([string]::IsNullOrWhiteSpace($Value)) { $Fallback } else { $Value } return [System.IO.Path]::GetFullPath((Join-Path $RepoRoot $raw)) } function Format-VoidBotDashboardBytes { param( [Parameter(Mandatory = $false)] [AllowNull()] [long] $Bytes ) if ($null -eq $Bytes -or $Bytes -lt 0) { return "missing" } $units = @("B", "KB", "MB", "GB", "TB") $value = [double]$Bytes $unitIndex = 0 while ($value -ge 1024 -and $unitIndex -lt ($units.Count - 1)) { $value /= 1024 $unitIndex += 1 } $format = if ($unitIndex -eq 0) { "{0:0} {1}" } else { "{0:0.0} {1}" } return $format -f $value, $units[$unitIndex] } function Get-VoidBotDashboardFileSummary { param( [Parameter(Mandatory = $true)] [string] $Path ) if (-not (Test-Path -LiteralPath $Path)) { return [PSCustomObject]@{ exists = $false path = $Path size = $null sizeText = "missing" updatedAt = $null updatedRelative = "missing" } } $item = Get-Item -LiteralPath $Path $updatedAt = $item.LastWriteTime.ToString("o") return [PSCustomObject]@{ exists = $true path = $Path size = $item.Length sizeText = Format-VoidBotDashboardBytes -Bytes $item.Length updatedAt = $updatedAt updatedRelative = Get-VoidBotDashboardRelativeTime -Timestamp $updatedAt } } function New-VoidBotDashboardListHtml { param( [Parameter(Mandatory = $false)] [AllowNull()] [string[]] $Items, [Parameter(Mandatory = $false)] [string] $EmptyText = "Nothing here yet." ) if ($null -eq $Items -or $Items.Count -eq 0) { return "$([System.Net.WebUtility]::HtmlEncode($EmptyText))" } return "
" + (($Items | ForEach-Object { "
$_
" }) -join "") + "
" } function Invoke-VoidBotDashboardJsonRequest { param( [Parameter(Mandatory = $true)] [string] $Url, [Parameter(Mandatory = $false)] [ValidateSet("Get", "Post")] [string] $Method = "Get", [Parameter(Mandatory = $false)] [AllowNull()] $Body ) if ($PSBoundParameters.ContainsKey("Body")) { $payload = $Body | ConvertTo-Json -Depth 8 -Compress return Invoke-RestMethod -Method $Method -Uri $Url -ContentType "application/json" -Body $payload } return Invoke-RestMethod -Method $Method -Uri $Url } function Get-VoidBotDashboardPostgresDiagnostics { param( [Parameter(Mandatory = $true)] [string] $DatabaseDsn, [Parameter(Mandatory = $false)] [AllowNull()] $Runtime ) $result = [ordered]@{ state = "missing" detail = "Postgres has not checked in yet." jobsTotal = $null jobsByState = [ordered]@{} auditEvents = $null interactionMemoryEvents = $null providerRuns = $null toolInvocations = $null latestJobAt = $null latestAuditAt = $null latestInteractionAt = $null latestProviderRunAt = $null latestToolInvocationAt = $null } $runtimePostgres = if ($null -ne $Runtime) { Get-VoidBotDashboardProperty -InputObject $Runtime -PropertyName "postgres" } else { $null } if ($null -eq $runtimePostgres) { return [PSCustomObject]$result } if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { $result.state = "warning" $result.detail = "Docker is unavailable, so live Postgres counts are hidden." return [PSCustomObject]$result } $sql = @' select json_build_object( 'jobsTotal', (select count(*) from jobs), 'jobsByState', coalesce((select json_object_agg(state, job_count) from (select state, count(*) as job_count from jobs group by state) s), '{}'::json), 'auditEvents', (select count(*) from audit_events), 'interactionMemoryEvents', (select count(*) from interaction_memory_events), 'providerRuns', (select count(*) from provider_runs), 'toolInvocations', (select count(*) from tool_invocations), 'latestJobAt', (select max(updated_at)::text from jobs), 'latestAuditAt', (select max(event_timestamp)::text from audit_events), 'latestInteractionAt', (select max(event_timestamp)::text from interaction_memory_events), 'latestProviderRunAt', (select max(created_at)::text from provider_runs), 'latestToolInvocationAt', (select max(created_at)::text from tool_invocations) ); '@ try { $json = & docker exec voidbot-postgres psql -U voidbot -d voidbot -t -A -c $sql 2>$null if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($json)) { throw "psql returned no data." } $parsed = $json.Trim() | ConvertFrom-Json $result.state = "healthy" $result.detail = "Live Postgres counts loaded." $result.jobsTotal = $parsed.jobsTotal $result.jobsByState = [ordered]@{} if ($null -ne $parsed.jobsByState) { foreach ($property in $parsed.jobsByState.PSObject.Properties) { $result.jobsByState[$property.Name] = $property.Value } } $result.auditEvents = $parsed.auditEvents $result.interactionMemoryEvents = $parsed.interactionMemoryEvents $result.providerRuns = $parsed.providerRuns $result.toolInvocations = $parsed.toolInvocations $result.latestJobAt = $parsed.latestJobAt $result.latestAuditAt = $parsed.latestAuditAt $result.latestInteractionAt = $parsed.latestInteractionAt $result.latestProviderRunAt = $parsed.latestProviderRunAt $result.latestToolInvocationAt = $parsed.latestToolInvocationAt } catch { $result.state = "warning" $result.detail = "Live Postgres query failed: $($_.Exception.Message)" } return [PSCustomObject]$result } function Get-VoidBotDashboardQdrantDiagnostics { param( [Parameter(Mandatory = $true)] [string] $QdrantUrl, [Parameter(Mandatory = $true)] [string] $HistoryCollection, [Parameter(Mandatory = $true)] [string] $SourceCollection ) $baseUrl = $QdrantUrl.TrimEnd("/") $result = [ordered]@{ state = "missing" detail = "Qdrant has not checked in yet." history = $null source = $null topRepos = @() topLanguages = @() } try { $historyInfo = Invoke-VoidBotDashboardJsonRequest -Url "$baseUrl/collections/$HistoryCollection" $sourceInfo = Invoke-VoidBotDashboardJsonRequest -Url "$baseUrl/collections/$SourceCollection" $repoPersonat = Invoke-VoidBotDashboardJsonRequest -Method Post -Url "$baseUrl/collections/$SourceCollection/facet" -Body @{ key = "repoName" limit = 8 } $languageFacet = Invoke-VoidBotDashboardJsonRequest -Method Post -Url "$baseUrl/collections/$SourceCollection/facet" -Body @{ key = "language" limit = 6 } $result.state = "healthy" $result.detail = "Live Qdrant collection stats loaded." $result.history = [PSCustomObject]@{ name = $HistoryCollection pointsCount = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $historyInfo -PropertyName "result") -PropertyName "points_count" indexedVectorsCount = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $historyInfo -PropertyName "result") -PropertyName "indexed_vectors_count" status = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $historyInfo -PropertyName "result") -PropertyName "status" segmentsCount = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $historyInfo -PropertyName "result") -PropertyName "segments_count" } $result.source = [PSCustomObject]@{ name = $SourceCollection pointsCount = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $sourceInfo -PropertyName "result") -PropertyName "points_count" indexedVectorsCount = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $sourceInfo -PropertyName "result") -PropertyName "indexed_vectors_count" status = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $sourceInfo -PropertyName "result") -PropertyName "status" segmentsCount = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $sourceInfo -PropertyName "result") -PropertyName "segments_count" } $result.topRepos = @((Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $repoPersonat -PropertyName "result") -PropertyName "hits")) $result.topLanguages = @((Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $languageFacet -PropertyName "result") -PropertyName "hits")) } catch { $result.state = "warning" $result.detail = "Live Qdrant query failed: $($_.Exception.Message)" } return [PSCustomObject]$result } function Get-VoidBotDashboardSourceHookDiagnostics { param( [Parameter(Mandatory = $true)] [string] $SourceHookStatusDir ) $result = [ordered]@{ state = "missing" detail = "No source hook statuses yet." total = 0 completed = 0 running = 0 failed = 0 recentHooks = @() latestActivityAt = $null } if (-not (Test-Path -LiteralPath $SourceHookStatusDir)) { return [PSCustomObject]$result } $hookFiles = @(Get-ChildItem -LiteralPath $SourceHookStatusDir -File -Filter "*.json") if ($hookFiles.Count -eq 0) { return [PSCustomObject]$result } $hooks = foreach ($file in $hookFiles) { $status = Read-VoidBotDashboardJson -Path $file.FullName $state = [string](Get-VoidBotDashboardProperty -InputObject $status -PropertyName "state") $activityAt = @( [string](Get-VoidBotDashboardProperty -InputObject $status -PropertyName "completedAt") [string](Get-VoidBotDashboardProperty -InputObject $status -PropertyName "lastSkippedAt") [string](Get-VoidBotDashboardProperty -InputObject $status -PropertyName "startedAt") $file.LastWriteTime.ToString("o") ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1 [PSCustomObject]@{ repoName = [string](Get-VoidBotDashboardProperty -InputObject $status -PropertyName "repoName") state = if ([string]::IsNullOrWhiteSpace($state)) { "unknown" } else { $state } activityAt = $activityAt logPath = [string](Get-VoidBotDashboardProperty -InputObject $status -PropertyName "logPath") note = @( [string](Get-VoidBotDashboardProperty -InputObject $status -PropertyName "lastSkipReason") [string](Get-VoidBotDashboardProperty -InputObject $status -PropertyName "repoPath") ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1 } } $orderedHooks = $hooks | Sort-Object { try { [DateTimeOffset]::Parse($_.activityAt) } catch { [DateTimeOffset]::MinValue } } -Descending $result.state = if (@($orderedHooks | Where-Object { $_.state -eq "failed" }).Count -gt 0) { "warning" } elseif (@($orderedHooks | Where-Object { $_.state -eq "running" }).Count -gt 0) { "running" } else { "healthy" } $result.detail = "Source hook statuses loaded." $result.total = $orderedHooks.Count $result.completed = @($orderedHooks | Where-Object { $_.state -eq "completed" }).Count $result.running = @($orderedHooks | Where-Object { $_.state -eq "running" }).Count $result.failed = @($orderedHooks | Where-Object { $_.state -eq "failed" }).Count $result.recentHooks = @($orderedHooks | Select-Object -First 6) $result.latestActivityAt = $orderedHooks[0].activityAt return [PSCustomObject]$result } function Get-VoidBotDashboardMcpUsageDiagnostics { param( [Parameter(Mandatory = $true)] [string] $ArtifactsRoot ) $result = [ordered]@{ state = "missing" detail = "No Codex trace artifacts yet." traceCount = 0 scannedTraceCount = 0 totalMcpCalls = 0 averageDurationMs = $null latestTraceAt = $null topTools = @() recentTraces = @() } if (-not (Test-Path -LiteralPath $ArtifactsRoot)) { return [PSCustomObject]$result } $traceFiles = @(Get-ChildItem -LiteralPath $ArtifactsRoot -Recurse -File -Filter "codex-turn-*-trace.json" | Sort-Object LastWriteTime -Descending) if ($traceFiles.Count -eq 0) { return [PSCustomObject]$result } $recentFiles = @($traceFiles | Select-Object -First 12) $toolCounts = @{} $recentTraces = @() $durations = @() $totalMcpCalls = 0 foreach ($file in $recentFiles) { $trace = Read-VoidBotDashboardJson -Path $file.FullName $events = @((Get-VoidBotDashboardProperty -InputObject $trace -PropertyName "events")) $mcpEvents = @($events | Where-Object { [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "kind") -eq "mcp_tool_completed" }) foreach ($event in $mcpEvents) { $toolName = [string](Get-VoidBotDashboardProperty -InputObject $event -PropertyName "tool") if ([string]::IsNullOrWhiteSpace($toolName)) { continue } if (-not $toolCounts.ContainsKey($toolName)) { $toolCounts[$toolName] = 0 } $toolCounts[$toolName] += 1 } $durationMs = Get-VoidBotDashboardProperty -InputObject $trace -PropertyName "durationMs" if ($durationMs -is [ValueType]) { $durations += [double]$durationMs } $tools = @($mcpEvents | ForEach-Object { [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "tool") } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique) $totalMcpCalls += $mcpEvents.Count $recentTraces += [PSCustomObject]@{ traceFile = $file.FullName jobId = $file.Directory.Name startedAt = [string](Get-VoidBotDashboardProperty -InputObject $trace -PropertyName "startedAt") durationMs = $durationMs mcpCalls = $mcpEvents.Count tools = $tools } } $topTools = $toolCounts.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 6 | ForEach-Object { [PSCustomObject]@{ name = $_.Key count = $_.Value } } $result.state = if ($totalMcpCalls -gt 0) { "healthy" } else { "neutral" } $result.detail = "Recent Codex trace usage loaded." $result.traceCount = $traceFiles.Count $result.scannedTraceCount = $recentFiles.Count $result.totalMcpCalls = $totalMcpCalls $result.averageDurationMs = if ($durations.Count -gt 0) { [Math]::Round((($durations | Measure-Object -Average).Average), 0) } else { $null } $result.latestTraceAt = $recentTraces[0].startedAt $result.topTools = @($topTools) $result.recentTraces = @($recentTraces) return [PSCustomObject]$result } function Get-VoidBotDashboardJobDiagnostics { param( [Parameter(Mandatory = $true)] [string] $DatabaseDsn, [Parameter(Mandatory = $false)] [AllowNull()] $Runtime, [Parameter(Mandatory = $true)] [string] $ArtifactsRoot ) $result = [ordered]@{ state = "missing" detail = "No job diagnostics yet." current = @() recent = @() counts = [ordered]@{} } $runtimePostgres = if ($null -ne $Runtime) { Get-VoidBotDashboardProperty -InputObject $Runtime -PropertyName "postgres" } else { $null } if ($null -eq $runtimePostgres) { return [PSCustomObject]$result } if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { $result.state = "warning" $result.detail = "Docker is unavailable, so live job details are hidden." return [PSCustomObject]$result } $sql = @' select json_build_object( 'current', coalesce((select json_agg(row_to_json(t)) from ( select id::text, state, provider_name, command_name, requester_discord_id, created_at::text, updated_at::text, request_message_id, coalesce(job_json->>'summary', '') as summary, coalesce(job_json->>'error', '') as error from jobs where state in ('queued','approved','running','awaiting_approval','awaiting_post_approval') order by updated_at desc limit 10 ) t), '[]'::json), 'recent', coalesce((select json_agg(row_to_json(t)) from ( select id::text, state, provider_name, command_name, requester_discord_id, created_at::text, updated_at::text, request_message_id, coalesce(job_json->>'summary', '') as summary, coalesce(job_json->>'error', '') as error from jobs order by updated_at desc limit 12 ) t), '[]'::json), 'counts', coalesce((select json_object_agg(state, job_count) from ( select state, count(*) as job_count from jobs group by state ) s), '{}'::json) ); '@ try { $json = & docker exec voidbot-postgres psql -U voidbot -d voidbot -t -A -c $sql 2>$null if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($json)) { throw "psql returned no data." } $parsed = $json.Trim() | ConvertFrom-Json function Expand-JobRow { param( [Parameter(Mandatory = $true)] $Job ) $jobId = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "id") $artifactDir = Join-Path $ArtifactsRoot $jobId $traceFile = $null $stdoutFile = $null $stderrFile = $null $debugFile = $null $handoffFile = $null $requestFile = $null $ragTranscriptFile = $null if (Test-Path -LiteralPath $artifactDir) { $traceFile = @(Get-ChildItem -LiteralPath $artifactDir -File -Filter "codex-turn-*-trace.json" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.FullName }) | Select-Object -First 1 $stdoutFile = @(Get-ChildItem -LiteralPath $artifactDir -File -Filter "codex-turn-*-stdout.txt" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.FullName }) | Select-Object -First 1 $stderrFile = @(Get-ChildItem -LiteralPath $artifactDir -File -Filter "codex-turn-*-stderr.txt" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.FullName }) | Select-Object -First 1 $debugFile = @(Get-ChildItem -LiteralPath $artifactDir -File -Filter "codex-turn-*-debug.md" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.FullName }) | Select-Object -First 1 $handoffFile = if (Test-Path -LiteralPath (Join-Path $artifactDir "handoff.md")) { Join-Path $artifactDir "handoff.md" } else { $null } $requestFile = if (Test-Path -LiteralPath (Join-Path $artifactDir "request.json")) { Join-Path $artifactDir "request.json" } else { $null } $ragTranscriptFile = if (Test-Path -LiteralPath (Join-Path $artifactDir "rag-tool-transcript.md")) { Join-Path $artifactDir "rag-tool-transcript.md" } else { $null } } return [PSCustomObject]@{ id = $jobId state = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "state") providerName = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "provider_name") commandName = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "command_name") requesterDiscordId = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "requester_discord_id") createdAt = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "created_at") updatedAt = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "updated_at") requestMessageId = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "request_message_id") summary = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "summary") error = [string](Get-VoidBotDashboardProperty -InputObject $Job -PropertyName "error") artifactDir = $(if (Test-Path -LiteralPath $artifactDir) { $artifactDir } else { $null }) traceFile = $traceFile stdoutFile = $stdoutFile stderrFile = $stderrFile debugFile = $debugFile handoffFile = $handoffFile requestFile = $requestFile ragTranscriptFile = $ragTranscriptFile } } $result.state = "healthy" $result.detail = "Live job history loaded." $result.current = @((Get-VoidBotDashboardProperty -InputObject $parsed -PropertyName "current") | ForEach-Object { Expand-JobRow -Job $_ }) $result.recent = @((Get-VoidBotDashboardProperty -InputObject $parsed -PropertyName "recent") | ForEach-Object { Expand-JobRow -Job $_ }) $result.counts = [ordered]@{} if ($null -ne $parsed.counts) { foreach ($property in $parsed.counts.PSObject.Properties) { $result.counts[$property.Name] = $property.Value } } } catch { $result.state = "warning" $result.detail = "Live job query failed: $($_.Exception.Message)" } return [PSCustomObject]$result } function Update-VoidBotOperationsDashboard { param( [Parameter(Mandatory = $true)] [string] $RepoRoot, [Parameter(Mandatory = $true)] [string] $StorageRoot ) $statusDir = Join-Path $StorageRoot "status" $logsDir = Join-Path $StorageRoot "logs" $artifactsRoot = Join-Path $StorageRoot "artifacts" $dashboardPath = Join-Path $statusDir "operations-dashboard.html" $snapshotPath = Join-Path $statusDir "operations-dashboard.json" $sourceHookStatusDir = Join-Path $statusDir "source-hooks" New-Item -ItemType Directory -Force -Path $statusDir | Out-Null $envPath = Join-Path $RepoRoot ".env" $config = Read-VoidBotDashboardDotEnv -Path $envPath $databaseDsn = if ($config.ContainsKey("DATABASE_DSN")) { $config["DATABASE_DSN"] } else { "postgres://voidbot:voidbot@localhost:5432/voidbot" } $vectorStoreKind = if ($config.ContainsKey("VECTOR_STORE_KIND")) { $config["VECTOR_STORE_KIND"] } else { "qdrant" } $stateStorageBackend = if ($config.ContainsKey("STATE_STORAGE_BACKEND")) { $config["STATE_STORAGE_BACKEND"] } else { "postgres" } $qdrantUrl = if ($config.ContainsKey("QDRANT_URL")) { $config["QDRANT_URL"] } else { "http://127.0.0.1:6333" } $qdrantHistoryCollection = if ($config.ContainsKey("QDRANT_HISTORY_COLLECTION")) { $config["QDRANT_HISTORY_COLLECTION"] } else { "voidbot_discord_history_chunks" } $qdrantSourceCollection = if ($config.ContainsKey("QDRANT_SOURCE_COLLECTION")) { $config["QDRANT_SOURCE_COLLECTION"] } else { "voidbot_repository_source_chunks" } $historyArchivePath = Resolve-VoidBotDashboardConfigPath -RepoRoot $RepoRoot -Value $config["RAG_ARCHIVE_PATH"] -Fallback ".voidbot/rag/messages.json" $sourceArchivePath = Resolve-VoidBotDashboardConfigPath -RepoRoot $RepoRoot -Value $config["RAG_SOURCE_ARCHIVE_PATH"] -Fallback ".voidbot/rag/source-documents.json" $importStatePath = Resolve-VoidBotDashboardConfigPath -RepoRoot $RepoRoot -Value $config["RAG_IMPORT_STATE_PATH"] -Fallback ".voidbot/rag/import-state.json" $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 $historyArchive = Get-VoidBotDashboardFileSummary -Path $historyArchivePath $sourceArchive = Get-VoidBotDashboardFileSummary -Path $sourceArchivePath $importState = Get-VoidBotDashboardFileSummary -Path $importStatePath $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" } } $postgresDiagnostics = if ($stateStorageBackend -eq "postgres") { Get-VoidBotDashboardPostgresDiagnostics -DatabaseDsn $databaseDsn -Runtime $runtime } else { [PSCustomObject]@{ state = "neutral" detail = "State storage backend is $stateStorageBackend." jobsTotal = $null jobsByState = [ordered]@{} auditEvents = $null interactionMemoryEvents = $null providerRuns = $null toolInvocations = $null latestJobAt = $null latestAuditAt = $null latestInteractionAt = $null latestProviderRunAt = $null latestToolInvocationAt = $null } } $qdrantDiagnostics = if ($vectorStoreKind -eq "qdrant") { Get-VoidBotDashboardQdrantDiagnostics -QdrantUrl $qdrantUrl -HistoryCollection $qdrantHistoryCollection -SourceCollection $qdrantSourceCollection } else { [PSCustomObject]@{ state = "neutral" detail = "Vector backend is $vectorStoreKind." history = $null source = $null topRepos = @() topLanguages = @() } } $sourceHookDiagnostics = Get-VoidBotDashboardSourceHookDiagnostics -SourceHookStatusDir $sourceHookStatusDir $mcpDiagnostics = Get-VoidBotDashboardMcpUsageDiagnostics -ArtifactsRoot $artifactsRoot $jobDiagnostics = if ($stateStorageBackend -eq "postgres") { Get-VoidBotDashboardJobDiagnostics -DatabaseDsn $databaseDsn -Runtime $runtime -ArtifactsRoot $artifactsRoot } else { [PSCustomObject]@{ state = "neutral" detail = "State storage backend is $stateStorageBackend." current = @() recent = @() counts = [ordered]@{} } } $ingestState = if ($sourceHookDiagnostics.failed -gt 0) { "warning" } elseif ($sourceHookDiagnostics.running -gt 0) { "running" } elseif ($historyArchive.exists -or $sourceArchive.exists) { "healthy" } else { "missing" } $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." } $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 } $stateStorageSubtitle = if ($postgresDiagnostics.jobsTotal -is [ValueType]) { "{0} jobs, {1} audit events, {2} memory events" -f ` $postgresDiagnostics.jobsTotal, ` (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "auditEvents"), ` (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "interactionMemoryEvents") } else { [string](Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "detail") } $vectorSubtitle = if ($null -ne (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "history") -and $null -ne (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "source")) { "{0} history points, {1} source points" -f ` (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "history") -PropertyName "pointsCount"), ` (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "source") -PropertyName "pointsCount") } else { [string](Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "detail") } $latestHook = @((Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "recentHooks")) | Select-Object -First 1 $ingestSubtitle = if ($latestHook) { "History archive {0}; latest source hook {1} {2}" -f ` $historyArchive.updatedRelative, ` ([string](Get-VoidBotDashboardProperty -InputObject $latestHook -PropertyName "repoName")), ` (Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $latestHook -PropertyName "activityAt"))) } elseif ($historyArchive.exists -or $sourceArchive.exists) { "Archives are present. Source hooks have not reported in yet." } else { "No archive or hook signal yet." } $mcpSubtitle = if ((Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "traceCount") -gt 0) { "{0} MCP calls across {1} recent traces" -f ` (Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "totalMcpCalls"), ` (Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "scannedTraceCount") } else { [string](Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "detail") } $jobCounts = Get-VoidBotDashboardProperty -InputObject $jobDiagnostics -PropertyName "counts" $currentJobs = @((Get-VoidBotDashboardProperty -InputObject $jobDiagnostics -PropertyName "current")) $recentJobs = @((Get-VoidBotDashboardProperty -InputObject $jobDiagnostics -PropertyName "recent")) $jobSubtitle = if ($recentJobs.Count -gt 0) { "{0} active, {1} completed, {2} failed" -f ` $currentJobs.Count, ` $(if ($jobCounts.Contains("completed")) { $jobCounts["completed"] } else { 0 }), ` $(if ($jobCounts.Contains("failed")) { $jobCounts["failed"] } else { 0 }) } else { [string](Get-VoidBotDashboardProperty -InputObject $jobDiagnostics -PropertyName "detail") } $offsiteSubtitle = if ($offsiteState -eq "completed") { "Latest offsite sync finished." } elseif ($offsiteState -eq "missing") { "No offsite sync status yet." } else { "Current phase: {0}" -f $offsiteState } $jobStateItems = @() foreach ($entry in (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "jobsByState").GetEnumerator() | Sort-Object Name) { $jobStateItems += "$([System.Net.WebUtility]::HtmlEncode($entry.Key)) $([System.Net.WebUtility]::HtmlEncode([string]$entry.Value))" } $topRepoItems = @((Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "topRepos") | ForEach-Object { "$([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'value'))) $([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'count')))" }) $topLanguageItems = @((Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "topLanguages") | ForEach-Object { "$([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'value'))) $([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'count')))" }) $recentHookItems = @((Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "recentHooks") | ForEach-Object { $repoName = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "repoName") $logPath = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "logPath") $repoHtml = if ([string]::IsNullOrWhiteSpace($logPath)) { "$([System.Net.WebUtility]::HtmlEncode($repoName))" } else { New-VoidBotDashboardFileLink -Path $logPath -Label $repoName } $note = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "note") $noteHtml = if ([string]::IsNullOrWhiteSpace($note)) { "" } else { "
$([System.Net.WebUtility]::HtmlEncode($note))
" } $repoHtml + " " + (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "state")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "state"))) + " $([System.Net.WebUtility]::HtmlEncode((Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'activityAt')))))" + $noteHtml }) $topToolItems = @((Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "topTools") | ForEach-Object { "$([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'name'))) $([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'count')))" }) $recentTraceItems = @((Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "recentTraces") | ForEach-Object { $traceFile = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "traceFile") $jobId = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "jobId") $traceLink = if ([string]::IsNullOrWhiteSpace($traceFile)) { "$([System.Net.WebUtility]::HtmlEncode($jobId))" } else { New-VoidBotDashboardFileLink -Path $traceFile -Label $jobId } $durationMs = Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "durationMs" $durationText = if ($durationMs -is [ValueType]) { "{0:N1}s" -f ([double]$durationMs / 1000) } else { "unknown" } $toolSummary = @((Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "tools")) -join ", " $toolHtml = if ([string]::IsNullOrWhiteSpace($toolSummary)) { "No MCP calls." } else { "
$([System.Net.WebUtility]::HtmlEncode($toolSummary))
" } $traceLink + " $([System.Net.WebUtility]::HtmlEncode((Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'startedAt'))))) / $([System.Net.WebUtility]::HtmlEncode($durationText)) / $([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'mcpCalls'))) calls" + $toolHtml }) $currentJobItems = @($currentJobs | ForEach-Object { $jobId = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "id") $artifactDir = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "artifactDir") $jobLink = if ([string]::IsNullOrWhiteSpace($artifactDir)) { "$([System.Net.WebUtility]::HtmlEncode($jobId))" } else { New-VoidBotDashboardFileLink -Path $artifactDir -Label $jobId } $links = @() foreach ($entry in @( @{ name = "request"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "requestFile") } @{ name = "trace"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "traceFile") } @{ name = "stdout"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "stdoutFile") } @{ name = "stderr"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "stderrFile") } @{ name = "debug"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "debugFile") } @{ name = "handoff"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "handoffFile") } )) { if (-not [string]::IsNullOrWhiteSpace($entry.path)) { $links += New-VoidBotDashboardFileLink -Path $entry.path -Label $entry.name } } $linksHtml = if ($links.Count -gt 0) { "
" + ($links -join " · ") + "
" } else { "" } $summary = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "summary") $error = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "error") $note = if (-not [string]::IsNullOrWhiteSpace($error)) { $error } elseif (-not [string]::IsNullOrWhiteSpace($summary)) { $summary } else { "" } $noteHtml = if ([string]::IsNullOrWhiteSpace($note)) { "" } else { "
$([System.Net.WebUtility]::HtmlEncode($note))
" } $jobLink + " " + (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "state")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "state"))) + " $([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'providerName'))) / $([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'commandName'))) / $([System.Net.WebUtility]::HtmlEncode((Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'updatedAt')))))" + $linksHtml + $noteHtml }) $recentJobItems = @($recentJobs | ForEach-Object { $jobId = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "id") $artifactDir = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "artifactDir") $jobLink = if ([string]::IsNullOrWhiteSpace($artifactDir)) { "$([System.Net.WebUtility]::HtmlEncode($jobId))" } else { New-VoidBotDashboardFileLink -Path $artifactDir -Label $jobId } $links = @() foreach ($entry in @( @{ name = "trace"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "traceFile") } @{ name = "stdout"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "stdoutFile") } @{ name = "stderr"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "stderrFile") } @{ name = "debug"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "debugFile") } @{ name = "handoff"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "handoffFile") } @{ name = "request"; path = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "requestFile") } )) { if (-not [string]::IsNullOrWhiteSpace($entry.path)) { $links += New-VoidBotDashboardFileLink -Path $entry.path -Label $entry.name } } $linksHtml = if ($links.Count -gt 0) { "
" + ($links -join " · ") + "
" } else { "" } $summary = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "summary") $error = [string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "error") $note = if (-not [string]::IsNullOrWhiteSpace($error)) { $error } elseif (-not [string]::IsNullOrWhiteSpace($summary)) { $summary } else { "" } $noteHtml = if ([string]::IsNullOrWhiteSpace($note)) { "" } else { "
$([System.Net.WebUtility]::HtmlEncode($note))
" } $jobLink + " " + (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "state")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName "state"))) + " $([System.Net.WebUtility]::HtmlEncode([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'providerName'))) / $([System.Net.WebUtility]::HtmlEncode((Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $_ -PropertyName 'updatedAt')))))" + $linksHtml + $noteHtml }) $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 "Runtime Stack" -State $runtimeState -Subtitle $runtimeSubtitle -Meta ("Updated " + (Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "completedAt")))) New-VoidBotDashboardCard -Title "State Storage" -State ([string](Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "state")) -Subtitle $stateStorageSubtitle -Meta ("Latest job " + (Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "latestJobAt")))) New-VoidBotDashboardCard -Title "Vector Store" -State ([string](Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "state")) -Subtitle $vectorSubtitle -Meta $qdrantUrl New-VoidBotDashboardCard -Title "Ingest & Sync" -State $ingestState -Subtitle $ingestSubtitle -Meta ("Source hooks: " + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "total"))) New-VoidBotDashboardCard -Title "MCP Usage" -State ([string](Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "state")) -Subtitle $mcpSubtitle -Meta ("Latest trace " + (Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "latestTraceAt")))) New-VoidBotDashboardCard -Title "Jobs" -State ([string](Get-VoidBotDashboardProperty -InputObject $jobDiagnostics -PropertyName "state")) -Subtitle $jobSubtitle -Meta ("Latest job " + (Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "latestJobAt")))) 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 "Started" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $runtime -PropertyName "startedAt")) 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")) ) $stateStorageRows = @( New-VoidBotDashboardFactRow -Label "Backend" -ValueHtml ("" + (Escape-VoidBotDashboardHtml $stateStorageBackend) + "") New-VoidBotDashboardFactRow -Label "State" -ValueHtml (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "state")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "state"))) New-VoidBotDashboardFactRow -Label "Detail" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "detail")) New-VoidBotDashboardFactRow -Label "Jobs total" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "jobsTotal")) + "") New-VoidBotDashboardFactRow -Label "Jobs by state" -ValueHtml (New-VoidBotDashboardListHtml -Items $jobStateItems -EmptyText "No jobs recorded.") New-VoidBotDashboardFactRow -Label "Audit events" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "auditEvents")) + "") New-VoidBotDashboardFactRow -Label "Memory events" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "interactionMemoryEvents")) + "") New-VoidBotDashboardFactRow -Label "Provider runs" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "providerRuns")) + "") New-VoidBotDashboardFactRow -Label "Tool invocations" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "toolInvocations")) + "") New-VoidBotDashboardFactRow -Label "Latest job" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "latestJobAt")) ) $vectorRows = @( New-VoidBotDashboardFactRow -Label "Backend" -ValueHtml ("" + (Escape-VoidBotDashboardHtml $vectorStoreKind) + "") New-VoidBotDashboardFactRow -Label "Qdrant URL" -ValueHtml ("" + (Escape-VoidBotDashboardHtml $qdrantUrl) + "") New-VoidBotDashboardFactRow -Label "State" -ValueHtml (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "state")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "state"))) New-VoidBotDashboardFactRow -Label "History collection" -ValueHtml ("" + (Escape-VoidBotDashboardHtml $qdrantHistoryCollection) + "") New-VoidBotDashboardFactRow -Label "History points" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "history") -PropertyName "pointsCount")) + "") New-VoidBotDashboardFactRow -Label "History indexed" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "history") -PropertyName "indexedVectorsCount")) + "") New-VoidBotDashboardFactRow -Label "Source collection" -ValueHtml ("" + (Escape-VoidBotDashboardHtml $qdrantSourceCollection) + "") New-VoidBotDashboardFactRow -Label "Source points" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "source") -PropertyName "pointsCount")) + "") New-VoidBotDashboardFactRow -Label "Source indexed" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "source") -PropertyName "indexedVectorsCount")) + "") New-VoidBotDashboardFactRow -Label "Top repos" -ValueHtml (New-VoidBotDashboardListHtml -Items $topRepoItems -EmptyText "No repo breakdown yet.") New-VoidBotDashboardFactRow -Label "Top languages" -ValueHtml (New-VoidBotDashboardListHtml -Items $topLanguageItems -EmptyText "No language breakdown yet.") ) $ingestRows = @( New-VoidBotDashboardFactRow -Label "History archive" -ValueHtml $(if ($historyArchive.exists) { (New-VoidBotDashboardFileLink -Path $historyArchive.path -Label "messages.json") + "
" + (Escape-VoidBotDashboardHtml ($historyArchive.sizeText + ", updated " + $historyArchive.updatedRelative)) + "
" } else { "missing" }) New-VoidBotDashboardFactRow -Label "Source archive" -ValueHtml $(if ($sourceArchive.exists) { (New-VoidBotDashboardFileLink -Path $sourceArchive.path -Label "source-documents.json") + "
" + (Escape-VoidBotDashboardHtml ($sourceArchive.sizeText + ", updated " + $sourceArchive.updatedRelative)) + "
" } else { "missing" }) New-VoidBotDashboardFactRow -Label "Import state" -ValueHtml $(if ($importState.exists) { (New-VoidBotDashboardFileLink -Path $importState.path -Label "import-state.json") + "
" + (Escape-VoidBotDashboardHtml ("updated " + $importState.updatedRelative)) + "
" } else { "missing" }) New-VoidBotDashboardFactRow -Label "Hook state" -ValueHtml (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "state")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "state"))) New-VoidBotDashboardFactRow -Label "Hook totals" -ValueHtml (Escape-VoidBotDashboardHtml ("total={0}, completed={1}, running={2}, failed={3}" -f (Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "total"), (Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "completed"), (Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "running"), (Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "failed"))) New-VoidBotDashboardFactRow -Label "Latest activity" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "latestActivityAt")) New-VoidBotDashboardFactRow -Label "Recent hooks" -ValueHtml (New-VoidBotDashboardListHtml -Items $recentHookItems -EmptyText "No source hook activity yet.") ) $mcpRows = @( New-VoidBotDashboardFactRow -Label "State" -ValueHtml (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "state")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "state"))) New-VoidBotDashboardFactRow -Label "Trace files" -ValueHtml (Escape-VoidBotDashboardHtml ("{0} total, {1} scanned" -f (Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "traceCount"), (Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "scannedTraceCount"))) New-VoidBotDashboardFactRow -Label "MCP calls" -ValueHtml ("" + (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "totalMcpCalls")) + "") New-VoidBotDashboardFactRow -Label "Avg duration" -ValueHtml (Escape-VoidBotDashboardHtml $(if ((Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "averageDurationMs") -is [ValueType]) { "{0:N1}s" -f ([double](Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "averageDurationMs") / 1000) } else { "missing" })) New-VoidBotDashboardFactRow -Label "Latest trace" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "latestTraceAt")) New-VoidBotDashboardFactRow -Label "Top tools" -ValueHtml (New-VoidBotDashboardListHtml -Items $topToolItems -EmptyText "No MCP tool calls yet.") New-VoidBotDashboardFactRow -Label "Recent traces" -ValueHtml (New-VoidBotDashboardListHtml -Items $recentTraceItems -EmptyText "No recent trace files yet.") ) $jobRows = @( New-VoidBotDashboardFactRow -Label "State" -ValueHtml (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject $jobDiagnostics -PropertyName "state")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $jobDiagnostics -PropertyName "state"))) New-VoidBotDashboardFactRow -Label "Active jobs" -ValueHtml ("" + (Escape-VoidBotDashboardHtml ([string]$currentJobs.Count)) + "") New-VoidBotDashboardFactRow -Label "Current jobs" -ValueHtml (New-VoidBotDashboardListHtml -Items $currentJobItems -EmptyText "No queued or running jobs.") New-VoidBotDashboardFactRow -Label "Recent history" -ValueHtml (New-VoidBotDashboardListHtml -Items $recentJobItems -EmptyText "No recent jobs recorded.") ) $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 "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 "Verification" -ValueHtml (New-VoidBotDashboardBadge -State ([string](Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "backupVerification") -PropertyName "status")) -Label ([string](Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "backupVerification") -PropertyName "status"))) 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 $(if (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "backupDirectory") { New-VoidBotDashboardFileLink -Path ([string](Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "backupDirectory")) -Label ([string](Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "backupLabel")) } else { "missing" }) New-VoidBotDashboardFactRow -Label "Archive path" -ValueHtml $(if (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "archivePath") { New-VoidBotDashboardFileLink -Path ([string](Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "archivePath")) -Label ([System.IO.Path]::GetFileName([string](Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "archivePath"))) } else { "missing" }) New-VoidBotDashboardFactRow -Label "Remote file" -ValueHtml (Escape-VoidBotDashboardHtml (Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "remoteFile") -PropertyName "path")) New-VoidBotDashboardFactRow -Label "Archive size" -ValueHtml (Escape-VoidBotDashboardHtml (Format-VoidBotDashboardBytes -Bytes (Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "archiveSize"))) 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 "History archive" -ValueHtml $(if ($historyArchive.exists) { New-VoidBotDashboardFileLink -Path $historyArchive.path -Label "messages.json" } else { "missing" }) New-VoidBotDashboardFactRow -Label "Source archive" -ValueHtml $(if ($sourceArchive.exists) { New-VoidBotDashboardFileLink -Path $sourceArchive.path -Label "source-documents.json" } else { "missing" }) New-VoidBotDashboardFactRow -Label "Import state" -ValueHtml $(if ($importState.exists) { New-VoidBotDashboardFileLink -Path $importState.path -Label "import-state.json" } else { "missing" }) 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 postgres = [string](Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "state") qdrant = [string](Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "state") ingest = $ingestState mcp = [string](Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "state") jobs = [string](Get-VoidBotDashboardProperty -InputObject $jobDiagnostics -PropertyName "state") watchdog = $watchdogState offsite = $offsiteState } diagnostics = [ordered]@{ postgres = [ordered]@{ jobsTotal = Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "jobsTotal" auditEvents = Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "auditEvents" interactionMemoryEvents = Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "interactionMemoryEvents" providerRuns = Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "providerRuns" toolInvocations = Get-VoidBotDashboardProperty -InputObject $postgresDiagnostics -PropertyName "toolInvocations" } qdrant = [ordered]@{ historyPoints = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "history") -PropertyName "pointsCount" sourcePoints = Get-VoidBotDashboardProperty -InputObject (Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "source") -PropertyName "pointsCount" topRepos = Get-VoidBotDashboardProperty -InputObject $qdrantDiagnostics -PropertyName "topRepos" } ingest = [ordered]@{ historyArchiveUpdatedAt = $historyArchive.updatedAt sourceArchiveUpdatedAt = $sourceArchive.updatedAt sourceHookCount = Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "total" latestSourceHookAt = Get-VoidBotDashboardProperty -InputObject $sourceHookDiagnostics -PropertyName "latestActivityAt" } mcp = [ordered]@{ traceCount = Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "traceCount" scannedTraceCount = Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "scannedTraceCount" totalMcpCalls = Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "totalMcpCalls" topTools = Get-VoidBotDashboardProperty -InputObject $mcpDiagnostics -PropertyName "topTools" } jobs = [ordered]@{ currentCount = $currentJobs.Count recentCount = $recentJobs.Count counts = Get-VoidBotDashboardProperty -InputObject $jobDiagnostics -PropertyName "counts" } } files = [ordered]@{ runtime = $runtimePath operations = $operationsPath watchdog = $watchdogPath offsite = $offsitePath operationsLog = Join-Path $logsDir "operations-watchdog.log" offsiteLog = Join-Path $logsDir "offsite-backup.log" historyArchive = $historyArchivePath sourceArchive = $sourceArchivePath importState = $importStatePath } } $snapshotJson = $snapshot | ConvertTo-Json -Depth 10 Write-VoidBotDashboardUtf8File -Path $snapshotPath -Content $snapshotJson $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 "State Storage" -Rows $stateStorageRows) $(New-VoidBotDashboardFactSection -Title "Vector Store" -Rows $vectorRows) $(New-VoidBotDashboardFactSection -Title "Ingest & Sync" -Rows $ingestRows) $(New-VoidBotDashboardFactSection -Title "MCP Usage" -Rows $mcpRows) $(New-VoidBotDashboardFactSection -Title "Jobs" -Rows $jobRows) $(New-VoidBotDashboardFactSection -Title "Watchdog Run" -Rows $watchdogRows) $(New-VoidBotDashboardFactSection -Title "Offsite Backup" -Rows $offsiteRows) $(New-VoidBotDashboardFactSection -Title "Useful Files" -Rows $fileRows)
$checksSection
"@ Write-VoidBotDashboardUtf8File -Path $dashboardPath -Content $html return [PSCustomObject]@{ DashboardPath = $dashboardPath SnapshotPath = $snapshotPath } }