Step 5 Complete

This commit is contained in:
2026-09-14 13:01:45 -07:00
parent 76928f7870
commit 961919e017
34 changed files with 3309 additions and 273 deletions
+36 -17
View File
@@ -1,12 +1,21 @@
import { ExhibitHost } from './host.js';
import { argumentSchema } from './validation.js';
import { postMessageTransport } from './transport/post-message.js';
import { ExhibitConnection } from './connection.js';
const host = new ExhibitHost();
const byId = id => document.getElementById(id);
const json = value => JSON.stringify(value, null, 2);
const rows = new Map();
let frame = null, renderedCatalog = null;
let renderedCatalog = null;
const connection = new ExhibitConnection({ host, base: location.href, transport: postMessageTransport, changed: render,
createFrame() {
const frame = document.createElement('iframe'); frame.title = 'Connected exhibit';
// Append only after src is assigned to avoid negotiating with the blank document.
queueMicrotask(() => { if (connection.frame === frame) byId('frame-container').append(frame); });
return frame;
}
});
function element(tag, text, parent) {
const node = document.createElement(tag); if (text !== undefined) node.textContent = text;
parent?.append(node); return node;
@@ -38,8 +47,15 @@ function editor(spec, label, parent) {
}
function buildCatalog() {
rows.clear(); byId('catalog').replaceChildren();
if (!host.catalog.length) element('p', 'Connect an exhibit to discover its controls.', byId('catalog'));
const groups = new Map();
for (const target of host.catalog) {
const section = element('article', undefined, byId('catalog')); section.className = 'target';
const category = target.category || 'Other';
if (!groups.has(category)) {
const group = element('div', undefined, byId('catalog')); group.className = 'target-group';
element('h3', category, group); groups.set(category, group);
}
const section = element('article', undefined, groups.get(category)); section.className = 'target';
element('h3', target.label ?? target.id, section); element('code', target.id, section);
element('p', `${target.kind} · ${target.readable ? 'readable' : 'not readable'} · ${target.writable ? 'writable' : 'not writable'}`, section);
const current = element('output', '', section);
@@ -85,16 +101,27 @@ function buildCatalog() {
}
}
function render() {
byId('status').textContent = `${host.status} · ${host.sync}`;
byId('status').textContent = connection.loading ? 'Loading exhibit…' : `${host.status} · ${host.sync}`;
byId('status').dataset.state = host.status;
byId('identity').textContent = host.exhibit
? `${host.exhibit.product} · ${host.exhibit.version ?? ''} · Contract ${host.contract.major}.${host.contract.minor}`
: 'No exhibit connected. Enter a served path to begin.';
byId('loaded-url').textContent = connection.url ? `Exhibit location: ${connection.url}` : '';
byId('metadata').textContent = json({ sessionId: host.sessionId, contract: host.contract, exhibit: host.exhibit,
registryRevision: host.registryRevision, stateRevision: host.stateRevision, sequence: host.sequence });
byId('capabilities').textContent = json(host.capabilities);
byId('capabilities').replaceChildren();
if (!host.capabilities.length) element('p', host.sessionId ? 'No capabilities declared.' : 'Capabilities appear after discovery.', byId('capabilities'));
for (const capability of host.capabilities) element('li', `${capability.id}: ${capability.state}`, byId('capabilities'));
byId('state').textContent = json(Object.fromEntries(host.values));
byId('events').textContent = json(host.eventLog);
byId('errors').textContent = json(host.logs);
const latest = host.logs.findLast(entry => entry.code !== 'SESSION');
byId('latest-error').textContent = latest ? `Latest diagnostic (${latest.time}): ${latest.code}: ${latest.message}` : '';
byId('latest-error').hidden = !latest;
byId('target-count').textContent = `(${host.catalog.length})`;
byId('refresh').disabled = host.status !== 'connected';
byId('reconnect').disabled = !frame;
byId('reconnect').disabled = !connection.url || connection.loading || host.status === 'negotiating';
byId('disconnect').disabled = !connection.frame;
if (host.catalog !== renderedCatalog) { renderedCatalog = host.catalog; buildCatalog(); }
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.';
@@ -106,22 +133,14 @@ function render() {
if (row.submit) row.submit.disabled = row.busy || host.status !== 'connected' || !!row.schemaError || unavailable.length > 0;
}
}
async function attach() {
if (!frame) return;
try { await host.connect(postMessageTransport(frame)); } catch (error) { if (!error.code) host.report(error); }
}
byId('connection').addEventListener('submit', event => {
event.preventDefault();
try {
const url = new URL(byId('exhibit-url').value, location.href);
if (url.origin !== location.origin || !['http:', 'https:'].includes(url.protocol)) throw new Error('Enter a same-origin HTTP exhibit URL.');
host.disconnect(); frame?.remove();
frame = document.createElement('iframe'); frame.title = 'Connected exhibit';
frame.addEventListener('load', attach); frame.src = url.href;
byId('frame-container').append(frame); render();
connection.load(byId('exhibit-url').value);
} catch (error) { host.report(error); }
});
byId('reconnect').addEventListener('click', attach);
byId('disconnect').addEventListener('click', () => { host.disconnect(); frame?.remove(); frame = null; render(); });
byId('reconnect').addEventListener('click', () => connection.reconnect());
byId('disconnect').addEventListener('click', () => connection.disconnect());
byId('refresh').addEventListener('click', () => host.refresh().catch(error => host.report(error)));
byId('clear-errors').addEventListener('click', () => { host.logs = []; render(); });
host.subscribe(render); render();