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)