From f2ae17f3428d756e29beafb3cb5efe4e4cfea892 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 31 May 2026 01:23:15 +0100 Subject: [PATCH] Broker native Eve dashboards --- .gitignore | 1 + docs/eve-dashboard.md | 76 ++- src/Mimir.EveDashboard/Program.cs | 484 +++++++++++++++--- .../Synchronization/MimirCultMeshContracts.cs | 45 ++ .../MimirNetworkTransportConfiguration.cs | 18 +- state/evidence.jsonl | 1 + state/map.yaml | 22 +- 7 files changed, 563 insertions(+), 84 deletions(-) diff --git a/.gitignore b/.gitignore index 5e96241..138486a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ artifacts/obs-sdk/ artifacts/asio/ artifacts/bioacoustic-contestants/ artifacts/birdsong-reference/ +artifacts/eve-dashboard/ artifacts/python-packages/ artifacts/spectra/**/*.wav tools/drivers/ diff --git a/docs/eve-dashboard.md b/docs/eve-dashboard.md index 66e81cd..d52453e 100644 --- a/docs/eve-dashboard.md +++ b/docs/eve-dashboard.md @@ -2,25 +2,34 @@ ## Objective -Use EveCanvas as a native Mimir operator dashboard: scene graph, source panels, -visibility/reset controls, and multitouch transform editing. Eve renders the -control surface locally and sends edit intents to Mimir; it does not own final -composition. +Use EveCanvas as a native operator dashboard substrate: Mimir, StreamPixels, and +other local/Yggdrasil-routed apps can publish retained dashboard trees while Eve +renders the control surface locally and sends edit intents back. Eve does not +own final composition or run remote UI code. ## Authority Map -- Owner: Mimir owns accepted dashboard state and transform truth. +- Owner: each dashboard provider owns its accepted state and command handling. + The Starfire broker owns provider discovery/routing. EveCanvas owns native + rendering, hit-testing, and touch affordances. - Inputs: Eve sends `select`, `move`, `scale`, `rotate`, `toggle-visibility`, - and `reset-transform` commands over `/eve/dashboard`. -- Outputs: Mimir sends `dashboard-state` snapshots with nodes, health, - visibility, dimensions, transform, selected node, and LUT preset. + `reset-transform`, and `open-provider` commands over `/eve/deck`. +- Outputs: providers send `dashboard-state` snapshots with provider id, title, + nodes, health, visibility, dimensions, transform, selected node, and LUT + preset. Provider catalogs are exposed at `/eve/deck/providers`. - Derived state: Eve's UIKit panels are local render projections of Mimir - state. + state/provider state. - Forbidden writers: Eve gestures do not mutate program layout directly; they - request changes and then re-render the accepted snapshot. + request changes and then re-render the accepted snapshot. Remote apps do not + ship arbitrary JavaScript or native code into Eve. - Cut line: no camera-stream fanout to Eve for the dashboard. Source previews may be added later as thumbnails/proxies, but dashboard control must not require Starfire to render every camera feed for the iPad. +- CultMesh line: dashboard manifests and states now have typed + `mimir.eve_dashboard_manifest.v1` and `mimir.eve_dashboard_state.v1` + document shapes. The live iPad lane remains WebSocket/TCP because it is the + proven transport and works over SSH tunnels; CultMesh should mirror compact + state where the local mesh substrate is available. ## Runtime @@ -32,16 +41,46 @@ dotnet run --project .\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- --por EveCanvas connects to: +```text +ws://192.168.1.66:8795/eve/deck +``` + +The compatibility endpoint remains: + ```text ws://192.168.1.66:8795/eve/dashboard ``` +Broker inspection endpoints: + +```text +http://192.168.1.66:8795/health +http://192.168.1.66:8795/eve/deck/manifest +http://192.168.1.66:8795/eve/deck/providers +ws://192.168.1.66:8795/eve/deck/{providerId} +``` + +Extra providers can be registered at launch: + +```powershell +dotnet run --project .\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- ` + --port 8795 ` + --provider "app.id|App Title|ws://127.0.0.1:14000/eve/deck" +``` + ## Current State -`Mimir.EveDashboard` currently serves fixture scene nodes for the first native -control proof. It accepts transform/visibility commands, mutates the server-side -state, increments the state version, and broadcasts the new snapshot. The next -cut is replacing fixture state with live `MimirPresentationControlState` and +`Mimir.EveDashboard` now runs as a small native dashboard broker. It includes: + +- `eve.dashboard.broker`: the switchboard rendered as provider cards. +- `mimir.stream.layout`: the existing Mimir source-layout fixture provider. +- `yggdrasil.streampixels.edge`: the first Yggdrasil/StreamPixels service + dashboard placeholder for the TCP/SSH-routed live edge. + +The Mimir provider still serves fixture scene nodes for the first native control +proof. It accepts transform/visibility commands, mutates the provider state, +increments the state version, and broadcasts the new snapshot. The next cut is +replacing fixture state with live `MimirPresentationControlState` and `MimirSceneEditorState` snapshots. ## Verification @@ -60,3 +99,12 @@ The first device screenshot proved the dashboard rendered on Eve, but also showed the legacy debug overlay occluding the hierarchy panel. EveCanvas hides that overlay while dashboard mode is active; dashboard status remains in the native dashboard footer. + +The broker smoke on 2026-05-31 verified: + +- `/health` reports active provider, client count, transport, and typed + CultMesh dashboard document name. +- `/eve/deck/providers` returns the three built-in provider manifests. +- WebSocket `/eve/deck` sends the switchboard snapshot. +- `open-provider` switches to `mimir.stream.layout` and broadcasts that + provider state. diff --git a/src/Mimir.EveDashboard/Program.cs b/src/Mimir.EveDashboard/Program.cs index d843f26..96a223d 100644 --- a/src/Mimir.EveDashboard/Program.cs +++ b/src/Mimir.EveDashboard/Program.cs @@ -7,7 +7,8 @@ using System.Text.Json; using System.Text.Json.Serialization; var port = ParseInt(args, "--port", 8795); -using var server = new EveDashboardServer(port); +var providers = EveDashboardProviderCatalog.Create(ParseProviderSpecs(args)); +using var server = new EveDashboardServer(port, providers); await server.RunAsync(); static int ParseInt(IReadOnlyList args, string name, int fallback) @@ -24,7 +25,21 @@ static int ParseInt(IReadOnlyList args, string name, int fallback) return fallback; } -internal sealed class EveDashboardServer(int port) : IDisposable +static IReadOnlyList ParseProviderSpecs(IReadOnlyList args) +{ + var specs = new List(); + for (var index = 0; index < args.Count - 1; index++) + { + if (string.Equals(args[index], "--provider", StringComparison.OrdinalIgnoreCase)) + { + specs.Add(args[index + 1]); + } + } + + return specs; +} + +internal sealed class EveDashboardServer(int port, EveDashboardProviderCatalog providers) : IDisposable { private static readonly JsonSerializerOptions JsonOptions = new() { @@ -35,12 +50,13 @@ internal sealed class EveDashboardServer(int port) : IDisposable private readonly TcpListener listener = new(IPAddress.Any, port); private readonly CancellationTokenSource stopping = new(); private readonly ConcurrentDictionary clients = new(); - private readonly DashboardState state = DashboardState.CreateDefault(); + private string activeProviderId = providers.DefaultProviderId; public async Task RunAsync() { listener.Start(); - Console.WriteLine($"Mimir Eve dashboard listening on ws://0.0.0.0:{port}/eve/dashboard"); + Console.WriteLine($"Mimir Eve dashboard broker listening on ws://0.0.0.0:{port}/eve/deck"); + Console.WriteLine($"Compatibility endpoint remains ws://0.0.0.0:{port}/eve/dashboard"); while (!stopping.IsCancellationRequested) { var client = await listener.AcceptTcpClientAsync(stopping.Token).ConfigureAwait(false); @@ -54,19 +70,50 @@ internal sealed class EveDashboardServer(int port) : IDisposable var request = await ReadHttpRequestAsync(stream).ConfigureAwait(false); Console.WriteLine($"EVE dashboard connected: {tcpClient.Client.RemoteEndPoint} {request.Path}"); Console.Out.Flush(); + if (request.Path == "/health") { - var health = JsonSerializer.Serialize(new { ok = true, clients = clients.Count, state.Version }, JsonOptions); + var provider = providers.Get(activeProviderId); + var health = JsonSerializer.Serialize(new + { + ok = true, + clients = clients.Count, + activeProviderId, + providerVersion = provider.State.Version, + providers = providers.Manifests.Count, + transport = "eve-deck-ws", + cultMeshDocument = "mimir.eve_dashboard_state", + }, JsonOptions); await WriteHttpResponseAsync(stream, "200 OK", "application/json", Encoding.UTF8.GetBytes(health)).ConfigureAwait(false); return; } - if (request.Path != "/eve/dashboard" || !request.Headers.TryGetValue("Sec-WebSocket-Key", out var key)) + if (request.Path is "/eve/deck/manifest" or "/eve/dashboard/manifest") + { + var manifest = JsonSerializer.Serialize(providers.BrokerManifest, JsonOptions); + await WriteHttpResponseAsync(stream, "200 OK", "application/json", Encoding.UTF8.GetBytes(manifest)).ConfigureAwait(false); + return; + } + + if (request.Path == "/eve/deck/providers") + { + var catalog = JsonSerializer.Serialize(new DashboardProviderCatalogDocument(providers.Manifests), JsonOptions); + await WriteHttpResponseAsync(stream, "200 OK", "application/json", Encoding.UTF8.GetBytes(catalog)).ConfigureAwait(false); + return; + } + + if (!IsDashboardPath(request.Path, out var requestedProviderId) || + !request.Headers.TryGetValue("Sec-WebSocket-Key", out var key)) { await WriteHttpResponseAsync(stream, "404 Not Found", "text/plain", Encoding.UTF8.GetBytes("not found")).ConfigureAwait(false); return; } + if (!string.IsNullOrWhiteSpace(requestedProviderId) && providers.Contains(requestedProviderId)) + { + activeProviderId = requestedProviderId; + } + await WriteWebSocketHandshakeAsync(stream, key).ConfigureAwait(false); var id = Guid.NewGuid(); var socket = new DashboardSocket(tcpClient, stream); @@ -101,6 +148,24 @@ internal sealed class EveDashboardServer(int port) : IDisposable } } + private static bool IsDashboardPath(string path, out string providerId) + { + providerId = ""; + if (path == "/eve/dashboard" || path == "/eve/deck") + { + return true; + } + + const string providerPrefix = "/eve/deck/"; + if (path.StartsWith(providerPrefix, StringComparison.Ordinal)) + { + providerId = Uri.UnescapeDataString(path[providerPrefix.Length..]); + return !string.IsNullOrWhiteSpace(providerId); + } + + return false; + } + private bool ApplyCommand(string text) { DashboardCommand? command; @@ -113,59 +178,46 @@ internal sealed class EveDashboardServer(int port) : IDisposable return false; } - if (command == null || string.IsNullOrWhiteSpace(command.NodeId)) + if (command == null) { return false; } - var node = state.Nodes.FirstOrDefault(candidate => string.Equals(candidate.Id, command.NodeId, StringComparison.Ordinal)); - if (node == null) + if (string.Equals(command.Type, "open-provider", StringComparison.OrdinalIgnoreCase)) { + var providerId = command.ProviderId; + if (string.IsNullOrWhiteSpace(providerId) && !string.IsNullOrWhiteSpace(command.NodeId)) + { + providerId = providers.Get(activeProviderId).State.Nodes + .FirstOrDefault(node => string.Equals(node.Id, command.NodeId, StringComparison.Ordinal)) + ?.ProviderId; + } + + if (!string.IsNullOrWhiteSpace(providerId) && providers.Contains(providerId)) + { + activeProviderId = providerId; + Console.WriteLine($"EVE dashboard provider switched: {activeProviderId}"); + Console.Out.Flush(); + return true; + } + return false; } - switch ((command.Type ?? "").Trim().ToLowerInvariant()) + var provider = providers.Get(activeProviderId); + if (provider.ApplyCommand(command)) { - case "select": - state.SelectedNodeId = node.Id; - break; - case "move": - state.SelectedNodeId = node.Id; - node.X = Clamp(command.X ?? node.X, -1.0, 1.0); - node.Y = Clamp(command.Y ?? node.Y, -1.0, 1.0); - break; - case "scale": - state.SelectedNodeId = node.Id; - node.Scale = Clamp(command.Scale ?? node.Scale, 0.25, 3.0); - break; - case "rotate": - state.SelectedNodeId = node.Id; - node.Rotation = command.Rotation ?? node.Rotation; - break; - case "toggle-visibility": - state.SelectedNodeId = node.Id; - node.Visible = command.Visible ?? !node.Visible; - break; - case "reset-transform": - state.SelectedNodeId = node.Id; - node.X = node.DefaultX; - node.Y = node.DefaultY; - node.Rotation = 0.0; - node.Scale = 1.0; - break; - default: - return false; + Console.WriteLine($"EVE dashboard command({activeProviderId}): {text}"); + Console.Out.Flush(); + return true; } - state.Version++; - state.UpdatedAt = DateTimeOffset.UtcNow; - Console.WriteLine($"EVE dashboard command: {text}"); - Console.Out.Flush(); - return true; + return false; } private async Task SendStateAsync(DashboardSocket socket) { + var state = providers.Get(activeProviderId).State; await SendTextFrameAsync(socket.Stream, JsonSerializer.Serialize(state, JsonOptions)).ConfigureAwait(false); } @@ -184,8 +236,6 @@ internal sealed class EveDashboardServer(int port) : IDisposable } } - private static double Clamp(double value, double min, double max) => Math.Min(max, Math.Max(min, value)); - private static async Task ReadHttpRequestAsync(NetworkStream stream) { var bytes = new List(); @@ -350,16 +400,335 @@ internal sealed class EveDashboardServer(int port) : IDisposable } } +internal sealed class EveDashboardProviderCatalog +{ + private readonly Dictionary providers; + + private EveDashboardProviderCatalog(IEnumerable providers) + { + this.providers = providers.ToDictionary(provider => provider.Manifest.Id, StringComparer.Ordinal); + DefaultProviderId = DashboardBrokerProvider.ProviderId; + Manifests = this.providers.Values.Select(provider => provider.Manifest).ToArray(); + BrokerManifest = new DashboardProviderManifest( + "eve.dashboard.broker", + "Eve Dashboard Broker", + "Native retained dashboard router for LAN and SSH-tunneled control surfaces.", + "1", + "/eve/deck", + ["scene2d", "provider-switching", "touch-commands", "cultmesh-state-documents"], + UsesCultMesh: true, + Transport: "WebSocket now; CultMesh typed state when available."); + } + + public string DefaultProviderId { get; } + + public IReadOnlyList Manifests { get; } + + public DashboardProviderManifest BrokerManifest { get; } + + public static EveDashboardProviderCatalog Create(IReadOnlyList remoteSpecs) + { + var providers = new List(); + var remoteProviders = remoteSpecs.Select(RemoteDashboardProvider.FromSpec).OfType().ToArray(); + providers.Add(new DashboardBrokerProvider(remoteProviders)); + providers.Add(new MimirStreamLayoutProvider()); + providers.Add(new YggdrasilStreamPixelsProvider()); + providers.AddRange(remoteProviders); + return new EveDashboardProviderCatalog(providers); + } + + public bool Contains(string providerId) => providers.ContainsKey(providerId); + + public IDashboardProvider Get(string providerId) => + providers.TryGetValue(providerId, out var provider) ? provider : providers[DefaultProviderId]; +} + +internal interface IDashboardProvider +{ + DashboardProviderManifest Manifest { get; } + + DashboardState State { get; } + + bool ApplyCommand(DashboardCommand command); +} + +internal abstract class MutableDashboardProvider : IDashboardProvider +{ + protected MutableDashboardProvider(DashboardProviderManifest manifest, DashboardState state) + { + Manifest = manifest; + State = state; + } + + public DashboardProviderManifest Manifest { get; } + + public DashboardState State { get; } + + public virtual bool ApplyCommand(DashboardCommand command) + { + if (string.IsNullOrWhiteSpace(command.NodeId)) + { + return false; + } + + var node = State.Nodes.FirstOrDefault(candidate => string.Equals(candidate.Id, command.NodeId, StringComparison.Ordinal)); + if (node == null) + { + return false; + } + + switch ((command.Type ?? "").Trim().ToLowerInvariant()) + { + case "select": + State.SelectedNodeId = node.Id; + break; + case "move": + State.SelectedNodeId = node.Id; + node.X = Clamp(command.X ?? node.X, -1.0, 1.0); + node.Y = Clamp(command.Y ?? node.Y, -1.0, 1.0); + break; + case "scale": + State.SelectedNodeId = node.Id; + node.Scale = Clamp(command.Scale ?? node.Scale, 0.25, 3.0); + break; + case "rotate": + State.SelectedNodeId = node.Id; + node.Rotation = command.Rotation ?? node.Rotation; + break; + case "toggle-visibility": + State.SelectedNodeId = node.Id; + node.Visible = command.Visible ?? !node.Visible; + break; + case "reset-transform": + State.SelectedNodeId = node.Id; + node.X = node.DefaultX; + node.Y = node.DefaultY; + node.Rotation = 0.0; + node.Scale = 1.0; + break; + default: + return false; + } + + Touch(); + return true; + } + + protected void Touch() + { + State.Version++; + State.UpdatedAt = DateTimeOffset.UtcNow; + } + + protected static double Clamp(double value, double min, double max) => Math.Min(max, Math.Max(min, value)); +} + +internal sealed class DashboardBrokerProvider : MutableDashboardProvider +{ + public const string ProviderId = "eve.dashboard.broker"; + + public DashboardBrokerProvider(IReadOnlyList remoteProviders) + : base( + new DashboardProviderManifest( + ProviderId, + "Dashboard Switchboard", + "Provider picker rendered as native Eve panels.", + "1", + "/eve/deck", + ["scene2d", "provider-switching"], + UsesCultMesh: true, + Transport: "broker-local"), + CreateState(remoteProviders)) + { + } + + private static DashboardState CreateState(IReadOnlyList remoteProviders) + { + var nodes = new List + { + new("provider-mimir", "Mimir Stream Layout", "dashboard-provider", -0.42, -0.34, 0.34, 0.22, "local") + { + ProviderId = MimirStreamLayoutProvider.ProviderId, + Command = "open-provider", + }, + new("provider-streampixels", "StreamPixels Edge", "dashboard-provider", 0.38, -0.34, 0.34, 0.22, "ssh ready") + { + ProviderId = YggdrasilStreamPixelsProvider.ProviderId, + Command = "open-provider", + }, + }; + + var y = 0.28; + foreach (var provider in remoteProviders) + { + nodes.Add(new DashboardNode( + $"provider-{provider.Manifest.Id}", + provider.Manifest.Title, + "dashboard-provider", + 0.0, + y, + 0.40, + 0.18, + "external") + { + ProviderId = provider.Manifest.Id, + Command = "open-provider", + }); + y = Math.Min(0.80, y + 0.22); + } + + return new DashboardState + { + ProviderId = ProviderId, + Title = "Eve Dashboard Switchboard", + SelectedNodeId = nodes[0].Id, + Nodes = nodes, + }; + } +} + +internal sealed class MimirStreamLayoutProvider : MutableDashboardProvider +{ + public const string ProviderId = "mimir.stream.layout"; + + public MimirStreamLayoutProvider() + : base( + new DashboardProviderManifest( + ProviderId, + "Mimir Stream Layout", + "Native scene graph editor for program layout, source transforms, visibility, and future LUT controls.", + "1", + "/eve/deck/mimir.stream.layout", + ["scene2d", "visibility", "transform", "lut-presets", "audio-mix"], + UsesCultMesh: true, + Transport: "local WebSocket; state document can mirror through CultMesh."), + CreateState()) + { + } + + private static DashboardState CreateState() => + new() + { + ProviderId = ProviderId, + Title = "Mimir Stream Layout", + Nodes = + [ + new DashboardNode("program", "Program Output", "camera", -0.05, -0.05, 0.62, 0.34, "live"), + new DashboardNode("raven-display", "Raven Display", "screen", -0.58, -0.50, 0.34, 0.20, "waiting"), + new DashboardNode("eve-camera", "Eve Camera", "camera", 0.46, -0.48, 0.30, 0.22, "armed"), + new DashboardNode("kiyo-pro", "Kiyo Pro", "camera", -0.42, 0.42, 0.24, 0.18, "live"), + new DashboardNode("leap-field", "Leap Field", "sensor", 0.38, 0.40, 0.28, 0.24, "calibrating"), + ], + }; +} + +internal sealed class YggdrasilStreamPixelsProvider : MutableDashboardProvider +{ + public const string ProviderId = "yggdrasil.streampixels.edge"; + + public YggdrasilStreamPixelsProvider() + : base( + new DashboardProviderManifest( + ProviderId, + "StreamPixels Edge", + "Yggdrasil/StreamPixels live edge control surface reachable over HTTPS or SSH-forwarded localhost.", + "1", + "/eve/deck/yggdrasil.streampixels.edge", + ["status", "health", "ssh-tunnel", "hls"], + UsesCultMesh: false, + Transport: "TCP/WebSocket over LAN or SSH tunnel."), + new DashboardState + { + ProviderId = ProviderId, + Title = "StreamPixels Edge", + SelectedNodeId = "hls-origin", + Nodes = + [ + new DashboardNode("hls-origin", "HLS Origin", "service", -0.34, -0.18, 0.34, 0.22, "streampixels"), + new DashboardNode("rtmp-ingest", "RTMP Ingest", "service", 0.34, -0.18, 0.34, 0.22, "localhost"), + new DashboardNode("ssh-tunnel", "Yggdrasil SSH Tunnel", "transport", 0.0, 0.34, 0.44, 0.18, "tcp only"), + ], + }) + { + } +} + +internal sealed class RemoteDashboardProvider : MutableDashboardProvider +{ + private RemoteDashboardProvider(DashboardProviderManifest manifest, string upstream) + : base( + manifest, + new DashboardState + { + ProviderId = manifest.Id, + Title = manifest.Title, + SelectedNodeId = "remote-endpoint", + Nodes = + [ + new DashboardNode("remote-endpoint", manifest.Title, "remote-dashboard", 0.0, -0.10, 0.46, 0.24, "registered") + { + Endpoint = upstream, + }, + new DashboardNode("remote-transport", "Transport", "transport", 0.0, 0.34, 0.40, 0.18, "tcp/ws") + { + Endpoint = upstream, + }, + ], + }) + { + } + + public static RemoteDashboardProvider? FromSpec(string spec) + { + var parts = spec.Split('|', 3, StringSplitOptions.TrimEntries); + if (parts.Length < 3 || string.IsNullOrWhiteSpace(parts[0]) || string.IsNullOrWhiteSpace(parts[1]) || string.IsNullOrWhiteSpace(parts[2])) + { + Console.Error.WriteLine($"Ignoring dashboard provider spec. Expected id|title|ws-url, got: {spec}"); + return null; + } + + return new RemoteDashboardProvider( + new DashboardProviderManifest( + parts[0], + parts[1], + "External EveDeck dashboard provider registered at broker launch.", + "1", + parts[2], + ["scene2d", "external-provider"], + UsesCultMesh: false, + Transport: "registered WebSocket endpoint"), + parts[2]); + } +} + internal sealed record HttpRequest(string Path, IReadOnlyDictionary Headers); internal sealed record WebSocketFrame(int Opcode, byte[] Payload); internal sealed record DashboardSocket(TcpClient Client, NetworkStream Stream); +internal sealed record DashboardProviderCatalogDocument(IReadOnlyList Providers); + +internal sealed record DashboardProviderManifest( + string Id, + string Title, + string Description, + string Version, + string Endpoint, + string[] Capabilities, + bool UsesCultMesh, + string Transport); + internal sealed class DashboardState { public string Type { get; set; } = "dashboard-state"; + public string Schema { get; set; } = "mimir.eve_dashboard_state.v1"; + + public string ProviderId { get; set; } = MimirStreamLayoutProvider.ProviderId; + + public string Title { get; set; } = "Mimir Dashboard"; + public long Version { get; set; } = 1; public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow; @@ -369,21 +738,6 @@ internal sealed class DashboardState public string LutPreset { get; set; } = "neutral"; public List Nodes { get; set; } = []; - - public static DashboardState CreateDefault() - { - return new DashboardState - { - Nodes = - [ - new DashboardNode("program", "Program Output", "camera", -0.05, -0.05, 0.62, 0.34, "live"), - new DashboardNode("raven-display", "Raven Display", "screen", -0.58, -0.50, 0.34, 0.20, "waiting"), - new DashboardNode("eve-camera", "Eve Camera", "camera", 0.46, -0.48, 0.30, 0.22, "armed"), - new DashboardNode("kiyo-pro", "Kiyo Pro", "camera", -0.42, 0.42, 0.24, 0.18, "live"), - new DashboardNode("leap-field", "Leap Field", "sensor", 0.38, 0.40, 0.28, 0.24, "calibrating"), - ], - }; - } } internal sealed class DashboardNode( @@ -423,6 +777,12 @@ internal sealed class DashboardNode( public double Height { get; set; } = height; public string Health { get; set; } = health; + + public string? ProviderId { get; set; } + + public string? Command { get; set; } + + public string? Endpoint { get; set; } } internal sealed class DashboardCommand @@ -431,6 +791,8 @@ internal sealed class DashboardCommand public string NodeId { get; set; } = ""; + public string? ProviderId { get; set; } + public double? X { get; set; } public double? Y { get; set; } diff --git a/src/Mimir.Runtime/Synchronization/MimirCultMeshContracts.cs b/src/Mimir.Runtime/Synchronization/MimirCultMeshContracts.cs index 2298b7b..1339298 100644 --- a/src/Mimir.Runtime/Synchronization/MimirCultMeshContracts.cs +++ b/src/Mimir.Runtime/Synchronization/MimirCultMeshContracts.cs @@ -104,6 +104,51 @@ public sealed record MimirFaustControlSnapshot( [property: Key(0)] string Path, [property: Key(1)] float Value); +[CultDocument("mimir.eve_dashboard_manifest", "mimir.eve_dashboard_manifest.v1")] +[MessagePackObject] +public sealed record MimirEveDashboardManifestDocument( + [property: Key(0)] + [property: CultName] + string ProviderId, + [property: Key(1)] string Title, + [property: Key(2)] string Description, + [property: Key(3)] string Version, + [property: Key(4)] string Endpoint, + [property: Key(5)] string[] Capabilities, + [property: Key(6)] bool UsesCultMesh, + [property: Key(7)] string Transport); + +[CultDocument("mimir.eve_dashboard_state", "mimir.eve_dashboard_state.v1")] +[MessagePackObject] +public sealed record MimirEveDashboardStateDocument( + [property: Key(0)] + [property: CultName] + string ProviderId, + [property: Key(1)] string Title, + [property: Key(2)] long Version, + [property: Key(3)] string UpdatedAtUtc, + [property: Key(4)] string SelectedNodeId, + [property: Key(5)] string LutPreset, + [property: Key(6)] MimirEveDashboardNodeSnapshot[] Nodes); + +[MessagePackObject] +public sealed record MimirEveDashboardNodeSnapshot( + [property: Key(0)] string Id, + [property: Key(1)] string Label, + [property: Key(2)] string Kind, + [property: Key(3)] bool Visible, + [property: Key(4)] double X, + [property: Key(5)] double Y, + [property: Key(6)] double Z, + [property: Key(7)] double Rotation, + [property: Key(8)] double Scale, + [property: Key(9)] double Width, + [property: Key(10)] double Height, + [property: Key(11)] string Health, + [property: Key(12)] string? ProviderId, + [property: Key(13)] string? Command, + [property: Key(14)] string? Endpoint); + public static class MimirCultMeshContractFactory { public static MimirBioacousticCodebookState CreateCodebookState( diff --git a/src/Mimir.Runtime/Synchronization/MimirNetworkTransportConfiguration.cs b/src/Mimir.Runtime/Synchronization/MimirNetworkTransportConfiguration.cs index 13b2636..530f47c 100644 --- a/src/Mimir.Runtime/Synchronization/MimirNetworkTransportConfiguration.cs +++ b/src/Mimir.Runtime/Synchronization/MimirNetworkTransportConfiguration.cs @@ -6,7 +6,8 @@ public enum MimirNetworkPayloadKind RawAudioDebugWindow, CompressedAudioProgramFeed, CompressedVideoProgramFeed, - RawVideoDiagnosticWindow + RawVideoDiagnosticWindow, + NativeDashboardState } public enum MimirNetworkTransportKind @@ -80,11 +81,24 @@ public static class MimirNetworkTransportConfigurations Notes = "Do not trust browser media timestamps without decoded witness state." }; + public static MimirNetworkTransportConfiguration EveDashboardState { get; } = new( + "eve-dashboard-state", + "Native retained dashboard trees and provider manifests for Eve-rendered control surfaces.", + MimirNetworkTransportKind.CultMeshTypedState, + MimirNetworkPayloadKind.NativeDashboardState, + MayAffectClock: false, + CarriesRawMedia: false, + TargetLatencyMilliseconds: 50.0, + RequiredDocuments: ["mimir.eve_dashboard_manifest", "mimir.eve_dashboard_state"], + RejectionConditions: ["unknown-provider", "untrusted-provider", "schema-version-mismatch", "command-owner-conflict"], + "Dashboard providers own state and commands. Eve owns native rendering and touch. No remote UI code runs on Eve."); + public static IReadOnlyList BuiltIn { get; } = [ CultMeshTimingState, RawAudioDebugWindow, SrtProgramBridge, - WebRtcExperiment + WebRtcExperiment, + EveDashboardState ]; } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index e3595e9..45b8df4 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -250,3 +250,4 @@ {"ts": "2026-05-31T01:03:07+01:00", "type": "verification", "status": "ok", "note": "Eve dashboard proof: EveCanvas rendered the native dashboard on device from Mimir.EveDashboard snapshots, showing scene graph/source panels and transform controls. A command smoke moved eve-camera and received version 2 state. Screenshot exposed legacy debug overlay occlusion; EveCanvas now hides the debug overlay while dashboard mode is active."} {"ts": "2026-05-31T01:06:44+01:00", "type": "verification", "status": "ok", "note": "Live Leap surface-claim proof captured artifacts/runtime/mimir-leap-surface-claims-clean.png. With AQUARIUM_DISABLE_RESERVOIR_HISTORY=1, Fensalir now presents the current reservoir resolver target instead of an unwritten history target; LeapUVC frames uploaded through the Fensalir-owned D3D12 texture, packed stereo depth dispatched, point-cloud compute generated 19,200 SDF/PBR/radiosity surface splats, point-list rendering stayed diagnostic at rendered 0, and visible Leap-derived splats reached presentation."} {"ts": "2026-05-31T01:20:43+01:00", "type": "verification", "status": "ok", "note": "Reservoir-history startup repaired for the Leap surface-claim path. Fensalir now compiles dedicated D3D12ReservoirHistoryUpdate.hlsl instead of the monolithic post shader for D3D12ReservoirHistoryUpdateCS; history-enabled live capture artifacts/runtime/mimir-leap-history-split-shader-visible.png completed without AQUARIUM_DISABLE_RESERVOIR_HISTORY, with 19,200 Leap surface splats, point-list rendering at 0, reservoir-update timing around 13 ms, and visible resolver output. Temporal/spatial reuse is intentionally not restored in the lean current-sample history shader yet."} +{"ts": "2026-05-31T01:22:17+01:00", "type": "verification", "status": "ok", "note": "Eve dashboard broker cut: /eve/deck now routes provider-owned retained dashboard trees, /eve/deck/providers exposes switchboard/Mimir/StreamPixels manifests, and EveCanvas connects to /eve/deck with provider-card open-provider commands. Added typed CultMesh-compatible document shapes mimir.eve_dashboard_manifest.v1 and mimir.eve_dashboard_state.v1 while keeping live iPad transport WebSocket/TCP for LAN and SSH tunnel use. Smoke verified /health, provider catalog, WebSocket switchboard state, and open-provider transition to mimir.stream.layout."} diff --git a/state/map.yaml b/state/map.yaml index d820fd5..dd7d410 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -301,13 +301,21 @@ accepted_design: Faust streaming DSP path. LUT presets are typed postprocess state; exposure and bloom map into current Fensalir `GraphicsSettings`, while actual LUT texture sampling remains the named renderer hook. - `Mimir.EveDashboard` is the first native Eve operator dashboard server: - `/eve/dashboard` sends scene/control snapshots and accepts select, move, - scale, rotate, visibility, and reset commands. EveCanvas renders the scene - graph and source panels natively in UIKit with multitouch transform editing. - This is currently fixture dashboard state for the transport/control proof; - live binding to `MimirPresentationControlState` and `MimirSceneEditorState` - is the next authority cut. + `Mimir.EveDashboard` is the native Eve dashboard broker: `/eve/deck` sends + retained dashboard trees, `/eve/dashboard` remains the compatibility + endpoint, and `/eve/deck/providers` exposes provider manifests. Built-in + providers are the switchboard, `mimir.stream.layout`, and + `yggdrasil.streampixels.edge`; launch-time external providers can be + registered as `id|title|ws-url` descriptors for LAN or SSH-forwarded TCP + surfaces. EveCanvas renders the scene graph and source panels natively in + UIKit with multitouch transform editing, and provider-card taps send + `open-provider`. Dashboard manifests and states now have typed + CultMesh-compatible document shapes `mimir.eve_dashboard_manifest.v1` and + `mimir.eve_dashboard_state.v1`, while the live iPad lane stays + WebSocket/TCP because it is the proven tunnelable transport. The Mimir + stream-layout provider still uses fixture dashboard state; live binding to + `MimirPresentationControlState` and `MimirSceneEditorState` is the next + authority cut. `MimirSceneEditorState` owns the separate Mimir-window editor graph: editor camera, scene nodes, selected node, visibility/locks, 2D-plane transforms, reset affordances, and grab/rotate/resize gizmo mode. Rolling