open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/world/index.html
2026-10-26 02:35:14 -04:00

103 lines
4.8 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 shared world client</title>
<link rel="icon" href="data:,">
<style>
:root { --bg: #f6f7f9; --fg: #15171a; --muted: #5d6470; --panel: #ffffff; --line: #d9dde3; }
@media (prefers-color-scheme: dark) { :root { --bg: #111317; --fg: #e8eaee; --muted: #9aa2ae; --panel: #1a1d23; --line: #2c313a; } }
* { box-sizing: border-box; }
body { margin: 0; padding: 16px; background: var(--bg); color: var(--fg); font: 14px/1.45 system-ui, sans-serif; }
h1 { font-size: 16px; margin: 0 0 12px; }
section { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 10px; margin-bottom: 12px; min-width: 0; }
.controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px 12px; margin: 0; font-size: 13px; }
dt { color: var(--muted); }
dd { margin: 0; font-family: ui-monospace, monospace; overflow-wrap: anywhere; }
#status { color: var(--muted); }
#log { white-space: pre-wrap; font-family: ui-monospace, monospace; font-size: 11px; color: var(--muted); max-height: 260px; overflow: auto; margin: 0; overflow-wrap: anywhere; }
</style>
</head>
<body>
<!--
The reference client of a room's shared world (WAI extensions/live-worlds.md), over a room
service: ?service=<the service's path or URL>[&role=viewer][&ws=<the relay's WebSocket URL>].
Enter takes an actor's grant for a key this page makes; each arrow moves this actor by one,
as a signed action published on its world track. Everything a test reads is on
window.worldTest.
-->
<h1>Shared world</h1>
<section class="controls">
<button id="enter" type="button">Enter</button>
<button type="button" data-move="-1,0" disabled>←</button>
<button type="button" data-move="1,0" disabled>→</button>
<button type="button" data-move="0,-1" disabled>↑</button>
<button type="button" data-move="0,1" disabled>↓</button>
<button id="leave" type="button" disabled>Leave</button>
<span id="status" role="status">Not in the room.</span>
</section>
<section>
<dl>
<dt>World</dt><dd id="kind">—</dd>
<dt>You</dt><dd id="me">—</dd>
<dt>Ops</dt><dd id="ops">0</dd>
<dt>Session hash</dt><dd id="hash">—</dd>
<dt>Receipts verified</dt><dd id="verified">0</dd>
<dt>Actors</dt><dd id="actors">—</dd>
</dl>
</section>
<section>
<pre id="log"></pre>
</section>
<script type="module">
import { WorldClient } from "./world.js";
const $ = (id) => document.getElementById(id);
const q = new URLSearchParams(location.search);
const service = q.get("service") || "/room-api";
const ws = q.get("ws") || `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/moq`;
const lines = [];
const log = (m) => {
const l = `${(performance.now() / 1000).toFixed(3)} ${m}`;
lines.push(l);
if (lines.length > 2000) lines.shift();
$("log").textContent += l + "\n";
console.log(l);
};
const render = () => {
const s = client.snapshot();
$("kind").textContent = s.room ? `${s.private ? "private" : "public"}, ${s.statement.capability} ${s.statement.width}×${s.statement.height}` : "—";
$("me").textContent = s.me ? s.me.slice(0, 16) + "…" : "—";
$("ops").textContent = String(s.ops);
$("hash").textContent = s.session_hash || "—";
$("verified").textContent = String(s.stats.verified);
$("actors").textContent = Object.entries(s.by_actor).map(([k, n]) => `${k.slice(0, 8)}: ${n}`).join(", ") || "—";
};
const client = new WorldClient({ service, ws, log, role: q.get("role") || "publisher", onChange: () => render() });
const fail = (e) => { client.fail(e); $("status").textContent = `Something went wrong: ${e.message || e}`; };
$("enter").addEventListener("click", async () => {
$("enter").disabled = true;
try {
const phase = await client.enter();
$("status").textContent = phase === "full" ? "The room is full." : phase === "acting" ? "In the room: each arrow is a signed action." : "Watching the world.";
for (const b of document.querySelectorAll("[data-move]")) b.disabled = phase !== "acting";
$("leave").disabled = false;
} catch (e) { fail(e); }
});
for (const b of document.querySelectorAll("[data-move]")) {
b.addEventListener("click", () => { const [dx, dy] = b.dataset.move.split(",").map(Number); client.act(dx, dy); });
}
$("leave").addEventListener("click", () => { client.leave(); $("status").textContent = "Left the room."; });
window.worldTest = {
client, lines,
enter: () => client.enter(),
act: (dx, dy) => client.act(dx, dy),
flushed: () => client.sending,
leave: () => client.leave(),
snapshot: () => client.snapshot(),
};
</script>
</body>
</html>