Broker native Eve dashboards

This commit is contained in:
Meta Vens 2026-05-31 01:23:15 +01:00
commit f2ae17f342
7 changed files with 559 additions and 80 deletions

1
.gitignore vendored
View file

@ -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/

View file

@ -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.

View file

@ -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<string> args, string name, int fallback)
@ -24,7 +25,21 @@ static int ParseInt(IReadOnlyList<string> args, string name, int fallback)
return fallback;
}
internal sealed class EveDashboardServer(int port) : IDisposable
static IReadOnlyList<string> ParseProviderSpecs(IReadOnlyList<string> args)
{
var specs = new List<string>();
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<Guid, DashboardSocket> 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<HttpRequest> ReadHttpRequestAsync(NetworkStream stream)
{
var bytes = new List<byte>();
@ -350,16 +400,335 @@ internal sealed class EveDashboardServer(int port) : IDisposable
}
}
internal sealed class EveDashboardProviderCatalog
{
private readonly Dictionary<string, IDashboardProvider> providers;
private EveDashboardProviderCatalog(IEnumerable<IDashboardProvider> 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<DashboardProviderManifest> Manifests { get; }
public DashboardProviderManifest BrokerManifest { get; }
public static EveDashboardProviderCatalog Create(IReadOnlyList<string> remoteSpecs)
{
var providers = new List<IDashboardProvider>();
var remoteProviders = remoteSpecs.Select(RemoteDashboardProvider.FromSpec).OfType<RemoteDashboardProvider>().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<RemoteDashboardProvider> 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<RemoteDashboardProvider> remoteProviders)
{
var nodes = new List<DashboardNode>
{
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<string, string> Headers);
internal sealed record WebSocketFrame(int Opcode, byte[] Payload);
internal sealed record DashboardSocket(TcpClient Client, NetworkStream Stream);
internal sealed record DashboardProviderCatalogDocument(IReadOnlyList<DashboardProviderManifest> 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<DashboardNode> 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; }

View file

@ -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(

View file

@ -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<MimirNetworkTransportConfiguration> BuiltIn { get; } =
[
CultMeshTimingState,
RawAudioDebugWindow,
SrtProgramBridge,
WebRtcExperiment
WebRtcExperiment,
EveDashboardState
];
}

View file

@ -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."}

View file

@ -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