open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/room/index.html
Transaction Science 984d95b273 room page: close the co-performance section, and test the page's markup
Train B's merge of #424 (co-performance in the room) with #413 (the browser
sink, 173981739) left `<section id="co">` without its `</section>`: the page
opened five sections and closed four. The parser then nested the binding table
(`#binding-debug`) and the Log inside the hidden `#co`, so in a room without
co-performance neither ever showed.

- index.html: the `</section>` after the `#co-lead` controls.
- web/room/markup.test.mjs (run by e2e/run.sh): every element the page opens is
  closed in order; the Log, the binding table and the co-watch section are not
  inside `#co`; each mode's controls are inside its own section.
- e2e: in the service scenario (a room without `[co]`), `#log`, `#binding-debug`
  and `#cowatch` are not descendants of `#co`, `#co` is hidden and the Log shows;
  in the cowatch scenario, the co-watch section and the Log show outside `#co`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 06:50:13 -04:00

157 lines
9.1 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MoQ room client</title>
<link rel="icon" href="data:,">
<style>
:root { --bg: #f6f7f9; --fg: #15171a; --muted: #5d6470; --panel: #ffffff; --line: #d9dde3; --human: #1f6f43; --machine: #8a4b00; }
@media (prefers-color-scheme: dark) { :root { --bg: #111317; --fg: #e8eaee; --muted: #9aa2ae; --panel: #1a1d23; --line: #2c313a; --human: #6fd39b; --machine: #f0a957; } }
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body { margin: 0; padding: 16px; background: var(--bg); color: var(--fg); font: 14px/1.45 system-ui, sans-serif; }
header { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; margin-bottom: 12px; }
h1 { font-size: 16px; margin: 0; }
#who { color: var(--muted); font-family: ui-monospace, monospace; }
main { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 12px; }
@media (max-width: 760px) { main { grid-template-columns: minmax(0, 1fr); } }
section { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 10px; min-width: 0; }
canvas { width: 100%; height: auto; display: block; background: #101218; border-radius: 4px; }
#caption { min-height: 2.6em; margin-top: 8px; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
#caption .origin { font-size: 12px; padding: 1px 6px; border-radius: 3px; border: 1px solid currentColor; }
#caption .origin-human { color: var(--human); }
#caption .origin-machine, #caption .origin-machine-reviewed { color: var(--machine); }
#timeline { list-style: none; margin: 0; padding: 0; max-height: 280px; overflow: auto; font-family: ui-monospace, monospace; font-size: 12px; }
#timeline li { padding: 2px 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
#log { white-space: pre-wrap; font-family: ui-monospace, monospace; font-size: 11px; color: var(--muted); max-height: 220px; overflow: auto; margin: 0; overflow-wrap: anywhere; }
h2 { font-size: 13px; margin: 0 0 6px; color: var(--muted); font-weight: 600; }
.controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
#status { color: var(--muted); }
/* Co-performance: the floor, what's next, the audience and fidelity. */
.co-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.co-grid h3 { font-size: 12px; margin: 0 0 4px; color: var(--muted); font-weight: 600; }
.co-grid ul, .co-grid ol { margin: 0; padding-left: 18px; font-size: 13px; }
.co-grid li { padding: 1px 0; overflow-wrap: anywhere; }
#co-now { margin: 0 0 6px; font-weight: 600; }
#co-marks, #co-left { color: var(--muted); font-size: 12px; margin: 4px 0 0; }
#co-intents li[data-outcome="upcoming"] { color: var(--machine); }
#co-intents li[data-outcome="kept"] { color: var(--human); }
#co-lead { margin-top: 8px; }
#co-lead input[type="number"] { width: 4.5em; }
#co-input { margin-top: 6px; }
#co-options { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
table.binding { border-collapse: collapse; font-family: ui-monospace, monospace; font-size: 12px; width: 100%; }
table.binding th, table.binding td { text-align: left; padding: 2px 8px 2px 0; border-bottom: 1px solid var(--line); white-space: nowrap; }
table.binding tr[data-state="verified"] td:nth-child(2) { color: var(--human); }
table.binding tr[data-state="refused"] td:nth-child(2), table.binding tr[data-state="unbound"] td:nth-child(2) { color: var(--machine); }
</style>
</head>
<body>
<header><h1>MoQ room client</h1><span id="who">loading</span></header>
<!-- An end-to-end encrypted room: its epoch and the members of its group. -->
<div id="e2ee" class="controls" hidden><strong>End-to-end encrypted</strong> <span id="e2ee-state"></span></div>
<!-- With a room service (?service=<url>): enter as a publisher or a viewer. -->
<div id="room-app" class="controls">
<button id="join" type="button">Join with camera and microphone</button>
<button id="watch" type="button">Watch</button>
<button id="leave" type="button" hidden>Leave</button>
<span id="status" role="status" aria-live="polite"></span>
<form id="caption-form" hidden><label for="caption-text">Caption</label> <input id="caption-text" maxlength="200" autocomplete="off"> <button type="submit">Send</button></form>
</div>
<main>
<section>
<h2>The room, composed</h2>
<canvas id="canvas" width="640" height="360"></canvas>
<div id="caption" aria-live="polite"></div>
<div id="dvr" class="controls" style="margin: 8px 0 0">
<label for="seek-s">Seconds back</label>
<input id="seek-s" type="number" min="1" max="900" value="10" style="width: 5em">
<button id="seek" type="button" disabled>Seek back</button>
<button id="live" type="button" hidden>Back to live</button>
<span id="dvr-status" role="status" aria-live="polite"></span>
</div>
<canvas id="dvr-canvas" width="640" height="360" hidden style="margin-top: 8px" aria-label="The participant's earlier video, from the relay's window"></canvas>
</section>
<!-- A watch room (WAI co-watch): the source the room watches, its shared controls and how
closely the room keeps together. -->
<section id="cowatch" hidden style="grid-column: 1 / -1">
<h2>Watching together</h2>
<video id="cowatch-media" playsinline style="width: 100%; max-height: 360px; background: #101218; border-radius: 4px"></video>
<div class="controls" style="margin: 8px 0 0">
<button id="cw-play" type="button">Play</button>
<button id="cw-pause" type="button">Pause</button>
<button id="cw-back" type="button">Back 10 s</button>
<button id="cw-fwd" type="button">Ahead 10 s</button>
<label for="cw-rate">Speed</label>
<select id="cw-rate"><option value="750000">0.75×</option><option value="1000000" selected>1×</option><option value="1250000">1.25×</option><option value="1500000">1.5×</option></select>
<span id="cw-sync" role="status" aria-live="polite"></span>
</div>
</section>
<section>
<h2>Events</h2>
<ul id="timeline"></ul>
</section>
<!-- A room with co-performance (WAI co-performance): see co.js. -->
<section id="co" hidden style="grid-column: 1 / -1" aria-label="Co-performance">
<h2>Co-performance</h2>
<div class="co-grid">
<div>
<h3>The floor</h3>
<p id="co-now" role="status" aria-live="polite"></p>
<ol id="co-turns"></ol>
<p id="co-marks"></p>
<div class="controls" style="margin: 6px 0 0"><button id="co-pass" type="button" hidden>Pass the floor</button></div>
</div>
<div>
<h3>What's next</h3>
<ul id="co-intents" aria-live="polite"></ul>
</div>
<div>
<h3>The audience</h3>
<p id="co-question"></p>
<form id="co-input" hidden>
<div id="co-options"></div>
<div id="co-prompt-row" hidden><label for="co-prompt">Your prompt</label> <input id="co-prompt" maxlength="200" autocomplete="off"> <button type="submit">Send</button></div>
<p id="co-left"></p>
</form>
<ul id="co-selections"></ul>
</div>
<div>
<h3>Fidelity</h3>
<ul id="co-fidelity"></ul>
</div>
</div>
<div id="co-lead" class="controls" hidden>
<label for="co-turn-s">Turn (s)</label> <input id="co-turn-s" type="number" min="1" max="60" value="4">
<label for="co-turns-n">Turns</label> <input id="co-turns-n" type="number" min="1" max="64" value="4">
<label for="co-mode">Out of turn</label> <select id="co-mode"><option value="mark">marked</option><option value="refuse">refused</option></select>
<button id="co-start" type="button">Start turns</button>
<label for="co-q">Ask</label> <input id="co-q" maxlength="200" placeholder="Which way next?">
<label for="co-options-n">Options</label> <input id="co-options-n" type="number" min="2" max="32" value="3">
<label for="co-window-s">Open for (s)</label> <input id="co-window-s" type="number" min="2" max="300" value="8">
<button id="co-vote" type="button">Open a vote</button>
<button id="co-prompts" type="button">Ask for prompts</button>
<button id="co-fid" type="button">State fidelity</button>
</div>
</section>
<!-- With ?debug=binding: the state of the frame each tile shows (WAI browser-sink §10). -->
<section id="binding-debug" style="grid-column: 1 / -1" hidden>
<h2>Frames on screen</h2>
<div style="overflow-x: auto"><table class="binding">
<thead><tr><th>Tile</th><th>State</th><th>Tier</th><th>Digest</th><th>Receipt</th><th>Chain</th><th>Provenance</th><th>Route</th><th>Power efficient</th><th>Energy</th></tr></thead>
<tbody></tbody>
</table></div>
</section>
<section style="grid-column: 1 / -1">
<h2>Log</h2>
<pre id="log"></pre>
</section>
</main>
<script type="module">
// The controls are for a room service alone; a fixture's page has no use for them.
if (!new URLSearchParams(location.search).get("service")) document.getElementById("room-app").hidden = true;
</script>
<script type="module" src="./app.js"></script>
</body>
</html>