Skip to main content

HOST_HTML

Constant HOST_HTML 

Source
pub const HOST_HTML: &str = "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Actor Host \u{2014} actr run --web</title>\n    <style>\n        * {\n            box-sizing: border-box\n        }\n\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;\n            max-width: 860px;\n            margin: 40px auto;\n            padding: 20px;\n            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n            min-height: 100vh;\n            color: #e0e0e0\n        }\n\n        .container {\n            background: rgba(255, 255, 255, .05);\n            padding: 28px;\n            border-radius: 14px;\n            box-shadow: 0 8px 32px rgba(0, 0, 0, .3);\n            border: 1px solid rgba(255, 255, 255, .1)\n        }\n\n        h1 {\n            color: #00d4aa;\n            margin: 0 0 6px;\n            font-size: 1.8em\n        }\n\n        .subtitle {\n            color: #888;\n            margin-bottom: 24px;\n            font-size: .88em\n        }\n\n        /* Status bar \u{2014} supports classes: ready, connected, error, connecting */\n        .status {\n            padding: 12px 18px;\n            border-radius: 8px;\n            margin-bottom: 16px;\n            font-weight: 500;\n            display: flex;\n            align-items: center;\n            gap: 10px;\n            background: rgba(255, 165, 0, .12);\n            border: 1px solid rgba(255, 165, 0, .25)\n        }\n\n        .status.ready,\n        .status.connected,\n        .status.ok {\n            background: rgba(0, 212, 170, .1);\n            border-color: rgba(0, 212, 170, .3)\n        }\n\n        .status.error,\n        .status.err {\n            background: rgba(255, 60, 60, .1);\n            border-color: rgba(255, 60, 60, .3)\n        }\n\n        .status.connecting {\n            background: rgba(255, 165, 0, .12);\n            border: 1px solid rgba(255, 165, 0, .25)\n        }\n\n        .info-grid {\n            display: grid;\n            grid-template-columns: 1fr 1fr;\n            gap: 8px 16px;\n            margin-bottom: 20px;\n            font-size: .88em\n        }\n\n        .info-grid dt {\n            color: #888\n        }\n\n        .info-grid dd {\n            margin: 0;\n            color: #ccc;\n            word-break: break-all\n        }\n\n        /* Stats row (server) */\n        .stats {\n            display: flex;\n            gap: 20px;\n            margin-bottom: 16px;\n            font-size: .92em\n        }\n\n        .stats .stat {\n            display: flex;\n            align-items: center;\n            gap: 6px\n        }\n\n        .stats .stat-label {\n            color: #888\n        }\n\n        .stats .stat-value {\n            font-weight: 600;\n            color: #00d4aa\n        }\n\n        /* Client send area */\n        .send-area {\n            display: flex;\n            gap: 8px;\n            margin-bottom: 16px\n        }\n\n        .send-area input {\n            flex: 1;\n            padding: 10px 14px;\n            border: 1px solid rgba(255, 255, 255, .15);\n            border-radius: 8px;\n            background: rgba(0, 0, 0, .3);\n            color: #e0e0e0;\n            font-size: .95em;\n            outline: none\n        }\n\n        .send-area input:focus {\n            border-color: #00d4aa\n        }\n\n        .send-area button {\n            padding: 10px 20px;\n            border: none;\n            border-radius: 8px;\n            background: #00d4aa;\n            color: #1a1a2e;\n            font-weight: 600;\n            cursor: pointer;\n            white-space: nowrap\n        }\n\n        .send-area button:disabled {\n            opacity: .4;\n            cursor: not-allowed\n        }\n\n        .send-area button:hover:not(:disabled) {\n            background: #00eabb\n        }\n\n        /* Log panels */\n        .log-panel {\n            background: rgba(0, 0, 0, .3);\n            border: 1px solid rgba(255, 255, 255, .08);\n            border-radius: 8px;\n            padding: 14px;\n            height: 300px;\n            overflow-y: auto;\n            font-family: \'Fira Code\', monospace;\n            font-size: .78em;\n            line-height: 1.65\n        }\n\n        .log-entry {\n            padding: 2px 0\n        }\n\n        .log-entry .time {\n            color: #666;\n            margin-right: 8px\n        }\n\n        .log-entry.info {\n            color: #8be9fd\n        }\n\n        .log-entry.success {\n            color: #50fa7b\n        }\n\n        .log-entry.warn {\n            color: #ffb86c\n        }\n\n        .log-entry.error {\n            color: #ff5555\n        }\n\n        /* Client result entries */\n        .entry {\n            padding: 2px 0\n        }\n\n        .entry .time {\n            color: #666;\n            margin-right: 8px\n        }\n\n        .entry .info {\n            color: #8be9fd\n        }\n\n        .entry .ok {\n            color: #50fa7b\n        }\n\n        .entry .err {\n            color: #ff5555\n        }\n\n        /* Legacy log classes for generic log */\n        .log-info {\n            color: #8be9fd\n        }\n\n        .log-warn {\n            color: #ffb86c\n        }\n\n        .log-error {\n            color: #ff5555\n        }\n\n        .log-success {\n            color: #50fa7b\n        }\n\n        .section-label {\n            color: #888;\n            font-size: .82em;\n            margin: 12px 0 6px;\n            text-transform: uppercase;\n            letter-spacing: .5px\n        }\n    </style>\n</head>\n\n<body>\n    <div class=\"container\">\n        <h1>\u{1f3ad} Actor Host</h1>\n        <p class=\"subtitle\">actr run --web</p>\n        <div id=\"status\" class=\"status\">\u{23f3} Initializing\u{2026}</div>\n\n        <!-- Info grid (always shown) -->\n        <dl class=\"info-grid\">\n            <dt>Actor Type</dt>\n            <dd id=\"actrType\">\u{2014}</dd>\n            <dt>Role</dt>\n            <dd id=\"role\">\u{2014}</dd>\n            <dt>Signaling</dt>\n            <dd id=\"sigUrl\">\u{2014}</dd>\n            <dt>Realm</dt>\n            <dd id=\"realmId\">\u{2014}</dd>\n            <dt>Service</dt>\n            <dd id=\"serviceName\">\u{2014}</dd>\n        </dl>\n\n        <!-- Inbound stats (populated whenever this actor handles a request) -->\n        <div id=\"inboundStats\">\n            <div class=\"stats\">\n                <div class=\"stat\"><span class=\"stat-label\">Requests:</span><span class=\"stat-value\"\n                        id=\"requestCount\">0</span></div>\n                <div class=\"stat\"><span class=\"stat-label\">Success:</span><span class=\"stat-value\"\n                        id=\"successCount\">0</span></div>\n                <div class=\"stat\"><span class=\"stat-label\">Errors:</span><span class=\"stat-value\"\n                        id=\"errorCount\">0</span></div>\n            </div>\n        </div>\n\n        <!-- Outbound send area: every actor can call peers, regardless of\n             whether it also accepts incoming calls. -->\n        <div id=\"sendArea\">\n            <div class=\"send-area\">\n                <input id=\"msgInput\" type=\"text\" placeholder=\"Enter message to echo\u{2026}\" />\n                <button id=\"sendBtn\" disabled>Send</button>\n            </div>\n            <div class=\"section-label\">Echo Results</div>\n            <div id=\"result\" class=\"log-panel\"></div>\n        </div>\n\n        <!-- Runtime / SW log -->\n        <div class=\"section-label\">Runtime Log</div>\n        <div id=\"log\" class=\"log-panel\"></div>\n    </div>\n\n    <script>\n        /* \u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\n           Inline @actr/dom \u{2014} ServiceWorkerBridge + WebRtcCoordinator + FastPathForwarder\n           \u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550} */\n\n        // \u{2500}\u{2500} FastPathForwarder \u{2500}\u{2500}\n        class FastPathForwarder {\n            constructor(bridge) { this.bridge = bridge; }\n            forward(streamId, data) {\n                const view = new Uint8Array(data);\n                this.bridge.sendToSW({ type: \'fast_path_data\', payload: { streamId, data: view, timestamp: Date.now() } }, [view.buffer]);\n            }\n            dispose() { }\n        }\n\n        // \u{2500}\u{2500} ServiceWorkerBridge \u{2500}\u{2500}\n        class ServiceWorkerBridge {\n            constructor() {\n                this.swPort = null;\n                this.swTarget = null;\n                this.handlers = new Set();\n                this._ready = null;\n                this._resolveReady = null;\n                this._ready = new Promise(r => { this._resolveReady = r; });\n                this.clientId = \'client_\' + Date.now() + \'_\' + Math.random().toString(36).slice(2, 8);\n            }\n            async initialize(swUrl, runtimeConfig) {\n                const reg = await navigator.serviceWorker.register(swUrl, { updateViaCache: \'none\' });\n                await reg.update();\n                await navigator.serviceWorker.ready;\n                if (!navigator.serviceWorker.controller) {\n                    await new Promise(r => {\n                        const h = () => { navigator.serviceWorker.removeEventListener(\'controllerchange\', h); r(); };\n                        navigator.serviceWorker.addEventListener(\'controllerchange\', h);\n                        setTimeout(h, 3000);\n                    });\n                }\n                const target = navigator.serviceWorker.controller ?? reg.active;\n                if (!target) throw new Error(\'SW target not available\');\n                this.swTarget = target;\n                const ch = new MessageChannel();\n                this.swPort = ch.port1;\n                this.swPort.onmessage = e => this._dispatch(e.data);\n                target.postMessage({ type: \'DOM_PORT_INIT\', port: ch.port2, clientId: this.clientId, runtimeConfig }, [ch.port2]);\n                target.postMessage({ type: \'PING\' });\n                this._resolveReady();\n            }\n            sendToSW(msg, transferables) {\n                if (!this.swPort) return;\n                transferables && transferables.length ? this.swPort.postMessage(msg, transferables) : this.swPort.postMessage(msg);\n            }\n            sendDataChannelPort(peerId, port) {\n                this.sendToSW({ type: \'register_datachannel_port\', payload: { peerId, port } }, [port]);\n            }\n            onMessage(handler) { this.handlers.add(handler); return () => this.handlers.delete(handler); }\n            _dispatch(msg) { for (const h of this.handlers) { try { h(msg); } catch (e) { console.error(\'[Bridge]\', e); } } }\n            getClientId() { return this.clientId; }\n            close() {\n                if (this.swTarget && this.swTarget.postMessage) {\n                    try { this.swTarget.postMessage({ type: \'CLIENT_UNREGISTER\', clientId: this.clientId }); } catch (_) { }\n                }\n                if (this.swPort) {\n                    try { this.swPort.postMessage({ type: \'unregister_client\' }); } catch (_) { }\n                    this.swPort.close();\n                    this.swPort = null;\n                }\n                this.swTarget = null;\n                this.handlers.clear();\n            }\n        }\n\n        // \u{2500}\u{2500} WebRtcCoordinator \u{2500}\u{2500}\n        class WebRtcCoordinator {\n            constructor(bridge, forwarder, config) {\n                this.bridge = bridge;\n                this.forwarder = forwarder;\n                this.peers = new Map();\n                this.pendingSends = new Map();\n                this.pendingPortFrames = new Map();\n                this.rpcPorts = new Map();\n                this.fragmentCounters = new Map();\n                this.reassembly = new Map();\n                this.turnCred = null;\n                this.FRAGMENT_HEADER_SIZE = 8;\n                this.DC_MAX_MESSAGE_SIZE = 65535;\n                this.DC_MAX_PAYLOAD_SIZE = this.DC_MAX_MESSAGE_SIZE - this.FRAGMENT_HEADER_SIZE;\n                this.iceServers = config.iceServers || [{ urls: \'stun:stun.l.google.com:19302\' }];\n                this.icePolicy = config.iceTransportPolicy;\n                this.lanes = [\n                    { id: 0, label: \'RPC_RELIABLE\', ordered: true },\n                    { id: 1, label: \'RPC_SIGNAL\', ordered: true },\n                    { id: 2, label: \'STREAM_RELIABLE\', ordered: true },\n                    { id: 3, label: \'STREAM_LATENCY_FIRST\', ordered: false, maxRetransmits: 3 },\n                ];\n                this.labelToId = new Map(this.lanes.map(l => [l.label, l.id]));\n                bridge.onMessage(m => {\n                    if (m.type === \'webrtc_command\') this._handle(m.payload);\n                    else if (m.type === \'update_turn_credential\') {\n                        this.turnCred = { username: m.payload.username, credential: m.payload.password };\n                    }\n                });\n            }\n            _canBindRpcPort(peer, ch) {\n                return !!(peer\n                    && ch\n                    && ch.readyState === \'open\'\n                    && peer.state === \'connected\'\n                    && peer.connection\n                    && peer.connection.connectionState === \'connected\');\n            }\n            _dropRpcPort(pid) {\n                const port = this.rpcPorts.get(pid);\n                if (port) {\n                    try { port.close(); } catch (_) { }\n                    this.rpcPorts.delete(pid);\n                }\n            }\n            _reportStaleRpcPeer(pid, peer, ch, reason = \'unknown\') {\n                const state = ch ? ch.readyState : (peer ? peer.state : \'missing\');\n                console.log(`[HostPage] staleRpcPeer peer=${pid} reason=${reason} state=${state}`);\n                this._notify(\'command_error\', {\n                    peerId: pid,\n                    action: \'send_port_frame\',\n                    error: `datachannel_not_open:${state}`,\n                });\n            }\n            async _handle(cmd) {\n                const { action, peerId } = cmd;\n                try {\n                    switch (action) {\n                        case \'create_peer\': return this._createPeer(peerId);\n                        case \'set_remote_description\': return await this._setRemote(peerId, cmd.payload.sdp);\n                        case \'set_local_description\': return await this._setLocal(peerId, cmd.payload.sdp);\n                        case \'add_ice_candidate\': return await this._addIce(peerId, cmd.payload.candidate);\n                        case \'create_offer\': await this._ensureChannels(peerId); return await this._createOffer(peerId);\n                        case \'create_ice_restart_offer\': return await this._iceRestart(peerId);\n                        case \'create_answer\': return await this._createAnswer(peerId);\n                        case \'close_peer\': return this._close(peerId);\n                        case \'send_data\': return this._send(peerId, cmd.payload.channelId, cmd.payload.data);\n                    }\n                } catch (e) { this._notify(\'command_error\', { peerId, action, error: String(e) }); }\n            }\n            _createPeer(peerId) {\n                const existing = this.peers.get(peerId);\n                if (existing) {\n                    const state = existing.connection && existing.connection.connectionState\n                        ? existing.connection.connectionState\n                        : existing.state;\n                    if (state === \'connected\' || state === \'connecting\') return;\n                    console.log(`[HostPage] replaceStalePeer peer=${peerId} state=${state}`);\n                    this._close(peerId);\n                }\n                const servers = (this.iceServers || []).map(s => {\n                    const urls = Array.isArray(s.urls) ? s.urls : [s.urls];\n                    if (urls.some(u => u.startsWith(\'turn:\') || u.startsWith(\'turns:\')) && this.turnCred) {\n                        return { urls: s.urls, username: this.turnCred.username, credential: this.turnCred.credential };\n                    }\n                    return s;\n                });\n                const pc = new RTCPeerConnection({ iceServers: servers, iceTransportPolicy: this.icePolicy });\n                const dcs = new Map();\n                pc.ondatachannel = e => { const lid = this.labelToId.get(e.channel.label); if (lid !== undefined) this._attach(peerId, lid, e.channel); };\n                pc.onicecandidate = e => { if (e.candidate) this._notify(\'ice_candidate\', { peerId, candidate: e.candidate.toJSON() }); };\n                pc.onconnectionstatechange = () => {\n                    this._notify(\'connection_state_changed\', { peerId, state: pc.connectionState });\n                    const p = this.peers.get(peerId); if (p) p.state = pc.connectionState;\n                    if (pc.connectionState === \'connected\') {\n                        const rpc = dcs.get(0);\n                        if (this._canBindRpcPort(this.peers.get(peerId), rpc)) this._bindRpcPort(peerId, rpc, \'connection_state_connected\');\n                    } else if (pc.connectionState === \'disconnected\' || pc.connectionState === \'failed\' || pc.connectionState === \'closed\') {\n                        this._dropRpcPort(peerId);\n                        this._dropPendingPeerFrames(peerId);\n                    }\n                };\n                this.peers.set(peerId, { peerId, connection: pc, dataChannels: dcs, state: pc.connectionState });\n            }\n            async _setRemote(pid, sdp) { const p = this.peers.get(pid); if (!p) throw new Error(\'no peer\'); await p.connection.setRemoteDescription(sdp); }\n            async _setLocal(pid, sdp) { const p = this.peers.get(pid); if (!p) throw new Error(\'no peer\'); await p.connection.setLocalDescription(sdp); }\n            async _createOffer(pid) { const p = this.peers.get(pid); if (!p) throw new Error(\'no peer\'); const o = await p.connection.createOffer(); await p.connection.setLocalDescription(o); this._notify(\'local_description\', { peerId: pid, sdp: o }); }\n            async _iceRestart(pid) { const p = this.peers.get(pid); if (!p) throw new Error(\'no peer\'); const o = await p.connection.createOffer({ iceRestart: true }); await p.connection.setLocalDescription(o); this._notify(\'ice_restart_local_description\', { peerId: pid, sdp: o }); }\n            async _createAnswer(pid) { const p = this.peers.get(pid); if (!p) throw new Error(\'no peer\'); const a = await p.connection.createAnswer(); await p.connection.setLocalDescription(a); this._notify(\'local_description\', { peerId: pid, sdp: a }); }\n            async _addIce(pid, cand) {\n                const p = this.peers.get(pid); if (!p) throw new Error(\'no peer\');\n                if (!cand || typeof cand !== \'object\') return;\n                const n = { ...cand, sdpMid: cand.sdpMid ?? cand.sdp_mid ?? null, sdpMLineIndex: cand.sdpMLineIndex ?? cand.sdp_mline_index ?? null, usernameFragment: cand.usernameFragment ?? cand.username_fragment ?? null };\n                if (n.sdpMid == null && n.sdpMLineIndex == null) n.sdpMLineIndex = 0;\n                await p.connection.addIceCandidate(new RTCIceCandidate(n));\n            }\n            _send(pid, cid, data) {\n                const p = this.peers.get(pid); if (!p) { this._notify(\'command_error\', { peerId: pid, action: \'send_data\', error: \'no peer\' }); return; }\n                const ch = p.dataChannels.get(cid);\n                if (!ch) { this._queuePendingSend(pid, cid, data); return; }\n                if (ch.readyState === \'open\') {\n                    this._sendFramed(pid, cid, ch, data);\n                    return;\n                }\n                if (ch.readyState === \'connecting\') {\n                    this._queuePendingSend(pid, cid, data);\n                    return;\n                }\n                this._dropPendingPeerFrames(pid);\n                this._notify(\'command_error\', { peerId: pid, action: \'send_data\', error: `datachannel_not_open:${ch.readyState}` });\n            }\n            _queuePendingSend(pid, cid, data) {\n                let byChannel = this.pendingSends.get(pid);\n                if (!byChannel) {\n                    byChannel = new Map();\n                    this.pendingSends.set(pid, byChannel);\n                }\n                let queue = byChannel.get(cid);\n                if (!queue) {\n                    queue = [];\n                    byChannel.set(cid, queue);\n                }\n                queue.push(new Uint8Array(data));\n            }\n            _flushPendingSends(pid, cid) {\n                const byChannel = this.pendingSends.get(pid);\n                const queue = byChannel && byChannel.get(cid);\n                if (!queue || queue.length === 0) return;\n                byChannel.delete(cid);\n                if (byChannel.size === 0) this.pendingSends.delete(pid);\n                for (const payload of queue) this._send(pid, cid, payload);\n            }\n            _queuePendingPortFrame(pid, frame) {\n                this._queuePendingPortFrameForChannel(pid, 0, frame);\n            }\n            _queuePendingPortFrameForChannel(pid, cid, payload) {\n                let queue = this.pendingPortFrames.get(pid);\n                if (!queue) {\n                    queue = [];\n                    this.pendingPortFrames.set(pid, queue);\n                }\n                queue.push({ channelId: cid, payload: new Uint8Array(payload) });\n            }\n            _dropPendingPeerFrames(pid) {\n                this.pendingSends.delete(pid);\n                this.pendingPortFrames.delete(pid);\n            }\n            _bindRpcPort(pid, ch, reason = \'unknown\') {\n                const peer = this.peers.get(pid);\n                if (!this._canBindRpcPort(peer, ch)) {\n                    console.log(`[HostPage] skipBindRpcPort peer=${pid} reason=${reason} state=${peer ? peer.state : \'missing\'} dc=${ch ? ch.readyState : \'missing\'}`);\n                    return;\n                }\n                const prev = this.rpcPorts.get(pid);\n                if (prev) {\n                    try { prev.close(); } catch (_) { }\n                }\n                console.log(`[HostPage] bindRpcPort peer=${pid} reason=${reason}`);\n                const mc = new MessageChannel();\n                mc.port1.onmessage = e => {\n                    const src = this._toU8(e.data);\n                    if (src.byteLength < 5) {\n                        console.warn(`[HostPage] drop short SW transport frame len=${src.byteLength}`);\n                        return;\n                    }\n                    const cid = src[0];\n                    const payload = src.subarray(5);\n                    const target = peer && peer.dataChannels.get(cid);\n                    if (target && target.readyState === \'open\') this._sendFramed(pid, cid, target, payload);\n                    else if (target && target.readyState === \'connecting\') this._queuePendingPortFrameForChannel(pid, cid, payload);\n                    else {\n                        this._dropPendingPeerFrames(pid);\n                        this._notify(\'command_error\', { peerId: pid, action: \'send_port_frame\', error: `datachannel_not_open:${target ? target.readyState : \'missing\'}` });\n                    }\n                };\n                this.rpcPorts.set(pid, mc.port1);\n                this.bridge.sendDataChannelPort(pid, mc.port2);\n                this._flushPendingPortFrames(pid, ch);\n            }\n            refreshRpcPorts(reason = \'manual_refresh\') {\n                for (const [pid, peer] of this.peers.entries()) {\n                    const rpc = peer.dataChannels.get(0);\n                    if (this._canBindRpcPort(peer, rpc)) {\n                        this._bindRpcPort(pid, rpc, reason);\n                    } else if (reason === \'call_raw\' && (rpc || peer.state !== \'connected\')) {\n                        this._reportStaleRpcPeer(pid, peer, rpc, reason);\n                    }\n                }\n            }\n            _flushPendingPortFrames(pid, ch) {\n                const queue = this.pendingPortFrames.get(pid);\n                if (!queue || queue.length === 0) return;\n                if (ch.readyState !== \'open\') return;\n                this.pendingPortFrames.delete(pid);\n                for (const frame of queue) {\n                    const target = this.peers.get(pid) && this.peers.get(pid).dataChannels.get(frame.channelId);\n                    if (!target || target.readyState !== \'open\') {\n                        this._queuePendingPortFrameForChannel(pid, frame.channelId, frame.payload);\n                        continue;\n                    }\n                    this._sendFramed(pid, frame.channelId, target, frame.payload);\n                }\n            }\n            _toU8(data) {\n                if (data instanceof ArrayBuffer) return new Uint8Array(data);\n                if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n                return new Uint8Array(data);\n            }\n            _sendFramed(pid, cid, ch, payload) {\n                for (const frame of this._createFrames(pid, cid, this._toU8(payload))) ch.send(frame);\n            }\n            _createFrames(pid, cid, payload) {\n                const total = Math.max(1, Math.ceil(payload.byteLength / this.DC_MAX_PAYLOAD_SIZE));\n                if (total > 0xffff) throw new Error(`DataChannel payload too large: ${payload.byteLength} bytes`);\n                const key = `${pid}:${cid}`;\n                const msgId = this.fragmentCounters.get(key) || 0;\n                this.fragmentCounters.set(key, (msgId + 1) >>> 0);\n                const frames = [];\n                for (let i = 0; i < total; i++) {\n                    const start = i * this.DC_MAX_PAYLOAD_SIZE;\n                    const end = Math.min(start + this.DC_MAX_PAYLOAD_SIZE, payload.byteLength);\n                    const chunk = payload.subarray(start, end);\n                    const frame = new Uint8Array(this.FRAGMENT_HEADER_SIZE + chunk.byteLength);\n                    const view = new DataView(frame.buffer);\n                    view.setUint32(0, msgId, false);\n                    view.setUint16(4, i, false);\n                    view.setUint16(6, total, false);\n                    frame.set(chunk, this.FRAGMENT_HEADER_SIZE);\n                    frames.push(frame);\n                }\n                return frames;\n            }\n            _decodeFrame(pid, cid, frame) {\n                const src = this._toU8(frame);\n                if (src.byteLength < this.FRAGMENT_HEADER_SIZE) {\n                    console.warn(`[HostPage] drop short DataChannel frame len=${src.byteLength}`);\n                    return null;\n                }\n                const view = new DataView(src.buffer, src.byteOffset, src.byteLength);\n                const msgId = view.getUint32(0, false);\n                const index = view.getUint16(4, false);\n                const total = view.getUint16(6, false);\n                const payload = src.subarray(this.FRAGMENT_HEADER_SIZE);\n                if (total === 0 || index >= total) {\n                    console.warn(`[HostPage] drop invalid DataChannel fragment msg=${msgId} index=${index} total=${total}`);\n                    return null;\n                }\n                if (total === 1) return payload;\n                const key = `${pid}:${cid}:${msgId}`;\n                let entry = this.reassembly.get(key);\n                if (!entry) {\n                    entry = { totalFrags: total, receivedBytes: 0, fragments: new Map() };\n                    this.reassembly.set(key, entry);\n                }\n                if (!entry.fragments.has(index)) {\n                    const copy = new Uint8Array(payload);\n                    entry.fragments.set(index, copy);\n                    entry.receivedBytes += copy.byteLength;\n                }\n                if (entry.fragments.size !== entry.totalFrags) return null;\n                const complete = new Uint8Array(entry.receivedBytes);\n                let offset = 0;\n                for (let i = 0; i < entry.totalFrags; i++) {\n                    const frag = entry.fragments.get(i);\n                    if (!frag) return null;\n                    complete.set(frag, offset);\n                    offset += frag.byteLength;\n                }\n                this.reassembly.delete(key);\n                return complete;\n            }\n            _attach(pid, lid, ch) {\n                ch.binaryType = \'arraybuffer\';\n                ch.onmessage = e => {\n                    const buf = e.data instanceof Blob ? null : e.data;\n                    if (buf instanceof ArrayBuffer) {\n                        const payload = this._decodeFrame(pid, lid, buf);\n                        if (payload) this.forwarder.forward(pid + \':\' + lid, payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength));\n                    } else if (e.data instanceof Blob) {\n                        e.data.arrayBuffer().then(ab => {\n                            const payload = this._decodeFrame(pid, lid, ab);\n                            if (payload) this.forwarder.forward(pid + \':\' + lid, payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength));\n                        });\n                    }\n                };\n                ch.onopen = () => {\n                    this._notify(\'datachannel_open\', { peerId: pid, channelId: lid, label: ch.label });\n                    if (lid === 0) {\n                        this._bindRpcPort(pid, ch, \'channel_open\');\n                    }\n                    this._flushPendingSends(pid, lid);\n                };\n                ch.onclose = () => {\n                    if (lid === 0) {\n                        this._dropRpcPort(pid);\n                    }\n                    this._dropPendingPeerFrames(pid);\n                    this._notify(\'datachannel_close\', { peerId: pid, channelId: lid });\n                };\n                const p = this.peers.get(pid); if (p) p.dataChannels.set(lid, ch);\n            }\n            async _ensureChannels(pid) {\n                const p = this.peers.get(pid); if (!p) throw new Error(\'no peer\');\n                for (const l of this.lanes) { if (p.dataChannels.has(l.id)) continue; const c = p.connection.createDataChannel(l.label, { ordered: l.ordered, maxRetransmits: l.maxRetransmits }); this._attach(pid, l.id, c); }\n            }\n            _close(pid) {\n                const p = this.peers.get(pid); if (!p) return;\n                for (const c of p.dataChannels.values()) c.close(); p.connection.close(); this.peers.delete(pid);\n                this._dropRpcPort(pid);\n                this.pendingSends.delete(pid);\n                this.pendingPortFrames.delete(pid);\n            }\n            _notify(eventType, data) { this.bridge.sendToSW({ type: \'webrtc_event\', payload: { eventType, data } }); }\n            dispose() { for (const pid of this.peers.keys()) this._close(pid); }\n        }\n\n        /* \u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\n           Minimal Protobuf helpers for EchoRequest / EchoResponse\n           EchoRequest  { string message = 1; }\n           EchoResponse { string reply = 1; uint64 timestamp = 2; }\n           \u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550} */\n        function encodeVarint(value) {\n            const bytes = [];\n            while (value > 0x7f) { bytes.push((value & 0x7f) | 0x80); value >>>= 7; }\n            bytes.push(value & 0x7f);\n            return bytes;\n        }\n        function readVarint(data, offset) {\n            let result = 0, shift = 0, bytesRead = 0;\n            while (offset < data.length) {\n                const b = data[offset++]; bytesRead++;\n                result |= (b & 0x7f) << shift;\n                if ((b & 0x80) === 0) break;\n                shift += 7;\n            }\n            return [result >>> 0, bytesRead];\n        }\n        function encodeEchoRequest(message) {\n            const msgBytes = new TextEncoder().encode(message);\n            const header = [0x0a, ...encodeVarint(msgBytes.length)];\n            const buf = new Uint8Array(header.length + msgBytes.length);\n            buf.set(header);\n            buf.set(msgBytes, header.length);\n            return buf;\n        }\n        function decodeEchoResponse(data) {\n            let reply = \'\', timestamp = 0, pos = 0;\n            while (pos < data.length) {\n                const tag = data[pos++];\n                const fieldNumber = tag >>> 3;\n                const wireType = tag & 0x07;\n                if (wireType === 2) {\n                    const [len, br] = readVarint(data, pos); pos += br;\n                    if (fieldNumber === 1) reply = new TextDecoder().decode(data.subarray(pos, pos + len));\n                    pos += len;\n                } else if (wireType === 0) {\n                    const [val, br] = readVarint(data, pos); pos += br;\n                    if (fieldNumber === 2) timestamp = val;\n                } else break;\n            }\n            return { reply, timestamp };\n        }\n\n        /* \u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\n           Actor Host Logic \u{2014} fetch config, start SW, create DOM bridge,\n           then render the unified actor view (send controls + runtime log).\n           Connection role is negotiated per-peer at runtime, so the host\n           page does not care whether this actor typically initiates or\n           accepts calls.\n           \u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550} */\n\n        const statusEl = document.getElementById(\'status\');\n        const logEl = document.getElementById(\'log\');\n        const resultEl = document.getElementById(\'result\');\n        const sendBtn = document.getElementById(\'sendBtn\');\n        const msgInput = document.getElementById(\'msgInput\');\n\n        // Inbound-request stats (incremented whenever this actor handles a peer call)\n        let requestCount = 0, successCount = 0, errorCount = 0;\n\n        // Global bridge ref (used by callRaw)\n        let _bridge = null;\n        let _coordinator = null;\n        let _rpcId = 0;\n        const _pendingRpc = new Map();\n\n        function escapeHtml(s) { return s.replace(/&/g, \'&amp;\').replace(/</g, \'&lt;\').replace(/>/g, \'&gt;\'); }\n        function timeStr() { return new Date().toLocaleTimeString(\'en-US\', { hour12: false, hour: \'2-digit\', minute: \'2-digit\', second: \'2-digit\' }); }\n\n        /** Inbound-side log panel (adds .log-entry elements to #log) */\n        function inboundLog(level, msg) {\n            const d = document.createElement(\'div\');\n            d.className = \'log-entry \' + level;\n            d.innerHTML = \'<span class=\"time\">\' + timeStr() + \'</span>\' + escapeHtml(msg);\n            logEl.appendChild(d); logEl.scrollTop = logEl.scrollHeight;\n            while (logEl.children.length > 200) logEl.removeChild(logEl.firstChild);\n        }\n\n        /** Outbound-side result panel (adds .entry elements to #result) */\n        function outboundLog(level, msg) {\n            const d = document.createElement(\'div\');\n            d.className = \'entry\';\n            d.innerHTML = \'<span class=\"time\">\' + timeStr() + \'</span><span class=\"\' + level + \'\">\' + escapeHtml(msg) + \'</span>\';\n            resultEl.appendChild(d); resultEl.scrollTop = resultEl.scrollHeight;\n            while (resultEl.children.length > 200) resultEl.removeChild(resultEl.firstChild);\n        }\n\n        function shouldMirrorRuntimeLog(msg) {\n            return msg.includes(\'route_candidates\')\n                || msg.includes(\'role_negotiation\')\n                || msg.includes(\'handle_dom_control\')\n                || msg.includes(\'workload_dom_response\')\n                || msg.includes(\'host.call-raw\')\n                || msg.includes(\'State Path: request\')\n                || msg.includes(\'[Scheduler]\')\n                || msg.includes(\'send_channel_data\')\n                || msg.includes(\'register_datachannel_port\')\n                || msg.includes(\'handle_rpc_response\')\n                || msg.includes(\'connection_state_changed\')\n                || msg.includes(\'datachannel_open\')\n                || msg.includes(\'datachannel_close\');\n        }\n\n        /** Generic runtime log (also writes .log-entry to #log, for SW events) */\n        function runtimeLog(level, msg) {\n            const d = document.createElement(\'div\');\n            d.className = \'log-entry \' + level;\n            d.innerHTML = \'<span class=\"time\">\' + timeStr() + \'</span>\' + escapeHtml(msg);\n            logEl.appendChild(d); logEl.scrollTop = logEl.scrollHeight;\n            while (logEl.children.length > 200) logEl.removeChild(logEl.firstChild);\n            if (shouldMirrorRuntimeLog(msg)) console.log(\'[HostRuntime]\', msg);\n        }\n\n        function updateInboundStats() {\n            document.getElementById(\'requestCount\').textContent = String(requestCount);\n            document.getElementById(\'successCount\').textContent = String(successCount);\n            document.getElementById(\'errorCount\').textContent = String(errorCount);\n        }\n\n        function applyInboundStatsFromRuntimeLog(msg) {\n            let changed = false;\n            if (msg.includes(\'[Scheduler] Processing RPC request:\') && msg.includes(\'route_key=echo.EchoService.Echo\')) {\n                requestCount++;\n                changed = true;\n            }\n            if (msg.includes(\'[Scheduler] Service handler success:\')) {\n                successCount++;\n                changed = true;\n            }\n            if (msg.includes(\'[Scheduler] Service handler error:\')) {\n                errorCount++;\n                changed = true;\n            }\n            if (changed) updateInboundStats();\n        }\n\n        /** callRaw \u{2014} send RPC via SW bridge (mirrors @actr/web ActorRef.callRaw) */\n        function callRaw(routeKey, payload, timeout) {\n            timeout = timeout || 30000;\n            if (_coordinator) _coordinator.refreshRpcPorts(\'call_raw\');\n            const requestId = \'req_\' + (++_rpcId) + \'_\' + Date.now();\n            console.log(\'[HostPage] callRaw send request_id=\' + requestId + \' clientId=\' + (_bridge ? _bridge.getClientId() : \'none\') + \' route=\' + routeKey);\n            return new Promise((resolve, reject) => {\n                const timer = setTimeout(() => {\n                    _pendingRpc.delete(requestId);\n                    reject(new Error(\'RPC timeout after \' + timeout + \'ms\'));\n                }, timeout);\n                _pendingRpc.set(requestId, { resolve, reject, timer });\n                _bridge.sendToSW({\n                    type: \'control\',\n                    payload: {\n                        action: \'rpc_call\',\n                        request_id: requestId,\n                        request: { route_key: routeKey, payload: payload, timeout: timeout },\n                    },\n                });\n            });\n        }\n\n        /** Send an outbound echo RPC (available on every actor). */\n        async function doSendEcho() {\n            if (!_bridge) { outboundLog(\'err\', \'\u{274c} Actor is not initialized\'); return; }\n            const message = msgInput.value.trim() || (\'Hello! (\' + new Date().toLocaleTimeString() + \')\');\n            try {\n                sendBtn.disabled = true;\n                outboundLog(\'info\', \'\u{1f4e4} Sending: \"\' + message + \'\"\');\n                const payload = encodeEchoRequest(message);\n                const responseData = await callRaw(\'echo.EchoService.Echo\', payload);\n                const response = decodeEchoResponse(new Uint8Array(responseData));\n                outboundLog(\'ok\', \'\u{1f4e5} Reply: \"\' + response.reply + \'\"\');\n                if (response.timestamp) outboundLog(\'info\', \'\u{23f1}\u{fe0f} Timestamp: \' + new Date(response.timestamp * 1000).toLocaleString());\n            } catch (e) {\n                console.error(\'Echo failed:\', e);\n                outboundLog(\'err\', \'\u{274c} Request failed: \' + e.message);\n            } finally {\n                sendBtn.disabled = false;\n            }\n        }\n\n        /* \u{2500}\u{2500} Main \u{2500}\u{2500} */\n        (async function main() {\n            try {\n                runtimeLog(\'info\', \'Fetching runtime config\u{2026}\');\n                const resp = await fetch(\'/actr-runtime-config.json\');\n                if (!resp.ok) throw new Error(\'Config fetch failed: \' + resp.status);\n                const cfg = await resp.json();\n\n                // Update info grid. Role is a per-peer concept at connect\n                // time, so the host page just shows the actor identity.\n                document.getElementById(\'actrType\').textContent = cfg.package.full_type || \'\u{2014}\';\n                document.getElementById(\'role\').textContent = \'Actor (symmetric)\';\n                document.getElementById(\'sigUrl\').textContent = cfg.signaling_url || \'\u{2014}\';\n                document.getElementById(\'realmId\').textContent = cfg.realm_id || \'\u{2014}\';\n                document.getElementById(\'serviceName\').textContent = cfg.package.full_type ? (\'echo.\' + cfg.package.actr_name || cfg.package.name || cfg.package.full_type) : \'\u{2014}\';\n\n                const swRuntimeConfig = {\n                    ais_endpoint: cfg.ais_endpoint,\n                    signaling_url: cfg.signaling_url,\n                    realm_id: cfg.realm_id,\n                    client_actr_type: cfg.package.full_type,\n                    target_actr_type: (cfg.acl_allow_types && cfg.acl_allow_types[0]) || \'\',\n                    service_fingerprint: \'\',\n                    acl_allow_types: cfg.acl_allow_types || [],\n                    package_url: cfg.package_url,\n                    runtime_wasm_url: cfg.runtime_wasm_url,\n                    trust: cfg.trust || [],\n                };\n\n                const iceServers = [\n                    ...(cfg.stun_urls || []).map(u => ({ urls: u })),\n                    ...(cfg.turn_urls || []).map(u => ({ urls: u })),\n                ];\n\n                runtimeLog(\'info\', \'Registering Service Worker\u{2026}\');\n                const bridge = new ServiceWorkerBridge();\n                _bridge = bridge;\n                await bridge.initialize(\'/actor.sw.js\', swRuntimeConfig);\n                runtimeLog(\'success\', \'Service Worker ready\');\n\n                const forwarder = new FastPathForwarder(bridge);\n                const coordinator = new WebRtcCoordinator(bridge, forwarder, { iceServers, iceTransportPolicy: cfg.force_relay ? \'relay\' : undefined });\n                _coordinator = coordinator;\n\n                // Handle RPC responses for callRaw (client mode)\n                bridge.onMessage(m => {\n                    if (m.type === \'control_response\' && m.payload) {\n                        const { request_id, data, error } = m.payload;\n                        console.log(\'[HostPage] control_response request_id=\' + request_id + \' clientId=\' + bridge.getClientId() + \' error=\' + (error ? (error.code || \'?\') + \':\' + (error.message || \'\') : \'none\'));\n                        const pending = _pendingRpc.get(request_id);\n                        if (pending) {\n                            clearTimeout(pending.timer);\n                            _pendingRpc.delete(request_id);\n                            if (error) pending.reject(new Error(\'RPC error [\' + (error.code || \'?\') + \']: \' + (error.message || \'\')));\n                            else pending.resolve(data || new Uint8Array());\n                        }\n                    }\n                });\n\n                // Listen for SW bridge messages (WebRTC events, sw_log)\n                bridge.onMessage(m => {\n                    if (m.type === \'webrtc_event\' && m.payload && m.payload.eventType === \'sw_log\') {\n                        const d = m.payload.data; runtimeLog(d.level || \'info\', d.message || JSON.stringify(d));\n                    } else if (m.type === \'webrtc_event\' && m.payload) {\n                        runtimeLog(\'info\', \'[WebRTC] \' + m.payload.eventType + \': \' + JSON.stringify(m.payload.data));\n                    }\n                });\n\n                // Listen for broadcast events from SW (echo_event, sw_log).\n                // echo_event is fired whenever this actor handles an inbound\n                // request, independent of whether it also initiates calls.\n                navigator.serviceWorker.addEventListener(\'message\', e => {\n                    if (!e.data || !e.data.type) return;\n                    if (e.data.type === \'sw_log\') {\n                        let msg = e.data.message || \'\';\n                        msg = msg.replace(/^\\s*INFO\\s+/, \'\');\n                        msg = msg.replace(/^\\s*WARN\\s+/, \'\u{26a0}\u{fe0f} \');\n                        msg = msg.replace(/^\\s*ERROR\\s+/, \'\u{274c} \');\n                        applyInboundStatsFromRuntimeLog(msg);\n                        runtimeLog(e.data.level || \'info\', msg);\n                    } else if (e.data.type === \'echo_event\') {\n                        switch (e.data.event) {\n                            case \'request\':\n                                requestCount++;\n                                inboundLog(\'info\', \'\u{1f4e8} Received Echo request: \"\' + (e.data.detail || \'\') + \'\"\');\n                                break;\n                            case \'response\':\n                                successCount++;\n                                inboundLog(\'success\', \'\u{2705} Sent Echo response: \"\' + (e.data.detail || \'\') + \'\"\');\n                                break;\n                            case \'error\':\n                                errorCount++;\n                                inboundLog(\'error\', \'\u{274c} Handling error: \' + (e.data.detail || \'\'));\n                                break;\n                        }\n                        updateInboundStats();\n                    }\n                });\n\n                // Unified actor status: every actor is ready both to accept\n                // inbound calls and to initiate outbound ones.\n                statusEl.innerHTML = \'<span>\u{2705}</span><span>Actor ready</span>\';\n                statusEl.className = \'status ready\';\n                sendBtn.disabled = false;\n                inboundLog(\'success\', \'\u{2705} Actor started \u{2014} inbound handlers registered\');\n                outboundLog(\'ok\', \'\u{2705} Actor initialized \u{2014} outbound send available\');\n\n                runtimeLog(\'success\', \'Actor initialized (\' + (cfg.package.full_type || \'unknown\') + \')\');\n\n                // Wire up the outbound send button (always available).\n                sendBtn.addEventListener(\'click\', doSendEcho);\n                msgInput.addEventListener(\'keydown\', e => { if (e.key === \'Enter\' && !sendBtn.disabled) doSendEcho(); });\n\n                // Keep-alive\n                setInterval(() => { if (navigator.serviceWorker.controller) navigator.serviceWorker.controller.postMessage({ type: \'PING\' }); }, 20000);\n\n                // Cleanup on page unload\n                window.addEventListener(\'beforeunload\', () => { bridge.close(); });\n\n            } catch (e) {\n                statusEl.textContent = \'\u{274c} \' + e.message;\n                statusEl.className = \'status error\';\n                runtimeLog(\'error\', String(e));\n                console.error(e);\n            }\n        })();\n    </script>\n</body>\n\n</html>\n";
Expand description

Self-contained HTML host page with inline @actr/dom (WebRTC coordinator).