From e1eb4b45a71cfd2019828b6f3f439a8c68db4cbc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 23 Jun 2026 18:36:59 +0100 Subject: [PATCH] Fix Odin health actuation and share discovery pass --- crates/idunn-daemon/src/main.rs | 20 +- scripts/health-odin.cmd | 2 +- scripts/health-odin.ps1 | 80 +++++ scripts/restart-odin.cmd | 2 +- scripts/restart-odin.ps1 | 35 ++ src/odin/interfaces.cjs | 571 +++++++++++++++++--------------- src/odin/state.cjs | 16 +- 7 files changed, 445 insertions(+), 281 deletions(-) create mode 100644 scripts/health-odin.ps1 create mode 100644 scripts/restart-odin.ps1 diff --git a/crates/idunn-daemon/src/main.rs b/crates/idunn-daemon/src/main.rs index b3b2bd6..64e3d37 100644 --- a/crates/idunn-daemon/src/main.rs +++ b/crates/idunn-daemon/src/main.rs @@ -1768,6 +1768,12 @@ fn daemon_surgery_plan(target: &DaemonTarget, updated_at: &str) -> IdunnDaemonSu fn swarm_targets(options: &SwarmOptions) -> Result> { let repo_root = options.repo_root.display().to_string(); let script = |name: &str| format!(r"{}\scripts\{}", repo_root, name); + let powershell_script = |name: &str| { + format!( + r#"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{}\scripts\{}""#, + repo_root, name + ) + }; let project = |name: &str| PathBuf::from(format!(r"E:\Projects\{name}")); match options.profile.as_str() { @@ -1777,9 +1783,9 @@ fn swarm_targets(options: &SwarmOptions) -> Result> { verse_id: "starfire.local".to_string(), name: "Odin all-seer".to_string(), health_contract: health_contract("odin.cultnet-rudp-provider-health", "failed"), - health_command: Some(script("health-odin.cmd")), + health_command: Some(powershell_script("health-odin.ps1")), deploy_command: None, - restart_command: Some(script("restart-odin.cmd")), + restart_command: Some(powershell_script("restart-odin.ps1")), release: None, enabled: true, interval_seconds: 30, @@ -2701,9 +2707,15 @@ mod tests { verse_id: "starfire.local".to_string(), name: "Odin".to_string(), health_contract: health_contract("odin.cultnet-rudp-provider-health", "failed"), - health_command: Some("health-odin.cmd".to_string()), + health_command: Some( + r#"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "E:\Projects\Odin\scripts\health-odin.ps1""# + .to_string(), + ), deploy_command: None, - restart_command: Some("restart-odin.cmd".to_string()), + restart_command: Some( + r#"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "E:\Projects\Odin\scripts\restart-odin.ps1""# + .to_string(), + ), release: None, enabled: true, interval_seconds: 30, diff --git a/scripts/health-odin.cmd b/scripts/health-odin.cmd index 274bc5e..881ac16 100644 --- a/scripts/health-odin.cmd +++ b/scripts/health-odin.cmd @@ -1,2 +1,2 @@ @echo off -curl.exe -fsS http://127.0.0.1:8797/health >NUL +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-odin.ps1 diff --git a/scripts/health-odin.ps1 b/scripts/health-odin.ps1 new file mode 100644 index 0000000..08ef26a --- /dev/null +++ b/scripts/health-odin.ps1 @@ -0,0 +1,80 @@ +param( + [int] $Port = 8797, + [string] $StateDir = "E:\Projects\Odin\scratch\odin", + [int] $MaxSilenceSeconds = 120, + [string] $CultNetRudpBind = "0.0.0.0:17871" +) + +$ErrorActionPreference = "Stop" + +$pidPath = Join-Path $StateDir "odin.pid" +$outLog = Join-Path $StateDir "odin.out.log" +$errLog = Join-Path $StateDir "odin.err.log" + +if (-not (Test-Path -LiteralPath $pidPath)) { + throw "Odin PID file is missing at $pidPath" +} + +$pidText = (Get-Content -LiteralPath $pidPath -Raw).Trim() +if ($pidText -notmatch '^\d+$') { + throw "Odin PID file is invalid at ${pidPath}: '$pidText'" +} + +$process = Get-CimInstance Win32_Process -Filter "ProcessId = $pidText" -ErrorAction SilentlyContinue +if ($null -eq $process) { + throw "Odin process $pidText is not running" +} + +foreach ($pattern in @( + 'node.exe', + 'src\odin-coordinator.cjs', + "--port $Port", + "--stateDir $StateDir", + "--cultnet-rudp-bind $CultNetRudpBind", + '--idunn-daemon odin', + '--idunn-health-contract odin.cultnet-rudp-provider-health' +)) { + if ($process.CommandLine -notlike "*$pattern*") { + throw "Odin process command line is missing expected segment ${pattern}: $($process.CommandLine)" + } +} + +foreach ($logPath in @($outLog, $errLog)) { + if (-not (Test-Path -LiteralPath $logPath)) { + throw "Odin log is missing at $logPath" + } +} + +$newestLogWriteUtc = @($outLog, $errLog) | + ForEach-Object { (Get-Item -LiteralPath $_).LastWriteTimeUtc } | + Sort-Object -Descending | + Select-Object -First 1 +$logAgeSeconds = ([DateTime]::UtcNow - $newestLogWriteUtc).TotalSeconds + +$healthUri = "http://127.0.0.1:$Port/health" +try { + $health = Invoke-RestMethod -Uri $healthUri -TimeoutSec 10 +} catch { + throw "Odin health endpoint is unavailable at ${healthUri}: $($_.Exception.Message)" +} + +if (-not $health.ok) { + throw "Odin health endpoint reports ok=false" +} + +$tcpListener = Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue | + Where-Object { $_.OwningProcess -eq [int]$pidText } | + Select-Object -First 1 +if ($null -eq $tcpListener) { + throw "Odin is not listening on TCP port $Port as process $pidText" +} + +$rudpPort = [int](($CultNetRudpBind -split ':')[-1]) +$udpListener = Get-NetUDPEndpoint -LocalPort $rudpPort -ErrorAction SilentlyContinue | + Where-Object { $_.OwningProcess -eq [int]$pidText } | + Select-Object -First 1 +if ($null -eq $udpListener) { + throw "Odin is not listening on UDP port $rudpPort as process $pidText" +} + +Write-Host "Odin healthy: pid=$pidText tcp=$Port udp=$rudpPort log_age=$([math]::Round($logAgeSeconds))s" diff --git a/scripts/restart-odin.cmd b/scripts/restart-odin.cmd index 8a0c7d9..f401819 100644 --- a/scripts/restart-odin.cmd +++ b/scripts/restart-odin.cmd @@ -1,2 +1,2 @@ @echo off -powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\start-odin.ps1 +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\restart-odin.ps1 diff --git a/scripts/restart-odin.ps1 b/scripts/restart-odin.ps1 new file mode 100644 index 0000000..779b7b9 --- /dev/null +++ b/scripts/restart-odin.ps1 @@ -0,0 +1,35 @@ +param( + [int] $Port = 8797, + [string] $StateDir = "E:\Projects\Odin\scratch\odin", + [string] $CultNetRudpBind = "0.0.0.0:17871", + [string] $IdunnRudpHealth = "127.0.0.1:17870", + [string] $IdunnDaemon = "odin", + [string] $IdunnHealthContract = "odin.cultnet-rudp-provider-health" +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$stopScript = Join-Path $PSScriptRoot "stop-odin.ps1" +$startScript = Join-Path $PSScriptRoot "start-odin.ps1" +$healthScript = Join-Path $PSScriptRoot "health-odin.ps1" +$powerShellExe = Join-Path $env:WINDIR "System32\WindowsPowerShell\v1.0\powershell.exe" + +if (-not (Test-Path -LiteralPath $powerShellExe)) { + throw "PowerShell executable is missing at $powerShellExe" +} + +& $powerShellExe -NoProfile -ExecutionPolicy Bypass -File $stopScript -StateDir $StateDir + +& $powerShellExe -NoProfile -ExecutionPolicy Bypass -File $startScript ` + -Port $Port ` + -StateDir $StateDir ` + -CultNetRudpBind $CultNetRudpBind ` + -IdunnRudpHealth $IdunnRudpHealth ` + -IdunnDaemon $IdunnDaemon ` + -IdunnHealthContract $IdunnHealthContract + +& $powerShellExe -NoProfile -ExecutionPolicy Bypass -File $healthScript ` + -Port $Port ` + -StateDir $StateDir ` + -CultNetRudpBind $CultNetRudpBind diff --git a/src/odin/interfaces.cjs b/src/odin/interfaces.cjs index 450b649..b174087 100644 --- a/src/odin/interfaces.cjs +++ b/src/odin/interfaces.cjs @@ -24,9 +24,102 @@ function createInterfaceDiscovery({ let lastLanScanAt = 0; async function discoverInterfaces() { + const { interfaces } = await discoverAll(); + return interfaces; + } + + async function discoverProviderAdvertisements() { + const { providerAdvertisements } = await discoverAll(); + return providerAdvertisements; + } + + async function discoverAll() { await refreshLanDeckDiscovery(); + const manifestsByProvider = await discoverDeckProviderManifests(); + const eveInterfaces = await Promise.all( + [...manifestsByProvider.values()].map(({ provider, deckUrl }) => fetchEveProvider(deckUrl, provider.id, provider)), + ); + const { interfaces: cultMeshInterfaces, providerAdvertisements } = await discoverCultMeshEntries(); + + const interfaces = [...eveInterfaces]; + for (const entry of cultMeshInterfaces) { + const existingIndex = interfaces.findIndex((candidate) => candidate.providerId === entry.providerId); + if (existingIndex >= 0) { + interfaces[existingIndex] = entry; + } else { + interfaces.push(entry); + } + } + interfaces.sort((left, right) => left.providerId.localeCompare(right.providerId)); + providerAdvertisements.sort((left, right) => String(left.id).localeCompare(String(right.id))); + return { interfaces, providerAdvertisements }; + } + + function cultMeshDocumentsAvailable() { + const { + interfaceBindingDefinition, + idunnDaemonHealthDefinition, + muninnCaptureStreamDefinition, + muninnCommandBoundaryDefinition, + muninnMoveControllerStateDefinition, + muninnMoveIdentityDefinition, + muninnMoveLightCommandDefinition, + muninnMoveMarkerCandidateDefinition, + muninnObsStreamCatalogDefinition, + muninnQuestAccessDefinition, + muninnTelemetrySurfaceDefinition, + muninnTransportProfileDefinition, + operatorStateDefinition, + providerAdvertisementDefinition, + stonksCommandBoundaryDefinition, + stonksMarketSnapshotDefinition, + stonksRequestEventDefinition, + stonksTransportProfileDefinition, + streamPixelsCommandBoundaryDefinition, + streamPixelsTransportProfileDefinition, + surfaceDefinition, + viliCommandBoundaryDefinition, + viliTransportProfileDefinition, + weksaCommandBoundaryDefinition, + weksaOperatorStateDefinition, + weksaTransportProfileDefinition, + voidbotSwarmSnapshotDefinition, + } = documents; + return Boolean( + CultMesh && + interfaceBindingDefinition && + idunnDaemonHealthDefinition && + muninnCaptureStreamDefinition && + muninnCommandBoundaryDefinition && + muninnMoveControllerStateDefinition && + muninnMoveIdentityDefinition && + muninnMoveLightCommandDefinition && + muninnMoveMarkerCandidateDefinition && + muninnObsStreamCatalogDefinition && + muninnQuestAccessDefinition && + muninnTelemetrySurfaceDefinition && + muninnTransportProfileDefinition && + operatorStateDefinition && + providerAdvertisementDefinition && + stonksCommandBoundaryDefinition && + stonksMarketSnapshotDefinition && + stonksRequestEventDefinition && + stonksTransportProfileDefinition && + streamPixelsCommandBoundaryDefinition && + streamPixelsTransportProfileDefinition && + surfaceDefinition && + viliCommandBoundaryDefinition && + viliTransportProfileDefinition && + weksaCommandBoundaryDefinition && + weksaOperatorStateDefinition && + weksaTransportProfileDefinition && + voidbotSwarmSnapshotDefinition + ); + } + + async function discoverDeckProviderManifests() { const manifestsByProvider = new Map(); - for (const deckUrl of discoveredDeckUrls) { + await Promise.all(discoveredDeckUrls.map(async (deckUrl) => { const manifestUrl = deckUrl.replace(/^ws:/, "http:").replace(/\/eve\/deck.*$/, "/eve/deck/providers"); try { const catalog = JSON.parse(await httpGet(manifestUrl, 2500)); @@ -40,25 +133,11 @@ function createInterfaceDiscovery({ } catch { // Discovery is opportunistic. Failed endpoints fall out of the next pass. } - } - - const interfaces = []; - for (const { provider, deckUrl } of manifestsByProvider.values()) { - interfaces.push(await fetchEveProvider(deckUrl, provider.id, provider)); - } - for (const entry of await discoverCultMeshInterfaceBindings()) { - const existingIndex = interfaces.findIndex((candidate) => candidate.providerId === entry.providerId); - if (existingIndex >= 0) { - interfaces[existingIndex] = entry; - } else { - interfaces.push(entry); - } - } - interfaces.sort((left, right) => left.providerId.localeCompare(right.providerId)); - return interfaces; + })); + return manifestsByProvider; } - async function discoverProviderAdvertisements() { + async function discoverCultMeshEntries() { const { interfaceBindingDefinition, idunnDaemonHealthDefinition, @@ -88,280 +167,87 @@ function createInterfaceDiscovery({ weksaTransportProfileDefinition, voidbotSwarmSnapshotDefinition, } = documents; - if (!CultMesh || !interfaceBindingDefinition || !idunnDaemonHealthDefinition || !muninnCaptureStreamDefinition || !muninnCommandBoundaryDefinition || !muninnMoveControllerStateDefinition || !muninnMoveIdentityDefinition || !muninnMoveLightCommandDefinition || !muninnMoveMarkerCandidateDefinition || !muninnObsStreamCatalogDefinition || !muninnQuestAccessDefinition || !muninnTelemetrySurfaceDefinition || !muninnTransportProfileDefinition || !operatorStateDefinition || !stonksCommandBoundaryDefinition || !stonksMarketSnapshotDefinition || !stonksRequestEventDefinition || !stonksTransportProfileDefinition || !streamPixelsCommandBoundaryDefinition || !streamPixelsTransportProfileDefinition || !surfaceDefinition || !viliCommandBoundaryDefinition || !viliTransportProfileDefinition || !weksaCommandBoundaryDefinition || !weksaOperatorStateDefinition || !weksaTransportProfileDefinition || !voidbotSwarmSnapshotDefinition || !providerAdvertisementDefinition) { - return []; + if (!cultMeshDocumentsAvailable()) { + return { interfaces: [], providerAdvertisements: [] }; } - const providers = []; - for (const storeSpec of interfaceBindingStores) { + const nodeDocuments = [ + voidbotSwarmSnapshotDefinition, + stonksRequestEventDefinition, + stonksMarketSnapshotDefinition, + providerAdvertisementDefinition, + interfaceBindingDefinition, + surfaceDefinition, + stonksCommandBoundaryDefinition, + stonksTransportProfileDefinition, + streamPixelsCommandBoundaryDefinition, + streamPixelsTransportProfileDefinition, + idunnDaemonHealthDefinition, + muninnCaptureStreamDefinition, + muninnCommandBoundaryDefinition, + muninnMoveControllerStateDefinition, + muninnMoveIdentityDefinition, + muninnMoveLightCommandDefinition, + muninnMoveMarkerCandidateDefinition, + muninnObsStreamCatalogDefinition, + muninnQuestAccessDefinition, + muninnTelemetrySurfaceDefinition, + muninnTransportProfileDefinition, + operatorStateDefinition, + viliCommandBoundaryDefinition, + viliTransportProfileDefinition, + weksaOperatorStateDefinition, + weksaCommandBoundaryDefinition, + weksaTransportProfileDefinition, + ]; + + const storeEntries = await Promise.all(interfaceBindingStores.map(async (storeSpec) => { let resolvedStore = null; try { resolvedStore = resolveInterfaceBindingStore(storeSpec); if (!resolvedStore) { - continue; + return { interfaces: [], providerAdvertisements: [] }; } const { localPath: storePath, sourceId } = resolvedStore; if (!fs.existsSync(storePath)) { - continue; + return { interfaces: [], providerAdvertisements: [] }; } - const node = await CultMesh.createNode(storePath, { - documents: [ - voidbotSwarmSnapshotDefinition, - stonksRequestEventDefinition, - stonksMarketSnapshotDefinition, - providerAdvertisementDefinition, - interfaceBindingDefinition, - surfaceDefinition, - stonksCommandBoundaryDefinition, - stonksTransportProfileDefinition, - streamPixelsCommandBoundaryDefinition, - streamPixelsTransportProfileDefinition, - idunnDaemonHealthDefinition, - muninnCaptureStreamDefinition, - muninnCommandBoundaryDefinition, - muninnMoveControllerStateDefinition, - muninnMoveIdentityDefinition, - muninnMoveLightCommandDefinition, - muninnMoveMarkerCandidateDefinition, - muninnObsStreamCatalogDefinition, - muninnQuestAccessDefinition, - muninnTelemetrySurfaceDefinition, - muninnTransportProfileDefinition, - operatorStateDefinition, - viliCommandBoundaryDefinition, - viliTransportProfileDefinition, - weksaOperatorStateDefinition, - weksaCommandBoundaryDefinition, - weksaTransportProfileDefinition, - ], + const node = await CultMesh.createNode(storePath, { documents: nodeDocuments }); + return inspectCultMeshStore({ + documents, + interfaceBindingDefinition, + muninnTelemetrySurfaceDefinition, + node, + providerAdvertisementDefinition, + sourceId, + storePath, }); - const advertisements = typeof node.cache?.getAll === "function" - ? node.cache.getAll(providerAdvertisementDefinition).map(unwrapDocumentRecord) - : []; - for (const advertisement of advertisements) { - const providerId = advertisement?.providerId || advertisement?.provider?.id || advertisement?.id; - if (!providerId) { - continue; - } - providers.push({ - id: providerId, - title: advertisement.title || advertisement.provider?.title || providerId, - description: advertisement.description || "Provider-owned CultMesh advertisement.", - version: String(advertisement.version || 0), - endpoint: advertisement.cultMeshAddress || advertisement.endpoints?.[0]?.address || advertisement.provider?.endpoint || `cultmesh:${sourceId}`, - canonicalService: advertisement.canonicalService || null, - locatedService: advertisement.locatedService || null, - cultMeshAddress: advertisement.cultMeshAddress || null, - endpoints: advertisement.endpoints || [], - routes: advertisement.routes || [], - transportEndpoint: advertisement.routes?.find((route) => route.transport === "cultnet")?.address - || advertisement.routes?.find((route) => route.transport === "compatibility-eve-deck")?.address - || advertisement.provider?.endpoint - || `cultmesh:${sourceId}`, - capabilities: advertisement.provider?.capabilities || advertisement.capabilities || [], - operatorState: providerRecord(node, [ - operatorStateDefinition, - weksaOperatorStateDefinition, - ], advertisement) || null, - commandBoundary: providerRecord(node, [ - muninnCommandBoundaryDefinition, - viliCommandBoundaryDefinition, - weksaCommandBoundaryDefinition, - stonksCommandBoundaryDefinition, - streamPixelsCommandBoundaryDefinition, - ], advertisement) || null, - transportProfile: providerRecord(node, [ - muninnTransportProfileDefinition, - viliTransportProfileDefinition, - weksaTransportProfileDefinition, - stonksTransportProfileDefinition, - streamPixelsTransportProfileDefinition, - ], advertisement) || null, - usesCultMesh: true, - transport: advertisement.provider?.transport || "CultMesh provider advertisement", - status: advertisement.status || "unknown", - updatedAt: advertisement.updatedAt || new Date().toISOString(), - source: `cultmesh:${sourceId}`, - commandSurface: advertisement.commandSurface || null, - }); - } - } catch { - // Provider advertisements are optional discovery hints; broken stores - // are still surfaced through interface discovery when possible. + } catch (error) { + const interfaces = []; + const providerAdvertisements = []; if (resolvedStore?.localPath && fs.existsSync(resolvedStore.localPath)) { - providers.push(...inspectProviderAdvertisementsFromStore( + interfaces.push(...inspectCultMeshInterfacesFromStore( + resolvedStore.localPath, + resolvedStore.sourceId, + )); + providerAdvertisements.push(...inspectProviderAdvertisementsFromStore( resolvedStore.localPath, resolvedStore.sourceId, )); } + if (interfaces.length === 0) { + interfaces.push(dashboardUnavailable(`cultmesh:${storeSpec}`, `cultmesh:${storeSpec}`, error.message)); + } + return { interfaces, providerAdvertisements }; } finally { resolvedStore?.cleanup?.(); } - } + })); - providers.sort((left, right) => String(left.id).localeCompare(String(right.id))); - return providers; - } - - async function discoverCultMeshInterfaceBindings() { - const { - interfaceBindingDefinition, - idunnDaemonHealthDefinition, - muninnCaptureStreamDefinition, - muninnCommandBoundaryDefinition, - muninnMoveControllerStateDefinition, - muninnMoveIdentityDefinition, - muninnMoveLightCommandDefinition, - muninnMoveMarkerCandidateDefinition, - muninnObsStreamCatalogDefinition, - muninnQuestAccessDefinition, - muninnTelemetrySurfaceDefinition, - muninnTransportProfileDefinition, - operatorStateDefinition, - providerAdvertisementDefinition, - stonksCommandBoundaryDefinition, - stonksMarketSnapshotDefinition, - stonksRequestEventDefinition, - stonksTransportProfileDefinition, - streamPixelsCommandBoundaryDefinition, - streamPixelsTransportProfileDefinition, - surfaceDefinition, - viliCommandBoundaryDefinition, - viliTransportProfileDefinition, - weksaCommandBoundaryDefinition, - weksaOperatorStateDefinition, - weksaTransportProfileDefinition, - voidbotSwarmSnapshotDefinition, - } = documents; - if (!CultMesh || !interfaceBindingDefinition || !idunnDaemonHealthDefinition || !muninnCaptureStreamDefinition || !muninnCommandBoundaryDefinition || !muninnMoveControllerStateDefinition || !muninnMoveIdentityDefinition || !muninnMoveLightCommandDefinition || !muninnMoveMarkerCandidateDefinition || !muninnObsStreamCatalogDefinition || !muninnQuestAccessDefinition || !muninnTelemetrySurfaceDefinition || !muninnTransportProfileDefinition || !operatorStateDefinition || !stonksCommandBoundaryDefinition || !stonksMarketSnapshotDefinition || !stonksRequestEventDefinition || !stonksTransportProfileDefinition || !streamPixelsCommandBoundaryDefinition || !streamPixelsTransportProfileDefinition || !surfaceDefinition || !viliCommandBoundaryDefinition || !viliTransportProfileDefinition || !weksaCommandBoundaryDefinition || !weksaOperatorStateDefinition || !weksaTransportProfileDefinition || !voidbotSwarmSnapshotDefinition || !providerAdvertisementDefinition) { - return []; - } - const interfaces = []; - for (const storeSpec of interfaceBindingStores) { - let resolvedStore = null; - try { - resolvedStore = resolveInterfaceBindingStore(storeSpec); - if (!resolvedStore) { - continue; - } - const { localPath: storePath, sourceId } = resolvedStore; - if (!fs.existsSync(storePath)) { - continue; - } - const node = await CultMesh.createNode(storePath, { - documents: [ - voidbotSwarmSnapshotDefinition, - stonksRequestEventDefinition, - stonksMarketSnapshotDefinition, - providerAdvertisementDefinition, - interfaceBindingDefinition, - surfaceDefinition, - stonksCommandBoundaryDefinition, - stonksTransportProfileDefinition, - streamPixelsCommandBoundaryDefinition, - streamPixelsTransportProfileDefinition, - idunnDaemonHealthDefinition, - muninnCaptureStreamDefinition, - muninnCommandBoundaryDefinition, - muninnMoveControllerStateDefinition, - muninnMoveIdentityDefinition, - muninnMoveLightCommandDefinition, - muninnMoveMarkerCandidateDefinition, - muninnObsStreamCatalogDefinition, - muninnQuestAccessDefinition, - muninnTelemetrySurfaceDefinition, - muninnTransportProfileDefinition, - operatorStateDefinition, - viliCommandBoundaryDefinition, - viliTransportProfileDefinition, - weksaOperatorStateDefinition, - weksaCommandBoundaryDefinition, - weksaTransportProfileDefinition, - ], - }); - const advertisements = typeof node.cache?.getAll === "function" - ? node.cache.getAll(providerAdvertisementDefinition).map(unwrapDocumentRecord) - : []; - for (const advertisement of advertisements) { - if (!advertisement?.providerId) { - continue; - } - const state = providerSurfaceState( - node, - advertisement, - surfaceDefinition, - muninnTelemetrySurfaceDefinition, - ); - if (!state?.surface?.root) { - continue; - } - interfaces.push(cultMeshProviderInterface({ - advertisement, - state, - storePath: sourceId, - operatorState: providerRecord(node, [ - operatorStateDefinition, - weksaOperatorStateDefinition, - ], advertisement) || null, - commandBoundary: providerRecord(node, [ - muninnCommandBoundaryDefinition, - viliCommandBoundaryDefinition, - weksaCommandBoundaryDefinition, - stonksCommandBoundaryDefinition, - streamPixelsCommandBoundaryDefinition, - ], advertisement) || null, - transportProfile: providerRecord(node, [ - muninnTransportProfileDefinition, - viliTransportProfileDefinition, - weksaTransportProfileDefinition, - stonksTransportProfileDefinition, - streamPixelsTransportProfileDefinition, - ], advertisement) || null, - })); - } - const bindings = typeof node.cache?.getAll === "function" - ? node.cache.getAll(interfaceBindingDefinition).map(unwrapDocumentRecord) - : [unwrapDocumentRecord(node.get(interfaceBindingDefinition, "voidbot.swarm"))].filter(Boolean); - for (const binding of bindings) { - if (!binding?.providerId) { - continue; - } - const state = unwrapDocumentRecord(node.get(surfaceDefinition, binding.providerId)); - const surface = state?.surface || binding.surface || null; - interfaces.push({ - providerId: binding.providerId, - title: binding.title || state?.title || binding.providerId, - state: "active", - detail: `${surface?.root?.kind || binding.kind || "surface"} ${countSurfaceNodes(surface, state)} nodes via CultMesh`, - version: state?.version || 0, - updatedAt: state?.updatedAt || binding.updatedAt || new Date().toISOString(), - source: `cultmesh:${sourceId}`, - manifest: binding.provider || null, - canonicalService: binding.provider?.canonicalService || null, - locatedService: binding.provider?.locatedService || null, - cultMeshAddress: binding.provider?.cultMeshAddress || binding.provider?.endpoint || null, - endpoints: binding.provider?.endpoints || [], - routes: binding.provider?.routes || [], - surface, - }); - } - } catch (error) { - if (resolvedStore?.localPath && fs.existsSync(resolvedStore.localPath)) { - const fallbackInterfaces = inspectCultMeshInterfacesFromStore( - resolvedStore.localPath, - resolvedStore.sourceId, - ); - if (fallbackInterfaces.length > 0) { - interfaces.push(...fallbackInterfaces); - continue; - } - } - interfaces.push(dashboardUnavailable(`cultmesh:${storeSpec}`, `cultmesh:${storeSpec}`, error.message)); - } finally { - resolvedStore?.cleanup?.(); - } - } - return interfaces; + return { + interfaces: storeEntries.flatMap((entry) => entry.interfaces), + providerAdvertisements: storeEntries.flatMap((entry) => entry.providerAdvertisements), + }; } async function refreshLanDeckDiscovery() { @@ -383,6 +269,7 @@ function createInterfaceDiscovery({ } return { + discoverAll, discoverProviderAdvertisements, discoverInterfaces, getDiscoveredDeckUrls: () => [...discoveredDeckUrls], @@ -421,6 +308,146 @@ function cultMeshProviderInterface({ }; } +function inspectCultMeshStore({ + documents, + interfaceBindingDefinition, + muninnTelemetrySurfaceDefinition, + node, + providerAdvertisementDefinition, + sourceId, +}) { + const { + operatorStateDefinition, + stonksCommandBoundaryDefinition, + stonksTransportProfileDefinition, + streamPixelsCommandBoundaryDefinition, + streamPixelsTransportProfileDefinition, + surfaceDefinition, + viliCommandBoundaryDefinition, + viliTransportProfileDefinition, + weksaCommandBoundaryDefinition, + weksaOperatorStateDefinition, + weksaTransportProfileDefinition, + muninnCommandBoundaryDefinition, + muninnTransportProfileDefinition, + } = documents; + + const providerAdvertisements = []; + const interfaces = []; + const advertisements = typeof node.cache?.getAll === "function" + ? node.cache.getAll(providerAdvertisementDefinition).map(unwrapDocumentRecord) + : []; + + for (const advertisement of advertisements) { + const providerId = advertisement?.providerId || advertisement?.provider?.id || advertisement?.id; + if (!providerId) { + continue; + } + providerAdvertisements.push({ + id: providerId, + title: advertisement.title || advertisement.provider?.title || providerId, + description: advertisement.description || "Provider-owned CultMesh advertisement.", + version: String(advertisement.version || 0), + endpoint: advertisement.cultMeshAddress || advertisement.endpoints?.[0]?.address || advertisement.provider?.endpoint || `cultmesh:${sourceId}`, + canonicalService: advertisement.canonicalService || null, + locatedService: advertisement.locatedService || null, + cultMeshAddress: advertisement.cultMeshAddress || null, + endpoints: advertisement.endpoints || [], + routes: advertisement.routes || [], + transportEndpoint: advertisement.routes?.find((route) => route.transport === "cultnet")?.address + || advertisement.routes?.find((route) => route.transport === "compatibility-eve-deck")?.address + || advertisement.provider?.endpoint + || `cultmesh:${sourceId}`, + capabilities: advertisement.provider?.capabilities || advertisement.capabilities || [], + operatorState: providerRecord(node, [ + operatorStateDefinition, + weksaOperatorStateDefinition, + ], advertisement) || null, + commandBoundary: providerRecord(node, [ + muninnCommandBoundaryDefinition, + viliCommandBoundaryDefinition, + weksaCommandBoundaryDefinition, + stonksCommandBoundaryDefinition, + streamPixelsCommandBoundaryDefinition, + ], advertisement) || null, + transportProfile: providerRecord(node, [ + muninnTransportProfileDefinition, + viliTransportProfileDefinition, + weksaTransportProfileDefinition, + stonksTransportProfileDefinition, + streamPixelsTransportProfileDefinition, + ], advertisement) || null, + usesCultMesh: true, + transport: advertisement.provider?.transport || "CultMesh provider advertisement", + status: advertisement.status || "unknown", + updatedAt: advertisement.updatedAt || new Date().toISOString(), + source: `cultmesh:${sourceId}`, + commandSurface: advertisement.commandSurface || null, + }); + + const state = providerSurfaceState( + node, + advertisement, + surfaceDefinition, + muninnTelemetrySurfaceDefinition, + ); + if (state?.surface?.root) { + interfaces.push(cultMeshProviderInterface({ + advertisement, + state, + storePath: sourceId, + operatorState: providerRecord(node, [ + operatorStateDefinition, + weksaOperatorStateDefinition, + ], advertisement) || null, + commandBoundary: providerRecord(node, [ + muninnCommandBoundaryDefinition, + viliCommandBoundaryDefinition, + weksaCommandBoundaryDefinition, + stonksCommandBoundaryDefinition, + streamPixelsCommandBoundaryDefinition, + ], advertisement) || null, + transportProfile: providerRecord(node, [ + muninnTransportProfileDefinition, + viliTransportProfileDefinition, + weksaTransportProfileDefinition, + stonksTransportProfileDefinition, + streamPixelsTransportProfileDefinition, + ], advertisement) || null, + })); + } + } + + const bindings = typeof node.cache?.getAll === "function" + ? node.cache.getAll(interfaceBindingDefinition).map(unwrapDocumentRecord) + : [unwrapDocumentRecord(node.get(interfaceBindingDefinition, "voidbot.swarm"))].filter(Boolean); + for (const binding of bindings) { + if (!binding?.providerId) { + continue; + } + const state = unwrapDocumentRecord(node.get(surfaceDefinition, binding.providerId)); + const surface = state?.surface || binding.surface || null; + interfaces.push({ + providerId: binding.providerId, + title: binding.title || state?.title || binding.providerId, + state: "active", + detail: `${surface?.root?.kind || binding.kind || "surface"} ${countSurfaceNodes(surface, state)} nodes via CultMesh`, + version: state?.version || 0, + updatedAt: state?.updatedAt || binding.updatedAt || new Date().toISOString(), + source: `cultmesh:${sourceId}`, + manifest: binding.provider || null, + canonicalService: binding.provider?.canonicalService || null, + locatedService: binding.provider?.locatedService || null, + cultMeshAddress: binding.provider?.cultMeshAddress || binding.provider?.endpoint || null, + endpoints: binding.provider?.endpoints || [], + routes: binding.provider?.routes || [], + surface, + }); + } + + return { interfaces, providerAdvertisements }; +} + function providerSurfaceState(node, advertisement, surfaceDefinition, muninnTelemetrySurfaceDefinition) { const surfaceKey = advertisement.surfaceId || advertisement.surface_id || advertisement.providerId; const surfaceState = unwrapDocumentRecord(node.get?.(surfaceDefinition, surfaceKey)); diff --git a/src/odin/state.cjs b/src/odin/state.cjs index 9f34d84..09f0508 100644 --- a/src/odin/state.cjs +++ b/src/odin/state.cjs @@ -39,17 +39,27 @@ function createStateBuilder({ cachePath, gamecultTextDocumentStorePath, interfac async function buildState() { version += 1; const observedAt = new Date().toISOString(); - const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, discoveredInterfaces, providerAdvertisements, observations] = await Promise.all([ + const [ + docker, + adb, + hosts, + yggdrasilServices, + nightwingServices, + nightwingGpu, + discovery, + observations, + ] = await Promise.all([ dockerSnapshot(), adbSnapshot(), hostChecks(), remoteServices("ygg", ["nginx", "streampixels-web", "streampixels-service", "heimdall", "repixelizer-gui", "bifrost"]), remoteServices("nightwing", ["ssh", "nightwing-eve-dashboard", "nightwing-eve-browser-reference", "gjallar", "docker"]), remoteGpu("nightwing"), - interfaceDiscovery.discoverInterfaces(), - interfaceDiscovery.discoverProviderAdvertisements(), + interfaceDiscovery.discoverAll(), observationSnapshot(observationLogPath, observationFreshSeconds), ]); + const discoveredInterfaces = discovery.interfaces; + const providerAdvertisements = discovery.providerAdvertisements; const interfaceById = new Map(discoveredInterfaces.map((entry) => [entry.providerId, entry])); const interfaces = [...interfaceById.values()]; const marqueeText = await buildMarqueeText({ interfaces, textDocumentStorePath: gamecultTextDocumentStorePath, stonksBurstSize, stonksStateUrl });