generated from Labyricorn/labyricorn-project-template
Steps 6.4-6.7B — Local surfaces, reference-exhibit validation, SciFi Observation surface
One commit for the work accumulated in the working tree since Step 6.3, which had never been split into per-step commits: - src/local-surfaces.js + src/surface-url.js (new); src/ui.js, src/validation.js, src/connection.js and public/index.html updated for local-surface hosting and generic surface rendering - tests: local-surfaces (20), scifi-surfaces (24) and postmessage-interop (7) new; connection/museum-gallery/surface-validation suites updated - reference exhibits: shared/contract-core.js defaults to Contract 5.3 (major 5, minor 3, xzbt 5.3); museum-gallery advertises its surface catalog; aquarium/haunted-house/planetarium adapters updated - SciFi-XZBT (Step 6.7A/6.7B): surface-mode.js + surface-bus.js, Observation-surface boot branch, local-change hooks, view.pillars / view.warp-flight targets; fixture byte-identical to G:/.vibe/SciFi-XZBT - SciFi-XZBT contract adapter handshake fix: the inbound bridge filter no longer gates on an exact advisory xzbt value (Contract 5.3 §6.5), only on its presence/type, matching the host's own envelope validation; the adapter now advertises contract minor 3 / version 5.3.0, which it already implemented via the 5.3 surfaces field. Root cause of the five failing postmessage-interop tests (host hello was silently dropped). - docs: architecture 6.4 and 6.7A, reference 6.6 and 6.7; evidence logs; test-fixtures/PROVENANCE.md resync record Test results: NGN 154/154 (was 149/154); postmessage-interop 7/7 (was 2/7); SciFi contract harness 21/21, real-adapter suite 32/32. git diff --check clean for changed files; two pre-existing trailing-whitespace lines remain in test-fixtures/reference-exhibits/scifi/index.html, copied verbatim from the authoritative SciFi source. Step 6.7 live verification (browser Observation, packaged standalone) is still pending and is not claimed here.
This commit is contained in:
@@ -2,6 +2,7 @@ import { ExhibitHost } from './host.js';
|
||||
import { argumentSchema } from './validation.js';
|
||||
import { postMessageTransport } from './transport/post-message.js';
|
||||
import { ExhibitConnection } from './connection.js';
|
||||
import { LocalSurfaces } from './local-surfaces.js';
|
||||
|
||||
const host = new ExhibitHost();
|
||||
const byId = id => document.getElementById(id);
|
||||
@@ -9,6 +10,7 @@ const json = value => JSON.stringify(value, null, 2);
|
||||
const rows = new Map();
|
||||
let renderedCatalog = null;
|
||||
let renderedSurfaces = null;
|
||||
const surfaceRows = new Map();
|
||||
const connection = new ExhibitConnection({ host, base: location.href, transport: postMessageTransport, changed: render,
|
||||
createFrame() {
|
||||
const frame = document.createElement('iframe'); frame.title = 'Connected exhibit';
|
||||
@@ -17,6 +19,16 @@ const connection = new ExhibitConnection({ host, base: location.href, transport:
|
||||
return frame;
|
||||
}
|
||||
});
|
||||
const localSurfaces = new LocalSurfaces({ host, changed: render, createFrame(entry) {
|
||||
const frame = document.createElement('iframe'); frame.title = entry.descriptor.label;
|
||||
// Like the control frame, append only after assigning src.
|
||||
queueMicrotask(() => {
|
||||
if (entry.frame === frame) surfaceRows.get(entry.descriptor.id)?.pane.append(frame);
|
||||
});
|
||||
return frame;
|
||||
} });
|
||||
new MutationObserver(() => localSurfaces.sweep()).observe(byId('surfaces'), { childList: true, subtree: true });
|
||||
window.addEventListener('pagehide', () => connection.disconnect());
|
||||
function element(tag, text, parent) {
|
||||
const node = document.createElement(tag); if (text !== undefined) node.textContent = text;
|
||||
parent?.append(node); return node;
|
||||
@@ -102,18 +114,39 @@ function buildCatalog() {
|
||||
}
|
||||
}
|
||||
function buildSurfaces() {
|
||||
const container = byId('surfaces'); container.replaceChildren();
|
||||
// Preserve live panes across registry refreshes; replacing their ancestors
|
||||
// would unload the documents even if their descriptor URLs did not change.
|
||||
const container = byId('surfaces');
|
||||
for (const [id, row] of surfaceRows) if (!localSurfaces.entries.has(id)) {
|
||||
row.card.remove(); surfaceRows.delete(id);
|
||||
}
|
||||
container.querySelectorAll(':scope > p').forEach(node => node.remove());
|
||||
byId('surface-count').textContent = host.surfaces.length ? `(${host.surfaces.length})` : '';
|
||||
if (!host.surfaces.length) {
|
||||
element('p', host.sessionId ? 'No presentation surfaces advertised.' : 'No presentation surfaces advertised.', container);
|
||||
element('p', 'No presentation surfaces advertised.', container);
|
||||
return;
|
||||
}
|
||||
for (const surface of host.surfaces) {
|
||||
const card = element('article', undefined, container); card.className = 'surface';
|
||||
element('h3', surface.label, card);
|
||||
element('code', surface.id, card);
|
||||
const badges = element('div', undefined, card);
|
||||
if (surface.primary) { const b = element('span', 'Primary', badges); b.className = 'surface-badge primary'; }
|
||||
let row = surfaceRows.get(surface.id);
|
||||
if (!row) {
|
||||
const card = element('article', undefined, container); card.className = 'surface';
|
||||
const metadata = element('div', undefined, card);
|
||||
const status = element('p', '', card); status.setAttribute('role', 'status');
|
||||
const controls = element('div', undefined, card); controls.className = 'controls';
|
||||
const buttons = {};
|
||||
for (const [label, method] of [['Open', 'open'], ['Reload', 'reload'], ['Close', 'close']]) {
|
||||
const button = element('button', label, controls); buttons[method] = button;
|
||||
button.addEventListener('click', () => localSurfaces[method](surface.id));
|
||||
}
|
||||
const pane = element('div', undefined, card);
|
||||
row = { card, metadata, status, buttons, pane };
|
||||
surfaceRows.set(surface.id, row);
|
||||
}
|
||||
row.metadata.replaceChildren();
|
||||
element('h3', surface.label, row.metadata);
|
||||
element('code', surface.id, row.metadata);
|
||||
const badges = element('div', undefined, row.metadata);
|
||||
const b = element('span', surface.primary ? 'Primary' : 'Non-primary', badges); b.className = 'surface-badge';
|
||||
if (surface.role) { const b = element('span', `Role: ${surface.role}`, badges); b.className = 'surface-badge'; }
|
||||
if (surface.category) { const b = element('span', `Category: ${surface.category}`, badges); b.className = 'surface-badge'; }
|
||||
const meta = [];
|
||||
@@ -121,7 +154,7 @@ function buildSurfaces() {
|
||||
if (surface.url) meta.push(`URL: ${surface.url}`);
|
||||
if (surface.aspectRatio) meta.push(`Aspect ratio: ${surface.aspectRatio}`);
|
||||
if (surface.requires?.length) meta.push(`Requires: ${surface.requires.join(', ')}`);
|
||||
if (meta.length) { const p = element('p', meta.join(' · '), card); p.className = 'surface-meta'; }
|
||||
if (meta.length) { const p = element('p', meta.join(' · '), row.metadata); p.className = 'surface-meta'; }
|
||||
}
|
||||
}
|
||||
function render() {
|
||||
@@ -148,6 +181,18 @@ function render() {
|
||||
byId('disconnect').disabled = !connection.frame;
|
||||
if (host.catalog !== renderedCatalog) { renderedCatalog = host.catalog; buildCatalog(); }
|
||||
if (host.surfaces !== renderedSurfaces) { renderedSurfaces = host.surfaces; buildSurfaces(); }
|
||||
for (const [id, row] of surfaceRows) {
|
||||
const entry = localSurfaces.entries.get(id);
|
||||
const state = entry?.state ?? 'unavailable';
|
||||
const requirements = (Array.isArray(entry?.descriptor.requires) ? entry.descriptor.requires : [])
|
||||
.filter(id => host.capabilities.find(c => c.id === id)?.state !== 'ready');
|
||||
row.status.textContent = state === 'control'
|
||||
? 'Open in the authoritative control frame. Use Connection controls to reload or disconnect.'
|
||||
: `${state}${entry?.error ? ': ' + entry.error : ''}${requirements.length ? ' · Unavailable: ' + requirements.join(', ') : ''}`;
|
||||
row.buttons.open.disabled = !entry || ['control', 'open', 'loading'].includes(state) || requirements.length > 0;
|
||||
row.buttons.reload.disabled = !entry || ['control', 'closed'].includes(state);
|
||||
row.buttons.close.disabled = !entry || ['control', 'closed'].includes(state);
|
||||
}
|
||||
for (const row of rows.values()) {
|
||||
row.current.textContent = host.values.has(row.target.id) ? `Reported value: ${json(host.values.get(row.target.id))}` : 'No persistent value reported.';
|
||||
if (!row.initialized && row.valueEditor && host.values.has(row.target.id)) {
|
||||
|
||||
Reference in New Issue
Block a user