diff --git a/scripts/voidbot-operations-dashboard-lib.ps1 b/scripts/voidbot-operations-dashboard-lib.ps1 index 96f0c91..8c050a4 100644 --- a/scripts/voidbot-operations-dashboard-lib.ps1 +++ b/scripts/voidbot-operations-dashboard-lib.ps1 @@ -310,6 +310,462 @@ function New-VoidBotDashboardCard { "@ } +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" + $repoFacet = 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 $repoFacet -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 Update-VoidBotOperationsDashboard { param( [Parameter(Mandatory = $true)] @@ -320,10 +776,25 @@ function Update-VoidBotOperationsDashboard { $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" @@ -334,6 +805,10 @@ function Update-VoidBotOperationsDashboard { $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" } @@ -374,6 +849,50 @@ function Update-VoidBotOperationsDashboard { $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 + + $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 ` @@ -384,22 +903,6 @@ function Update-VoidBotOperationsDashboard { "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") { @@ -408,6 +911,43 @@ function Update-VoidBotOperationsDashboard { "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") + } + $offsiteSubtitle = if ($offsiteState -eq "completed") { "Latest offsite sync finished." } elseif ($offsiteState -eq "missing") { @@ -416,10 +956,66 @@ function Update-VoidBotOperationsDashboard { "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 + }) + $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 "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 "Offsite Backup" -State $offsiteState -Subtitle $offsiteSubtitle -Meta ("Updated " + (Get-VoidBotDashboardRelativeTime -Timestamp ([string](Get-VoidBotDashboardProperty -InputObject $offsite -PropertyName "completedAt")))) ) -join [Environment]::NewLine @@ -427,6 +1023,7 @@ function Update-VoidBotOperationsDashboard { 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")) + "") @@ -434,6 +1031,53 @@ function Update-VoidBotOperationsDashboard { 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.") + ) + $watchdogRows = @( New-VoidBotDashboardFactRow -Label "Status file" -ValueHtml (New-VoidBotDashboardFileLink -Path $watchdogPath) New-VoidBotDashboardFactRow -Label "Run status" -ValueHtml (New-VoidBotDashboardBadge -State $watchdogState -Label $watchdogState) @@ -441,7 +1085,7 @@ function Update-VoidBotOperationsDashboard { 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 "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" }) @@ -450,11 +1094,13 @@ function Update-VoidBotOperationsDashboard { $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 (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 "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") }) ) @@ -464,6 +1110,9 @@ function Update-VoidBotOperationsDashboard { 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) + "") ) @@ -499,9 +1148,39 @@ $($checkRows -join [Environment]::NewLine) 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") 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" + } + } files = [ordered]@{ runtime = $runtimePath operations = $operationsPath @@ -509,9 +1188,12 @@ $($checkRows -join [Environment]::NewLine) offsite = $offsitePath operationsLog = Join-Path $logsDir "operations-watchdog.log" offsiteLog = Join-Path $logsDir "offsite-backup.log" + historyArchive = $historyArchivePath + sourceArchive = $sourceArchivePath + importState = $importStatePath } } - $snapshot | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $snapshotPath -Encoding utf8 + $snapshot | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $snapshotPath -Encoding utf8 $html = @" @@ -770,6 +1452,26 @@ $($checkRows -join [Environment]::NewLine) color: rgba(238, 245, 255, 0.9); } + .stack-list { + display: grid; + gap: 0.45rem; + } + + .stack-item { + padding-top: 0.2rem; + border-top: 1px dashed rgba(89, 183, 255, 0.1); + } + + .stack-item:first-child { + padding-top: 0; + border-top: none; + } + + .muted-inline { + color: rgba(183, 199, 217, 0.72); + font-size: 0.92em; + } + @media (max-width: 720px) { body { padding: 16px; } .hero, .panel, .card { border-radius: 16px; } @@ -792,6 +1494,10 @@ $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 "Watchdog Run" -Rows $watchdogRows) $(New-VoidBotDashboardFactSection -Title "Offsite Backup" -Rows $offsiteRows) $(New-VoidBotDashboardFactSection -Title "Useful Files" -Rows $fileRows)