diff --git a/README.md b/README.md
index 270f0b7..7264f91 100644
--- a/README.md
+++ b/README.md
@@ -174,6 +174,9 @@ Open `http://127.0.0.1:8891/`. The page can spawn:
- `Sai VN Surface`, a fixture for visual-novel scenes exported as
`gamecult.eve.surface.v1`, including embedded Norn and TeX surfaces placed
diegetically inside the scene.
+- `Huginn .cc`, a read-only `.eve` fixture emitted by CultCache/Huginn for
+ `.cc` files: source witness, schema catalog, persisted records, payload byte
+ counts, and decode status. Huginn emits Eve DSL; Eve compiles and renders it.
- `Reactive DSL`, a browser-reference fixture that compiles a small `.eve`
composition language into the same surface contract and binds UI controls to
live CultMesh-shaped vars, collections, and derived fields.
diff --git a/web/fixtures/huginn-cc-surface.eve b/web/fixtures/huginn-cc-surface.eve
new file mode 100644
index 0000000..6deaeab
--- /dev/null
+++ b/web/fixtures/huginn-cc-surface.eve
@@ -0,0 +1,30 @@
+# Huginn emits Eve DSL. Renderers compile and lower it.
+surface cultcache.huginn.inspector "Huginn .cc Inspector"
+version 1
+
+collection huginn.catalog.entries
+item huginn.catalog.entries "gamecult.persona_state | gamecult.persona_state.v0 | sha256:persona-state | 12 members"
+item huginn.catalog.entries "voidbot.moderation_cursor | voidbot.moderation_cursor.v1 | sha256:moderation-cursor | 4 members"
+item huginn.catalog.entries "voidbot.face_affect | voidbot.face_affect.v1 | sha256:face-affect | 7 members"
+
+collection huginn.records.entries
+item huginn.records.entries "persona:huginn | gamecult.persona_state | 2026-05-31T22:14:12.000Z | 2048 bytes | decoded"
+item huginn.records.entries "cursor:moderation | voidbot.moderation_cursor | 2026-05-31T22:12:03.000Z | 640 bytes | decoded"
+item huginn.records.entries "affect:face | voidbot.face_affect | 2026-05-31T22:13:44.000Z | 768 bytes | decoded"
+
+card source "Source Witness"
+text "file: E:\\Projects\\CultCacheTS\\.voidbot\\state\\huginn.cc"
+text "format: cultcache.store.v1"
+text "bytes: 3456"
+text ".cc bytes are authority; Huginn only emits Eve DSL."
+end
+
+card catalog "Schema Catalog"
+text "schemas: 3"
+list "Catalog Entries" bind huginn.catalog.entries
+end
+
+card records "Persisted Records"
+text "records: 3"
+list "Record Entries" bind huginn.records.entries
+end
diff --git a/web/index.html b/web/index.html
index b1490c7..2a3a48e 100644
--- a/web/index.html
+++ b/web/index.html
@@ -23,6 +23,7 @@
+
diff --git a/web/surface.js b/web/surface.js
index f3f57a3..8de04c0 100644
--- a/web/surface.js
+++ b/web/surface.js
@@ -7,6 +7,7 @@ const surfaceVersion = document.querySelector("#surface-version");
const voidBotTab = document.querySelector("#voidbot-tab");
const fensalirTab = document.querySelector("#fensalir-tab");
const saiTab = document.querySelector("#sai-tab");
+const huginnTab = document.querySelector("#huginn-tab");
const dslTab = document.querySelector("#dsl-tab");
let socket;
@@ -30,6 +31,13 @@ saiTab.addEventListener("click", async () => {
renderSurface(await response.json(), "fixture");
});
+huginnTab.addEventListener("click", async () => {
+ setActiveTab(huginnTab);
+ closeSocket();
+ const response = await fetch("./fixtures/huginn-cc-surface.eve");
+ renderSurface(compileEveDsl(await response.text()), "dsl");
+});
+
dslTab.addEventListener("click", async () => {
setActiveTab(dslTab);
closeSocket();
@@ -40,7 +48,7 @@ dslTab.addEventListener("click", async () => {
openVoidBot();
function setActiveTab(tab) {
- for (const button of [voidBotTab, fensalirTab, saiTab, dslTab]) {
+ for (const button of [voidBotTab, fensalirTab, saiTab, huginnTab, dslTab]) {
button.classList.toggle("active", button === tab);
}
}
@@ -52,20 +60,25 @@ function openVoidBot() {
const url = `${scheme}//${host}:8795/eve/deck`;
statusEl.textContent = `connecting ${url}`;
socket = new WebSocket(url);
+ const activeSocket = socket;
socket.addEventListener("open", () => {
+ if (socket !== activeSocket) return;
statusEl.textContent = "connected to Mimir Eve broker";
- socket.send(JSON.stringify({ type: "open-provider", providerId: "voidbot.swarm" }));
+ activeSocket.send(JSON.stringify({ type: "open-provider", providerId: "voidbot.swarm" }));
});
socket.addEventListener("message", event => {
+ if (socket !== activeSocket) return;
const state = JSON.parse(event.data);
if (state.providerId === "voidbot.swarm") {
renderSurface(state, "live");
}
});
socket.addEventListener("close", () => {
+ if (socket !== activeSocket) return;
statusEl.textContent = "Mimir broker disconnected";
});
socket.addEventListener("error", () => {
+ if (socket !== activeSocket) return;
statusEl.textContent = "Mimir broker error; is port 8795 running?";
});
}