From 173c858eb5a844516ff1f4e86d9d51693a616aab Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 31 May 2026 16:23:22 +0100 Subject: [PATCH 001/742] Scaffold Odin all-seer service --- .gitignore | 5 + README.md | 49 +++++ docs/architecture.md | 50 +++++ package.json | 15 ++ scripts/start-odin.ps1 | 52 +++++ scripts/stop-odin.ps1 | 22 ++ src/odin-coordinator.cjs | 446 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 639 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/architecture.md create mode 100644 package.json create mode 100644 scripts/start-odin.ps1 create mode 100644 scripts/stop-odin.ps1 create mode 100644 src/odin-coordinator.cjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b6c145 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +scratch/ +*.log +*.pid + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2742f3f --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Odin + +Odin is the GameCult all-seer: the central CultMesh node every Verse can use to discover the other Verses, inspect schema catalogs, and find translation paths between local realities. + +It is not the renderer. It is not Eve. It is not a Starfire utility script wearing a bigger coat. Odin owns discovery, schema awareness, translation planning, and accepted operator surfaces. Eve and the Nightwing compact TUI lower Odin's published CultUI surface into whatever body they have. + +## Authority Map + +- Owner: Odin owns the network-wide Verse registry, schema catalog index, translation map, and the accepted `odin.allseer` Eve surface. +- Inputs: CultMesh/CultNet peer announcements, schema catalog responses, local host probes, Docker/ADB host facts, SSH-reachable ops hosts, and later direct Verse subscriptions. +- Outputs: CultCache-backed Odin state, CultMesh documents, CultNet schema/catalog messages, and an Eve-compatible `/eve/deck` surface for dashboards. +- Derived state: Nightwing's attached TUI, browser dashboards, and future Eve clients are projections of Odin state. +- Forbidden writers: renderers do not probe the network or decide Verse truth; individual projects do not maintain private incompatible discovery ledgers once Odin can see them. +- Shared paths: human dashboards, worker schedulers, Verse bootstrap code, and compact TUI views consume the same registry and schema catalog. +- Deletion line: old per-host coordinator scripts should be deleted or reduced to deployment wrappers that start Odin. + +## Run Locally On Starfire + +```powershell +.\scripts\start-odin.ps1 +curl.exe -fsS http://127.0.0.1:8797/health +``` + +The Eve/CultUI deck endpoint is: + +```text +ws://127.0.0.1:8797/eve/deck +``` + +Nightwing can consume the LAN endpoint: + +```text +ws://192.168.1.66:8797/eve/deck +``` + +State and logs live under ignored `scratch/odin/`. + +## Current First Body + +The first executable is deliberately narrow: + +- publishes provider `odin.allseer`; +- writes the latest surface to `scratch/odin/latest-surface.json`; +- persists the same document through local CultMesh/CultCache when `CultLib` packages are available at `E:\Projects\CultLib\packages`; +- observes Starfire Docker, Periwinkle ADB, Nightwing, Raven, EVE, and Yggdrasil reachability; +- exposes Yggdrasil service status when the local SSH alias can reach it. + +The next real cut is to replace static host probes with CultNet schema discovery and CultMesh Verse peer exchange. The probe layer is an input adapter. It must not become the architecture. + diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..6c1047c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,50 @@ +# Odin Architecture + +## Objective + +Odin is the central all-seer node for GameCult's CultMesh world: every Verse can discover every other Verse, learn which schemas they speak, and ask for a translation route when their local document shape differs. + +## Current Mechanism + +```text +host / device / service probes + -> Odin observation cycle + -> Odin state document + -> CultMesh/CultCache persistence + -> Eve dashboard state + -> Eve, browser, compact TUI, and future renderers +``` + +This first path proves the operator surface and persistent state. It does not yet pretend to be full peer exchange. + +## Target Mechanism + +```text +Verse announcement + -> CultNet hello and schema catalog exchange + -> Odin registry + -> compatibility and translation index + -> subscriptions / worker routing / dashboard projection +``` + +## Invariants + +- Odin owns the accepted registry of known Verses. +- A Verse owns its own schemas and authority model; Odin indexes and translates, it does not silently rewrite local truth. +- Translation paths must name source schema, target schema, lossiness, authority, and version. +- Renderers lower surfaces only. If a renderer fixes network truth, the machine is split-brained. +- CultCache is the durable state substrate; CultNet is the wire vocabulary; CultMesh is the Verse and peer-consensus layer. + +## First Translation Model + +Odin's translation registry should start as data, not magic: + +- `sourceSchema` +- `targetSchema` +- `translationKind`: `identity`, `projection`, `lossyProjection`, `adapter`, or `unsupported` +- `owner` +- `version` +- `notes` + +No regex tribunals for meaning. Natural-language schema interpretation can be assisted by models later, but accepted translation routes must be inspectable typed state. + diff --git a/package.json b/package.json new file mode 100644 index 0000000..c52021e --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "@gamecult/odin", + "version": "0.1.0", + "private": true, + "description": "GameCult all-seer: CultMesh Verse discovery, schema awareness, translation, and Eve surface publication.", + "type": "commonjs", + "scripts": { + "check": "node --check src/odin-coordinator.cjs", + "start": "node src/odin-coordinator.cjs" + }, + "engines": { + "node": ">=20" + } +} + diff --git a/scripts/start-odin.ps1 b/scripts/start-odin.ps1 new file mode 100644 index 0000000..ad3ea56 --- /dev/null +++ b/scripts/start-odin.ps1 @@ -0,0 +1,52 @@ +param( + [int] $Port = 8797, + [string] $StateDir = "E:\Projects\Odin\scratch\odin", + [switch] $Foreground +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$scriptPath = Join-Path $repoRoot "src\odin-coordinator.cjs" +$pidPath = Join-Path $StateDir "odin.pid" +$outLog = Join-Path $StateDir "odin.out.log" +$errLog = Join-Path $StateDir "odin.err.log" + +New-Item -ItemType Directory -Force -Path $StateDir | Out-Null + +if (Test-Path $pidPath) { + $oldPid = (Get-Content -Raw $pidPath).Trim() + if ($oldPid -match "^\d+$") { + $old = Get-Process -Id ([int] $oldPid) -ErrorAction SilentlyContinue + if ($old) { + throw "Odin already appears to be running as PID $oldPid." + } + } + Remove-Item -LiteralPath $pidPath -Force +} + +$env:NODE_PATH = "E:\Projects\CultLib\packages" +$args = @( + $scriptPath, + "--port", "$Port", + "--stateDir", $StateDir +) + +if ($Foreground) { + & node @args + exit $LASTEXITCODE +} + +$proc = Start-Process -FilePath "node" -ArgumentList $args -WorkingDirectory $repoRoot -WindowStyle Hidden -PassThru -RedirectStandardOutput $outLog -RedirectStandardError $errLog +$proc.Id | Set-Content -Encoding ASCII -LiteralPath $pidPath +Start-Sleep -Seconds 1 +if ($proc.HasExited) { + $detail = "" + if (Test-Path $outLog) { $detail += Get-Content -Raw $outLog } + if (Test-Path $errLog) { $detail += Get-Content -Raw $errLog } + throw "Odin exited immediately with code $($proc.ExitCode).`n$detail" +} + +Write-Host "Odin started as PID $($proc.Id)." +Write-Host "Health: http://127.0.0.1:$Port/health" +Write-Host "Eve deck: ws://127.0.0.1:$Port/eve/deck" diff --git a/scripts/stop-odin.ps1 b/scripts/stop-odin.ps1 new file mode 100644 index 0000000..3b04071 --- /dev/null +++ b/scripts/stop-odin.ps1 @@ -0,0 +1,22 @@ +param( + [string] $StateDir = "E:\Projects\Odin\scratch\odin" +) + +$ErrorActionPreference = "Stop" + +$pidPath = Join-Path $StateDir "odin.pid" +if (-not (Test-Path $pidPath)) { + Write-Host "No Odin PID file found at $pidPath." + exit 0 +} + +$pid = (Get-Content -Raw $pidPath).Trim() +if ($pid -match "^\d+$") { + $process = Get-Process -Id ([int] $pid) -ErrorAction SilentlyContinue + if ($process) { + Stop-Process -Id $process.Id + Write-Host "Stopped Odin PID $pid." + } +} + +Remove-Item -LiteralPath $pidPath -Force diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs new file mode 100644 index 0000000..55919ba --- /dev/null +++ b/src/odin-coordinator.cjs @@ -0,0 +1,446 @@ +#!/usr/bin/env node +"use strict"; + +const crypto = require("crypto"); +const fs = require("fs"); +const http = require("http"); +const net = require("net"); +const os = require("os"); +const path = require("path"); +const { execFile } = require("child_process"); +const { promisify } = require("util"); +const Module = require("module"); + +const execFileAsync = promisify(execFile); + +const repoRoot = path.resolve(__dirname, ".."); +process.env.NODE_PATH = [ + path.resolve(repoRoot, "..", "CultLib", "packages"), + process.env.NODE_PATH || "", +].filter(Boolean).join(path.delimiter); +Module._initPaths(); + +let CultMesh; +let defineDocumentType; +try { + ({ CultMesh } = require("cultmesh-ts/dist/index.js")); + ({ defineDocumentType } = require("cultcache-ts/dist/index.js")); +} catch (error) { + console.error("CultMesh runtime unavailable; durable mesh snapshot disabled:", error.message); +} + +const args = parseArgs(process.argv.slice(2)); +const port = Number(args.port || 8797); +const host = args.host || "0.0.0.0"; +const intervalMs = Number(args.intervalMs || 5000); +const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin"); +const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp"); +const surfaceKey = "surface:gamecult.network.status"; + +fs.mkdirSync(stateDir, { recursive: true }); + +const surfaceDefinition = defineDocumentType + ? defineDocumentType({ + type: "gamecult.eve.surface_state", + schemaName: "gamecult.eve.surface_state", + schemaVersion: "gamecult.eve.surface_state.v1", + global: true, + name: (value) => value?.providerId || "surface", + schema: { parse: (value) => value }, + members: [ + { slot: 0, memberName: "providerId", typeName: "string", isName: true }, + { slot: 1, memberName: "title", typeName: "string" }, + { slot: 2, memberName: "version", typeName: "long" }, + { slot: 3, memberName: "updatedAt", typeName: "string" }, + { slot: 4, memberName: "surface", typeName: "object" }, + ], + }) + : null; + +let meshNodePromise = null; +let version = 0; +let currentState = buildPendingState("Coordinator starting"); +const clients = new Set(); + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); + +async function main() { + if (CultMesh && surfaceDefinition) { + meshNodePromise = CultMesh.createNode(cachePath, { documents: [surfaceDefinition] }); + } + + const server = http.createServer(handleHttp); + server.on("upgrade", handleUpgrade); + server.listen(port, host, () => { + console.log(`Odin coordinator listening on ws://${host}:${port}/eve/deck`); + console.log(`Durable surface cache: ${cachePath}`); + }); + + await refresh(); + setInterval(() => { + refresh().catch((error) => console.error("refresh failed:", error)); + }, intervalMs); +} + +async function refresh() { + currentState = await buildState(); + await persistState(currentState); + const payload = JSON.stringify(currentState); + for (const client of [...clients]) { + try { + sendFrame(client, 0x1, Buffer.from(payload, "utf8")); + } catch { + clients.delete(client); + client.destroy(); + } + } +} + +async function persistState(state) { + fs.writeFileSync(path.join(stateDir, "latest-surface.json"), JSON.stringify(state, null, 2), "utf8"); + if (!meshNodePromise || !surfaceDefinition) { + return; + } + + try { + const node = await meshNodePromise; + await node.put(surfaceDefinition, surfaceKey, state); + await node.flush?.(true); + } catch (error) { + console.error("CultMesh snapshot write failed:", error.message); + } +} + +async function handleHttp(req, res) { + if (req.url === "/health") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ + ok: true, + providerId: currentState.providerId, + version: currentState.version, + clients: clients.size, + cachePath, + })); + return; + } + + if (req.url === "/eve/deck/providers") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ + providers: [{ + id: currentState.providerId, + title: currentState.title, + description: "Odin central coordinator for GameCult Verse discovery, schema awareness, translation, and network status.", + version: String(currentState.version), + endpoint: "/eve/deck", + capabilities: ["network-status", "cultmesh-verses", "cultui-surface"], + usesCultMesh: true, + transport: "CultMesh durable surface + Eve WebSocket", + }], + })); + return; + } + + res.writeHead(404, { "content-type": "text/plain" }); + res.end("not found"); +} + +function handleUpgrade(req, socket) { + if (!req.url.startsWith("/eve/deck")) { + socket.end("HTTP/1.1 404 Not Found\r\n\r\n"); + return; + } + + const key = req.headers["sec-websocket-key"]; + if (!key) { + socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); + return; + } + + const accept = crypto + .createHash("sha1") + .update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + .digest("base64"); + socket.write( + "HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + `Sec-WebSocket-Accept: ${accept}\r\n\r\n`, + ); + clients.add(socket); + sendFrame(socket, 0x1, Buffer.from(JSON.stringify(currentState), "utf8")); + socket.on("data", (chunk) => handleClientFrame(socket, chunk)); + socket.on("close", () => clients.delete(socket)); + socket.on("error", () => clients.delete(socket)); +} + +function handleClientFrame(socket, chunk) { + const opcode = chunk[0] & 0x0f; + if (opcode === 0x8) { + clients.delete(socket); + socket.end(); + } +} + +function sendFrame(socket, opcode, payload) { + const header = [0x80 | opcode]; + if (payload.length < 126) { + header.push(payload.length); + } else if (payload.length <= 0xffff) { + header.push(126, payload.length >> 8, payload.length & 0xff); + } else { + const length = Buffer.alloc(8); + length.writeBigUInt64BE(BigInt(payload.length)); + header.push(127, ...length); + } + socket.write(Buffer.concat([Buffer.from(header), payload])); +} + +async function buildState() { + version += 1; + const observedAt = new Date().toISOString(); + const [docker, adb, hosts, yggdrasilServices] = await Promise.all([ + dockerSnapshot(), + adbSnapshot(), + hostChecks(), + remoteServices("ygg", ["nginx", "streampixels-web", "streampixels-service", "heimdall", "repixelizer-gui", "bifrost"]), + ]); + + const verses = [ + verse("starfire.local", "Starfire", "coordinator", "active", ["docker", "adb", "eve-provider"]), + verse("nightwing.local", "Nightwing", "dashboard-renderer", hostState(hosts.Nightwing), ["eve-tui", "gpu-worker"]), + verse("eve.ipad", "EVE", "ios-client", hostState(hosts.EVE), ["ssh", "native-eve"]), + verse("periwinkle.android", "Periwinkle", "android-client", adb.devices.length ? "connected" : "waiting", ["adb", "sensor-edge"]), + verse("raven.local", "Raven", "local-peer", hostState(hosts.Raven), ["ssh"]), + verse("yggdrasil.ops", "Yggdrasil", "ops-host", hostState(hosts.Yggdrasil), ["ssh", "https", "services"]), + ]; + + return { + type: "dashboard-state", + schema: "mimir.eve_dashboard_state.v1", + providerId: "odin.allseer", + title: "Odin All-Seer", + version, + updatedAt: observedAt, + selectedNodeId: "verse-starfire.local", + lutPreset: "terminal", + nodes: verses.map((entry, index) => ({ + id: `verse-${entry.verseId}`, + label: `${entry.name}\n${entry.role}`, + kind: "cultmesh-verse", + visible: true, + x: -0.8 + (index % 3) * 0.8, + y: -0.45 + Math.floor(index / 3) * 0.55, + z: 0, + rotation: 0, + scale: 1, + width: 0.42, + height: 0.2, + health: entry.status, + detail: entry.capabilities.join(", "), + })), + surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses }), + }; +} + +function buildPendingState(message) { + return { + type: "dashboard-state", + schema: "mimir.eve_dashboard_state.v1", + providerId: "odin.allseer", + title: "Odin All-Seer", + version, + updatedAt: new Date().toISOString(), + selectedNodeId: "coordinator-starting", + lutPreset: "terminal", + nodes: [], + surface: { + schema: "gamecult.eve.surface.v1", + id: "gamecult.network.status.surface", + title: "Odin", + root: stack("root", [pane("Coordinator", [text("status", message)])]), + assets: [], + }, + }; +} + +function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses }) { + return { + schema: "gamecult.eve.surface.v1", + id: "gamecult.network.status.surface", + title: "Odin", + root: { + id: "network-root", + kind: "stack", + props: { title: "GameCult Network" }, + children: [ + pane("Coordinator", [ + text("observed", `observed ${observedAt}`), + text("authority", "Odin owns Verse discovery, schema awareness, translation planning, and accepted surface publication. Renderers lower only."), + metric("docker-count", "Docker containers", docker.containers.length, docker.state === "ok" ? "ok" : "warn"), + metric("adb-count", "ADB devices", adb.devices.length, adb.devices.length ? "ok" : "warn"), + ]), + pane("Verses", verses.map((entry) => + card(`verse-card-${entry.verseId}`, [ + text(`verse-title-${entry.verseId}`, `${entry.name} :: ${entry.verseId}`), + text(`verse-status-${entry.verseId}`, `${entry.role} / ${entry.status}`), + text(`verse-caps-${entry.verseId}`, entry.capabilities.join(", ")), + ], entry.status), + )), + pane("Hosts", Object.entries(hosts).map(([name, checks]) => + card(`host-${name}`, [ + text(`host-name-${name}`, name), + ...checks.map((check) => text(`host-${name}-${check.name}`, `${check.name}: ${check.state}${check.detail ? ` ${check.detail}` : ""}`)), + ], checks.some((check) => check.state === "open" || check.state === "ok") ? "ok" : "warn"), + )), + pane("Yggdrasil Services", yggdrasilServices.map((service) => + text(`ygg-${service.name}`, `${service.name}: ${service.state}`), + )), + pane("Docker", docker.containers.length + ? docker.containers.slice(0, 10).map((container) => text(`docker-${container.name}`, `${container.name}: ${container.status} (${container.image})`)) + : [text("docker-empty", docker.error || "no running containers")]), + pane("Periwinkle / ADB", adb.devices.length + ? adb.devices.map((device) => text(`adb-${device.serial}`, `${device.serial}: ${device.state}`)) + : [text("adb-empty", adb.error || "no devices")]), + ], + }, + assets: [], + }; +} + +function stack(id, children) { + return { id, kind: "stack", props: {}, children }; +} + +function pane(title, children) { + return { id: stableId(`pane-${title}`), kind: "pane", props: { title }, children }; +} + +function card(id, children, tone = "default") { + return { id, kind: "card", props: { title: id }, style: { tone }, children }; +} + +function text(id, value) { + return { id, kind: "text", props: { text: value }, children: [] }; +} + +function metric(id, label, value, tone) { + return { id, kind: "metric", props: { label, text: `${label}: ${value}`, value, tone }, children: [] }; +} + +function verse(verseId, name, role, status, capabilities) { + return { verseId, name, role, status, capabilities }; +} + +function hostState(checks) { + if (!checks) return "unknown"; + if (checks.some((check) => check.state === "open" || check.state === "ok")) return "reachable"; + if (checks.some((check) => check.state === "timeout")) return "timeout"; + return "offline"; +} + +async function hostChecks() { + return { + Starfire: [{ name: "local", state: "ok", detail: os.hostname() }], + Nightwing: await checksFor("192.168.1.75", [["ssh", 22], ["eve-broker", 8795], ["eve-browser", 8891]]), + Raven: await checksFor("192.168.1.84", [["ssh", 22]]), + EVE: await checksFor("192.168.1.72", [["ssh", 22]]), + Yggdrasil: await checksFor("yggdrasil.gamecult.org", [["ssh", 22], ["http", 80], ["https", 443]]), + }; +} + +async function checksFor(host, specs) { + return Promise.all(specs.map(async ([name, port]) => ({ + name, + ...(await tcpCheck(host, port)), + }))); +} + +function tcpCheck(host, port) { + return new Promise((resolve) => { + const socket = net.createConnection({ host, port, timeout: 900 }); + socket.on("connect", () => { + socket.destroy(); + resolve({ state: "open", detail: `${host}:${port}` }); + }); + socket.on("timeout", () => { + socket.destroy(); + resolve({ state: "timeout", detail: `${host}:${port}` }); + }); + socket.on("error", (error) => { + resolve({ state: "closed", detail: error.code || error.message }); + }); + }); +} + +async function dockerSnapshot() { + try { + const { stdout } = await execFileAsync("docker", ["ps", "--format", "{{json .}}"], { timeout: 3000 }); + const containers = stdout.split(/\r?\n/) + .filter(Boolean) + .map((line) => JSON.parse(line)) + .map((item) => ({ + name: item.Names, + image: item.Image, + status: item.Status, + ports: item.Ports, + })); + return { state: "ok", containers, error: "" }; + } catch (error) { + return { state: "error", containers: [], error: error.message }; + } +} + +async function adbSnapshot() { + const adb = path.join(process.env.LOCALAPPDATA || "", "Android", "Sdk", "platform-tools", "adb.exe"); + if (!fs.existsSync(adb)) { + return { state: "missing", devices: [], error: `missing ${adb}` }; + } + try { + const { stdout } = await execFileAsync(adb, ["devices"], { timeout: 3000 }); + const devices = stdout.split(/\r?\n/) + .map((line) => line.match(/^(\S+)\s+(device|offline|unauthorized)$/)) + .filter(Boolean) + .map((match) => ({ serial: match[1], state: match[2] })); + return { state: "ok", devices, error: "" }; + } catch (error) { + return { state: "error", devices: [], error: error.message }; + } +} + +async function remoteServices(target, services) { + const script = `for s in ${services.join(" ")}; do printf '%s=' "$s"; systemctl is-active "$s" 2>/dev/null || true; done`; + try { + const { stdout } = await execFileAsync("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=3", target, script], { timeout: 6000 }); + return stdout.split(/\r?\n/) + .map((line) => line.match(/^([^=]+)=(.+)$/)) + .filter(Boolean) + .map((match) => ({ name: match[1], state: match[2].trim() })); + } catch (error) { + return services.map((name) => ({ name, state: `unknown: ${error.message}` })); + } +} + +function stableId(value) { + return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); +} + +function parseArgs(argv) { + const parsed = {}; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (!arg.startsWith("--")) continue; + const key = arg.slice(2); + const next = argv[index + 1]; + if (!next || next.startsWith("--")) { + parsed[key] = true; + } else { + parsed[key] = next; + index += 1; + } + } + return parsed; +} From 5dac8d3f184b7640926794a2e0cf36c3b1a2d7ec Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 31 May 2026 16:36:09 +0100 Subject: [PATCH 002/742] Expand Odin all-seer service surface --- README.md | 4 +- docs/architecture.md | 16 ++++++- scripts/stop-odin.ps1 | 8 ++-- src/odin-coordinator.cjs | 93 +++++++++++++++++++++++++++++++++++----- 4 files changed, 103 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 2742f3f..5b7bf65 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ The first executable is deliberately narrow: - publishes provider `odin.allseer`; - writes the latest surface to `scratch/odin/latest-surface.json`; - persists the same document through local CultMesh/CultCache when `CultLib` packages are available at `E:\Projects\CultLib\packages`; -- observes Starfire Docker, Periwinkle ADB, Nightwing, Raven, EVE, and Yggdrasil reachability; +- observes Starfire Docker, Periwinkle ADB, Nightwing services/GPU, Raven, EVE, and Yggdrasil reachability; - exposes Yggdrasil service status when the local SSH alias can reach it. +- publishes explicit `verse` and `service` nodes for compact Eve/CultUI lowerers. The next real cut is to replace static host probes with CultNet schema discovery and CultMesh Verse peer exchange. The probe layer is an input adapter. It must not become the architecture. - diff --git a/docs/architecture.md b/docs/architecture.md index 6c1047c..5224f7d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,6 +9,7 @@ Odin is the central all-seer node for GameCult's CultMesh world: every Verse can ```text host / device / service probes -> Odin observation cycle + -> Verse-owned service records -> Odin state document -> CultMesh/CultCache persistence -> Eve dashboard state @@ -34,6 +35,20 @@ Verse announcement - Translation paths must name source schema, target schema, lossiness, authority, and version. - Renderers lower surfaces only. If a renderer fixes network truth, the machine is split-brained. - CultCache is the durable state substrate; CultNet is the wire vocabulary; CultMesh is the Verse and peer-consensus layer. +- The Eve surface carries explicit `verse` and `service` nodes. Compact renderers may derive visual facets from those nodes, but may not invent observation truth. + +## Current Service Surface + +Odin currently publishes service squares for: + +- Starfire: Odin, Docker, ADB, Odin CultCache, and running Docker containers. +- Nightwing: SSH, Eve broker, Eve browser reference, visible TUI, Docker unit state, and NVIDIA GPU state. +- EVE: SSH/native Eve reachability. +- Periwinkle: ADB reachability. +- Raven: SSH reachability. +- Yggdrasil: SSH/HTTP/HTTPS reachability plus nginx, StreamPixels, Heimdall, Repixelizer, and Bifrost systemd state. + +The compact Nightwing TUI lowers these into dense cells and fills surplus screen space with derived schema, route, owner, and probe facets from the same Odin-owned service records. ## First Translation Model @@ -47,4 +62,3 @@ Odin's translation registry should start as data, not magic: - `notes` No regex tribunals for meaning. Natural-language schema interpretation can be assisted by models later, but accepted translation routes must be inspectable typed state. - diff --git a/scripts/stop-odin.ps1 b/scripts/stop-odin.ps1 index 3b04071..cca0a5f 100644 --- a/scripts/stop-odin.ps1 +++ b/scripts/stop-odin.ps1 @@ -10,12 +10,12 @@ if (-not (Test-Path $pidPath)) { exit 0 } -$pid = (Get-Content -Raw $pidPath).Trim() -if ($pid -match "^\d+$") { - $process = Get-Process -Id ([int] $pid) -ErrorAction SilentlyContinue +$odinPid = (Get-Content -Raw $pidPath).Trim() +if ($odinPid -match "^\d+$") { + $process = Get-Process -Id ([int] $odinPid) -ErrorAction SilentlyContinue if ($process) { Stop-Process -Id $process.Id - Write-Host "Stopped Odin PID $pid." + Write-Host "Stopped Odin PID $odinPid." } } diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index 55919ba..3f4c64e 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -202,20 +202,37 @@ function sendFrame(socket, opcode, payload) { async function buildState() { version += 1; const observedAt = new Date().toISOString(); - const [docker, adb, hosts, yggdrasilServices] = await Promise.all([ + const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu] = 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", "gamecult-visible-ops", "docker"]), + remoteGpu("nightwing"), ]); const verses = [ - verse("starfire.local", "Starfire", "coordinator", "active", ["docker", "adb", "eve-provider"]), - verse("nightwing.local", "Nightwing", "dashboard-renderer", hostState(hosts.Nightwing), ["eve-tui", "gpu-worker"]), - verse("eve.ipad", "EVE", "ios-client", hostState(hosts.EVE), ["ssh", "native-eve"]), - verse("periwinkle.android", "Periwinkle", "android-client", adb.devices.length ? "connected" : "waiting", ["adb", "sensor-edge"]), - verse("raven.local", "Raven", "local-peer", hostState(hosts.Raven), ["ssh"]), - verse("yggdrasil.ops", "Yggdrasil", "ops-host", hostState(hosts.Yggdrasil), ["ssh", "https", "services"]), + verse("starfire.local", "Starfire", "coordinator", "active", ["docker", "adb", "eve-provider"], [ + service("odin", "Odin all-seer", "active", "ws://0.0.0.0:8797/eve/deck"), + service("docker", "Docker", docker.state === "ok" ? "active" : "warn", `${docker.containers.length} running`), + service("adb", "Periwinkle ADB", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), + service("cultcache", "Odin CultCache", fs.existsSync(cachePath) ? "active" : "waiting", path.basename(cachePath)), + ...docker.containers.map((container) => service(`docker-${container.name}`, container.name, "active", container.image)), + ]), + verse("nightwing.local", "Nightwing", "dashboard-renderer", hostState(hosts.Nightwing), ["eve-tui", "gpu-worker"], [ + ...hostServices("Nightwing", hosts.Nightwing), + ...nightwingServices.map((entry) => service(`systemd-${entry.name}`, entry.name, systemdState(entry.state), entry.state)), + service("gpu", "GTX 860M", nightwingGpu.state, nightwingGpu.detail), + ]), + verse("eve.ipad", "EVE", "ios-client", hostState(hosts.EVE), ["ssh", "native-eve"], hostServices("EVE", hosts.EVE)), + verse("periwinkle.android", "Periwinkle", "android-client", adb.devices.length ? "connected" : "waiting", ["adb", "sensor-edge"], [ + service("adb-device", "ADB device", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), + ]), + verse("raven.local", "Raven", "local-peer", hostState(hosts.Raven), ["ssh"], hostServices("Raven", hosts.Raven)), + verse("yggdrasil.ops", "Yggdrasil", "ops-host", hostState(hosts.Yggdrasil), ["ssh", "https", "services"], [ + ...hostServices("Yggdrasil", hosts.Yggdrasil), + ...yggdrasilServices.map((entry) => service(`systemd-${entry.name}`, entry.name, systemdState(entry.state), entry.state)), + ]), ]; return { @@ -274,8 +291,12 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse title: "Odin", root: { id: "network-root", - kind: "stack", - props: { title: "GameCult Network" }, + kind: "dashboard", + props: { + title: "Odin All-Seer", + observedAt, + summary: `${verses.length} Verses / ${verses.reduce((sum, entry) => sum + entry.services.length, 0)} service squares`, + }, children: [ pane("Coordinator", [ text("observed", `observed ${observedAt}`), @@ -283,6 +304,24 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse metric("docker-count", "Docker containers", docker.containers.length, docker.state === "ok" ? "ok" : "warn"), metric("adb-count", "ADB devices", adb.devices.length, adb.devices.length ? "ok" : "warn"), ]), + ...verses.map((entry) => ({ + id: `verse-${entry.verseId}`, + kind: "verse", + props: { + title: entry.name, + verseId: entry.verseId, + role: entry.role, + status: entry.status, + capabilities: entry.capabilities, + services: entry.services, + }, + children: entry.services.map((item) => ({ + id: `service-${entry.verseId}-${item.id}`, + kind: "service", + props: item, + children: [], + })), + })), pane("Verses", verses.map((entry) => card(`verse-card-${entry.verseId}`, [ text(`verse-title-${entry.verseId}`, `${entry.name} :: ${entry.verseId}`), @@ -331,8 +370,25 @@ function metric(id, label, value, tone) { return { id, kind: "metric", props: { label, text: `${label}: ${value}`, value, tone }, children: [] }; } -function verse(verseId, name, role, status, capabilities) { - return { verseId, name, role, status, capabilities }; +function verse(verseId, name, role, status, capabilities, services = []) { + return { verseId, name, role, status, capabilities, services }; +} + +function service(id, name, state, detail = "") { + return { id: stableId(id), name, state, detail: String(detail || "") }; +} + +function hostServices(hostName, checks) { + return (checks || []).map((check) => + service(`${hostName}-${check.name}`, `${hostName} ${check.name}`, check.state === "open" || check.state === "ok" ? "active" : check.state, check.detail), + ); +} + +function systemdState(state) { + if (state === "active") return "active"; + if (state === "inactive") return "inactive"; + if (state === "failed") return "failed"; + return state.startsWith("unknown") ? "unknown" : "warn"; } function hostState(checks) { @@ -424,6 +480,21 @@ async function remoteServices(target, services) { } } +async function remoteGpu(target) { + try { + const { stdout } = await execFileAsync("ssh", [ + "-o", "BatchMode=yes", + "-o", "ConnectTimeout=3", + target, + "nvidia-smi --query-gpu=name,driver_version,memory.total,utilization.gpu --format=csv,noheader,nounits 2>/dev/null | head -n 1", + ], { timeout: 6000 }); + const detail = stdout.trim(); + return detail ? { state: "active", detail } : { state: "unknown", detail: "nvidia-smi returned no GPU" }; + } catch (error) { + return { state: "unknown", detail: error.message }; + } +} + function stableId(value) { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } From 2ae3ea3411008b54db494844db32b340163493ef Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 31 May 2026 16:43:58 +0100 Subject: [PATCH 003/742] Ingest VoidBot Eve interface surface --- README.md | 1 + docs/architecture.md | 9 ++ src/odin-coordinator.cjs | 182 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 188 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5b7bf65..6e12fb5 100644 --- a/README.md +++ b/README.md @@ -45,5 +45,6 @@ The first executable is deliberately narrow: - observes Starfire Docker, Periwinkle ADB, Nightwing services/GPU, Raven, EVE, and Yggdrasil reachability; - exposes Yggdrasil service status when the local SSH alias can reach it. - publishes explicit `verse` and `service` nodes for compact Eve/CultUI lowerers. +- ingests the `voidbot.swarm` Eve/CultUI dashboard from Mimir's `/eve/deck` provider surface and embeds it as an Odin `interface` node. The next real cut is to replace static host probes with CultNet schema discovery and CultMesh Verse peer exchange. The probe layer is an input adapter. It must not become the architecture. diff --git a/docs/architecture.md b/docs/architecture.md index 5224f7d..70a2c95 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,8 +8,10 @@ Odin is the central all-seer node for GameCult's CultMesh world: every Verse can ```text host / device / service probes + + Eve/CultUI provider surfaces -> Odin observation cycle -> Verse-owned service records + -> Provider-owned interface records -> Odin state document -> CultMesh/CultCache persistence -> Eve dashboard state @@ -33,6 +35,7 @@ Verse announcement - Odin owns the accepted registry of known Verses. - A Verse owns its own schemas and authority model; Odin indexes and translates, it does not silently rewrite local truth. - Translation paths must name source schema, target schema, lossiness, authority, and version. +- Service dashboards are CultMesh/Eve/CultUI interface projections. Odin aggregates those projection graphs; it does not replace them with nameplate summaries. - Renderers lower surfaces only. If a renderer fixes network truth, the machine is split-brained. - CultCache is the durable state substrate; CultNet is the wire vocabulary; CultMesh is the Verse and peer-consensus layer. - The Eve surface carries explicit `verse` and `service` nodes. Compact renderers may derive visual facets from those nodes, but may not invent observation truth. @@ -50,6 +53,12 @@ Odin currently publishes service squares for: The compact Nightwing TUI lowers these into dense cells and fills surplus screen space with derived schema, route, owner, and probe facets from the same Odin-owned service records. +## Current Interface Surface + +Odin currently ingests `voidbot.swarm` from the local Mimir Eve deck at `ws://127.0.0.1:8795/eve/deck`. That provider owns the VoidBot swarm composition: CTB rail, selected Face summary, state graph, and state detail. Odin embeds the provider's `surface.root` as an `interface` child with provenance, version, status, and source endpoint. + +This is the model for future services: if a service publishes an operator interface, ingest the Eve/CultUI composition graph and lower it. Do not collapse it into a service-status tile unless the graph is unavailable and the tile is explicitly a temporary probe. + ## First Translation Model Odin's translation registry should start as data, not magic: diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index 3f4c64e..8d4b3ff 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -36,6 +36,7 @@ const intervalMs = Number(args.intervalMs || 5000); const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin"); const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp"); const surfaceKey = "surface:gamecult.network.status"; +const eveDeckUrl = args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck"; fs.mkdirSync(stateDir, { recursive: true }); @@ -202,13 +203,14 @@ function sendFrame(socket, opcode, payload) { async function buildState() { version += 1; const observedAt = new Date().toISOString(); - const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu] = await Promise.all([ + const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, voidBotDashboard] = 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", "gamecult-visible-ops", "docker"]), remoteGpu("nightwing"), + fetchEveProvider(eveDeckUrl, "voidbot.swarm"), ]); const verses = [ @@ -217,6 +219,7 @@ async function buildState() { service("docker", "Docker", docker.state === "ok" ? "active" : "warn", `${docker.containers.length} running`), service("adb", "Periwinkle ADB", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), service("cultcache", "Odin CultCache", fs.existsSync(cachePath) ? "active" : "waiting", path.basename(cachePath)), + service("voidbot-swarm", "VoidBot Swarm", voidBotDashboard.state, voidBotDashboard.detail), ...docker.containers.map((container) => service(`docker-${container.name}`, container.name, "active", container.image)), ]), verse("nightwing.local", "Nightwing", "dashboard-renderer", hostState(hosts.Nightwing), ["eve-tui", "gpu-worker"], [ @@ -259,7 +262,7 @@ async function buildState() { health: entry.status, detail: entry.capabilities.join(", "), })), - surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses }), + surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces: [voidBotDashboard] }), }; } @@ -284,7 +287,8 @@ function buildPendingState(message) { }; } -function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses }) { +function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces }) { + const activeInterfaces = interfaces.filter((entry) => entry.surface?.root); return { schema: "gamecult.eve.surface.v1", id: "gamecult.network.status.surface", @@ -295,7 +299,7 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse props: { title: "Odin All-Seer", observedAt, - summary: `${verses.length} Verses / ${verses.reduce((sum, entry) => sum + entry.services.length, 0)} service squares`, + summary: `${verses.length} Verses / ${verses.reduce((sum, entry) => sum + entry.services.length, 0)} services / ${activeInterfaces.length} interfaces`, }, children: [ pane("Coordinator", [ @@ -304,6 +308,22 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse metric("docker-count", "Docker containers", docker.containers.length, docker.state === "ok" ? "ok" : "warn"), metric("adb-count", "ADB devices", adb.devices.length, adb.devices.length ? "ok" : "warn"), ]), + ...interfaces.map((entry) => ({ + id: `interface-${stableId(entry.providerId)}`, + kind: "interface", + props: { + title: entry.title, + providerId: entry.providerId, + source: entry.source, + status: entry.state, + detail: entry.detail, + version: entry.version, + updatedAt: entry.updatedAt, + }, + children: entry.surface?.root ? [entry.surface.root] : [ + text(`interface-${stableId(entry.providerId)}-missing`, entry.detail || "interface unavailable"), + ], + })), ...verses.map((entry) => ({ id: `verse-${entry.verseId}`, kind: "verse", @@ -495,6 +515,160 @@ async function remoteGpu(target) { } } +async function fetchEveProvider(url, providerId) { + try { + const socket = await openWebSocket(url); + try { + sendClientFrame(socket, JSON.stringify({ type: "open-provider", providerId })); + for (let index = 0; index < 8; index += 1) { + const message = await readServerTextFrame(socket, 2500); + const state = JSON.parse(message); + if (state?.providerId === providerId) { + return { + providerId, + title: state.title || providerId, + state: "active", + detail: `${state.surface?.root?.kind || "surface"} ${state.nodes?.length || 0} nodes`, + version: state.version, + updatedAt: state.updatedAt, + source: url, + surface: state.surface, + }; + } + } + return dashboardUnavailable(providerId, url, "provider did not publish matching state"); + } finally { + socket.destroy(); + } + } catch (error) { + return dashboardUnavailable(providerId, url, error.message); + } +} + +function dashboardUnavailable(providerId, source, detail) { + return { + providerId, + title: providerId, + state: "unavailable", + detail, + version: 0, + updatedAt: new Date().toISOString(), + source, + surface: null, + }; +} + +function openWebSocket(url) { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const port = Number(parsed.port || (parsed.protocol === "wss:" ? 443 : 80)); + const socket = net.createConnection({ host: parsed.hostname, port, timeout: 2500 }); + const key = crypto.randomBytes(16).toString("base64"); + const pathName = `${parsed.pathname || "/"}${parsed.search || ""}`; + let buffer = Buffer.alloc(0); + socket.on("connect", () => { + socket.write([ + `GET ${pathName} HTTP/1.1`, + `Host: ${parsed.hostname}:${port}`, + "Upgrade: websocket", + "Connection: Upgrade", + `Sec-WebSocket-Key: ${key}`, + "Sec-WebSocket-Version: 13", + "", + "", + ].join("\r\n")); + }); + socket.on("data", function onHandshake(chunk) { + buffer = Buffer.concat([buffer, chunk]); + const marker = buffer.indexOf("\r\n\r\n"); + if (marker < 0) return; + const header = buffer.subarray(0, marker).toString("latin1"); + if (!header.startsWith("HTTP/1.1 101")) { + reject(new Error(header.split(/\r?\n/)[0] || "websocket handshake failed")); + socket.destroy(); + return; + } + socket.off("data", onHandshake); + socket.unshift(buffer.subarray(marker + 4)); + resolve(socket); + }); + socket.on("timeout", () => { + reject(new Error("websocket connection timed out")); + socket.destroy(); + }); + socket.on("error", reject); + }); +} + +function sendClientFrame(socket, textValue) { + const payload = Buffer.from(textValue, "utf8"); + const mask = crypto.randomBytes(4); + const header = [0x81]; + if (payload.length < 126) { + header.push(0x80 | payload.length); + } else if (payload.length <= 0xffff) { + header.push(0x80 | 126, payload.length >> 8, payload.length & 0xff); + } else { + const length = Buffer.alloc(8); + length.writeBigUInt64BE(BigInt(payload.length)); + header.push(0x80 | 127, ...length); + } + const masked = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4])); + socket.write(Buffer.concat([Buffer.from(header), mask, masked])); +} + +function readServerTextFrame(socket, timeoutMs) { + return new Promise((resolve, reject) => { + let buffer = Buffer.alloc(0); + const timer = setTimeout(() => cleanup(new Error("timed out waiting for dashboard frame")), timeoutMs); + function cleanup(error, value) { + clearTimeout(timer); + socket.off("data", onData); + socket.off("error", onError); + if (error) reject(error); + else resolve(value); + } + function onError(error) { + cleanup(error); + } + function onData(chunk) { + buffer = Buffer.concat([buffer, chunk]); + const frame = tryReadFrame(buffer); + if (!frame) return; + buffer = buffer.subarray(frame.consumed); + if (frame.opcode === 0x1) cleanup(null, frame.payload.toString("utf8")); + if (frame.opcode === 0x8) cleanup(new Error("dashboard websocket closed")); + } + socket.on("data", onData); + socket.on("error", onError); + }); +} + +function tryReadFrame(buffer) { + if (buffer.length < 2) return null; + const opcode = buffer[0] & 0x0f; + const masked = Boolean(buffer[1] & 0x80); + let length = buffer[1] & 0x7f; + let offset = 2; + if (length === 126) { + if (buffer.length < offset + 2) return null; + length = buffer.readUInt16BE(offset); + offset += 2; + } else if (length === 127) { + if (buffer.length < offset + 8) return null; + length = Number(buffer.readBigUInt64BE(offset)); + offset += 8; + } + const mask = masked ? buffer.subarray(offset, offset + 4) : null; + if (masked) offset += 4; + if (buffer.length < offset + length) return null; + let payload = buffer.subarray(offset, offset + length); + if (mask) { + payload = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4])); + } + return { opcode, payload, consumed: offset + length }; +} + function stableId(value) { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } From b5df9a831d7113d5815e326c22e75c6e801af019 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 31 May 2026 16:51:46 +0100 Subject: [PATCH 004/742] Publish device observation streams through Odin --- README.md | 3 +- docs/architecture.md | 12 ++- src/odin-coordinator.cjs | 178 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 187 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6e12fb5..1e56ccd 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ It is not the renderer. It is not Eve. It is not a Starfire utility script weari ## Authority Map - Owner: Odin owns the network-wide Verse registry, schema catalog index, translation map, and the accepted `odin.allseer` Eve surface. -- Inputs: CultMesh/CultNet peer announcements, schema catalog responses, local host probes, Docker/ADB host facts, SSH-reachable ops hosts, and later direct Verse subscriptions. +- Inputs: CultMesh/CultNet peer announcements, schema catalog responses, local host probes, Docker/ADB host facts, SSH-reachable ops hosts, Mimir's normalized Eve observation ledger, and later direct Verse subscriptions. - Outputs: CultCache-backed Odin state, CultMesh documents, CultNet schema/catalog messages, and an Eve-compatible `/eve/deck` surface for dashboards. - Derived state: Nightwing's attached TUI, browser dashboards, and future Eve clients are projections of Odin state. - Forbidden writers: renderers do not probe the network or decide Verse truth; individual projects do not maintain private incompatible discovery ledgers once Odin can see them. @@ -45,6 +45,7 @@ The first executable is deliberately narrow: - observes Starfire Docker, Periwinkle ADB, Nightwing services/GPU, Raven, EVE, and Yggdrasil reachability; - exposes Yggdrasil service status when the local SSH alias can reach it. - publishes explicit `verse` and `service` nodes for compact Eve/CultUI lowerers. +- publishes explicit `observation-stream` nodes for Periwinkle/EVE sensor, microphone, touch, and camera stream summaries when Mimir's CultMesh observation ledger is present. - ingests the `voidbot.swarm` Eve/CultUI dashboard from Mimir's `/eve/deck` provider surface and embeds it as an Odin `interface` node. The next real cut is to replace static host probes with CultNet schema discovery and CultMesh Verse peer exchange. The probe layer is an input adapter. It must not become the architecture. diff --git a/docs/architecture.md b/docs/architecture.md index 70a2c95..03a179f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,9 +8,11 @@ Odin is the central all-seer node for GameCult's CultMesh world: every Verse can ```text host / device / service probes + + Mimir device observation ledger + Eve/CultUI provider surfaces -> Odin observation cycle -> Verse-owned service records + -> Device observation-stream records -> Provider-owned interface records -> Odin state document -> CultMesh/CultCache persistence @@ -34,11 +36,12 @@ Verse announcement - Odin owns the accepted registry of known Verses. - A Verse owns its own schemas and authority model; Odin indexes and translates, it does not silently rewrite local truth. +- Device clients own sensor and media capture; Mimir owns the normalized ingest ledger; Odin owns the aggregate operator projection. - Translation paths must name source schema, target schema, lossiness, authority, and version. - Service dashboards are CultMesh/Eve/CultUI interface projections. Odin aggregates those projection graphs; it does not replace them with nameplate summaries. - Renderers lower surfaces only. If a renderer fixes network truth, the machine is split-brained. - CultCache is the durable state substrate; CultNet is the wire vocabulary; CultMesh is the Verse and peer-consensus layer. -- The Eve surface carries explicit `verse` and `service` nodes. Compact renderers may derive visual facets from those nodes, but may not invent observation truth. +- The Eve surface carries explicit `verse`, `service`, and `observation-stream` nodes. Compact renderers may derive visual facets from those nodes, but may not invent observation truth. ## Current Service Surface @@ -48,6 +51,7 @@ Odin currently publishes service squares for: - Nightwing: SSH, Eve broker, Eve browser reference, visible TUI, Docker unit state, and NVIDIA GPU state. - EVE: SSH/native Eve reachability. - Periwinkle: ADB reachability. +- Periwinkle: latest typed motion, microphone, touch, and camera stream summaries from Mimir's CultMesh observation ledger when present. - Raven: SSH reachability. - Yggdrasil: SSH/HTTP/HTTPS reachability plus nginx, StreamPixels, Heimdall, Repixelizer, and Bifrost systemd state. @@ -59,6 +63,12 @@ Odin currently ingests `voidbot.swarm` from the local Mimir Eve deck at `ws://12 This is the model for future services: if a service publishes an operator interface, ingest the Eve/CultUI composition graph and lower it. Do not collapse it into a service-status tile unless the graph is unavailable and the tile is explicitly a temporary probe. +## Current Observation Surface + +Odin tails Mimir's normalized observation JSONL at `E:\Projects\Mimir\artifacts\runtime\periwinkle-cultmesh-sensors.out.log` by default. It accepts the typed CultMesh records `mimir.eve_sensor_observation.v1` and `mimir.eve_media_observation.v1`, keeps only the latest record for each `(deviceId, streamId, kind)`, and publishes those summaries as `observation-stream` nodes inside `odin.allseer`. Streams are considered active for 120 seconds by default so a compact dashboard refresh does not turn a briefly quiet sensor into false red noise. + +This is an ingest projection, not a new capture authority. Device clients still own capture, Mimir still owns the evidence ledger, and Odin only publishes the dashboard-ready state that the Eve GUI and Nightwing TUI lower. + ## First Translation Model Odin's translation registry should start as data, not magic: diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index 8d4b3ff..da85106 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -37,6 +37,8 @@ const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin"); const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp"); const surfaceKey = "surface:gamecult.network.status"; const eveDeckUrl = args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck"; +const observationLogPath = args.observationLogPath || path.join(repoRoot, "..", "Mimir", "artifacts", "runtime", "periwinkle-cultmesh-sensors.out.log"); +const observationFreshSeconds = Number(args.observationFreshSeconds || 120); fs.mkdirSync(stateDir, { recursive: true }); @@ -203,7 +205,7 @@ function sendFrame(socket, opcode, payload) { async function buildState() { version += 1; const observedAt = new Date().toISOString(); - const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, voidBotDashboard] = await Promise.all([ + const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, voidBotDashboard, observations] = await Promise.all([ dockerSnapshot(), adbSnapshot(), hostChecks(), @@ -211,6 +213,7 @@ async function buildState() { remoteServices("nightwing", ["ssh", "nightwing-eve-dashboard", "nightwing-eve-browser-reference", "gamecult-visible-ops", "docker"]), remoteGpu("nightwing"), fetchEveProvider(eveDeckUrl, "voidbot.swarm"), + observationSnapshot(observationLogPath, observationFreshSeconds), ]); const verses = [ @@ -220,6 +223,7 @@ async function buildState() { service("adb", "Periwinkle ADB", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), service("cultcache", "Odin CultCache", fs.existsSync(cachePath) ? "active" : "waiting", path.basename(cachePath)), service("voidbot-swarm", "VoidBot Swarm", voidBotDashboard.state, voidBotDashboard.detail), + service("mimir-observation-ledger", "Mimir Observation Ledger", observations.state, observations.detail), ...docker.containers.map((container) => service(`docker-${container.name}`, container.name, "active", container.image)), ]), verse("nightwing.local", "Nightwing", "dashboard-renderer", hostState(hosts.Nightwing), ["eve-tui", "gpu-worker"], [ @@ -230,6 +234,7 @@ async function buildState() { verse("eve.ipad", "EVE", "ios-client", hostState(hosts.EVE), ["ssh", "native-eve"], hostServices("EVE", hosts.EVE)), verse("periwinkle.android", "Periwinkle", "android-client", adb.devices.length ? "connected" : "waiting", ["adb", "sensor-edge"], [ service("adb-device", "ADB device", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), + ...observationServices(observations, "periwinkle"), ]), verse("raven.local", "Raven", "local-peer", hostState(hosts.Raven), ["ssh"], hostServices("Raven", hosts.Raven)), verse("yggdrasil.ops", "Yggdrasil", "ops-host", hostState(hosts.Yggdrasil), ["ssh", "https", "services"], [ @@ -262,7 +267,7 @@ async function buildState() { health: entry.status, detail: entry.capabilities.join(", "), })), - surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces: [voidBotDashboard] }), + surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces: [voidBotDashboard], observations }), }; } @@ -287,8 +292,9 @@ function buildPendingState(message) { }; } -function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces }) { +function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces, observations }) { const activeInterfaces = interfaces.filter((entry) => entry.surface?.root); + const activeObservationStreams = observations.streams.filter((entry) => entry.state === "active"); return { schema: "gamecult.eve.surface.v1", id: "gamecult.network.status.surface", @@ -299,7 +305,7 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse props: { title: "Odin All-Seer", observedAt, - summary: `${verses.length} Verses / ${verses.reduce((sum, entry) => sum + entry.services.length, 0)} services / ${activeInterfaces.length} interfaces`, + summary: `${verses.length} Verses / ${verses.reduce((sum, entry) => sum + entry.services.length, 0)} services / ${activeInterfaces.length} interfaces / ${activeObservationStreams.length} live streams`, }, children: [ pane("Coordinator", [ @@ -307,7 +313,10 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse text("authority", "Odin owns Verse discovery, schema awareness, translation planning, and accepted surface publication. Renderers lower only."), metric("docker-count", "Docker containers", docker.containers.length, docker.state === "ok" ? "ok" : "warn"), metric("adb-count", "ADB devices", adb.devices.length, adb.devices.length ? "ok" : "warn"), + metric("observation-stream-count", "Observation streams", observations.streams.length, activeObservationStreams.length ? "ok" : "warn"), + text("observation-ledger", `observation ledger: ${observations.detail}`), ]), + observationPane(observations), ...interfaces.map((entry) => ({ id: `interface-${stableId(entry.providerId)}`, kind: "interface", @@ -370,6 +379,35 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse }; } +function observationPane(observations) { + return { + id: "observation-streams", + kind: "pane", + props: { + title: "Device Observation Streams", + source: observations.source, + status: observations.state, + detail: observations.detail, + }, + children: observations.streams.length + ? observations.streams.map(observationStreamNode) + : [text("observation-streams-empty", observations.detail || "no observation streams")], + }; +} + +function observationStreamNode(entry) { + return { + id: `observation-stream-${stableId(entry.deviceId)}-${stableId(entry.streamId)}-${stableId(entry.kind)}`, + kind: "observation-stream", + props: entry, + children: [ + text(`observation-${stableId(entry.streamId)}-schema`, entry.document), + text(`observation-${stableId(entry.streamId)}-latest`, `${entry.kind} seq ${entry.sequence} age ${entry.ageSeconds}s`), + text(`observation-${stableId(entry.streamId)}-shape`, observationShape(entry)), + ], + }; +} + function stack(id, children) { return { id, kind: "stack", props: {}, children }; } @@ -398,6 +436,17 @@ function service(id, name, state, detail = "") { return { id: stableId(id), name, state, detail: String(detail || "") }; } +function observationServices(observations, deviceId) { + return observations.streams + .filter((entry) => entry.deviceId === deviceId) + .map((entry) => service( + `observation-${entry.streamId}-${entry.kind}`, + `${entry.kind} observation`, + entry.state, + observationShape(entry), + )); +} + function hostServices(hostName, checks) { return (checks || []).map((check) => service(`${hostName}-${check.name}`, `${hostName} ${check.name}`, check.state === "open" || check.state === "ok" ? "active" : check.state, check.detail), @@ -487,6 +536,127 @@ async function adbSnapshot() { } } +async function observationSnapshot(source, freshSeconds) { + if (!fs.existsSync(source)) { + return { + state: "missing", + source, + detail: `missing ${source}`, + streams: [], + }; + } + + try { + const lines = tailTextFile(source, 512 * 1024) + .split(/\r?\n/) + .filter(Boolean); + const nowMs = Date.now(); + const byStream = new Map(); + let accepted = 0; + for (const line of lines) { + let item; + try { + item = JSON.parse(line); + } catch { + continue; + } + + if (item.type !== "cultmesh-observation" && item.type !== "cultmesh-media-observation") { + continue; + } + + accepted += 1; + const key = `${item.DeviceId || "unknown"}:${item.StreamId || "unknown"}:${item.Kind || "unknown"}`; + byStream.set(key, item); + } + + const streams = [...byStream.values()] + .map((item) => observationStream(item, nowMs, freshSeconds)) + .sort((left, right) => left.deviceId.localeCompare(right.deviceId) + || left.streamId.localeCompare(right.streamId) + || left.kind.localeCompare(right.kind)); + + const activeCount = streams.filter((entry) => entry.state === "active").length; + return { + state: streams.length ? (activeCount ? "active" : "stale") : "waiting", + source, + detail: `${streams.length} streams, ${activeCount} active, ${accepted} recent records`, + streams, + }; + } catch (error) { + return { + state: "error", + source, + detail: error.message, + streams: [], + }; + } +} + +function tailTextFile(filePath, maxBytes) { + const stat = fs.statSync(filePath); + const length = Math.min(stat.size, maxBytes); + const buffer = Buffer.alloc(length); + const fd = fs.openSync(filePath, "r"); + try { + fs.readSync(fd, buffer, 0, length, stat.size - length); + } finally { + fs.closeSync(fd); + } + + return buffer.toString("utf8"); +} + +function observationStream(item, nowMs, freshSeconds) { + const latestMs = Date.parse(item.WallClockUtc || ""); + const ageSeconds = Number.isFinite(latestMs) ? Math.max(0, Math.round((nowMs - latestMs) / 1000)) : null; + return { + deviceId: String(item.DeviceId || "unknown"), + streamId: String(item.StreamId || "unknown"), + kind: String(item.Kind || "unknown"), + document: String(item.document || "unknown"), + state: ageSeconds === null ? "unknown" : ageSeconds <= freshSeconds ? "active" : "stale", + sequence: item.Sequence ?? null, + latestAt: item.WallClockUtc || "", + ageSeconds, + clockDomainId: item.ClockDomainId || "", + format: item.Format || "", + width: item.Width ?? null, + height: item.Height ?? null, + sampleRate: item.SampleRate ?? null, + channels: item.Channels ?? null, + frameCount: item.FrameCount ?? null, + payloadEncoding: item.PayloadEncoding || "", + payloadBytes: item.PayloadBytes ?? null, + values: Array.isArray(item.Values) ? item.Values : null, + accuracy: item.Accuracy ?? null, + action: item.Action ?? null, + pointerCount: item.PointerCount ?? null, + x: item.X ?? null, + y: item.Y ?? null, + }; +} + +function observationShape(entry) { + if (entry.width && entry.height) { + return `${entry.format || "media"} ${entry.width}x${entry.height}, ${entry.payloadBytes || 0} bytes`; + } + + if (entry.sampleRate) { + return `${entry.format || "audio"} ${entry.sampleRate} Hz x ${entry.channels || 1}, ${entry.frameCount || 0} frames, ${entry.payloadBytes || 0} bytes`; + } + + if (entry.values) { + return `${entry.values.map((value) => Number(value).toFixed(3)).join(", ")} accuracy ${entry.accuracy ?? "unknown"}`; + } + + if (entry.action) { + return `${entry.action} pointers ${entry.pointerCount ?? "unknown"} @ ${entry.x ?? "?"},${entry.y ?? "?"}`; + } + + return entry.state; +} + async function remoteServices(target, services) { const script = `for s in ${services.join(" ")}; do printf '%s=' "$s"; systemctl is-active "$s" 2>/dev/null || true; done`; try { From f47b8940a6d3ecd3d25d8bfcaee60c01851d0471 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 31 May 2026 17:02:26 +0100 Subject: [PATCH 005/742] Ingest Mimir live stats surface --- README.md | 2 +- docs/architecture.md | 2 +- src/odin-coordinator.cjs | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1e56ccd..f6ca355 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,6 @@ The first executable is deliberately narrow: - exposes Yggdrasil service status when the local SSH alias can reach it. - publishes explicit `verse` and `service` nodes for compact Eve/CultUI lowerers. - publishes explicit `observation-stream` nodes for Periwinkle/EVE sensor, microphone, touch, and camera stream summaries when Mimir's CultMesh observation ledger is present. -- ingests the `voidbot.swarm` Eve/CultUI dashboard from Mimir's `/eve/deck` provider surface and embeds it as an Odin `interface` node. +- ingests the `mimir.live.stats` and `voidbot.swarm` Eve/CultUI dashboards from Mimir's `/eve/deck` provider surfaces and embeds them as Odin `interface` nodes. The next real cut is to replace static host probes with CultNet schema discovery and CultMesh Verse peer exchange. The probe layer is an input adapter. It must not become the architecture. diff --git a/docs/architecture.md b/docs/architecture.md index 03a179f..c989e59 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -59,7 +59,7 @@ The compact Nightwing TUI lowers these into dense cells and fills surplus screen ## Current Interface Surface -Odin currently ingests `voidbot.swarm` from the local Mimir Eve deck at `ws://127.0.0.1:8795/eve/deck`. That provider owns the VoidBot swarm composition: CTB rail, selected Face summary, state graph, and state detail. Odin embeds the provider's `surface.root` as an `interface` child with provenance, version, status, and source endpoint. +Odin currently ingests `mimir.live.stats` and `voidbot.swarm` from the local Mimir Eve deck at `ws://127.0.0.1:8795/eve/deck`. `mimir.live.stats` owns the compact Mimir telemetry composition: RMS bars, sync confidence, runtime buffer facts, actuator state, and device observation streams. `voidbot.swarm` owns the VoidBot swarm composition: CTB rail, selected Face summary, state graph, and state detail. Odin embeds each provider's `surface.root` as an `interface` child with provenance, version, status, and source endpoint. This is the model for future services: if a service publishes an operator interface, ingest the Eve/CultUI composition graph and lower it. Do not collapse it into a service-status tile unless the graph is unavailable and the tile is explicitly a temporary probe. diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index da85106..9c0ee54 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -205,7 +205,7 @@ function sendFrame(socket, opcode, payload) { async function buildState() { version += 1; const observedAt = new Date().toISOString(); - const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, voidBotDashboard, observations] = await Promise.all([ + const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, voidBotDashboard, mimirLiveStats, observations] = await Promise.all([ dockerSnapshot(), adbSnapshot(), hostChecks(), @@ -213,6 +213,7 @@ async function buildState() { remoteServices("nightwing", ["ssh", "nightwing-eve-dashboard", "nightwing-eve-browser-reference", "gamecult-visible-ops", "docker"]), remoteGpu("nightwing"), fetchEveProvider(eveDeckUrl, "voidbot.swarm"), + fetchEveProvider(eveDeckUrl, "mimir.live.stats"), observationSnapshot(observationLogPath, observationFreshSeconds), ]); @@ -223,6 +224,7 @@ async function buildState() { service("adb", "Periwinkle ADB", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), service("cultcache", "Odin CultCache", fs.existsSync(cachePath) ? "active" : "waiting", path.basename(cachePath)), service("voidbot-swarm", "VoidBot Swarm", voidBotDashboard.state, voidBotDashboard.detail), + service("mimir-live-stats", "Mimir Live Stats", mimirLiveStats.state, mimirLiveStats.detail), service("mimir-observation-ledger", "Mimir Observation Ledger", observations.state, observations.detail), ...docker.containers.map((container) => service(`docker-${container.name}`, container.name, "active", container.image)), ]), @@ -267,7 +269,7 @@ async function buildState() { health: entry.status, detail: entry.capabilities.join(", "), })), - surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces: [voidBotDashboard], observations }), + surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces: [mimirLiveStats, voidBotDashboard], observations }), }; } From cb996eaec67384662b160c42c3ed9f7ce683504a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 31 May 2026 17:10:24 +0100 Subject: [PATCH 006/742] Discover Eve interfaces and persist layout intents --- README.md | 4 +- docs/architecture.md | 4 +- src/odin-coordinator.cjs | 179 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 176 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f6ca355..5f2bac4 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ The first executable is deliberately narrow: - exposes Yggdrasil service status when the local SSH alias can reach it. - publishes explicit `verse` and `service` nodes for compact Eve/CultUI lowerers. - publishes explicit `observation-stream` nodes for Periwinkle/EVE sensor, microphone, touch, and camera stream summaries when Mimir's CultMesh observation ledger is present. -- ingests the `mimir.live.stats` and `voidbot.swarm` Eve/CultUI dashboards from Mimir's `/eve/deck` provider surfaces and embeds them as Odin `interface` nodes. +- discovers Eve deck provider manifests from known/LAN deck endpoints; +- ingests provider-owned Eve/CultUI dashboards, including `mimir.live.stats` and `voidbot.swarm`, and embeds them as Odin `interface` nodes; +- persists operator tiling intent as `odin.interface_layout.v1` under ignored `scratch/odin/interface-layout.json`. The next real cut is to replace static host probes with CultNet schema discovery and CultMesh Verse peer exchange. The probe layer is an input adapter. It must not become the architecture. diff --git a/docs/architecture.md b/docs/architecture.md index c989e59..27910b0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -59,10 +59,12 @@ The compact Nightwing TUI lowers these into dense cells and fills surplus screen ## Current Interface Surface -Odin currently ingests `mimir.live.stats` and `voidbot.swarm` from the local Mimir Eve deck at `ws://127.0.0.1:8795/eve/deck`. `mimir.live.stats` owns the compact Mimir telemetry composition: RMS bars, sync confidence, runtime buffer facts, actuator state, and device observation streams. `voidbot.swarm` owns the VoidBot swarm composition: CTB rail, selected Face summary, state graph, and state detail. Odin embeds each provider's `surface.root` as an `interface` child with provenance, version, status, and source endpoint. +Odin discovers Eve deck provider manifests from known deck endpoints and LAN hosts exposing the standard Eve deck port. It then ingests each provider's Eve/CultUI state through the provider switch command path. Providers own their compositions; Odin embeds each provider's `surface.root` as an `interface` child with provenance, version, status, source endpoint, and layout metadata. This is the model for future services: if a service publishes an operator interface, ingest the Eve/CultUI composition graph and lower it. Do not collapse it into a service-status tile unless the graph is unavailable and the tile is explicitly a temporary probe. +Odin persists operator layout intent as `odin.interface_layout.v1` under `scratch/odin/interface-layout.json` for the current Starfire body. The durable CultMesh document should replace this local file once the layout schema is promoted. Layout intents name the provider id and request focus, move, resize, or visibility changes; renderers are input devices for those intents, not local layout owners. + ## Current Observation Surface Odin tails Mimir's normalized observation JSONL at `E:\Projects\Mimir\artifacts\runtime\periwinkle-cultmesh-sensors.out.log` by default. It accepts the typed CultMesh records `mimir.eve_sensor_observation.v1` and `mimir.eve_media_observation.v1`, keeps only the latest record for each `(deviceId, streamId, kind)`, and publishes those summaries as `observation-stream` nodes inside `odin.allseer`. Streams are considered active for 120 seconds by default so a compact dashboard refresh does not turn a briefly quiet sensor into false red noise. diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index 9c0ee54..83d0144 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -36,7 +36,11 @@ const intervalMs = Number(args.intervalMs || 5000); const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin"); const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp"); const surfaceKey = "surface:gamecult.network.status"; -const eveDeckUrl = args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck"; +const layoutPath = args.layoutPath || path.join(stateDir, "interface-layout.json"); +const seedDeckUrls = String(args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck,ws://192.168.1.75:8795/eve/deck") + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean); const observationLogPath = args.observationLogPath || path.join(repoRoot, "..", "Mimir", "artifacts", "runtime", "periwinkle-cultmesh-sensors.out.log"); const observationFreshSeconds = Number(args.observationFreshSeconds || 120); @@ -63,6 +67,8 @@ const surfaceDefinition = defineDocumentType let meshNodePromise = null; let version = 0; let currentState = buildPendingState("Coordinator starting"); +let discoveredDeckUrls = [...seedDeckUrls]; +let lastLanScanAt = 0; const clients = new Set(); main().catch((error) => { @@ -181,10 +187,21 @@ function handleUpgrade(req, socket) { } function handleClientFrame(socket, chunk) { - const opcode = chunk[0] & 0x0f; + const frame = tryReadFrame(chunk); + const opcode = frame?.opcode ?? (chunk[0] & 0x0f); if (opcode === 0x8) { clients.delete(socket); socket.end(); + return; + } + if (opcode !== 0x1 || !frame) { + return; + } + try { + const command = JSON.parse(frame.payload.toString("utf8")); + applyClientCommand(command); + } catch { + // Renderer input is advisory. Bad client frames do not get to kill Odin. } } @@ -205,17 +222,21 @@ function sendFrame(socket, opcode, payload) { async function buildState() { version += 1; const observedAt = new Date().toISOString(); - const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, voidBotDashboard, mimirLiveStats, observations] = await Promise.all([ + const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, discoveredInterfaces, 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", "gamecult-visible-ops", "docker"]), remoteGpu("nightwing"), - fetchEveProvider(eveDeckUrl, "voidbot.swarm"), - fetchEveProvider(eveDeckUrl, "mimir.live.stats"), + discoverInterfaces(), observationSnapshot(observationLogPath, observationFreshSeconds), ]); + const interfaceById = new Map(discoveredInterfaces.map((entry) => [entry.providerId, entry])); + const interfaces = [...interfaceById.values()]; + const interfaceSummary = interfaces.map((entry) => `${entry.providerId}:${entry.state}`).join(", "); + const voidBotDashboard = interfaceById.get("voidbot.swarm") || dashboardUnavailable("voidbot.swarm", "discovery", "not discovered"); + const mimirLiveStats = interfaceById.get("mimir.live.stats") || dashboardUnavailable("mimir.live.stats", "discovery", "not discovered"); const verses = [ verse("starfire.local", "Starfire", "coordinator", "active", ["docker", "adb", "eve-provider"], [ @@ -223,6 +244,7 @@ async function buildState() { service("docker", "Docker", docker.state === "ok" ? "active" : "warn", `${docker.containers.length} running`), service("adb", "Periwinkle ADB", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), service("cultcache", "Odin CultCache", fs.existsSync(cachePath) ? "active" : "waiting", path.basename(cachePath)), + service("interfaces", "Eve interfaces", interfaces.some((entry) => entry.surface?.root) ? "active" : "waiting", interfaceSummary || "no provider surfaces"), service("voidbot-swarm", "VoidBot Swarm", voidBotDashboard.state, voidBotDashboard.detail), service("mimir-live-stats", "Mimir Live Stats", mimirLiveStats.state, mimirLiveStats.detail), service("mimir-observation-ledger", "Mimir Observation Ledger", observations.state, observations.detail), @@ -269,7 +291,7 @@ async function buildState() { health: entry.status, detail: entry.capabilities.join(", "), })), - surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces: [mimirLiveStats, voidBotDashboard], observations }), + surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces, observations }), }; } @@ -297,6 +319,7 @@ function buildPendingState(message) { function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces, observations }) { const activeInterfaces = interfaces.filter((entry) => entry.surface?.root); const activeObservationStreams = observations.streams.filter((entry) => entry.state === "active"); + const layout = readLayout(); return { schema: "gamecult.eve.surface.v1", id: "gamecult.network.status.surface", @@ -330,6 +353,7 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse detail: entry.detail, version: entry.version, updatedAt: entry.updatedAt, + layout: layout.tiles?.[entry.providerId] || defaultLayoutFor(entry, interfaces), }, children: entry.surface?.root ? [entry.surface.root] : [ text(`interface-${stableId(entry.providerId)}-missing`, entry.detail || "interface unavailable"), @@ -687,7 +711,81 @@ async function remoteGpu(target) { } } -async function fetchEveProvider(url, providerId) { +async function discoverInterfaces() { + await refreshLanDeckDiscovery(); + const manifestsByProvider = new Map(); + for (const deckUrl of discoveredDeckUrls) { + const manifestUrl = deckUrl.replace(/^ws:/, "http:").replace(/\/eve\/deck.*$/, "/eve/deck/providers"); + try { + const catalog = JSON.parse(await httpGet(manifestUrl, 2500)); + for (const provider of catalog.providers || []) { + if (!provider?.id || provider.id === "eve.dashboard.broker") continue; + const existing = manifestsByProvider.get(provider.id); + if (!existing || deckUrl.startsWith("ws://127.0.0.1")) { + manifestsByProvider.set(provider.id, { provider, deckUrl }); + } + } + } 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)); + } + interfaces.sort((left, right) => left.providerId.localeCompare(right.providerId)); + return interfaces; +} + +async function refreshLanDeckDiscovery() { + const now = Date.now(); + if (now - lastLanScanAt < 60000) return; + lastLanScanAt = now; + const urls = new Set(seedDeckUrls); + const checks = []; + for (const prefix of localIpv4Prefixes()) { + for (let host = 1; host <= 254; host += 1) { + const address = `${prefix}.${host}`; + checks.push(tcpCheck(address, 8795).then((check) => { + if (check.state === "open") urls.add(`ws://${address}:8795/eve/deck`); + })); + } + } + await Promise.allSettled(checks); + discoveredDeckUrls = [...urls].sort(); +} + +function localIpv4Prefixes() { + const prefixes = new Set(); + for (const entries of Object.values(os.networkInterfaces())) { + for (const entry of entries || []) { + if (entry.family !== "IPv4" || entry.internal) continue; + const parts = entry.address.split("."); + if (parts.length === 4) prefixes.add(parts.slice(0, 3).join(".")); + } + } + return [...prefixes]; +} + +function httpGet(url, timeoutMs) { + return new Promise((resolve, reject) => { + const req = http.get(url, { timeout: timeoutMs }, (res) => { + if ((res.statusCode || 0) >= 400) { + reject(new Error(`HTTP ${res.statusCode}`)); + res.resume(); + return; + } + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); + }); + req.on("timeout", () => req.destroy(new Error("HTTP request timed out"))); + req.on("error", reject); + }); +} + +async function fetchEveProvider(url, providerId, manifest = null) { try { const socket = await openWebSocket(url); try { @@ -698,12 +796,13 @@ async function fetchEveProvider(url, providerId) { if (state?.providerId === providerId) { return { providerId, - title: state.title || providerId, + title: state.title || manifest?.title || providerId, state: "active", detail: `${state.surface?.root?.kind || "surface"} ${state.nodes?.length || 0} nodes`, version: state.version, updatedAt: state.updatedAt, source: url, + manifest, surface: state.surface, }; } @@ -726,6 +825,7 @@ function dashboardUnavailable(providerId, source, detail) { version: 0, updatedAt: new Date().toISOString(), source, + manifest: null, surface: null, }; } @@ -841,8 +941,69 @@ function tryReadFrame(buffer) { return { opcode, payload, consumed: offset + length }; } +function readLayout() { + try { + return JSON.parse(fs.readFileSync(layoutPath, "utf8")); + } catch { + return { schema: "odin.interface_layout.v1", tiles: {} }; + } +} + +function writeLayout(layout) { + fs.writeFileSync(layoutPath, JSON.stringify(layout, null, 2), "utf8"); +} + +function defaultLayoutFor(entry, interfaces) { + const index = Math.max(0, interfaces.findIndex((candidate) => candidate.providerId === entry.providerId)); + return { + tileId: stableId(entry.providerId || "interface"), + visible: true, + priority: index, + x: index % 2, + y: Math.floor(index / 2), + w: 1, + h: entry.providerId === "voidbot.swarm" ? 2 : 1, + minWidth: 48, + minHeight: 8, + preferredWidth: 96, + preferredHeight: entry.providerId === "voidbot.swarm" ? 16 : 10, + viewportMode: "adaptive", + }; +} + +function applyClientCommand(command) { + if (!command || command.type !== "odin-layout-intent" || !command.providerId) return; + const layout = readLayout(); + layout.schema = "odin.interface_layout.v1"; + layout.updatedAt = new Date().toISOString(); + layout.tiles ||= {}; + const current = layout.tiles[command.providerId] || defaultLayoutFor({ providerId: command.providerId }, []); + const next = { ...current }; + if (command.action === "focus") { + next.visible = true; + next.priority = -1; + } else if (command.action === "resize") { + next.w = clampNumber((next.w || 1) + Number(command.dw || 0), 1, 4); + next.h = clampNumber((next.h || 1) + Number(command.dh || 0), 1, 4); + } else if (command.action === "move") { + next.x = clampNumber((next.x || 0) + Number(command.dx || 0), 0, 12); + next.y = clampNumber((next.y || 0) + Number(command.dy || 0), 0, 12); + } else if (command.action === "toggle") { + next.visible = !next.visible; + } else { + return; + } + layout.tiles[command.providerId] = next; + writeLayout(layout); +} + +function clampNumber(value, min, max) { + if (!Number.isFinite(value)) return min; + return Math.max(min, Math.min(max, Math.round(value))); +} + function stableId(value) { - return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); + return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } function parseArgs(argv) { From ba4da8bd8e4954647a176c34365f22957977b223 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 1 Jun 2026 15:10:32 +0100 Subject: [PATCH 007/742] Ingest VoidBot swarm Eve binding from CultMesh --- src/odin-coordinator.cjs | 100 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index 83d0144..ccb3fdf 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -43,6 +43,13 @@ const seedDeckUrls = String(args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck,ws: .filter(Boolean); const observationLogPath = args.observationLogPath || path.join(repoRoot, "..", "Mimir", "artifacts", "runtime", "periwinkle-cultmesh-sensors.out.log"); const observationFreshSeconds = Number(args.observationFreshSeconds || 120); +const interfaceBindingStores = String( + args.interfaceBindingStore || + path.join(repoRoot, "..", "VoidBot", ".voidbot", "status", "cultmesh", "voidbot-swarm-state.cc"), +) + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean); fs.mkdirSync(stateDir, { recursive: true }); @@ -50,6 +57,7 @@ const surfaceDefinition = defineDocumentType ? defineDocumentType({ type: "gamecult.eve.surface_state", schemaName: "gamecult.eve.surface_state", + schemaId: "gamecult.eve.surface_state.v1", schemaVersion: "gamecult.eve.surface_state.v1", global: true, name: (value) => value?.providerId || "surface", @@ -63,6 +71,38 @@ const surfaceDefinition = defineDocumentType ], }) : null; +const interfaceBindingDefinition = defineDocumentType + ? defineDocumentType({ + type: "gamecult.eve.interface_binding", + schemaName: "gamecult.eve.interface_binding", + schemaId: "gamecult.eve.interface_binding.v1", + schemaVersion: "gamecult.eve.interface_binding.v1", + global: true, + name: (value) => value?.bindingId || value?.providerId || "interface", + schema: parseObjectDocument("Eve interface binding"), + }) + : null; +const providerAdvertisementDefinition = defineDocumentType + ? defineDocumentType({ + type: "gamecult.eve.provider_advertisement", + schemaName: "gamecult.eve.provider_advertisement", + schemaId: "gamecult.eve.provider_advertisement.v1", + schemaVersion: "gamecult.eve.provider_advertisement.v1", + global: true, + name: (value) => value?.providerId || "provider", + schema: parseObjectDocument("Eve provider advertisement"), + }) + : null; +const voidbotSwarmSnapshotDefinition = defineDocumentType + ? defineDocumentType({ + type: "voidbot.swarm_state_snapshot", + schemaName: "voidbot.swarm_state_snapshot", + schemaId: "voidbot.swarm_state_snapshot.v1", + schemaVersion: "voidbot.swarm_state_snapshot.v1", + global: true, + schema: parseObjectDocument("VoidBot swarm snapshot"), + }) + : null; let meshNodePromise = null; let version = 0; @@ -316,6 +356,17 @@ function buildPendingState(message) { }; } +function parseObjectDocument(label) { + return { + parse(value) { + if (!value || typeof value !== "object") { + throw new Error(`${label} must be an object`); + } + return value; + }, + }; +} + function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces, observations }) { const activeInterfaces = interfaces.filter((entry) => entry.surface?.root); const activeObservationStreams = observations.streams.filter((entry) => entry.state === "active"); @@ -734,10 +785,59 @@ async function discoverInterfaces() { 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; } +async function discoverCultMeshInterfaceBindings() { + if (!CultMesh || !interfaceBindingDefinition || !surfaceDefinition || !voidbotSwarmSnapshotDefinition || !providerAdvertisementDefinition) { + return []; + } + const interfaces = []; + for (const storePath of interfaceBindingStores) { + try { + if (!fs.existsSync(storePath)) { + continue; + } + const node = await CultMesh.createNode(storePath, { + documents: [ + voidbotSwarmSnapshotDefinition, + providerAdvertisementDefinition, + interfaceBindingDefinition, + surfaceDefinition, + ], + }); + const binding = node.get(interfaceBindingDefinition, "voidbot.swarm"); + if (!binding?.providerId) { + continue; + } + const state = node.get(surfaceDefinition, binding.providerId); + interfaces.push({ + providerId: binding.providerId, + title: binding.title || state?.title || binding.providerId, + state: "active", + detail: `${state?.surface?.root?.kind || binding.kind || "surface"} ${state?.nodes?.length || 0} nodes via CultMesh`, + version: state?.version || 0, + updatedAt: state?.updatedAt || binding.updatedAt || new Date().toISOString(), + source: `cultmesh:${storePath}`, + manifest: binding.provider || null, + surface: state?.surface || binding.surface || null, + }); + } catch (error) { + interfaces.push(dashboardUnavailable("voidbot.swarm", `cultmesh:${storePath}`, error.message)); + } + } + return interfaces; +} + async function refreshLanDeckDiscovery() { const now = Date.now(); if (now - lastLanScanAt < 60000) return; From cfd1196996c536115a19790e0ab32d50e7e1f64d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 11:57:14 +0100 Subject: [PATCH 008/742] Honor Eve provider layout intent --- src/odin-coordinator.cjs | 61 ++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index ccb3fdf..05736f6 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -404,7 +404,7 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse detail: entry.detail, version: entry.version, updatedAt: entry.updatedAt, - layout: layout.tiles?.[entry.providerId] || defaultLayoutFor(entry, interfaces), + layout: mergeLayoutIntent(layout.tiles?.[entry.providerId], entry, interfaces), }, children: entry.surface?.root ? [entry.surface.root] : [ text(`interface-${stableId(entry.providerId)}-missing`, entry.detail || "interface unavailable"), @@ -1055,22 +1055,67 @@ function writeLayout(layout) { function defaultLayoutFor(entry, interfaces) { const index = Math.max(0, interfaces.findIndex((candidate) => candidate.providerId === entry.providerId)); + const intent = surfaceLayoutIntent(entry); + const preferredHeight = positiveNumber(intent.preferredHeight, entry.providerId === "voidbot.swarm" ? 16 : 10); + const minHeight = positiveNumber(intent.minHeight, 8); + const preferredWidth = positiveNumber(intent.preferredWidth, 96); + const minWidth = positiveNumber(intent.minWidth, 48); + const priority = Number.isFinite(intent.priority) ? intent.priority : index; return { tileId: stableId(entry.providerId || "interface"), visible: true, - priority: index, + priority, x: index % 2, y: Math.floor(index / 2), w: 1, - h: entry.providerId === "voidbot.swarm" ? 2 : 1, - minWidth: 48, - minHeight: 8, - preferredWidth: 96, - preferredHeight: entry.providerId === "voidbot.swarm" ? 16 : 10, - viewportMode: "adaptive", + h: Math.max(1, Math.ceil(preferredHeight / 12)), + minWidth, + minHeight, + preferredWidth, + preferredHeight, + density: intent.density || "adaptive", + viewportMode: intent.viewportMode || "adaptive", }; } +function mergeLayoutIntent(existing, entry, interfaces) { + const base = existing && typeof existing === "object" + ? { ...existing } + : defaultLayoutFor(entry, interfaces); + const intent = surfaceLayoutIntent(entry); + const fallback = defaultLayoutFor(entry, interfaces); + const minWidth = positiveNumber(intent.minWidth, positiveNumber(base.minWidth, fallback.minWidth)); + const minHeight = positiveNumber(intent.minHeight, positiveNumber(base.minHeight, fallback.minHeight)); + const preferredWidth = positiveNumber(intent.preferredWidth, positiveNumber(base.preferredWidth, fallback.preferredWidth)); + const preferredHeight = positiveNumber(intent.preferredHeight, positiveNumber(base.preferredHeight, fallback.preferredHeight)); + return { + ...base, + minWidth, + minHeight, + preferredWidth: Math.max(positiveNumber(base.preferredWidth, preferredWidth), preferredWidth), + preferredHeight: Math.max(positiveNumber(base.preferredHeight, preferredHeight), preferredHeight), + h: Math.max(positiveNumber(base.h, fallback.h), Math.ceil(preferredHeight / 12)), + w: Math.max(positiveNumber(base.w, fallback.w), Math.ceil(preferredWidth / 96)), + priority: Number.isFinite(intent.priority) ? Math.min(Number(base.priority ?? intent.priority), intent.priority) : base.priority, + density: intent.density || base.density || fallback.density, + viewportMode: intent.viewportMode || base.viewportMode || fallback.viewportMode, + }; +} + +function surfaceLayoutIntent(entry) { + const root = entry?.surface?.root; + if (!root || typeof root !== "object") return {}; + const layout = root.layout && typeof root.layout === "object" ? root.layout : {}; + const props = root.props && typeof root.props === "object" ? root.props : {}; + const propLayout = props.layout && typeof props.layout === "object" ? props.layout : {}; + return { ...propLayout, ...layout }; +} + +function positiveNumber(value, fallback) { + const number = Number(value); + return Number.isFinite(number) && number > 0 ? number : fallback; +} + function applyClientCommand(command) { if (!command || command.type !== "odin-layout-intent" || !command.providerId) return; const layout = readLayout(); From 21f6a5e4747f3101007a520b32a74157be8a394b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 15:41:15 +0100 Subject: [PATCH 009/742] Publish Odin fullscreen dashboard intent --- src/odin-coordinator.cjs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index 05736f6..4136a6f 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -382,6 +382,7 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse title: "Odin All-Seer", observedAt, summary: `${verses.length} Verses / ${verses.reduce((sum, entry) => sum + entry.services.length, 0)} services / ${activeInterfaces.length} interfaces / ${activeObservationStreams.length} live streams`, + layout: fullscreenLayoutIntent("odin.allseer", -100), }, children: [ pane("Coordinator", [ @@ -1078,6 +1079,24 @@ function defaultLayoutFor(entry, interfaces) { }; } +function fullscreenLayoutIntent(tileId, priority = -100) { + return { + tileId: stableId(tileId || "fullscreen"), + visible: true, + priority, + x: 0, + y: 0, + w: 4, + h: 4, + minWidth: 96, + minHeight: 36, + preferredWidth: 192, + preferredHeight: 48, + density: "comfortable", + viewportMode: "fullscreen", + }; +} + function mergeLayoutIntent(existing, entry, interfaces) { const base = existing && typeof existing === "object" ? { ...existing } @@ -1125,8 +1144,7 @@ function applyClientCommand(command) { const current = layout.tiles[command.providerId] || defaultLayoutFor({ providerId: command.providerId }, []); const next = { ...current }; if (command.action === "focus") { - next.visible = true; - next.priority = -1; + Object.assign(next, fullscreenLayoutIntent(command.providerId, -100)); } else if (command.action === "resize") { next.w = clampNumber((next.w || 1) + Number(command.dw || 0), 1, 4); next.h = clampNumber((next.h || 1) + Number(command.dh || 0), 1, 4); From ecd1480fe1c8746770b3001accdf60630acd1a5d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 15:52:07 +0100 Subject: [PATCH 010/742] Seed Spotiverse provider discovery --- README.md | 1 + src/odin-coordinator.cjs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f2bac4..891c785 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ The first executable is deliberately narrow: - publishes explicit `verse` and `service` nodes for compact Eve/CultUI lowerers. - publishes explicit `observation-stream` nodes for Periwinkle/EVE sensor, microphone, touch, and camera stream summaries when Mimir's CultMesh observation ledger is present. - discovers Eve deck provider manifests from known/LAN deck endpoints; +- includes local Spotiverse on `ws://127.0.0.1:8796/eve/deck` in the known provider seed list; - ingests provider-owned Eve/CultUI dashboards, including `mimir.live.stats` and `voidbot.swarm`, and embeds them as Odin `interface` nodes; - persists operator tiling intent as `odin.interface_layout.v1` under ignored `scratch/odin/interface-layout.json`. diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index 4136a6f..015ffb0 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -37,7 +37,7 @@ const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin"); const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp"); const surfaceKey = "surface:gamecult.network.status"; const layoutPath = args.layoutPath || path.join(stateDir, "interface-layout.json"); -const seedDeckUrls = String(args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck,ws://192.168.1.75:8795/eve/deck") +const seedDeckUrls = String(args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck,ws://127.0.0.1:8796/eve/deck,ws://192.168.1.75:8795/eve/deck") .split(",") .map((entry) => entry.trim()) .filter(Boolean); From 7c01732598ef662f624e3a3c67401d4dcef2a024 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 16:04:49 +0100 Subject: [PATCH 011/742] Assemble Odin runtime organs --- docs/architecture.md | 17 + package.json | 3 +- scripts/check-syntax.cjs | 34 + src/odin-coordinator.cjs | 1232 +++---------------------------------- src/odin/config.cjs | 64 ++ src/odin/documents.cjs | 66 ++ src/odin/interfaces.cjs | 185 ++++++ src/odin/layout.cjs | 129 ++++ src/odin/observations.cjs | 158 +++++ src/odin/probes.cjs | 144 +++++ src/odin/state.cjs | 128 ++++ src/odin/surface.cjs | 134 ++++ src/odin/utils.cjs | 86 +++ src/odin/websocket.cjs | 239 +++++++ 14 files changed, 1476 insertions(+), 1143 deletions(-) create mode 100644 scripts/check-syntax.cjs create mode 100644 src/odin/config.cjs create mode 100644 src/odin/documents.cjs create mode 100644 src/odin/interfaces.cjs create mode 100644 src/odin/layout.cjs create mode 100644 src/odin/observations.cjs create mode 100644 src/odin/probes.cjs create mode 100644 src/odin/state.cjs create mode 100644 src/odin/surface.cjs create mode 100644 src/odin/utils.cjs create mode 100644 src/odin/websocket.cjs diff --git a/docs/architecture.md b/docs/architecture.md index 27910b0..9d1b30e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,6 +22,23 @@ host / device / service probes This first path proves the operator surface and persistent state. It does not yet pretend to be full peer exchange. +## Runtime Body + +Odin's executable body is split by ownership: + +- `src/odin-coordinator.cjs`: process lifecycle, refresh loop, persistence, health, and transport wiring. +- `src/odin/config.cjs`: runtime paths, seed deck URLs, intervals, and CultLib module path setup. +- `src/odin/documents.cjs`: CultCache/CultMesh document definitions accepted by Odin. +- `src/odin/probes.cjs`: local Docker/ADB/TCP probes plus named SSH service and GPU probes. +- `src/odin/observations.cjs`: Mimir observation ledger tailing and dashboard-ready stream projection. +- `src/odin/interfaces.cjs`: Eve provider manifest discovery, provider WebSocket fetches, and CultMesh interface bindings. +- `src/odin/layout.cjs`: `odin.interface_layout.v1` read/write and merge policy. +- `src/odin/surface.cjs`: `gamecult.eve.surface.v1` tree projection. +- `src/odin/state.cjs`: one refresh's input records into `odin.allseer` dashboard state. +- `src/odin/websocket.cjs`: Eve deck HTTP/WebSocket serving and client WebSocket helpers. + +The entrypoint is not allowed to grow new probe, surface, provider, layout, or renderer policy. If a new owner is needed, name the owner and its invariant before adding code. + ## Target Mechanism ```text diff --git a/package.json b/package.json index c52021e..b2f85d4 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,10 @@ "description": "GameCult all-seer: CultMesh Verse discovery, schema awareness, translation, and Eve surface publication.", "type": "commonjs", "scripts": { - "check": "node --check src/odin-coordinator.cjs", + "check": "node scripts/check-syntax.cjs", "start": "node src/odin-coordinator.cjs" }, "engines": { "node": ">=20" } } - diff --git a/scripts/check-syntax.cjs b/scripts/check-syntax.cjs new file mode 100644 index 0000000..f08990b --- /dev/null +++ b/scripts/check-syntax.cjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const repoRoot = path.resolve(__dirname, ".."); +const files = []; + +collect(path.join(repoRoot, "src")); + +for (const file of files) { + const result = spawnSync(process.execPath, ["--check", file], { + cwd: repoRoot, + encoding: "utf8", + }); + if (result.stdout) process.stdout.write(result.stdout); + if (result.stderr) process.stderr.write(result.stderr); + if (result.status !== 0) process.exit(result.status); +} + +console.log(`Checked ${files.length} CommonJS files.`); + +function collect(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + collect(fullPath); + } else if (entry.isFile() && entry.name.endsWith(".cjs")) { + files.push(fullPath); + } + } +} diff --git a/src/odin-coordinator.cjs b/src/odin-coordinator.cjs index 015ffb0..cd86200 100644 --- a/src/odin-coordinator.cjs +++ b/src/odin-coordinator.cjs @@ -1,115 +1,48 @@ #!/usr/bin/env node "use strict"; -const crypto = require("crypto"); const fs = require("fs"); -const http = require("http"); -const net = require("net"); -const os = require("os"); const path = require("path"); -const { execFile } = require("child_process"); -const { promisify } = require("util"); -const Module = require("module"); -const execFileAsync = promisify(execFile); +const { buildConfig, loadCultRuntime } = require("./odin/config.cjs"); +const { defineOdinDocuments } = require("./odin/documents.cjs"); +const { createInterfaceDiscovery } = require("./odin/interfaces.cjs"); +const { createLayoutStore } = require("./odin/layout.cjs"); +const { createStateBuilder } = require("./odin/state.cjs"); +const { broadcastState, createDashboardServer } = require("./odin/websocket.cjs"); -const repoRoot = path.resolve(__dirname, ".."); -process.env.NODE_PATH = [ - path.resolve(repoRoot, "..", "CultLib", "packages"), - process.env.NODE_PATH || "", -].filter(Boolean).join(path.delimiter); -Module._initPaths(); +const config = buildConfig(process.argv.slice(2)); +fs.mkdirSync(config.stateDir, { recursive: true }); -let CultMesh; -let defineDocumentType; -try { - ({ CultMesh } = require("cultmesh-ts/dist/index.js")); - ({ defineDocumentType } = require("cultcache-ts/dist/index.js")); -} catch (error) { - console.error("CultMesh runtime unavailable; durable mesh snapshot disabled:", error.message); +const cultRuntime = loadCultRuntime(); +if (cultRuntime.error) { + console.error("CultMesh runtime unavailable; durable mesh snapshot disabled:", cultRuntime.error.message); } -const args = parseArgs(process.argv.slice(2)); -const port = Number(args.port || 8797); -const host = args.host || "0.0.0.0"; -const intervalMs = Number(args.intervalMs || 5000); -const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin"); -const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp"); -const surfaceKey = "surface:gamecult.network.status"; -const layoutPath = args.layoutPath || path.join(stateDir, "interface-layout.json"); -const seedDeckUrls = String(args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck,ws://127.0.0.1:8796/eve/deck,ws://192.168.1.75:8795/eve/deck") - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean); -const observationLogPath = args.observationLogPath || path.join(repoRoot, "..", "Mimir", "artifacts", "runtime", "periwinkle-cultmesh-sensors.out.log"); -const observationFreshSeconds = Number(args.observationFreshSeconds || 120); -const interfaceBindingStores = String( - args.interfaceBindingStore || - path.join(repoRoot, "..", "VoidBot", ".voidbot", "status", "cultmesh", "voidbot-swarm-state.cc"), -) - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean); - -fs.mkdirSync(stateDir, { recursive: true }); - -const surfaceDefinition = defineDocumentType - ? defineDocumentType({ - type: "gamecult.eve.surface_state", - schemaName: "gamecult.eve.surface_state", - schemaId: "gamecult.eve.surface_state.v1", - schemaVersion: "gamecult.eve.surface_state.v1", - global: true, - name: (value) => value?.providerId || "surface", - schema: { parse: (value) => value }, - members: [ - { slot: 0, memberName: "providerId", typeName: "string", isName: true }, - { slot: 1, memberName: "title", typeName: "string" }, - { slot: 2, memberName: "version", typeName: "long" }, - { slot: 3, memberName: "updatedAt", typeName: "string" }, - { slot: 4, memberName: "surface", typeName: "object" }, - ], - }) - : null; -const interfaceBindingDefinition = defineDocumentType - ? defineDocumentType({ - type: "gamecult.eve.interface_binding", - schemaName: "gamecult.eve.interface_binding", - schemaId: "gamecult.eve.interface_binding.v1", - schemaVersion: "gamecult.eve.interface_binding.v1", - global: true, - name: (value) => value?.bindingId || value?.providerId || "interface", - schema: parseObjectDocument("Eve interface binding"), - }) - : null; -const providerAdvertisementDefinition = defineDocumentType - ? defineDocumentType({ - type: "gamecult.eve.provider_advertisement", - schemaName: "gamecult.eve.provider_advertisement", - schemaId: "gamecult.eve.provider_advertisement.v1", - schemaVersion: "gamecult.eve.provider_advertisement.v1", - global: true, - name: (value) => value?.providerId || "provider", - schema: parseObjectDocument("Eve provider advertisement"), - }) - : null; -const voidbotSwarmSnapshotDefinition = defineDocumentType - ? defineDocumentType({ - type: "voidbot.swarm_state_snapshot", - schemaName: "voidbot.swarm_state_snapshot", - schemaId: "voidbot.swarm_state_snapshot.v1", - schemaVersion: "voidbot.swarm_state_snapshot.v1", - global: true, - schema: parseObjectDocument("VoidBot swarm snapshot"), - }) - : null; +const documents = defineOdinDocuments(cultRuntime.defineDocumentType); +const layoutStore = createLayoutStore(config.layoutPath); +const interfaceDiscovery = createInterfaceDiscovery({ + CultMesh: cultRuntime.CultMesh, + documents, + interfaceBindingStores: config.interfaceBindingStores, + seedDeckUrls: config.seedDeckUrls, +}); +const stateBuilder = createStateBuilder({ + cachePath: config.cachePath, + interfaceDiscovery, + layoutStore, + observationFreshSeconds: config.observationFreshSeconds, + observationLogPath: config.observationLogPath, +}); let meshNodePromise = null; -let version = 0; -let currentState = buildPendingState("Coordinator starting"); -let discoveredDeckUrls = [...seedDeckUrls]; -let lastLanScanAt = 0; -const clients = new Set(); +let currentState = stateBuilder.buildPendingState("Coordinator starting"); +let lastRefresh = { + completedAt: null, + durationMs: null, + error: null, + startedAt: null, +}; main().catch((error) => { console.error(error); @@ -117,1071 +50,88 @@ main().catch((error) => { }); async function main() { - if (CultMesh && surfaceDefinition) { - meshNodePromise = CultMesh.createNode(cachePath, { documents: [surfaceDefinition] }); + if (cultRuntime.CultMesh && documents.surfaceDefinition) { + meshNodePromise = cultRuntime.CultMesh.createNode(config.cachePath, { documents: [documents.surfaceDefinition] }); } - const server = http.createServer(handleHttp); - server.on("upgrade", handleUpgrade); - server.listen(port, host, () => { - console.log(`Odin coordinator listening on ws://${host}:${port}/eve/deck`); - console.log(`Durable surface cache: ${cachePath}`); + const dashboardServer = createDashboardServer({ + applyClientCommand: (command) => layoutStore.applyClientCommand(command), + getCurrentState: () => currentState, + getHealth: () => health(dashboardServer.clients), + host: config.host, + port: config.port, }); + console.log(`Durable surface cache: ${config.cachePath}`); - await refresh(); + await refresh(dashboardServer.clients); setInterval(() => { - refresh().catch((error) => console.error("refresh failed:", error)); - }, intervalMs); + refresh(dashboardServer.clients).catch((error) => console.error("refresh failed:", error)); + }, config.intervalMs); } -async function refresh() { - currentState = await buildState(); - await persistState(currentState); - const payload = JSON.stringify(currentState); - for (const client of [...clients]) { - try { - sendFrame(client, 0x1, Buffer.from(payload, "utf8")); - } catch { - clients.delete(client); - client.destroy(); - } +async function refresh(clients) { + const started = Date.now(); + lastRefresh = { + completedAt: null, + durationMs: null, + error: null, + startedAt: new Date(started).toISOString(), + }; + try { + currentState = await stateBuilder.buildState(); + await persistState(currentState); + broadcastState(clients, currentState); + lastRefresh = { + completedAt: new Date().toISOString(), + durationMs: Date.now() - started, + error: null, + startedAt: new Date(started).toISOString(), + }; + } catch (error) { + lastRefresh = { + completedAt: new Date().toISOString(), + durationMs: Date.now() - started, + error: error.message, + startedAt: new Date(started).toISOString(), + }; + throw error; } } async function persistState(state) { - fs.writeFileSync(path.join(stateDir, "latest-surface.json"), JSON.stringify(state, null, 2), "utf8"); - if (!meshNodePromise || !surfaceDefinition) { + fs.writeFileSync(path.join(config.stateDir, "latest-surface.json"), JSON.stringify(state, null, 2), "utf8"); + if (!meshNodePromise || !documents.surfaceDefinition) { return; } try { const node = await meshNodePromise; - await node.put(surfaceDefinition, surfaceKey, state); + await node.put(documents.surfaceDefinition, config.surfaceKey, state); await node.flush?.(true); } catch (error) { console.error("CultMesh snapshot write failed:", error.message); } } -async function handleHttp(req, res) { - if (req.url === "/health") { - res.writeHead(200, { "content-type": "application/json" }); - res.end(JSON.stringify({ - ok: true, - providerId: currentState.providerId, - version: currentState.version, - clients: clients.size, - cachePath, - })); - return; - } - - if (req.url === "/eve/deck/providers") { - res.writeHead(200, { "content-type": "application/json" }); - res.end(JSON.stringify({ - providers: [{ - id: currentState.providerId, - title: currentState.title, - description: "Odin central coordinator for GameCult Verse discovery, schema awareness, translation, and network status.", - version: String(currentState.version), - endpoint: "/eve/deck", - capabilities: ["network-status", "cultmesh-verses", "cultui-surface"], - usesCultMesh: true, - transport: "CultMesh durable surface + Eve WebSocket", - }], - })); - return; - } - - res.writeHead(404, { "content-type": "text/plain" }); - res.end("not found"); -} - -function handleUpgrade(req, socket) { - if (!req.url.startsWith("/eve/deck")) { - socket.end("HTTP/1.1 404 Not Found\r\n\r\n"); - return; - } - - const key = req.headers["sec-websocket-key"]; - if (!key) { - socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); - return; - } - - const accept = crypto - .createHash("sha1") - .update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") - .digest("base64"); - socket.write( - "HTTP/1.1 101 Switching Protocols\r\n" + - "Upgrade: websocket\r\n" + - "Connection: Upgrade\r\n" + - `Sec-WebSocket-Accept: ${accept}\r\n\r\n`, - ); - clients.add(socket); - sendFrame(socket, 0x1, Buffer.from(JSON.stringify(currentState), "utf8")); - socket.on("data", (chunk) => handleClientFrame(socket, chunk)); - socket.on("close", () => clients.delete(socket)); - socket.on("error", () => clients.delete(socket)); -} - -function handleClientFrame(socket, chunk) { - const frame = tryReadFrame(chunk); - const opcode = frame?.opcode ?? (chunk[0] & 0x0f); - if (opcode === 0x8) { - clients.delete(socket); - socket.end(); - return; - } - if (opcode !== 0x1 || !frame) { - return; - } - try { - const command = JSON.parse(frame.payload.toString("utf8")); - applyClientCommand(command); - } catch { - // Renderer input is advisory. Bad client frames do not get to kill Odin. - } -} - -function sendFrame(socket, opcode, payload) { - const header = [0x80 | opcode]; - if (payload.length < 126) { - header.push(payload.length); - } else if (payload.length <= 0xffff) { - header.push(126, payload.length >> 8, payload.length & 0xff); - } else { - const length = Buffer.alloc(8); - length.writeBigUInt64BE(BigInt(payload.length)); - header.push(127, ...length); - } - socket.write(Buffer.concat([Buffer.from(header), payload])); -} - -async function buildState() { - version += 1; - const observedAt = new Date().toISOString(); - const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, discoveredInterfaces, 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", "gamecult-visible-ops", "docker"]), - remoteGpu("nightwing"), - discoverInterfaces(), - observationSnapshot(observationLogPath, observationFreshSeconds), - ]); - const interfaceById = new Map(discoveredInterfaces.map((entry) => [entry.providerId, entry])); - const interfaces = [...interfaceById.values()]; - const interfaceSummary = interfaces.map((entry) => `${entry.providerId}:${entry.state}`).join(", "); - const voidBotDashboard = interfaceById.get("voidbot.swarm") || dashboardUnavailable("voidbot.swarm", "discovery", "not discovered"); - const mimirLiveStats = interfaceById.get("mimir.live.stats") || dashboardUnavailable("mimir.live.stats", "discovery", "not discovered"); - - const verses = [ - verse("starfire.local", "Starfire", "coordinator", "active", ["docker", "adb", "eve-provider"], [ - service("odin", "Odin all-seer", "active", "ws://0.0.0.0:8797/eve/deck"), - service("docker", "Docker", docker.state === "ok" ? "active" : "warn", `${docker.containers.length} running`), - service("adb", "Periwinkle ADB", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), - service("cultcache", "Odin CultCache", fs.existsSync(cachePath) ? "active" : "waiting", path.basename(cachePath)), - service("interfaces", "Eve interfaces", interfaces.some((entry) => entry.surface?.root) ? "active" : "waiting", interfaceSummary || "no provider surfaces"), - service("voidbot-swarm", "VoidBot Swarm", voidBotDashboard.state, voidBotDashboard.detail), - service("mimir-live-stats", "Mimir Live Stats", mimirLiveStats.state, mimirLiveStats.detail), - service("mimir-observation-ledger", "Mimir Observation Ledger", observations.state, observations.detail), - ...docker.containers.map((container) => service(`docker-${container.name}`, container.name, "active", container.image)), - ]), - verse("nightwing.local", "Nightwing", "dashboard-renderer", hostState(hosts.Nightwing), ["eve-tui", "gpu-worker"], [ - ...hostServices("Nightwing", hosts.Nightwing), - ...nightwingServices.map((entry) => service(`systemd-${entry.name}`, entry.name, systemdState(entry.state), entry.state)), - service("gpu", "GTX 860M", nightwingGpu.state, nightwingGpu.detail), - ]), - verse("eve.ipad", "EVE", "ios-client", hostState(hosts.EVE), ["ssh", "native-eve"], hostServices("EVE", hosts.EVE)), - verse("periwinkle.android", "Periwinkle", "android-client", adb.devices.length ? "connected" : "waiting", ["adb", "sensor-edge"], [ - service("adb-device", "ADB device", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), - ...observationServices(observations, "periwinkle"), - ]), - verse("raven.local", "Raven", "local-peer", hostState(hosts.Raven), ["ssh"], hostServices("Raven", hosts.Raven)), - verse("yggdrasil.ops", "Yggdrasil", "ops-host", hostState(hosts.Yggdrasil), ["ssh", "https", "services"], [ - ...hostServices("Yggdrasil", hosts.Yggdrasil), - ...yggdrasilServices.map((entry) => service(`systemd-${entry.name}`, entry.name, systemdState(entry.state), entry.state)), - ]), - ]; - +function health(clients) { return { - type: "dashboard-state", - schema: "mimir.eve_dashboard_state.v1", - providerId: "odin.allseer", - title: "Odin All-Seer", - version, - updatedAt: observedAt, - selectedNodeId: "verse-starfire.local", - lutPreset: "terminal", - nodes: verses.map((entry, index) => ({ - id: `verse-${entry.verseId}`, - label: `${entry.name}\n${entry.role}`, - kind: "cultmesh-verse", - visible: true, - x: -0.8 + (index % 3) * 0.8, - y: -0.45 + Math.floor(index / 3) * 0.55, - z: 0, - rotation: 0, - scale: 1, - width: 0.42, - height: 0.2, - health: entry.status, - detail: entry.capabilities.join(", "), - })), - surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces, observations }), - }; -} - -function buildPendingState(message) { - return { - type: "dashboard-state", - schema: "mimir.eve_dashboard_state.v1", - providerId: "odin.allseer", - title: "Odin All-Seer", - version, - updatedAt: new Date().toISOString(), - selectedNodeId: "coordinator-starting", - lutPreset: "terminal", - nodes: [], - surface: { - schema: "gamecult.eve.surface.v1", - id: "gamecult.network.status.surface", - title: "Odin", - root: stack("root", [pane("Coordinator", [text("status", message)])]), - assets: [], + ok: !lastRefresh.error, + providerId: currentState.providerId, + version: currentState.version, + clients: clients.size, + cachePath: config.cachePath, + stateDir: config.stateDir, + layoutPath: config.layoutPath, + intervalMs: config.intervalMs, + cultMesh: { + available: Boolean(cultRuntime.CultMesh && documents.surfaceDefinition), + error: cultRuntime.error?.message || null, }, - }; -} - -function parseObjectDocument(label) { - return { - parse(value) { - if (!value || typeof value !== "object") { - throw new Error(`${label} must be an object`); - } - return value; + discovery: { + seedDeckUrls: config.seedDeckUrls, + discoveredDeckUrls: interfaceDiscovery.getDiscoveredDeckUrls(), + interfaceBindingStores: config.interfaceBindingStores, }, + refresh: lastRefresh, }; } - -function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces, observations }) { - const activeInterfaces = interfaces.filter((entry) => entry.surface?.root); - const activeObservationStreams = observations.streams.filter((entry) => entry.state === "active"); - const layout = readLayout(); - return { - schema: "gamecult.eve.surface.v1", - id: "gamecult.network.status.surface", - title: "Odin", - root: { - id: "network-root", - kind: "dashboard", - props: { - title: "Odin All-Seer", - observedAt, - summary: `${verses.length} Verses / ${verses.reduce((sum, entry) => sum + entry.services.length, 0)} services / ${activeInterfaces.length} interfaces / ${activeObservationStreams.length} live streams`, - layout: fullscreenLayoutIntent("odin.allseer", -100), - }, - children: [ - pane("Coordinator", [ - text("observed", `observed ${observedAt}`), - text("authority", "Odin owns Verse discovery, schema awareness, translation planning, and accepted surface publication. Renderers lower only."), - metric("docker-count", "Docker containers", docker.containers.length, docker.state === "ok" ? "ok" : "warn"), - metric("adb-count", "ADB devices", adb.devices.length, adb.devices.length ? "ok" : "warn"), - metric("observation-stream-count", "Observation streams", observations.streams.length, activeObservationStreams.length ? "ok" : "warn"), - text("observation-ledger", `observation ledger: ${observations.detail}`), - ]), - observationPane(observations), - ...interfaces.map((entry) => ({ - id: `interface-${stableId(entry.providerId)}`, - kind: "interface", - props: { - title: entry.title, - providerId: entry.providerId, - source: entry.source, - status: entry.state, - detail: entry.detail, - version: entry.version, - updatedAt: entry.updatedAt, - layout: mergeLayoutIntent(layout.tiles?.[entry.providerId], entry, interfaces), - }, - children: entry.surface?.root ? [entry.surface.root] : [ - text(`interface-${stableId(entry.providerId)}-missing`, entry.detail || "interface unavailable"), - ], - })), - ...verses.map((entry) => ({ - id: `verse-${entry.verseId}`, - kind: "verse", - props: { - title: entry.name, - verseId: entry.verseId, - role: entry.role, - status: entry.status, - capabilities: entry.capabilities, - services: entry.services, - }, - children: entry.services.map((item) => ({ - id: `service-${entry.verseId}-${item.id}`, - kind: "service", - props: item, - children: [], - })), - })), - pane("Verses", verses.map((entry) => - card(`verse-card-${entry.verseId}`, [ - text(`verse-title-${entry.verseId}`, `${entry.name} :: ${entry.verseId}`), - text(`verse-status-${entry.verseId}`, `${entry.role} / ${entry.status}`), - text(`verse-caps-${entry.verseId}`, entry.capabilities.join(", ")), - ], entry.status), - )), - pane("Hosts", Object.entries(hosts).map(([name, checks]) => - card(`host-${name}`, [ - text(`host-name-${name}`, name), - ...checks.map((check) => text(`host-${name}-${check.name}`, `${check.name}: ${check.state}${check.detail ? ` ${check.detail}` : ""}`)), - ], checks.some((check) => check.state === "open" || check.state === "ok") ? "ok" : "warn"), - )), - pane("Yggdrasil Services", yggdrasilServices.map((service) => - text(`ygg-${service.name}`, `${service.name}: ${service.state}`), - )), - pane("Docker", docker.containers.length - ? docker.containers.slice(0, 10).map((container) => text(`docker-${container.name}`, `${container.name}: ${container.status} (${container.image})`)) - : [text("docker-empty", docker.error || "no running containers")]), - pane("Periwinkle / ADB", adb.devices.length - ? adb.devices.map((device) => text(`adb-${device.serial}`, `${device.serial}: ${device.state}`)) - : [text("adb-empty", adb.error || "no devices")]), - ], - }, - assets: [], - }; -} - -function observationPane(observations) { - return { - id: "observation-streams", - kind: "pane", - props: { - title: "Device Observation Streams", - source: observations.source, - status: observations.state, - detail: observations.detail, - }, - children: observations.streams.length - ? observations.streams.map(observationStreamNode) - : [text("observation-streams-empty", observations.detail || "no observation streams")], - }; -} - -function observationStreamNode(entry) { - return { - id: `observation-stream-${stableId(entry.deviceId)}-${stableId(entry.streamId)}-${stableId(entry.kind)}`, - kind: "observation-stream", - props: entry, - children: [ - text(`observation-${stableId(entry.streamId)}-schema`, entry.document), - text(`observation-${stableId(entry.streamId)}-latest`, `${entry.kind} seq ${entry.sequence} age ${entry.ageSeconds}s`), - text(`observation-${stableId(entry.streamId)}-shape`, observationShape(entry)), - ], - }; -} - -function stack(id, children) { - return { id, kind: "stack", props: {}, children }; -} - -function pane(title, children) { - return { id: stableId(`pane-${title}`), kind: "pane", props: { title }, children }; -} - -function card(id, children, tone = "default") { - return { id, kind: "card", props: { title: id }, style: { tone }, children }; -} - -function text(id, value) { - return { id, kind: "text", props: { text: value }, children: [] }; -} - -function metric(id, label, value, tone) { - return { id, kind: "metric", props: { label, text: `${label}: ${value}`, value, tone }, children: [] }; -} - -function verse(verseId, name, role, status, capabilities, services = []) { - return { verseId, name, role, status, capabilities, services }; -} - -function service(id, name, state, detail = "") { - return { id: stableId(id), name, state, detail: String(detail || "") }; -} - -function observationServices(observations, deviceId) { - return observations.streams - .filter((entry) => entry.deviceId === deviceId) - .map((entry) => service( - `observation-${entry.streamId}-${entry.kind}`, - `${entry.kind} observation`, - entry.state, - observationShape(entry), - )); -} - -function hostServices(hostName, checks) { - return (checks || []).map((check) => - service(`${hostName}-${check.name}`, `${hostName} ${check.name}`, check.state === "open" || check.state === "ok" ? "active" : check.state, check.detail), - ); -} - -function systemdState(state) { - if (state === "active") return "active"; - if (state === "inactive") return "inactive"; - if (state === "failed") return "failed"; - return state.startsWith("unknown") ? "unknown" : "warn"; -} - -function hostState(checks) { - if (!checks) return "unknown"; - if (checks.some((check) => check.state === "open" || check.state === "ok")) return "reachable"; - if (checks.some((check) => check.state === "timeout")) return "timeout"; - return "offline"; -} - -async function hostChecks() { - return { - Starfire: [{ name: "local", state: "ok", detail: os.hostname() }], - Nightwing: await checksFor("192.168.1.75", [["ssh", 22], ["eve-broker", 8795], ["eve-browser", 8891]]), - Raven: await checksFor("192.168.1.84", [["ssh", 22]]), - EVE: await checksFor("192.168.1.72", [["ssh", 22]]), - Yggdrasil: await checksFor("yggdrasil.gamecult.org", [["ssh", 22], ["http", 80], ["https", 443]]), - }; -} - -async function checksFor(host, specs) { - return Promise.all(specs.map(async ([name, port]) => ({ - name, - ...(await tcpCheck(host, port)), - }))); -} - -function tcpCheck(host, port) { - return new Promise((resolve) => { - const socket = net.createConnection({ host, port, timeout: 900 }); - socket.on("connect", () => { - socket.destroy(); - resolve({ state: "open", detail: `${host}:${port}` }); - }); - socket.on("timeout", () => { - socket.destroy(); - resolve({ state: "timeout", detail: `${host}:${port}` }); - }); - socket.on("error", (error) => { - resolve({ state: "closed", detail: error.code || error.message }); - }); - }); -} - -async function dockerSnapshot() { - try { - const { stdout } = await execFileAsync("docker", ["ps", "--format", "{{json .}}"], { timeout: 3000 }); - const containers = stdout.split(/\r?\n/) - .filter(Boolean) - .map((line) => JSON.parse(line)) - .map((item) => ({ - name: item.Names, - image: item.Image, - status: item.Status, - ports: item.Ports, - })); - return { state: "ok", containers, error: "" }; - } catch (error) { - return { state: "error", containers: [], error: error.message }; - } -} - -async function adbSnapshot() { - const adb = path.join(process.env.LOCALAPPDATA || "", "Android", "Sdk", "platform-tools", "adb.exe"); - if (!fs.existsSync(adb)) { - return { state: "missing", devices: [], error: `missing ${adb}` }; - } - try { - const { stdout } = await execFileAsync(adb, ["devices"], { timeout: 3000 }); - const devices = stdout.split(/\r?\n/) - .map((line) => line.match(/^(\S+)\s+(device|offline|unauthorized)$/)) - .filter(Boolean) - .map((match) => ({ serial: match[1], state: match[2] })); - return { state: "ok", devices, error: "" }; - } catch (error) { - return { state: "error", devices: [], error: error.message }; - } -} - -async function observationSnapshot(source, freshSeconds) { - if (!fs.existsSync(source)) { - return { - state: "missing", - source, - detail: `missing ${source}`, - streams: [], - }; - } - - try { - const lines = tailTextFile(source, 512 * 1024) - .split(/\r?\n/) - .filter(Boolean); - const nowMs = Date.now(); - const byStream = new Map(); - let accepted = 0; - for (const line of lines) { - let item; - try { - item = JSON.parse(line); - } catch { - continue; - } - - if (item.type !== "cultmesh-observation" && item.type !== "cultmesh-media-observation") { - continue; - } - - accepted += 1; - const key = `${item.DeviceId || "unknown"}:${item.StreamId || "unknown"}:${item.Kind || "unknown"}`; - byStream.set(key, item); - } - - const streams = [...byStream.values()] - .map((item) => observationStream(item, nowMs, freshSeconds)) - .sort((left, right) => left.deviceId.localeCompare(right.deviceId) - || left.streamId.localeCompare(right.streamId) - || left.kind.localeCompare(right.kind)); - - const activeCount = streams.filter((entry) => entry.state === "active").length; - return { - state: streams.length ? (activeCount ? "active" : "stale") : "waiting", - source, - detail: `${streams.length} streams, ${activeCount} active, ${accepted} recent records`, - streams, - }; - } catch (error) { - return { - state: "error", - source, - detail: error.message, - streams: [], - }; - } -} - -function tailTextFile(filePath, maxBytes) { - const stat = fs.statSync(filePath); - const length = Math.min(stat.size, maxBytes); - const buffer = Buffer.alloc(length); - const fd = fs.openSync(filePath, "r"); - try { - fs.readSync(fd, buffer, 0, length, stat.size - length); - } finally { - fs.closeSync(fd); - } - - return buffer.toString("utf8"); -} - -function observationStream(item, nowMs, freshSeconds) { - const latestMs = Date.parse(item.WallClockUtc || ""); - const ageSeconds = Number.isFinite(latestMs) ? Math.max(0, Math.round((nowMs - latestMs) / 1000)) : null; - return { - deviceId: String(item.DeviceId || "unknown"), - streamId: String(item.StreamId || "unknown"), - kind: String(item.Kind || "unknown"), - document: String(item.document || "unknown"), - state: ageSeconds === null ? "unknown" : ageSeconds <= freshSeconds ? "active" : "stale", - sequence: item.Sequence ?? null, - latestAt: item.WallClockUtc || "", - ageSeconds, - clockDomainId: item.ClockDomainId || "", - format: item.Format || "", - width: item.Width ?? null, - height: item.Height ?? null, - sampleRate: item.SampleRate ?? null, - channels: item.Channels ?? null, - frameCount: item.FrameCount ?? null, - payloadEncoding: item.PayloadEncoding || "", - payloadBytes: item.PayloadBytes ?? null, - values: Array.isArray(item.Values) ? item.Values : null, - accuracy: item.Accuracy ?? null, - action: item.Action ?? null, - pointerCount: item.PointerCount ?? null, - x: item.X ?? null, - y: item.Y ?? null, - }; -} - -function observationShape(entry) { - if (entry.width && entry.height) { - return `${entry.format || "media"} ${entry.width}x${entry.height}, ${entry.payloadBytes || 0} bytes`; - } - - if (entry.sampleRate) { - return `${entry.format || "audio"} ${entry.sampleRate} Hz x ${entry.channels || 1}, ${entry.frameCount || 0} frames, ${entry.payloadBytes || 0} bytes`; - } - - if (entry.values) { - return `${entry.values.map((value) => Number(value).toFixed(3)).join(", ")} accuracy ${entry.accuracy ?? "unknown"}`; - } - - if (entry.action) { - return `${entry.action} pointers ${entry.pointerCount ?? "unknown"} @ ${entry.x ?? "?"},${entry.y ?? "?"}`; - } - - return entry.state; -} - -async function remoteServices(target, services) { - const script = `for s in ${services.join(" ")}; do printf '%s=' "$s"; systemctl is-active "$s" 2>/dev/null || true; done`; - try { - const { stdout } = await execFileAsync("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=3", target, script], { timeout: 6000 }); - return stdout.split(/\r?\n/) - .map((line) => line.match(/^([^=]+)=(.+)$/)) - .filter(Boolean) - .map((match) => ({ name: match[1], state: match[2].trim() })); - } catch (error) { - return services.map((name) => ({ name, state: `unknown: ${error.message}` })); - } -} - -async function remoteGpu(target) { - try { - const { stdout } = await execFileAsync("ssh", [ - "-o", "BatchMode=yes", - "-o", "ConnectTimeout=3", - target, - "nvidia-smi --query-gpu=name,driver_version,memory.total,utilization.gpu --format=csv,noheader,nounits 2>/dev/null | head -n 1", - ], { timeout: 6000 }); - const detail = stdout.trim(); - return detail ? { state: "active", detail } : { state: "unknown", detail: "nvidia-smi returned no GPU" }; - } catch (error) { - return { state: "unknown", detail: error.message }; - } -} - -async function discoverInterfaces() { - await refreshLanDeckDiscovery(); - const manifestsByProvider = new Map(); - for (const deckUrl of discoveredDeckUrls) { - const manifestUrl = deckUrl.replace(/^ws:/, "http:").replace(/\/eve\/deck.*$/, "/eve/deck/providers"); - try { - const catalog = JSON.parse(await httpGet(manifestUrl, 2500)); - for (const provider of catalog.providers || []) { - if (!provider?.id || provider.id === "eve.dashboard.broker") continue; - const existing = manifestsByProvider.get(provider.id); - if (!existing || deckUrl.startsWith("ws://127.0.0.1")) { - manifestsByProvider.set(provider.id, { provider, deckUrl }); - } - } - } 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; -} - -async function discoverCultMeshInterfaceBindings() { - if (!CultMesh || !interfaceBindingDefinition || !surfaceDefinition || !voidbotSwarmSnapshotDefinition || !providerAdvertisementDefinition) { - return []; - } - const interfaces = []; - for (const storePath of interfaceBindingStores) { - try { - if (!fs.existsSync(storePath)) { - continue; - } - const node = await CultMesh.createNode(storePath, { - documents: [ - voidbotSwarmSnapshotDefinition, - providerAdvertisementDefinition, - interfaceBindingDefinition, - surfaceDefinition, - ], - }); - const binding = node.get(interfaceBindingDefinition, "voidbot.swarm"); - if (!binding?.providerId) { - continue; - } - const state = node.get(surfaceDefinition, binding.providerId); - interfaces.push({ - providerId: binding.providerId, - title: binding.title || state?.title || binding.providerId, - state: "active", - detail: `${state?.surface?.root?.kind || binding.kind || "surface"} ${state?.nodes?.length || 0} nodes via CultMesh`, - version: state?.version || 0, - updatedAt: state?.updatedAt || binding.updatedAt || new Date().toISOString(), - source: `cultmesh:${storePath}`, - manifest: binding.provider || null, - surface: state?.surface || binding.surface || null, - }); - } catch (error) { - interfaces.push(dashboardUnavailable("voidbot.swarm", `cultmesh:${storePath}`, error.message)); - } - } - return interfaces; -} - -async function refreshLanDeckDiscovery() { - const now = Date.now(); - if (now - lastLanScanAt < 60000) return; - lastLanScanAt = now; - const urls = new Set(seedDeckUrls); - const checks = []; - for (const prefix of localIpv4Prefixes()) { - for (let host = 1; host <= 254; host += 1) { - const address = `${prefix}.${host}`; - checks.push(tcpCheck(address, 8795).then((check) => { - if (check.state === "open") urls.add(`ws://${address}:8795/eve/deck`); - })); - } - } - await Promise.allSettled(checks); - discoveredDeckUrls = [...urls].sort(); -} - -function localIpv4Prefixes() { - const prefixes = new Set(); - for (const entries of Object.values(os.networkInterfaces())) { - for (const entry of entries || []) { - if (entry.family !== "IPv4" || entry.internal) continue; - const parts = entry.address.split("."); - if (parts.length === 4) prefixes.add(parts.slice(0, 3).join(".")); - } - } - return [...prefixes]; -} - -function httpGet(url, timeoutMs) { - return new Promise((resolve, reject) => { - const req = http.get(url, { timeout: timeoutMs }, (res) => { - if ((res.statusCode || 0) >= 400) { - reject(new Error(`HTTP ${res.statusCode}`)); - res.resume(); - return; - } - const chunks = []; - res.on("data", (chunk) => chunks.push(chunk)); - res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); - }); - req.on("timeout", () => req.destroy(new Error("HTTP request timed out"))); - req.on("error", reject); - }); -} - -async function fetchEveProvider(url, providerId, manifest = null) { - try { - const socket = await openWebSocket(url); - try { - sendClientFrame(socket, JSON.stringify({ type: "open-provider", providerId })); - for (let index = 0; index < 8; index += 1) { - const message = await readServerTextFrame(socket, 2500); - const state = JSON.parse(message); - if (state?.providerId === providerId) { - return { - providerId, - title: state.title || manifest?.title || providerId, - state: "active", - detail: `${state.surface?.root?.kind || "surface"} ${state.nodes?.length || 0} nodes`, - version: state.version, - updatedAt: state.updatedAt, - source: url, - manifest, - surface: state.surface, - }; - } - } - return dashboardUnavailable(providerId, url, "provider did not publish matching state"); - } finally { - socket.destroy(); - } - } catch (error) { - return dashboardUnavailable(providerId, url, error.message); - } -} - -function dashboardUnavailable(providerId, source, detail) { - return { - providerId, - title: providerId, - state: "unavailable", - detail, - version: 0, - updatedAt: new Date().toISOString(), - source, - manifest: null, - surface: null, - }; -} - -function openWebSocket(url) { - return new Promise((resolve, reject) => { - const parsed = new URL(url); - const port = Number(parsed.port || (parsed.protocol === "wss:" ? 443 : 80)); - const socket = net.createConnection({ host: parsed.hostname, port, timeout: 2500 }); - const key = crypto.randomBytes(16).toString("base64"); - const pathName = `${parsed.pathname || "/"}${parsed.search || ""}`; - let buffer = Buffer.alloc(0); - socket.on("connect", () => { - socket.write([ - `GET ${pathName} HTTP/1.1`, - `Host: ${parsed.hostname}:${port}`, - "Upgrade: websocket", - "Connection: Upgrade", - `Sec-WebSocket-Key: ${key}`, - "Sec-WebSocket-Version: 13", - "", - "", - ].join("\r\n")); - }); - socket.on("data", function onHandshake(chunk) { - buffer = Buffer.concat([buffer, chunk]); - const marker = buffer.indexOf("\r\n\r\n"); - if (marker < 0) return; - const header = buffer.subarray(0, marker).toString("latin1"); - if (!header.startsWith("HTTP/1.1 101")) { - reject(new Error(header.split(/\r?\n/)[0] || "websocket handshake failed")); - socket.destroy(); - return; - } - socket.off("data", onHandshake); - socket.unshift(buffer.subarray(marker + 4)); - resolve(socket); - }); - socket.on("timeout", () => { - reject(new Error("websocket connection timed out")); - socket.destroy(); - }); - socket.on("error", reject); - }); -} - -function sendClientFrame(socket, textValue) { - const payload = Buffer.from(textValue, "utf8"); - const mask = crypto.randomBytes(4); - const header = [0x81]; - if (payload.length < 126) { - header.push(0x80 | payload.length); - } else if (payload.length <= 0xffff) { - header.push(0x80 | 126, payload.length >> 8, payload.length & 0xff); - } else { - const length = Buffer.alloc(8); - length.writeBigUInt64BE(BigInt(payload.length)); - header.push(0x80 | 127, ...length); - } - const masked = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4])); - socket.write(Buffer.concat([Buffer.from(header), mask, masked])); -} - -function readServerTextFrame(socket, timeoutMs) { - return new Promise((resolve, reject) => { - let buffer = Buffer.alloc(0); - const timer = setTimeout(() => cleanup(new Error("timed out waiting for dashboard frame")), timeoutMs); - function cleanup(error, value) { - clearTimeout(timer); - socket.off("data", onData); - socket.off("error", onError); - if (error) reject(error); - else resolve(value); - } - function onError(error) { - cleanup(error); - } - function onData(chunk) { - buffer = Buffer.concat([buffer, chunk]); - const frame = tryReadFrame(buffer); - if (!frame) return; - buffer = buffer.subarray(frame.consumed); - if (frame.opcode === 0x1) cleanup(null, frame.payload.toString("utf8")); - if (frame.opcode === 0x8) cleanup(new Error("dashboard websocket closed")); - } - socket.on("data", onData); - socket.on("error", onError); - }); -} - -function tryReadFrame(buffer) { - if (buffer.length < 2) return null; - const opcode = buffer[0] & 0x0f; - const masked = Boolean(buffer[1] & 0x80); - let length = buffer[1] & 0x7f; - let offset = 2; - if (length === 126) { - if (buffer.length < offset + 2) return null; - length = buffer.readUInt16BE(offset); - offset += 2; - } else if (length === 127) { - if (buffer.length < offset + 8) return null; - length = Number(buffer.readBigUInt64BE(offset)); - offset += 8; - } - const mask = masked ? buffer.subarray(offset, offset + 4) : null; - if (masked) offset += 4; - if (buffer.length < offset + length) return null; - let payload = buffer.subarray(offset, offset + length); - if (mask) { - payload = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4])); - } - return { opcode, payload, consumed: offset + length }; -} - -function readLayout() { - try { - return JSON.parse(fs.readFileSync(layoutPath, "utf8")); - } catch { - return { schema: "odin.interface_layout.v1", tiles: {} }; - } -} - -function writeLayout(layout) { - fs.writeFileSync(layoutPath, JSON.stringify(layout, null, 2), "utf8"); -} - -function defaultLayoutFor(entry, interfaces) { - const index = Math.max(0, interfaces.findIndex((candidate) => candidate.providerId === entry.providerId)); - const intent = surfaceLayoutIntent(entry); - const preferredHeight = positiveNumber(intent.preferredHeight, entry.providerId === "voidbot.swarm" ? 16 : 10); - const minHeight = positiveNumber(intent.minHeight, 8); - const preferredWidth = positiveNumber(intent.preferredWidth, 96); - const minWidth = positiveNumber(intent.minWidth, 48); - const priority = Number.isFinite(intent.priority) ? intent.priority : index; - return { - tileId: stableId(entry.providerId || "interface"), - visible: true, - priority, - x: index % 2, - y: Math.floor(index / 2), - w: 1, - h: Math.max(1, Math.ceil(preferredHeight / 12)), - minWidth, - minHeight, - preferredWidth, - preferredHeight, - density: intent.density || "adaptive", - viewportMode: intent.viewportMode || "adaptive", - }; -} - -function fullscreenLayoutIntent(tileId, priority = -100) { - return { - tileId: stableId(tileId || "fullscreen"), - visible: true, - priority, - x: 0, - y: 0, - w: 4, - h: 4, - minWidth: 96, - minHeight: 36, - preferredWidth: 192, - preferredHeight: 48, - density: "comfortable", - viewportMode: "fullscreen", - }; -} - -function mergeLayoutIntent(existing, entry, interfaces) { - const base = existing && typeof existing === "object" - ? { ...existing } - : defaultLayoutFor(entry, interfaces); - const intent = surfaceLayoutIntent(entry); - const fallback = defaultLayoutFor(entry, interfaces); - const minWidth = positiveNumber(intent.minWidth, positiveNumber(base.minWidth, fallback.minWidth)); - const minHeight = positiveNumber(intent.minHeight, positiveNumber(base.minHeight, fallback.minHeight)); - const preferredWidth = positiveNumber(intent.preferredWidth, positiveNumber(base.preferredWidth, fallback.preferredWidth)); - const preferredHeight = positiveNumber(intent.preferredHeight, positiveNumber(base.preferredHeight, fallback.preferredHeight)); - return { - ...base, - minWidth, - minHeight, - preferredWidth: Math.max(positiveNumber(base.preferredWidth, preferredWidth), preferredWidth), - preferredHeight: Math.max(positiveNumber(base.preferredHeight, preferredHeight), preferredHeight), - h: Math.max(positiveNumber(base.h, fallback.h), Math.ceil(preferredHeight / 12)), - w: Math.max(positiveNumber(base.w, fallback.w), Math.ceil(preferredWidth / 96)), - priority: Number.isFinite(intent.priority) ? Math.min(Number(base.priority ?? intent.priority), intent.priority) : base.priority, - density: intent.density || base.density || fallback.density, - viewportMode: intent.viewportMode || base.viewportMode || fallback.viewportMode, - }; -} - -function surfaceLayoutIntent(entry) { - const root = entry?.surface?.root; - if (!root || typeof root !== "object") return {}; - const layout = root.layout && typeof root.layout === "object" ? root.layout : {}; - const props = root.props && typeof root.props === "object" ? root.props : {}; - const propLayout = props.layout && typeof props.layout === "object" ? props.layout : {}; - return { ...propLayout, ...layout }; -} - -function positiveNumber(value, fallback) { - const number = Number(value); - return Number.isFinite(number) && number > 0 ? number : fallback; -} - -function applyClientCommand(command) { - if (!command || command.type !== "odin-layout-intent" || !command.providerId) return; - const layout = readLayout(); - layout.schema = "odin.interface_layout.v1"; - layout.updatedAt = new Date().toISOString(); - layout.tiles ||= {}; - const current = layout.tiles[command.providerId] || defaultLayoutFor({ providerId: command.providerId }, []); - const next = { ...current }; - if (command.action === "focus") { - Object.assign(next, fullscreenLayoutIntent(command.providerId, -100)); - } else if (command.action === "resize") { - next.w = clampNumber((next.w || 1) + Number(command.dw || 0), 1, 4); - next.h = clampNumber((next.h || 1) + Number(command.dh || 0), 1, 4); - } else if (command.action === "move") { - next.x = clampNumber((next.x || 0) + Number(command.dx || 0), 0, 12); - next.y = clampNumber((next.y || 0) + Number(command.dy || 0), 0, 12); - } else if (command.action === "toggle") { - next.visible = !next.visible; - } else { - return; - } - layout.tiles[command.providerId] = next; - writeLayout(layout); -} - -function clampNumber(value, min, max) { - if (!Number.isFinite(value)) return min; - return Math.max(min, Math.min(max, Math.round(value))); -} - -function stableId(value) { - return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); -} - -function parseArgs(argv) { - const parsed = {}; - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index]; - if (!arg.startsWith("--")) continue; - const key = arg.slice(2); - const next = argv[index + 1]; - if (!next || next.startsWith("--")) { - parsed[key] = true; - } else { - parsed[key] = next; - index += 1; - } - } - return parsed; -} diff --git a/src/odin/config.cjs b/src/odin/config.cjs new file mode 100644 index 0000000..af9cd22 --- /dev/null +++ b/src/odin/config.cjs @@ -0,0 +1,64 @@ +"use strict"; + +const Module = require("module"); +const path = require("path"); +const { parseArgs } = require("./utils.cjs"); + +const repoRoot = path.resolve(__dirname, "..", ".."); + +process.env.NODE_PATH = [ + path.resolve(repoRoot, "..", "CultLib", "packages"), + process.env.NODE_PATH || "", +].filter(Boolean).join(path.delimiter); +Module._initPaths(); + +function buildConfig(argv) { + const args = parseArgs(argv); + const port = Number(args.port || 8797); + const host = args.host || "0.0.0.0"; + const intervalMs = Number(args.intervalMs || 5000); + const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin"); + const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp"); + const layoutPath = args.layoutPath || path.join(stateDir, "interface-layout.json"); + const seedDeckUrls = String(args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck,ws://127.0.0.1:8796/eve/deck,ws://192.168.1.75:8795/eve/deck") + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean); + const observationLogPath = args.observationLogPath || path.join(repoRoot, "..", "Mimir", "artifacts", "runtime", "periwinkle-cultmesh-sensors.out.log"); + const observationFreshSeconds = Number(args.observationFreshSeconds || 120); + const interfaceBindingStores = String( + args.interfaceBindingStore || + path.join(repoRoot, "..", "VoidBot", ".voidbot", "status", "cultmesh", "voidbot-swarm-state.cc"), + ) + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean); + + return { + args, + cachePath, + host, + interfaceBindingStores, + intervalMs, + layoutPath, + observationFreshSeconds, + observationLogPath, + port, + repoRoot, + seedDeckUrls, + stateDir, + surfaceKey: "surface:gamecult.network.status", + }; +} + +function loadCultRuntime() { + try { + const { CultMesh } = require("cultmesh-ts/dist/index.js"); + const { defineDocumentType } = require("cultcache-ts/dist/index.js"); + return { CultMesh, defineDocumentType, error: null }; + } catch (error) { + return { CultMesh: null, defineDocumentType: null, error }; + } +} + +module.exports = { buildConfig, loadCultRuntime, repoRoot }; diff --git a/src/odin/documents.cjs b/src/odin/documents.cjs new file mode 100644 index 0000000..204952d --- /dev/null +++ b/src/odin/documents.cjs @@ -0,0 +1,66 @@ +"use strict"; + +const { parseObjectDocument } = require("./utils.cjs"); + +function defineOdinDocuments(defineDocumentType) { + if (!defineDocumentType) { + return { + interfaceBindingDefinition: null, + providerAdvertisementDefinition: null, + surfaceDefinition: null, + voidbotSwarmSnapshotDefinition: null, + }; + } + + const surfaceDefinition = defineDocumentType({ + type: "gamecult.eve.surface_state", + schemaName: "gamecult.eve.surface_state", + schemaId: "gamecult.eve.surface_state.v1", + schemaVersion: "gamecult.eve.surface_state.v1", + global: true, + name: (value) => value?.providerId || "surface", + schema: { parse: (value) => value }, + members: [ + { slot: 0, memberName: "providerId", typeName: "string", isName: true }, + { slot: 1, memberName: "title", typeName: "string" }, + { slot: 2, memberName: "version", typeName: "long" }, + { slot: 3, memberName: "updatedAt", typeName: "string" }, + { slot: 4, memberName: "surface", typeName: "object" }, + ], + }); + const interfaceBindingDefinition = defineDocumentType({ + type: "gamecult.eve.interface_binding", + schemaName: "gamecult.eve.interface_binding", + schemaId: "gamecult.eve.interface_binding.v1", + schemaVersion: "gamecult.eve.interface_binding.v1", + global: true, + name: (value) => value?.bindingId || value?.providerId || "interface", + schema: parseObjectDocument("Eve interface binding"), + }); + const providerAdvertisementDefinition = defineDocumentType({ + type: "gamecult.eve.provider_advertisement", + schemaName: "gamecult.eve.provider_advertisement", + schemaId: "gamecult.eve.provider_advertisement.v1", + schemaVersion: "gamecult.eve.provider_advertisement.v1", + global: true, + name: (value) => value?.providerId || "provider", + schema: parseObjectDocument("Eve provider advertisement"), + }); + const voidbotSwarmSnapshotDefinition = defineDocumentType({ + type: "voidbot.swarm_state_snapshot", + schemaName: "voidbot.swarm_state_snapshot", + schemaId: "voidbot.swarm_state_snapshot.v1", + schemaVersion: "voidbot.swarm_state_snapshot.v1", + global: true, + schema: parseObjectDocument("VoidBot swarm snapshot"), + }); + + return { + interfaceBindingDefinition, + providerAdvertisementDefinition, + surfaceDefinition, + voidbotSwarmSnapshotDefinition, + }; +} + +module.exports = { defineOdinDocuments }; diff --git a/src/odin/interfaces.cjs b/src/odin/interfaces.cjs new file mode 100644 index 0000000..55694cf --- /dev/null +++ b/src/odin/interfaces.cjs @@ -0,0 +1,185 @@ +"use strict"; + +const fs = require("fs"); +const os = require("os"); +const { httpGet } = require("./utils.cjs"); +const { openWebSocket, readServerTextFrame, sendClientFrame } = require("./websocket.cjs"); +const { tcpCheck } = require("./probes.cjs"); + +function createInterfaceDiscovery({ + CultMesh, + documents, + interfaceBindingStores, + seedDeckUrls, +}) { + let discoveredDeckUrls = [...seedDeckUrls]; + let lastLanScanAt = 0; + + async function discoverInterfaces() { + await refreshLanDeckDiscovery(); + const manifestsByProvider = new Map(); + for (const deckUrl of discoveredDeckUrls) { + const manifestUrl = deckUrl.replace(/^ws:/, "http:").replace(/\/eve\/deck.*$/, "/eve/deck/providers"); + try { + const catalog = JSON.parse(await httpGet(manifestUrl, 2500)); + for (const provider of catalog.providers || []) { + if (!provider?.id || provider.id === "eve.dashboard.broker") continue; + const existing = manifestsByProvider.get(provider.id); + if (!existing || deckUrl.startsWith("ws://127.0.0.1")) { + manifestsByProvider.set(provider.id, { provider, deckUrl }); + } + } + } 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; + } + + async function discoverCultMeshInterfaceBindings() { + const { + interfaceBindingDefinition, + providerAdvertisementDefinition, + surfaceDefinition, + voidbotSwarmSnapshotDefinition, + } = documents; + if (!CultMesh || !interfaceBindingDefinition || !surfaceDefinition || !voidbotSwarmSnapshotDefinition || !providerAdvertisementDefinition) { + return []; + } + const interfaces = []; + for (const storePath of interfaceBindingStores) { + try { + if (!fs.existsSync(storePath)) { + continue; + } + const node = await CultMesh.createNode(storePath, { + documents: [ + voidbotSwarmSnapshotDefinition, + providerAdvertisementDefinition, + interfaceBindingDefinition, + surfaceDefinition, + ], + }); + const binding = node.get(interfaceBindingDefinition, "voidbot.swarm"); + if (!binding?.providerId) { + continue; + } + const state = node.get(surfaceDefinition, binding.providerId); + interfaces.push({ + providerId: binding.providerId, + title: binding.title || state?.title || binding.providerId, + state: "active", + detail: `${state?.surface?.root?.kind || binding.kind || "surface"} ${state?.nodes?.length || 0} nodes via CultMesh`, + version: state?.version || 0, + updatedAt: state?.updatedAt || binding.updatedAt || new Date().toISOString(), + source: `cultmesh:${storePath}`, + manifest: binding.provider || null, + surface: state?.surface || binding.surface || null, + }); + } catch (error) { + interfaces.push(dashboardUnavailable("voidbot.swarm", `cultmesh:${storePath}`, error.message)); + } + } + return interfaces; + } + + async function refreshLanDeckDiscovery() { + const now = Date.now(); + if (now - lastLanScanAt < 60000) return; + lastLanScanAt = now; + const urls = new Set(seedDeckUrls); + const checks = []; + for (const prefix of localIpv4Prefixes()) { + for (let host = 1; host <= 254; host += 1) { + const address = `${prefix}.${host}`; + checks.push(tcpCheck(address, 8795).then((check) => { + if (check.state === "open") urls.add(`ws://${address}:8795/eve/deck`); + })); + } + } + await Promise.allSettled(checks); + discoveredDeckUrls = [...urls].sort(); + } + + return { + discoverInterfaces, + getDiscoveredDeckUrls: () => [...discoveredDeckUrls], + }; +} + +function localIpv4Prefixes() { + const prefixes = new Set(); + for (const entries of Object.values(os.networkInterfaces())) { + for (const entry of entries || []) { + if (entry.family !== "IPv4" || entry.internal) continue; + const parts = entry.address.split("."); + if (parts.length === 4) prefixes.add(parts.slice(0, 3).join(".")); + } + } + return [...prefixes]; +} + +async function fetchEveProvider(url, providerId, manifest = null) { + try { + const socket = await openWebSocket(url); + try { + sendClientFrame(socket, JSON.stringify({ type: "open-provider", providerId })); + for (let index = 0; index < 8; index += 1) { + const message = await readServerTextFrame(socket, 2500); + const state = JSON.parse(message); + if (state?.providerId === providerId) { + return { + providerId, + title: state.title || manifest?.title || providerId, + state: "active", + detail: `${state.surface?.root?.kind || "surface"} ${state.nodes?.length || 0} nodes`, + version: state.version, + updatedAt: state.updatedAt, + source: url, + manifest, + surface: state.surface, + }; + } + } + return dashboardUnavailable(providerId, url, "provider did not publish matching state"); + } finally { + socket.destroy(); + } + } catch (error) { + return dashboardUnavailable(providerId, url, error.message); + } +} + +function dashboardUnavailable(providerId, source, detail) { + return { + providerId, + title: providerId, + state: "unavailable", + detail, + version: 0, + updatedAt: new Date().toISOString(), + source, + manifest: null, + surface: null, + }; +} + +module.exports = { + createInterfaceDiscovery, + dashboardUnavailable, + fetchEveProvider, +}; diff --git a/src/odin/layout.cjs b/src/odin/layout.cjs new file mode 100644 index 0000000..0487063 --- /dev/null +++ b/src/odin/layout.cjs @@ -0,0 +1,129 @@ +"use strict"; + +const fs = require("fs"); +const { clampNumber, positiveNumber, stableId } = require("./utils.cjs"); + +function createLayoutStore(layoutPath) { + return { + readLayout() { + try { + return JSON.parse(fs.readFileSync(layoutPath, "utf8")); + } catch { + return { schema: "odin.interface_layout.v1", tiles: {} }; + } + }, + + writeLayout(layout) { + fs.writeFileSync(layoutPath, JSON.stringify(layout, null, 2), "utf8"); + }, + + applyClientCommand(command) { + if (!command || command.type !== "odin-layout-intent" || !command.providerId) return; + const layout = this.readLayout(); + layout.schema = "odin.interface_layout.v1"; + layout.updatedAt = new Date().toISOString(); + layout.tiles ||= {}; + const current = layout.tiles[command.providerId] || defaultLayoutFor({ providerId: command.providerId }, []); + const next = { ...current }; + if (command.action === "focus") { + Object.assign(next, fullscreenLayoutIntent(command.providerId, -100)); + } else if (command.action === "resize") { + next.w = clampNumber((next.w || 1) + Number(command.dw || 0), 1, 4); + next.h = clampNumber((next.h || 1) + Number(command.dh || 0), 1, 4); + } else if (command.action === "move") { + next.x = clampNumber((next.x || 0) + Number(command.dx || 0), 0, 12); + next.y = clampNumber((next.y || 0) + Number(command.dy || 0), 0, 12); + } else if (command.action === "toggle") { + next.visible = !next.visible; + } else { + return; + } + layout.tiles[command.providerId] = next; + this.writeLayout(layout); + }, + }; +} + +function defaultLayoutFor(entry, interfaces) { + const index = Math.max(0, interfaces.findIndex((candidate) => candidate.providerId === entry.providerId)); + const intent = surfaceLayoutIntent(entry); + const preferredHeight = positiveNumber(intent.preferredHeight, entry.providerId === "voidbot.swarm" ? 16 : 10); + const minHeight = positiveNumber(intent.minHeight, 8); + const preferredWidth = positiveNumber(intent.preferredWidth, 96); + const minWidth = positiveNumber(intent.minWidth, 48); + const priority = Number.isFinite(intent.priority) ? intent.priority : index; + return { + tileId: stableId(entry.providerId || "interface"), + visible: true, + priority, + x: index % 2, + y: Math.floor(index / 2), + w: 1, + h: Math.max(1, Math.ceil(preferredHeight / 12)), + minWidth, + minHeight, + preferredWidth, + preferredHeight, + density: intent.density || "adaptive", + viewportMode: intent.viewportMode || "adaptive", + }; +} + +function fullscreenLayoutIntent(tileId, priority = -100) { + return { + tileId: stableId(tileId || "fullscreen"), + visible: true, + priority, + x: 0, + y: 0, + w: 4, + h: 4, + minWidth: 96, + minHeight: 36, + preferredWidth: 192, + preferredHeight: 48, + density: "comfortable", + viewportMode: "fullscreen", + }; +} + +function mergeLayoutIntent(existing, entry, interfaces) { + const base = existing && typeof existing === "object" + ? { ...existing } + : defaultLayoutFor(entry, interfaces); + const intent = surfaceLayoutIntent(entry); + const fallback = defaultLayoutFor(entry, interfaces); + const minWidth = positiveNumber(intent.minWidth, positiveNumber(base.minWidth, fallback.minWidth)); + const minHeight = positiveNumber(intent.minHeight, positiveNumber(base.minHeight, fallback.minHeight)); + const preferredWidth = positiveNumber(intent.preferredWidth, positiveNumber(base.preferredWidth, fallback.preferredWidth)); + const preferredHeight = positiveNumber(intent.preferredHeight, positiveNumber(base.preferredHeight, fallback.preferredHeight)); + return { + ...base, + minWidth, + minHeight, + preferredWidth: Math.max(positiveNumber(base.preferredWidth, preferredWidth), preferredWidth), + preferredHeight: Math.max(positiveNumber(base.preferredHeight, preferredHeight), preferredHeight), + h: Math.max(positiveNumber(base.h, fallback.h), Math.ceil(preferredHeight / 12)), + w: Math.max(positiveNumber(base.w, fallback.w), Math.ceil(preferredWidth / 96)), + priority: Number.isFinite(intent.priority) ? Math.min(Number(base.priority ?? intent.priority), intent.priority) : base.priority, + density: intent.density || base.density || fallback.density, + viewportMode: intent.viewportMode || base.viewportMode || fallback.viewportMode, + }; +} + +function surfaceLayoutIntent(entry) { + const root = entry?.surface?.root; + if (!root || typeof root !== "object") return {}; + const layout = root.layout && typeof root.layout === "object" ? root.layout : {}; + const props = root.props && typeof root.props === "object" ? root.props : {}; + const propLayout = props.layout && typeof props.layout === "object" ? props.layout : {}; + return { ...propLayout, ...layout }; +} + +module.exports = { + createLayoutStore, + defaultLayoutFor, + fullscreenLayoutIntent, + mergeLayoutIntent, + surfaceLayoutIntent, +}; diff --git a/src/odin/observations.cjs b/src/odin/observations.cjs new file mode 100644 index 0000000..4d77ad7 --- /dev/null +++ b/src/odin/observations.cjs @@ -0,0 +1,158 @@ +"use strict"; + +const fs = require("fs"); +const { stableId, tailTextFile } = require("./utils.cjs"); + +async function observationSnapshot(source, freshSeconds) { + if (!fs.existsSync(source)) { + return { + state: "missing", + source, + detail: `missing ${source}`, + streams: [], + }; + } + + try { + const lines = tailTextFile(source, 512 * 1024) + .split(/\r?\n/) + .filter(Boolean); + const nowMs = Date.now(); + const byStream = new Map(); + let accepted = 0; + for (const line of lines) { + let item; + try { + item = JSON.parse(line); + } catch { + continue; + } + + if (item.type !== "cultmesh-observation" && item.type !== "cultmesh-media-observation") { + continue; + } + + accepted += 1; + const key = `${item.DeviceId || "unknown"}:${item.StreamId || "unknown"}:${item.Kind || "unknown"}`; + byStream.set(key, item); + } + + const streams = [...byStream.values()] + .map((item) => observationStream(item, nowMs, freshSeconds)) + .sort((left, right) => left.deviceId.localeCompare(right.deviceId) + || left.streamId.localeCompare(right.streamId) + || left.kind.localeCompare(right.kind)); + + const activeCount = streams.filter((entry) => entry.state === "active").length; + return { + state: streams.length ? (activeCount ? "active" : "stale") : "waiting", + source, + detail: `${streams.length} streams, ${activeCount} active, ${accepted} recent records`, + streams, + }; + } catch (error) { + return { + state: "error", + source, + detail: error.message, + streams: [], + }; + } +} + +function observationStream(item, nowMs, freshSeconds) { + const latestMs = Date.parse(item.WallClockUtc || ""); + const ageSeconds = Number.isFinite(latestMs) ? Math.max(0, Math.round((nowMs - latestMs) / 1000)) : null; + return { + deviceId: String(item.DeviceId || "unknown"), + streamId: String(item.StreamId || "unknown"), + kind: String(item.Kind || "unknown"), + document: String(item.document || "unknown"), + state: ageSeconds === null ? "unknown" : ageSeconds <= freshSeconds ? "active" : "stale", + sequence: item.Sequence ?? null, + latestAt: item.WallClockUtc || "", + ageSeconds, + clockDomainId: item.ClockDomainId || "", + format: item.Format || "", + width: item.Width ?? null, + height: item.Height ?? null, + sampleRate: item.SampleRate ?? null, + channels: item.Channels ?? null, + frameCount: item.FrameCount ?? null, + payloadEncoding: item.PayloadEncoding || "", + payloadBytes: item.PayloadBytes ?? null, + values: Array.isArray(item.Values) ? item.Values : null, + accuracy: item.Accuracy ?? null, + action: item.Action ?? null, + pointerCount: item.PointerCount ?? null, + x: item.X ?? null, + y: item.Y ?? null, + }; +} + +function observationServices(observations, deviceId, service) { + return observations.streams + .filter((entry) => entry.deviceId === deviceId) + .map((entry) => service( + `observation-${entry.streamId}-${entry.kind}`, + `${entry.kind} observation`, + entry.state, + observationShape(entry), + )); +} + +function observationPane(observations, text) { + return { + id: "observation-streams", + kind: "pane", + props: { + title: "Device Observation Streams", + source: observations.source, + status: observations.state, + detail: observations.detail, + }, + children: observations.streams.length + ? observations.streams.map((entry) => observationStreamNode(entry, text)) + : [text("observation-streams-empty", observations.detail || "no observation streams")], + }; +} + +function observationStreamNode(entry, text) { + return { + id: `observation-stream-${stableId(entry.deviceId)}-${stableId(entry.streamId)}-${stableId(entry.kind)}`, + kind: "observation-stream", + props: entry, + children: [ + text(`observation-${stableId(entry.streamId)}-schema`, entry.document), + text(`observation-${stableId(entry.streamId)}-latest`, `${entry.kind} seq ${entry.sequence} age ${entry.ageSeconds}s`), + text(`observation-${stableId(entry.streamId)}-shape`, observationShape(entry)), + ], + }; +} + +function observationShape(entry) { + if (entry.width && entry.height) { + return `${entry.format || "media"} ${entry.width}x${entry.height}, ${entry.payloadBytes || 0} bytes`; + } + + if (entry.sampleRate) { + return `${entry.format || "audio"} ${entry.sampleRate} Hz x ${entry.channels || 1}, ${entry.frameCount || 0} frames, ${entry.payloadBytes || 0} bytes`; + } + + if (entry.values) { + return `${entry.values.map((value) => Number(value).toFixed(3)).join(", ")} accuracy ${entry.accuracy ?? "unknown"}`; + } + + if (entry.action) { + return `${entry.action} pointers ${entry.pointerCount ?? "unknown"} @ ${entry.x ?? "?"},${entry.y ?? "?"}`; + } + + return entry.state; +} + +module.exports = { + observationPane, + observationServices, + observationShape, + observationSnapshot, +}; diff --git a/src/odin/probes.cjs b/src/odin/probes.cjs new file mode 100644 index 0000000..ac17ba0 --- /dev/null +++ b/src/odin/probes.cjs @@ -0,0 +1,144 @@ +"use strict"; + +const fs = require("fs"); +const net = require("net"); +const os = require("os"); +const path = require("path"); +const { execFile } = require("child_process"); +const { promisify } = require("util"); + +const execFileAsync = promisify(execFile); + +async function hostChecks() { + return { + Starfire: [{ name: "local", state: "ok", detail: os.hostname() }], + Nightwing: await checksFor("192.168.1.75", [["ssh", 22], ["eve-broker", 8795], ["eve-browser", 8891]]), + Raven: await checksFor("192.168.1.84", [["ssh", 22]]), + EVE: await checksFor("192.168.1.72", [["ssh", 22]]), + Yggdrasil: await checksFor("yggdrasil.gamecult.org", [["ssh", 22], ["http", 80], ["https", 443]]), + }; +} + +async function checksFor(host, specs) { + return Promise.all(specs.map(async ([name, port]) => ({ + name, + ...(await tcpCheck(host, port)), + }))); +} + +function tcpCheck(host, port) { + return new Promise((resolve) => { + const socket = net.createConnection({ host, port, timeout: 900 }); + socket.on("connect", () => { + socket.destroy(); + resolve({ state: "open", detail: `${host}:${port}` }); + }); + socket.on("timeout", () => { + socket.destroy(); + resolve({ state: "timeout", detail: `${host}:${port}` }); + }); + socket.on("error", (error) => { + resolve({ state: "closed", detail: error.code || error.message }); + }); + }); +} + +async function dockerSnapshot() { + try { + const { stdout } = await execFileAsync("docker", ["ps", "--format", "{{json .}}"], { timeout: 3000 }); + const containers = stdout.split(/\r?\n/) + .filter(Boolean) + .map((line) => JSON.parse(line)) + .map((item) => ({ + name: item.Names, + image: item.Image, + status: item.Status, + ports: item.Ports, + })); + return { state: "ok", containers, error: "" }; + } catch (error) { + return { state: "error", containers: [], error: error.message }; + } +} + +async function adbSnapshot() { + const adb = path.join(process.env.LOCALAPPDATA || "", "Android", "Sdk", "platform-tools", "adb.exe"); + if (!fs.existsSync(adb)) { + return { state: "missing", devices: [], error: `missing ${adb}` }; + } + try { + const { stdout } = await execFileAsync(adb, ["devices"], { timeout: 3000 }); + const devices = stdout.split(/\r?\n/) + .map((line) => line.match(/^(\S+)\s+(device|offline|unauthorized)$/)) + .filter(Boolean) + .map((match) => ({ serial: match[1], state: match[2] })); + return { state: "ok", devices, error: "" }; + } catch (error) { + return { state: "error", devices: [], error: error.message }; + } +} + +async function remoteServices(target, services) { + const script = `for s in ${services.join(" ")}; do printf '%s=' "$s"; systemctl is-active "$s" 2>/dev/null || true; done`; + try { + const { stdout } = await execFileAsync("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=3", target, script], { timeout: 6000 }); + return stdout.split(/\r?\n/) + .map((line) => line.match(/^([^=]+)=(.+)$/)) + .filter(Boolean) + .map((match) => ({ name: match[1], state: match[2].trim() })); + } catch (error) { + return services.map((name) => ({ name, state: `unknown: ${error.message}` })); + } +} + +async function remoteGpu(target) { + try { + const { stdout } = await execFileAsync("ssh", [ + "-o", "BatchMode=yes", + "-o", "ConnectTimeout=3", + target, + "nvidia-smi --query-gpu=name,driver_version,memory.total,utilization.gpu --format=csv,noheader,nounits 2>/dev/null | head -n 1", + ], { timeout: 6000 }); + const detail = stdout.trim(); + return detail ? { state: "active", detail } : { state: "unknown", detail: "nvidia-smi returned no GPU" }; + } catch (error) { + return { state: "unknown", detail: error.message }; + } +} + +function hostState(checks) { + if (!checks) return "unknown"; + if (checks.some((check) => check.state === "open" || check.state === "ok")) return "reachable"; + if (checks.some((check) => check.state === "timeout")) return "timeout"; + return "offline"; +} + +function hostServices(hostName, checks) { + return (checks || []).map((check) => + service(`${hostName}-${check.name}`, `${hostName} ${check.name}`, check.state === "open" || check.state === "ok" ? "active" : check.state, check.detail), + ); +} + +function systemdState(state) { + if (state === "active") return "active"; + if (state === "inactive") return "inactive"; + if (state === "failed") return "failed"; + return state.startsWith("unknown") ? "unknown" : "warn"; +} + +function service(id, name, state, detail = "") { + return { id: require("./utils.cjs").stableId(id), name, state, detail: String(detail || "") }; +} + +module.exports = { + adbSnapshot, + checksFor, + dockerSnapshot, + hostChecks, + hostServices, + hostState, + remoteGpu, + remoteServices, + systemdState, + tcpCheck, +}; diff --git a/src/odin/state.cjs b/src/odin/state.cjs new file mode 100644 index 0000000..8f82318 --- /dev/null +++ b/src/odin/state.cjs @@ -0,0 +1,128 @@ +"use strict"; + +const fs = require("fs"); +const { dashboardUnavailable } = require("./interfaces.cjs"); +const { observationServices, observationSnapshot } = require("./observations.cjs"); +const { + adbSnapshot, + dockerSnapshot, + hostChecks, + hostServices, + hostState, + remoteGpu, + remoteServices, + systemdState, +} = require("./probes.cjs"); +const { buildPendingSurface, buildSurface } = require("./surface.cjs"); +const { stableId } = require("./utils.cjs"); + +function createStateBuilder({ cachePath, interfaceDiscovery, layoutStore, observationFreshSeconds, observationLogPath }) { + let version = 0; + + function buildPendingState(message) { + return { + type: "dashboard-state", + schema: "mimir.eve_dashboard_state.v1", + providerId: "odin.allseer", + title: "Odin All-Seer", + version, + updatedAt: new Date().toISOString(), + selectedNodeId: "coordinator-starting", + lutPreset: "terminal", + nodes: [], + surface: buildPendingSurface(message), + }; + } + + async function buildState() { + version += 1; + const observedAt = new Date().toISOString(); + const [docker, adb, hosts, yggdrasilServices, nightwingServices, nightwingGpu, discoveredInterfaces, 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", "gamecult-visible-ops", "docker"]), + remoteGpu("nightwing"), + interfaceDiscovery.discoverInterfaces(), + observationSnapshot(observationLogPath, observationFreshSeconds), + ]); + const interfaceById = new Map(discoveredInterfaces.map((entry) => [entry.providerId, entry])); + const interfaces = [...interfaceById.values()]; + const interfaceSummary = interfaces.map((entry) => `${entry.providerId}:${entry.state}`).join(", "); + const voidBotDashboard = interfaceById.get("voidbot.swarm") || dashboardUnavailable("voidbot.swarm", "discovery", "not discovered"); + const mimirLiveStats = interfaceById.get("mimir.live.stats") || dashboardUnavailable("mimir.live.stats", "discovery", "not discovered"); + + const verses = [ + verse("starfire.local", "Starfire", "coordinator", "active", ["docker", "adb", "eve-provider"], [ + service("odin", "Odin all-seer", "active", "ws://0.0.0.0:8797/eve/deck"), + service("docker", "Docker", docker.state === "ok" ? "active" : "warn", `${docker.containers.length} running`), + service("adb", "Periwinkle ADB", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), + service("cultcache", "Odin CultCache", fs.existsSync(cachePath) ? "active" : "waiting", require("path").basename(cachePath)), + service("interfaces", "Eve interfaces", interfaces.some((entry) => entry.surface?.root) ? "active" : "waiting", interfaceSummary || "no provider surfaces"), + service("voidbot-swarm", "VoidBot Swarm", voidBotDashboard.state, voidBotDashboard.detail), + service("mimir-live-stats", "Mimir Live Stats", mimirLiveStats.state, mimirLiveStats.detail), + service("mimir-observation-ledger", "Mimir Observation Ledger", observations.state, observations.detail), + ...docker.containers.map((container) => service(`docker-${container.name}`, container.name, "active", container.image)), + ]), + verse("nightwing.local", "Nightwing", "dashboard-renderer", hostState(hosts.Nightwing), ["eve-tui", "gpu-worker"], [ + ...hostServices("Nightwing", hosts.Nightwing), + ...nightwingServices.map((entry) => service(`systemd-${entry.name}`, entry.name, systemdState(entry.state), entry.state)), + service("gpu", "GTX 860M", nightwingGpu.state, nightwingGpu.detail), + ]), + verse("eve.ipad", "EVE", "ios-client", hostState(hosts.EVE), ["ssh", "native-eve"], hostServices("EVE", hosts.EVE)), + verse("periwinkle.android", "Periwinkle", "android-client", adb.devices.length ? "connected" : "waiting", ["adb", "sensor-edge"], [ + service("adb-device", "ADB device", adb.devices.length ? "active" : "waiting", adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ") || adb.error || "no devices"), + ...observationServices(observations, "periwinkle", service), + ]), + verse("raven.local", "Raven", "local-peer", hostState(hosts.Raven), ["ssh"], hostServices("Raven", hosts.Raven)), + verse("yggdrasil.ops", "Yggdrasil", "ops-host", hostState(hosts.Yggdrasil), ["ssh", "https", "services"], [ + ...hostServices("Yggdrasil", hosts.Yggdrasil), + ...yggdrasilServices.map((entry) => service(`systemd-${entry.name}`, entry.name, systemdState(entry.state), entry.state)), + ]), + ]; + + return { + type: "dashboard-state", + schema: "mimir.eve_dashboard_state.v1", + providerId: "odin.allseer", + title: "Odin All-Seer", + version, + updatedAt: observedAt, + selectedNodeId: "verse-starfire.local", + lutPreset: "terminal", + nodes: verses.map((entry, index) => ({ + id: `verse-${entry.verseId}`, + label: `${entry.name}\n${entry.role}`, + kind: "cultmesh-verse", + visible: true, + x: -0.8 + (index % 3) * 0.8, + y: -0.45 + Math.floor(index / 3) * 0.55, + z: 0, + rotation: 0, + scale: 1, + width: 0.42, + height: 0.2, + health: entry.status, + detail: entry.capabilities.join(", "), + })), + surface: buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces, observations, layout: layoutStore.readLayout() }), + }; + } + + return { buildPendingState, buildState }; +} + +function verse(verseId, name, role, status, capabilities, services = []) { + return { verseId, name, role, status, capabilities, services }; +} + +function service(id, name, state, detail = "") { + return { id: stableId(id), name, state, detail: String(detail || "") }; +} + +module.exports = { + createStateBuilder, + service, + verse, +}; diff --git a/src/odin/surface.cjs b/src/odin/surface.cjs new file mode 100644 index 0000000..1cfab24 --- /dev/null +++ b/src/odin/surface.cjs @@ -0,0 +1,134 @@ +"use strict"; + +const { fullscreenLayoutIntent, mergeLayoutIntent } = require("./layout.cjs"); +const { observationPane } = require("./observations.cjs"); +const { stableId } = require("./utils.cjs"); + +function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verses, interfaces, observations, layout }) { + const activeInterfaces = interfaces.filter((entry) => entry.surface?.root); + const activeObservationStreams = observations.streams.filter((entry) => entry.state === "active"); + return { + schema: "gamecult.eve.surface.v1", + id: "gamecult.network.status.surface", + title: "Odin", + root: { + id: "network-root", + kind: "dashboard", + props: { + title: "Odin All-Seer", + observedAt, + summary: `${verses.length} Verses / ${verses.reduce((sum, entry) => sum + entry.services.length, 0)} services / ${activeInterfaces.length} interfaces / ${activeObservationStreams.length} live streams`, + layout: fullscreenLayoutIntent("odin.allseer", -100), + }, + children: [ + pane("Coordinator", [ + text("observed", `observed ${observedAt}`), + text("authority", "Odin owns Verse discovery, schema awareness, translation planning, and accepted surface publication. Renderers lower only."), + metric("docker-count", "Docker containers", docker.containers.length, docker.state === "ok" ? "ok" : "warn"), + metric("adb-count", "ADB devices", adb.devices.length, adb.devices.length ? "ok" : "warn"), + metric("observation-stream-count", "Observation streams", observations.streams.length, activeObservationStreams.length ? "ok" : "warn"), + text("observation-ledger", `observation ledger: ${observations.detail}`), + ]), + observationPane(observations, text), + ...interfaces.map((entry) => ({ + id: `interface-${stableId(entry.providerId)}`, + kind: "interface", + props: { + title: entry.title, + providerId: entry.providerId, + source: entry.source, + status: entry.state, + detail: entry.detail, + version: entry.version, + updatedAt: entry.updatedAt, + layout: mergeLayoutIntent(layout.tiles?.[entry.providerId], entry, interfaces), + }, + children: entry.surface?.root ? [entry.surface.root] : [ + text(`interface-${stableId(entry.providerId)}-missing`, entry.detail || "interface unavailable"), + ], + })), + ...verses.map((entry) => ({ + id: `verse-${entry.verseId}`, + kind: "verse", + props: { + title: entry.name, + verseId: entry.verseId, + role: entry.role, + status: entry.status, + capabilities: entry.capabilities, + services: entry.services, + }, + children: entry.services.map((item) => ({ + id: `service-${entry.verseId}-${item.id}`, + kind: "service", + props: item, + children: [], + })), + })), + pane("Verses", verses.map((entry) => + card(`verse-card-${entry.verseId}`, [ + text(`verse-title-${entry.verseId}`, `${entry.name} :: ${entry.verseId}`), + text(`verse-status-${entry.verseId}`, `${entry.role} / ${entry.status}`), + text(`verse-caps-${entry.verseId}`, entry.capabilities.join(", ")), + ], entry.status), + )), + pane("Hosts", Object.entries(hosts).map(([name, checks]) => + card(`host-${name}`, [ + text(`host-name-${name}`, name), + ...checks.map((check) => text(`host-${name}-${check.name}`, `${check.name}: ${check.state}${check.detail ? ` ${check.detail}` : ""}`)), + ], checks.some((check) => check.state === "open" || check.state === "ok") ? "ok" : "warn"), + )), + pane("Yggdrasil Services", yggdrasilServices.map((service) => + text(`ygg-${service.name}`, `${service.name}: ${service.state}`), + )), + pane("Docker", docker.containers.length + ? docker.containers.slice(0, 10).map((container) => text(`docker-${container.name}`, `${container.name}: ${container.status} (${container.image})`)) + : [text("docker-empty", docker.error || "no running containers")]), + pane("Periwinkle / ADB", adb.devices.length + ? adb.devices.map((device) => text(`adb-${device.serial}`, `${device.serial}: ${device.state}`)) + : [text("adb-empty", adb.error || "no devices")]), + ], + }, + assets: [], + }; +} + +function buildPendingSurface(message) { + return { + schema: "gamecult.eve.surface.v1", + id: "gamecult.network.status.surface", + title: "Odin", + root: stack("root", [pane("Coordinator", [text("status", message)])]), + assets: [], + }; +} + +function stack(id, children) { + return { id, kind: "stack", props: {}, children }; +} + +function pane(title, children) { + return { id: stableId(`pane-${title}`), kind: "pane", props: { title }, children }; +} + +function card(id, children, tone = "default") { + return { id, kind: "card", props: { title: id }, style: { tone }, children }; +} + +function text(id, value) { + return { id, kind: "text", props: { text: value }, children: [] }; +} + +function metric(id, label, value, tone) { + return { id, kind: "metric", props: { label, text: `${label}: ${value}`, value, tone }, children: [] }; +} + +module.exports = { + buildPendingSurface, + buildSurface, + card, + metric, + pane, + stack, + text, +}; diff --git a/src/odin/utils.cjs b/src/odin/utils.cjs new file mode 100644 index 0000000..fea9fb7 --- /dev/null +++ b/src/odin/utils.cjs @@ -0,0 +1,86 @@ +"use strict"; + +const fs = require("fs"); + +function parseArgs(argv) { + const parsed = {}; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (!arg.startsWith("--")) continue; + const key = arg.slice(2); + const next = argv[index + 1]; + if (!next || next.startsWith("--")) { + parsed[key] = true; + } else { + parsed[key] = next; + index += 1; + } + } + return parsed; +} + +function parseObjectDocument(label) { + return { + parse(value) { + if (!value || typeof value !== "object") { + throw new Error(`${label} must be an object`); + } + return value; + }, + }; +} + +function positiveNumber(value, fallback) { + const number = Number(value); + return Number.isFinite(number) && number > 0 ? number : fallback; +} + +function stableId(value) { + return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); +} + +function clampNumber(value, min, max) { + if (!Number.isFinite(value)) return min; + return Math.max(min, Math.min(max, Math.round(value))); +} + +function tailTextFile(filePath, maxBytes) { + const stat = fs.statSync(filePath); + const length = Math.min(stat.size, maxBytes); + const buffer = Buffer.alloc(length); + const fd = fs.openSync(filePath, "r"); + try { + fs.readSync(fd, buffer, 0, length, stat.size - length); + } finally { + fs.closeSync(fd); + } + + return buffer.toString("utf8"); +} + +function httpGet(url, timeoutMs) { + return new Promise((resolve, reject) => { + const req = require("http").get(url, { timeout: timeoutMs }, (res) => { + if ((res.statusCode || 0) >= 400) { + reject(new Error(`HTTP ${res.statusCode}`)); + res.resume(); + return; + } + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); + }); + req.on("timeout", () => req.destroy(new Error("HTTP request timed out"))); + req.on("error", reject); + }); +} + +module.exports = { + clampNumber, + httpGet, + parseArgs, + parseObjectDocument, + positiveNumber, + stableId, + tailTextFile, +}; diff --git a/src/odin/websocket.cjs b/src/odin/websocket.cjs new file mode 100644 index 0000000..f3bcb40 --- /dev/null +++ b/src/odin/websocket.cjs @@ -0,0 +1,239 @@ +"use strict"; + +const crypto = require("crypto"); +const http = require("http"); +const net = require("net"); + +function createDashboardServer({ applyClientCommand, getCurrentState, getHealth, host, port }) { + const clients = new Set(); + const server = http.createServer((req, res) => handleHttp(req, res, getCurrentState, getHealth)); + server.on("upgrade", (req, socket) => handleUpgrade(req, socket, clients, getCurrentState, applyClientCommand)); + server.listen(port, host, () => { + console.log(`Odin coordinator listening on ws://${host}:${port}/eve/deck`); + }); + return { clients, server }; +} + +function handleHttp(req, res, getCurrentState, getHealth) { + const currentState = getCurrentState(); + if (req.url === "/health") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify(getHealth())); + return; + } + + if (req.url === "/eve/deck/providers") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ + providers: [{ + id: currentState.providerId, + title: currentState.title, + description: "Odin central coordinator for GameCult Verse discovery, schema awareness, translation, and network status.", + version: String(currentState.version), + endpoint: "/eve/deck", + capabilities: ["network-status", "cultmesh-verses", "cultui-surface"], + usesCultMesh: true, + transport: "CultMesh durable surface + Eve WebSocket", + }], + })); + return; + } + + res.writeHead(404, { "content-type": "text/plain" }); + res.end("not found"); +} + +function handleUpgrade(req, socket, clients, getCurrentState, applyClientCommand) { + if (!req.url.startsWith("/eve/deck")) { + socket.end("HTTP/1.1 404 Not Found\r\n\r\n"); + return; + } + + const key = req.headers["sec-websocket-key"]; + if (!key) { + socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); + return; + } + + const accept = crypto + .createHash("sha1") + .update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + .digest("base64"); + socket.write( + "HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + `Sec-WebSocket-Accept: ${accept}\r\n\r\n`, + ); + clients.add(socket); + sendFrame(socket, 0x1, Buffer.from(JSON.stringify(getCurrentState()), "utf8")); + socket.on("data", (chunk) => handleClientFrame(socket, chunk, clients, applyClientCommand)); + socket.on("close", () => clients.delete(socket)); + socket.on("error", () => clients.delete(socket)); +} + +function handleClientFrame(socket, chunk, clients, applyClientCommand) { + const frame = tryReadFrame(chunk); + const opcode = frame?.opcode ?? (chunk[0] & 0x0f); + if (opcode === 0x8) { + clients.delete(socket); + socket.end(); + return; + } + if (opcode !== 0x1 || !frame) { + return; + } + try { + const command = JSON.parse(frame.payload.toString("utf8")); + applyClientCommand(command); + } catch { + // Renderer input is advisory. Bad client frames do not get to kill Odin. + } +} + +function broadcastState(clients, state) { + const payload = Buffer.from(JSON.stringify(state), "utf8"); + for (const client of [...clients]) { + try { + sendFrame(client, 0x1, payload); + } catch { + clients.delete(client); + client.destroy(); + } + } +} + +function sendFrame(socket, opcode, payload) { + const header = [0x80 | opcode]; + if (payload.length < 126) { + header.push(payload.length); + } else if (payload.length <= 0xffff) { + header.push(126, payload.length >> 8, payload.length & 0xff); + } else { + const length = Buffer.alloc(8); + length.writeBigUInt64BE(BigInt(payload.length)); + header.push(127, ...length); + } + socket.write(Buffer.concat([Buffer.from(header), payload])); +} + +function openWebSocket(url) { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const port = Number(parsed.port || (parsed.protocol === "wss:" ? 443 : 80)); + const socket = net.createConnection({ host: parsed.hostname, port, timeout: 2500 }); + const key = crypto.randomBytes(16).toString("base64"); + const pathName = `${parsed.pathname || "/"}${parsed.search || ""}`; + let buffer = Buffer.alloc(0); + socket.on("connect", () => { + socket.write([ + `GET ${pathName} HTTP/1.1`, + `Host: ${parsed.hostname}:${port}`, + "Upgrade: websocket", + "Connection: Upgrade", + `Sec-WebSocket-Key: ${key}`, + "Sec-WebSocket-Version: 13", + "", + "", + ].join("\r\n")); + }); + socket.on("data", function onHandshake(chunk) { + buffer = Buffer.concat([buffer, chunk]); + const marker = buffer.indexOf("\r\n\r\n"); + if (marker < 0) return; + const header = buffer.subarray(0, marker).toString("latin1"); + if (!header.startsWith("HTTP/1.1 101")) { + reject(new Error(header.split(/\r?\n/)[0] || "websocket handshake failed")); + socket.destroy(); + return; + } + socket.off("data", onHandshake); + socket.unshift(buffer.subarray(marker + 4)); + resolve(socket); + }); + socket.on("timeout", () => { + reject(new Error("websocket connection timed out")); + socket.destroy(); + }); + socket.on("error", reject); + }); +} + +function sendClientFrame(socket, textValue) { + const payload = Buffer.from(textValue, "utf8"); + const mask = crypto.randomBytes(4); + const header = [0x81]; + if (payload.length < 126) { + header.push(0x80 | payload.length); + } else if (payload.length <= 0xffff) { + header.push(0x80 | 126, payload.length >> 8, payload.length & 0xff); + } else { + const length = Buffer.alloc(8); + length.writeBigUInt64BE(BigInt(payload.length)); + header.push(0x80 | 127, ...length); + } + const masked = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4])); + socket.write(Buffer.concat([Buffer.from(header), mask, masked])); +} + +function readServerTextFrame(socket, timeoutMs) { + return new Promise((resolve, reject) => { + let buffer = Buffer.alloc(0); + const timer = setTimeout(() => cleanup(new Error("timed out waiting for dashboard frame")), timeoutMs); + function cleanup(error, value) { + clearTimeout(timer); + socket.off("data", onData); + socket.off("error", onError); + if (error) reject(error); + else resolve(value); + } + function onError(error) { + cleanup(error); + } + function onData(chunk) { + buffer = Buffer.concat([buffer, chunk]); + const frame = tryReadFrame(buffer); + if (!frame) return; + buffer = buffer.subarray(frame.consumed); + if (frame.opcode === 0x1) cleanup(null, frame.payload.toString("utf8")); + if (frame.opcode === 0x8) cleanup(new Error("dashboard websocket closed")); + } + socket.on("data", onData); + socket.on("error", onError); + }); +} + +function tryReadFrame(buffer) { + if (buffer.length < 2) return null; + const opcode = buffer[0] & 0x0f; + const masked = Boolean(buffer[1] & 0x80); + let length = buffer[1] & 0x7f; + let offset = 2; + if (length === 126) { + if (buffer.length < offset + 2) return null; + length = buffer.readUInt16BE(offset); + offset += 2; + } else if (length === 127) { + if (buffer.length < offset + 8) return null; + length = Number(buffer.readBigUInt64BE(offset)); + offset += 8; + } + const mask = masked ? buffer.subarray(offset, offset + 4) : null; + if (masked) offset += 4; + if (buffer.length < offset + length) return null; + let payload = buffer.subarray(offset, offset + length); + if (mask) { + payload = Buffer.from(payload.map((byte, index) => byte ^ mask[index % 4])); + } + return { opcode, payload, consumed: offset + length }; +} + +module.exports = { + broadcastState, + createDashboardServer, + openWebSocket, + readServerTextFrame, + sendClientFrame, + sendFrame, + tryReadFrame, +}; From 3d0257d4772b4640a4883b2f26d456d190ec2610 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 18:13:07 +0100 Subject: [PATCH 012/742] Document Odin service compliance witness --- README.md | 5 +++++ docs/architecture.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/README.md b/README.md index 891c785..bb2ab75 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ Odin is the GameCult all-seer: the central CultMesh node every Verse can use to It is not the renderer. It is not Eve. It is not a Starfire utility script wearing a bigger coat. Odin owns discovery, schema awareness, translation planning, and accepted operator surfaces. Eve and the Nightwing compact TUI lower Odin's published CultUI surface into whatever body they have. +Odin is also the compliance witness for the GameCult service architecture: +durable service state in CultCache `.cc`, local Verse visibility through +CultMesh, meaningful dashboards as Eve GUI/TUI DSL, and renderers as lowerers +only. + ## Authority Map - Owner: Odin owns the network-wide Verse registry, schema catalog index, translation map, and the accepted `odin.allseer` Eve surface. diff --git a/docs/architecture.md b/docs/architecture.md index 9d1b30e..2a4b94e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,6 +60,32 @@ Verse announcement - CultCache is the durable state substrate; CultNet is the wire vocabulary; CultMesh is the Verse and peer-consensus layer. - The Eve surface carries explicit `verse`, `service`, and `observation-stream` nodes. Compact renderers may derive visual facets from those nodes, but may not invent observation truth. +## Service Architecture Contract + +Odin is the witness for the GameCult service contract: + +```text +durable service state -> CultCache .cc +shared local visibility -> CultMesh +interactive operator surface -> Eve GUI/TUI DSL +discovery and aggregation -> Odin +renderer bodies -> Eve clients, browser, compact TUI, native surfaces, overlays +``` + +When Odin sees a service, it should be able to answer: + +- What Verse owns this service? +- Which typed schemas does it publish? +- Where is its durable `.cc` state or CultCache-compatible store? +- Which CultMesh documents or providers make it visible locally? +- Which Eve GUI/TUI surface represents its meaningful operator interface? +- Which command boundary accepts, denies, forwards, or reconciles user intent? +- Which fields are stale, predicted, denied, or authoritative? + +This is not a reporting nicety. It is how Odin prevents services from becoming +private little islands with separate dashboards, separate state formats, and +separate command languages. + ## Current Service Surface Odin currently publishes service squares for: @@ -80,6 +106,12 @@ Odin discovers Eve deck provider manifests from known deck endpoints and LAN hos This is the model for future services: if a service publishes an operator interface, ingest the Eve/CultUI composition graph and lower it. Do not collapse it into a service-status tile unless the graph is unavailable and the tile is explicitly a temporary probe. +The expected provider output is Eve DSL or an equivalent +`gamecult.eve.surface.v1` retained tree. GUI and TUI are lowerings of the same +interactive language; they are not separate dashboard products. Huginn's `.cc` +inspection surface is the current clean example: Huginn inspects CultCache bytes +and emits Eve DSL, while Eve or any other runtime owns presentation. + Odin persists operator layout intent as `odin.interface_layout.v1` under `scratch/odin/interface-layout.json` for the current Starfire body. The durable CultMesh document should replace this local file once the layout schema is promoted. Layout intents name the provider id and request focus, move, resize, or visibility changes; renderers are input devices for those intents, not local layout owners. ## Current Observation Surface From 58ba7431948f4fb46b57717559a801e9a9aa4cec Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 18:45:23 +0100 Subject: [PATCH 013/742] Discover Fensalir daemon provider --- src/odin/config.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/odin/config.cjs b/src/odin/config.cjs index af9cd22..f7a7f58 100644 --- a/src/odin/config.cjs +++ b/src/odin/config.cjs @@ -20,7 +20,7 @@ function buildConfig(argv) { const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin"); const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp"); const layoutPath = args.layoutPath || path.join(stateDir, "interface-layout.json"); - const seedDeckUrls = String(args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck,ws://127.0.0.1:8796/eve/deck,ws://192.168.1.75:8795/eve/deck") + const seedDeckUrls = String(args.eveDeckUrl || "ws://127.0.0.1:8795/eve/deck,ws://127.0.0.1:8796/eve/deck,ws://127.0.0.1:8799/eve/deck,ws://192.168.1.75:8795/eve/deck") .split(",") .map((entry) => entry.trim()) .filter(Boolean); From acdd4fc936f187330562acf9055885fb9885dad6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 19:54:20 +0100 Subject: [PATCH 014/742] Mark Odin overview as Bifrost pride surface --- src/odin/surface.cjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/odin/surface.cjs b/src/odin/surface.cjs index 1cfab24..a21d0ea 100644 --- a/src/odin/surface.cjs +++ b/src/odin/surface.cjs @@ -19,6 +19,12 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse observedAt, summary: `${verses.length} Verses / ${verses.reduce((sum, entry) => sum + entry.services.length, 0)} services / ${activeInterfaces.length} interfaces / ${activeObservationStreams.length} live streams`, layout: fullscreenLayoutIntent("odin.allseer", -100), + presentation: { + theme: "bifrost-pride", + frame: "animated-rainbow", + dividerMotion: "bifrost-rainbow-dance", + intent: "Nightwing Odin Overview should render with an animated rainbow frame and dancing dividers; renderer owns lowering, Odin owns truth.", + }, }, children: [ pane("Coordinator", [ From 5ecbd8750a62cb6293a0befb6bef72485ed5b788 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 22:47:21 +0100 Subject: [PATCH 015/742] Package Gjallar herald persona --- README.md | 13 +++ assets/personas/gjallar-avatar.png | Bin 0 -> 2477819 bytes docs/architecture.md | 7 ++ docs/gjallar.md | 91 ++++++++++++++++++++ package.json | 1 + personas/gjallar.persona_state.json | 123 ++++++++++++++++++++++++++++ 6 files changed, 235 insertions(+) create mode 100644 assets/personas/gjallar-avatar.png create mode 100644 docs/gjallar.md create mode 100644 personas/gjallar.persona_state.json diff --git a/README.md b/README.md index bb2ab75..7f5800b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,19 @@ durable service state in CultCache `.cc`, local Verse visibility through CultMesh, meaningful dashboards as Eve GUI/TUI DSL, and renderers as lowerers only. +## Gjallar + +Gjallar is Odin's herald organ: the daemon/persona package that should carry +Odin's gathered sight into agent context. Odin sees the Verses; Gjallar +transmits the usable tapestry of affordances without becoming the owner of +discovery, probing, rendering, or schema truth. + +Local package surfaces: + +- Organ contract: `docs/gjallar.md` +- Persona state: `personas/gjallar.persona_state.json` +- Avatar asset: `assets/personas/gjallar-avatar.png` + ## Authority Map - Owner: Odin owns the network-wide Verse registry, schema catalog index, translation map, and the accepted `odin.allseer` Eve surface. diff --git a/assets/personas/gjallar-avatar.png b/assets/personas/gjallar-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..7779bf307c5221f1d861f4a8fee7695bbbd1d444 GIT binary patch literal 2477819 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 HDU2Q+}NMD!z&N>cHjR?!?3z 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

I; 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

HP);d<-8^Tdl#QoCBuL(G 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