Add browser Eve renderer parity surfaces

This commit is contained in:
Meta Vens 2026-05-31 03:33:00 +01:00
commit e1f21cea87
7 changed files with 635 additions and 0 deletions

View file

@ -86,6 +86,22 @@ Mimir dashboard broker, and surfaces timestamped touch/motion sensor data.
accepted state, commands, and side effects.
See `docs/cultmesh-streaming-ui-framework.md` for the target architecture.
See `docs/renderer-parity.md` for current browser/iOS/Android/Fensalir/Flutter
renderer parity.
## Browser Reference
Start the browser reference surface runner:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\start-browser-reference.ps1
```
Open `http://127.0.0.1:8891/`. The page can spawn:
- `VoidBot Live`, connected to Mimir's `/eve/deck` broker.
- `Fensalir Direct2D`, a fixture surface for the planned
CultMesh-to-`AquariumUiDocument` lowering.
## VoidBot CEF Stream

36
docs/renderer-parity.md Normal file
View file

@ -0,0 +1,36 @@
# Eve Renderer Parity
Parity is currently uneven by design. The shared surface contract is forming,
but the browser reference is the first renderer that can be used as a practical
comparison oracle.
## Current State
| Renderer | Status | Proof | Gap |
| --- | --- | --- | --- |
| Browser | First reference runner | `web/` renders live VoidBot and Fensalir fixture surfaces | Needs visual regression fixtures and command round-trip tests |
| iOS / EveCanvas | Native proof | Renders Mimir provider trees and a custom VoidBot cockpit with avatar images | Uses provider-specific UIKit rendering for VoidBot |
| Android / Periwinkle | Device-edge proof | APK builds and shows broker/sensor status | Install blocked until device allows ADB sideload; dashboard rendering is not implemented |
| Fensalir Direct2D | Documented landing zone | Existing `AquariumUiDocument` and `DirectWriteOverlay` path | Needs adapter from Eve surface document to `AquariumUiDocument` |
| Flutter | Candidate shared native path | Not installed locally | Needs toolchain and client scaffold |
## Spawned Surfaces
The browser reference runner spawns two surfaces:
- `VoidBot Live`: connects to Mimir's `/eve/deck` broker and opens
`voidbot.swarm`.
- `Fensalir Direct2D`: loads `web/fixtures/fensalir-client-surface.json`, a
recorded surface describing the Direct2D client lowering path.
Start it with:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\start-browser-reference.ps1
```
Then open:
```text
http://127.0.0.1:8891/
```

View file

@ -0,0 +1,56 @@
param(
[int] $Port = 8891
)
$ErrorActionPreference = "Stop"
$projectRoot = Split-Path -Parent $PSScriptRoot
$webRoot = Join-Path $projectRoot "web"
$logRoot = Join-Path $projectRoot "artifacts\web"
New-Item -ItemType Directory -Force $logRoot | Out-Null
$pidPath = Join-Path $logRoot "eve-browser-reference.pid"
$outLog = Join-Path $logRoot "eve-browser-reference.out.log"
$errLog = Join-Path $logRoot "eve-browser-reference.err.log"
if (Test-Path $pidPath) {
$oldPid = Get-Content $pidPath -ErrorAction SilentlyContinue
if ($oldPid) {
Stop-Process -Id ([int]$oldPid) -ErrorAction SilentlyContinue
}
}
$nodeScript = @'
const http = require('http');
const fs = require('fs');
const path = require('path');
const root = process.argv[2];
const port = Number(process.argv[3]);
const types = {'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json; charset=utf-8'};
http.createServer((req, res) => {
const urlPath = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
const clean = urlPath === '/' ? '/index.html' : urlPath;
const file = path.normalize(path.join(root, clean));
if (!file.startsWith(root)) {
res.writeHead(403);
res.end('forbidden');
return;
}
fs.readFile(file, (err, data) => {
if (err) {
res.writeHead(404);
res.end('not found');
return;
}
res.writeHead(200, {'content-type': types[path.extname(file)] || 'application/octet-stream'});
res.end(data);
});
}).listen(port, '127.0.0.1', () => console.log(`Eve browser reference listening on http://127.0.0.1:${port}`));
'@
$scriptPath = Join-Path $logRoot "serve-eve-reference.cjs"
Set-Content -LiteralPath $scriptPath -Value $nodeScript -Encoding UTF8
$proc = Start-Process -FilePath "node" -ArgumentList @($scriptPath, $webRoot, $Port) -WorkingDirectory $projectRoot -RedirectStandardOutput $outLog -RedirectStandardError $errLog -PassThru -WindowStyle Hidden
$proc.Id | Set-Content -LiteralPath $pidPath
Start-Sleep -Milliseconds 500
Get-Content $outLog -Tail 5
Write-Host "PID $($proc.Id)"

View file

@ -0,0 +1,88 @@
{
"type": "dashboard-state",
"schema": "mimir.eve_dashboard_state.v1",
"providerId": "fensalir.direct2d",
"title": "Fensalir Direct2D Client Surface",
"version": 1,
"updatedAt": "2026-05-31T03:34:00.000Z",
"selectedNodeId": "surface-contract",
"nodes": [
{
"id": "surface-contract",
"label": "Eve Surface Contract\nCultMesh -> AquariumUiDocument",
"kind": "state-detail",
"x": -0.62,
"y": -0.18,
"width": 0.34,
"height": 0.34,
"health": "owner",
"detail": "Browser Eve is the reference renderer. Fensalir consumes the same provider-owned surface document and lowers it into AquariumUiDocument. Provider repos own state and command semantics; Fensalir owns DirectWrite/Direct2D rendering, focus, keyboard, pointer dispatch, and overlay timing."
},
{
"id": "direct2d-overlay",
"label": "DirectWriteOverlay\nD3D11-on-12 Direct2D",
"kind": "renderer",
"x": -0.12,
"y": -0.54,
"width": 0.34,
"height": 0.22,
"health": "ready",
"detail": "Draws Eve panels after the D3D12 scene. Existing Fensalir mechanism: DirectWriteOverlay.Render(frame, debugUi, clientUiPanels, performanceText)."
},
{
"id": "aquarium-ui",
"label": "AquariumUiDocument\npanels and controls",
"kind": "surface",
"x": 0.36,
"y": -0.48,
"width": 0.34,
"height": 0.22,
"health": "adapter target",
"detail": "First lowering target for shared Eve documents: readouts, buttons, toggles, sliders, options, text boxes, tree items, and detail panes."
},
{
"id": "field-reservoir",
"label": "Field Reservoir\nengine timing witness",
"kind": "telemetry",
"x": -0.12,
"y": 0.02,
"width": 0.34,
"height": 0.22,
"health": "live",
"detail": "Fensalir remains a field/evidence engine. Eve surface rendering must stay overlay/client UI; it must not become another field-truth path."
},
{
"id": "program-output",
"label": "Program Output\nshared D3D12 texture",
"kind": "media",
"x": 0.36,
"y": 0.06,
"width": 0.34,
"height": 0.22,
"health": "external surface",
"detail": "The program-output texture is external publication. Eve UI controls it; the resolved HDR scene and shared texture publication stay renderer-owned."
},
{
"id": "command-sink",
"label": "Command Sink\nEve/CultNet outbound",
"kind": "command",
"x": -0.38,
"y": 0.48,
"width": 0.30,
"height": 0.18,
"health": "pending",
"detail": "The first implementation cut needs a provider command sink so Direct2D control activation sends the same command messages as browser, iOS, and Android."
},
{
"id": "parity-probe",
"label": "Parity Probe\nbrowser reference fixture",
"kind": "test",
"x": 0.28,
"y": 0.50,
"width": 0.34,
"height": 0.18,
"health": "required",
"detail": "Recorded surface fixtures should render in browser and Fensalir with comparable hierarchy, selection, controls, and detail text before provider-specific polish."
}
]
}

31
web/index.html Normal file
View file

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Eve Surface Reference</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<header class="topbar">
<div>
<h1>Eve Surface Reference</h1>
<p id="status">booting browser reference renderer</p>
</div>
<div class="hud">
<strong>EVE</strong>
<span id="surface-id">surface none</span>
<span id="surface-version">v0</span>
</div>
</header>
<nav class="tabs">
<button id="voidbot-tab" class="active" type="button">VoidBot Live</button>
<button id="fensalir-tab" type="button">Fensalir Direct2D</button>
</nav>
<main id="app" class="surface"></main>
<script type="module" src="./surface.js"></script>
</body>
</html>

226
web/styles.css Normal file
View file

@ -0,0 +1,226 @@
:root {
color-scheme: dark;
--bg: #020909;
--panel: #071918;
--panel-2: #0a2423;
--line: rgba(103, 240, 228, 0.28);
--line-hot: rgba(142, 252, 255, 0.72);
--text: #e7f1f1;
--quiet: #8ba5a3;
--accent: #ffb84f;
--cyan: #8efcff;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: Inter, "Segoe UI", system-ui, sans-serif;
}
.topbar {
height: 84px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid var(--line);
background: #010707;
}
h1 {
margin: 0;
font-size: 20px;
letter-spacing: 0;
}
p {
margin: 4px 0 0;
color: var(--quiet);
font-size: 13px;
}
.hud {
min-width: 168px;
padding: 8px;
border: 1px solid var(--line-hot);
border-radius: 6px;
background: rgba(7, 25, 24, 0.88);
color: var(--accent);
font-family: "Cascadia Mono", Consolas, monospace;
font-size: 11px;
display: grid;
gap: 2px;
}
.tabs {
display: flex;
gap: 8px;
padding: 10px 16px 0;
}
.tabs button {
border: 1px solid var(--line);
border-radius: 6px 6px 0 0;
background: var(--panel);
color: var(--quiet);
padding: 8px 12px;
font: 600 13px "Cascadia Mono", Consolas, monospace;
}
.tabs button.active {
color: var(--cyan);
border-color: var(--line-hot);
background: var(--panel-2);
}
.surface {
padding: 10px 16px 16px;
}
.cockpit {
display: grid;
grid-template-columns: 0.78fr 1.08fr 1.34fr;
gap: 10px;
min-height: calc(100vh - 138px);
}
.ctb {
grid-column: 1 / -1;
min-height: 88px;
overflow-x: auto;
display: flex;
gap: 8px;
padding-bottom: 8px;
}
.pane {
min-height: 0;
border: 1px solid var(--line);
border-radius: 6px;
background: rgba(7, 25, 24, 0.92);
padding: 12px;
}
.pane h2 {
margin: 0 0 10px;
color: var(--quiet);
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.card {
border: 1px solid var(--line);
border-radius: 6px;
background: rgba(10, 36, 35, 0.96);
padding: 10px;
}
.ctb .card {
flex: 0 0 96px;
min-height: 76px;
padding: 6px;
}
.avatar {
width: 34px;
height: 34px;
border-radius: 50%;
border: 1px solid var(--line-hot);
object-fit: cover;
background: #123;
}
.card-title {
white-space: pre-line;
margin-top: 4px;
font: 700 11px "Cascadia Mono", Consolas, monospace;
}
.health {
color: var(--accent);
font: 700 10px "Cascadia Mono", Consolas, monospace;
}
.detail {
white-space: pre-wrap;
font: 600 12px "Cascadia Mono", Consolas, monospace;
line-height: 1.45;
}
.list {
display: grid;
gap: 8px;
}
.metrics {
display: grid;
gap: 9px;
margin: 12px 0;
}
.metric label {
display: flex;
justify-content: space-between;
color: var(--quiet);
font: 700 10px "Cascadia Mono", Consolas, monospace;
}
.bar {
height: 7px;
border-radius: 7px;
background: #010707;
border: 1px solid rgba(255, 255, 255, 0.12);
overflow: hidden;
}
.bar span {
display: block;
height: 100%;
background: linear-gradient(90deg, #bca8ff, #8efcff);
}
.graph {
position: relative;
min-height: calc(100vh - 166px);
border: 1px solid var(--line);
border-radius: 6px;
background:
linear-gradient(rgba(142, 252, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(142, 252, 255, 0.04) 1px, transparent 1px),
rgba(3, 15, 17, 0.94);
background-size: 44px 44px;
}
.node {
position: absolute;
transform: translate(-50%, -50%);
border: 1px solid var(--line);
border-radius: 7px;
background: rgba(10, 36, 35, 0.95);
padding: 10px;
overflow: hidden;
}
.node.selected {
border-color: var(--line-hot);
box-shadow: 0 0 0 1px rgba(142, 252, 255, 0.38);
}
.node .detail {
margin-top: 6px;
color: var(--quiet);
font-size: 10px;
}
@media (max-width: 900px) {
.cockpit {
grid-template-columns: 1fr;
}
}

182
web/surface.js Normal file
View file

@ -0,0 +1,182 @@
const statusEl = document.querySelector("#status");
const app = document.querySelector("#app");
const surfaceId = document.querySelector("#surface-id");
const surfaceVersion = document.querySelector("#surface-version");
const voidBotTab = document.querySelector("#voidbot-tab");
const fensalirTab = document.querySelector("#fensalir-tab");
let socket;
voidBotTab.addEventListener("click", () => {
setActiveTab(voidBotTab);
openVoidBot();
});
fensalirTab.addEventListener("click", async () => {
setActiveTab(fensalirTab);
closeSocket();
const response = await fetch("./fixtures/fensalir-client-surface.json");
renderSurface(await response.json(), "fixture");
});
openVoidBot();
function setActiveTab(tab) {
for (const button of [voidBotTab, fensalirTab]) {
button.classList.toggle("active", button === tab);
}
}
function openVoidBot() {
closeSocket();
const url = "ws://127.0.0.1:8795/eve/deck";
statusEl.textContent = `connecting ${url}`;
socket = new WebSocket(url);
socket.addEventListener("open", () => {
statusEl.textContent = "connected to Mimir Eve broker";
socket.send(JSON.stringify({ type: "open-provider", providerId: "voidbot.swarm" }));
});
socket.addEventListener("message", event => {
const state = JSON.parse(event.data);
if (state.providerId === "voidbot.swarm") {
renderSurface(state, "live");
}
});
socket.addEventListener("close", () => {
statusEl.textContent = "Mimir broker disconnected";
});
socket.addEventListener("error", () => {
statusEl.textContent = "Mimir broker error; is port 8795 running?";
});
}
function closeSocket() {
if (socket) {
socket.close();
socket = undefined;
}
}
function renderSurface(state, source) {
surfaceId.textContent = state.providerId || "surface unknown";
surfaceVersion.textContent = `v${state.version ?? "?"}`;
statusEl.textContent = `${state.title || "surface"} (${source})`;
if (state.providerId === "voidbot.swarm") {
renderVoidBot(state);
} else {
renderGraphSurface(state);
}
}
function renderVoidBot(state) {
const nodes = state.nodes || [];
const ctb = nodes.filter(n => n.kind === "ctb-turn");
const leaves = nodes.filter(n => n.kind === "state-leaf");
const summary = nodeById(nodes, "voidbot-summary");
const agent = nodeById(nodes, "agent-detail");
const detail = nodeById(nodes, state.selectedNodeId) || nodeById(nodes, "state-detail") || agent || summary;
app.innerHTML = "";
const root = el("section", "cockpit");
const rail = el("div", "ctb");
for (const turn of ctb) {
const card = el("button", "card");
card.type = "button";
if (turn.avatarUrl) {
const img = el("img", "avatar");
img.src = turn.avatarUrl;
img.alt = "";
card.append(img);
}
card.append(el("div", "card-title", turn.label || turn.id));
card.append(el("div", "health", turn.health || ""));
rail.append(card);
}
const left = el("section", "pane");
left.append(el("h2", "", "Controls / Selected Face"));
left.append(el("div", "detail", `${summary?.label || "VoidBot Swarm"}\n${summary?.detail || ""}`));
left.append(metricStack());
left.append(el("div", "detail", agent?.detail || "No selected Face detail."));
const middle = el("section", "pane");
middle.append(el("h2", "", "State Graph"));
const list = el("div", "list");
for (const leaf of leaves) {
const card = el("div", `card ${leaf.id === state.selectedNodeId ? "selected" : ""}`);
card.append(el("div", "card-title", leaf.label || leaf.id));
card.append(el("div", "health", leaf.health || leaf.statePath || ""));
list.append(card);
}
middle.append(list);
const right = el("section", "pane");
right.append(el("h2", "", "State Detail"));
right.append(el("div", "detail", `${detail?.label || "detail"}\n\n${detail?.detail || ""}`));
root.append(rail, left, middle, right);
app.append(root);
}
function renderGraphSurface(state) {
app.innerHTML = "";
const graph = el("section", "graph");
for (const node of state.nodes || []) {
const view = el("article", `node ${node.id === state.selectedNodeId ? "selected" : ""}`);
const width = Math.max(160, graphWidth(node.width));
const height = Math.max(92, graphHeight(node.height));
view.style.width = `${width}px`;
view.style.minHeight = `${height}px`;
view.style.left = `${((node.x ?? 0) + 1) * 50}%`;
view.style.top = `${((node.y ?? 0) + 1) * 50}%`;
view.append(el("div", "card-title", node.label || node.id));
view.append(el("div", "health", `${node.kind || "node"} ${node.health || ""}`));
if (node.detail) {
view.append(el("div", "detail", node.detail));
}
graph.append(view);
}
app.append(graph);
}
function graphWidth(value) {
return Math.round((value || 0.25) * Math.max(900, window.innerWidth - 32));
}
function graphHeight(value) {
return Math.round((value || 0.16) * Math.max(520, window.innerHeight - 150));
}
function metricStack() {
const wrapper = el("div", "metrics");
const metrics = [
["TURN", 0.96],
["MEMORY", 0.70],
["PRESSURE", 0.42],
["HEAT", 0.34],
["LOAD", 0.99],
["SPEED", 0.88],
];
for (const [label, value] of metrics) {
const metric = el("div", "metric");
metric.append(el("label", "", `${label} ${Math.round(value * 100)}%`));
const bar = el("div", "bar");
const fill = el("span");
fill.style.width = `${Math.round(value * 100)}%`;
bar.append(fill);
metric.append(bar);
wrapper.append(metric);
}
return wrapper;
}
function nodeById(nodes, id) {
return nodes.find(node => node.id === id);
}
function el(tag, className = "", text = "") {
const element = document.createElement(tag);
if (className) element.className = className;
if (text) element.textContent = text;
return element;
}