diff --git a/src/odin/state.cjs b/src/odin/state.cjs index 8f82318..cb93039 100644 --- a/src/odin/state.cjs +++ b/src/odin/state.cjs @@ -52,12 +52,16 @@ function createStateBuilder({ cachePath, interfaceDiscovery, layoutStore, observ 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 periwinkleServices = [ + ...adbFaultServices(adb), + ...observationServices(observations, "periwinkle", service), + ]; 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("adb", "ADB transport", adbState(adb), adbDetail(adb)), 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), @@ -71,16 +75,13 @@ function createStateBuilder({ cachePath, interfaceDiscovery, layoutStore, observ 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), - ]), + periwinkleServices.length ? verse("periwinkle.android", "Periwinkle", "android-client", periwinkleStatus(periwinkleServices), ["sensor-edge", "adb-transport"], periwinkleServices) : null, 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)), ]), - ]; + ].filter(Boolean); return { type: "dashboard-state", @@ -121,6 +122,38 @@ function service(id, name, state, detail = "") { return { id: stableId(id), name, state, detail: String(detail || "") }; } +function adbState(adb) { + if (adb.state !== "ok") return adb.state; + return adbFaultDevices(adb).length ? "warn" : "ok"; +} + +function adbDetail(adb) { + if (adb.error) return adb.error; + if (adb.devices.length === 0) return "no devices"; + return adb.devices.map((device) => `${device.serial}:${device.state}`).join(", "); +} + +function adbFaultDevices(adb) { + return adb.devices.filter((device) => device.state !== "device"); +} + +function adbFaultServices(adb) { + if (adb.state !== "ok") { + return [service("adb-transport", "ADB transport", adb.state, adbDetail(adb))]; + } + + return adbFaultDevices(adb).map((device) => + service(`adb-${device.serial}`, "ADB device fault", device.state, `${device.serial}:${device.state}`), + ); +} + +function periwinkleStatus(services) { + if (services.some((entry) => entry.state === "active")) return "active"; + if (services.some((entry) => entry.state === "stale")) return "stale"; + if (services.some((entry) => entry.state === "offline" || entry.state === "unauthorized" || entry.state === "error" || entry.state === "missing")) return "warn"; + return "waiting"; +} + module.exports = { createStateBuilder, service, diff --git a/src/odin/surface.cjs b/src/odin/surface.cjs index a21d0ea..d7cc687 100644 --- a/src/odin/surface.cjs +++ b/src/odin/surface.cjs @@ -31,7 +31,7 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse 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("adb-count", "ADB devices", adb.devices.length, adbTone(adb)), metric("observation-stream-count", "Observation streams", observations.streams.length, activeObservationStreams.length ? "ok" : "warn"), text("observation-ledger", `observation ledger: ${observations.detail}`), ]), @@ -90,9 +90,6 @@ function buildSurface({ observedAt, docker, adb, hosts, yggdrasilServices, verse 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: [], @@ -129,6 +126,11 @@ function metric(id, label, value, tone) { return { id, kind: "metric", props: { label, text: `${label}: ${value}`, value, tone }, children: [] }; } +function adbTone(adb) { + if (adb.state !== "ok") return "warn"; + return adb.devices.some((device) => device.state !== "device") ? "warn" : "ok"; +} + module.exports = { buildPendingSurface, buildSurface,