12<html lang="en">3<head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>Portal</title>7 <link rel="stylesheet" href="./style.css" />8</head>9<body>10 <div class="overlay-screen hidden" id="enrollment-overlay" role="dialog" aria-modal="true" aria-labelledby="enrollment-title">11 <div class="overlay-content enrollment-content">12 <h1 id="enrollment-title">mark this portal</h1>13 <p id="enrollment-desc">Use the security key once to reveal this Portal's peer ID, then create a one-time invitation on the Sigil host.</p>14 <div class="overlay-form" id="enrollment-pin-form">15 <input class="overlay-input" id="enrollment-pin" data-controller-focus data-pin-target="enrollment-pin" type="password" inputmode="numeric" maxlength="64" placeholder="PIN" autocomplete="off" />16 <button class="overlay-btn controller-pin-btn" id="controller-pin-enrollment" data-controller-focus onclick="openPinPad('enrollment-pin', this)">D-pad PIN</button>17 </div>18 <div class="overlay-actions">19 <button class="overlay-btn primary" id="derive-portal-id" data-controller-focus onclick="derivePortalIdentity()">show portal id</button>20 </div>21 <div class="enrollment-peer hidden" id="enrollment-peer-wrap">22 <span>portal peer</span>23 <output id="enrollment-peer-id"></output>24 </div>25 <div class="overlay-actions">26 <button class="overlay-btn" id="choose-invitation" data-controller-focus onclick="chooseInvitationFile()">open invitation</button>27 </div>28 <div class="overlay-status" id="enrollment-status"></div>29 <p class="controller-hint">D-pad / left stick to move · A select · B close PIN pad</p>30 </div>31 </div>3233 <div class="overlay-screen hidden" id="invitation-overlay" role="dialog" aria-modal="true" aria-labelledby="invitation-title">34 <div class="overlay-content invitation-content">35 <h1 id="invitation-title">open this rift?</h1>36 <p>Confirm the exact host, Portal peer, and controls before enrolling.</p>37 <dl class="invitation-summary">38 <div><dt>sigil</dt><dd id="invitation-host"></dd></div>39 <div><dt>portal</dt><dd id="invitation-peer"></dd></div>40 <div><dt>expires</dt><dd id="invitation-expiry"></dd></div>41 <div><dt>grants</dt><dd id="invitation-grants"></dd></div>42 </dl>43 <div class="overlay-actions">44 <button class="overlay-btn primary" id="confirm-invitation" data-controller-focus onclick="confirmInvitation()">enroll</button>45 <button class="overlay-btn" data-controller-focus onclick="cancelInvitation()">cancel</button>46 </div>47 <div class="overlay-status" id="invitation-status"></div>48 </div>49 </div>5051 <div class="overlay-screen hidden" id="reset-enrollment-overlay" role="alertdialog" aria-modal="true" aria-labelledby="reset-enrollment-title">52 <div class="overlay-content invitation-content">53 <h1 id="reset-enrollment-title">erase this portal mark?</h1>54 <p>Only continue after revoking enrollment on Sigil. Portal will forget this host and require a new invitation.</p>55 <div class="enrollment-peer">56 <span>sigil</span>57 <output id="reset-enrollment-host"></output>58 </div>59 <div class="overlay-actions">60 <button class="overlay-btn danger" id="confirm-reset-enrollment" data-controller-focus onclick="confirmEnrollmentReset()">erase enrollment</button>61 <button class="overlay-btn" data-controller-focus onclick="cancelEnrollmentReset()">keep enrollment</button>62 </div>63 <div class="overlay-status" id="reset-enrollment-status"></div>64 </div>65 </div>6667 <div class="overlay-screen hidden" id="tap-overlay" style="z-index: 200;">68 <div class="overlay-content">69 <div class="flow-steps" aria-hidden="true">70 <span class="flow-step">01 pin</span>71 <span class="flow-step active">02 tap</span>72 <span class="flow-step">03 play</span>73 </div>74 <svg class="pixel-key" viewBox="0 0 26 12" width="78" height="36" aria-hidden="true">75 <rect x="0" y="1" width="8" height="10" fill="var(--terracotta)" />76 <rect x="3" y="5" width="2" height="2" fill="var(--paper)" />77 <rect x="8" y="5" width="18" height="2" fill="var(--ochre)" />78 <rect x="14" y="7" width="2" height="3" fill="var(--moss)" />79 <rect x="19" y="7" width="2" height="2" fill="var(--moss)" />80 </svg>81 <h1 id="tap-overlay-title">tap key</h1>82 <p id="tap-overlay-desc">Touch the sensor on your key when it blinks.</p>83 <div class="overlay-status pending" id="tap-status">waiting...</div>84 </div>85 </div>8687 <div class="overlay-screen hidden" id="pin-pad-overlay" role="dialog" aria-modal="true" aria-labelledby="pin-pad-title">88 <div class="overlay-content pin-pad-content">89 <h1 id="pin-pad-title">enter pin</h1>90 <div class="pin-pad-display" id="pin-pad-display" aria-live="polite">empty</div>91 <p class="controller-hint">D-pad / left stick to move · A select · B close</p>92 <div class="pin-pad-grid">93 <button class="pin-pad-key" data-controller-focus data-pin-digit="1" onclick="pinPadDigit('1')">1</button>94 <button class="pin-pad-key" data-controller-focus data-pin-digit="2" onclick="pinPadDigit('2')">2</button>95 <button class="pin-pad-key" data-controller-focus data-pin-digit="3" onclick="pinPadDigit('3')">3</button>96 <button class="pin-pad-key" data-controller-focus data-pin-digit="4" onclick="pinPadDigit('4')">4</button>97 <button class="pin-pad-key" data-controller-focus data-pin-digit="5" onclick="pinPadDigit('5')">5</button>98 <button class="pin-pad-key" data-controller-focus data-pin-digit="6" onclick="pinPadDigit('6')">6</button>99 <button class="pin-pad-key" data-controller-focus data-pin-digit="7" onclick="pinPadDigit('7')">7</button>100 <button class="pin-pad-key" data-controller-focus data-pin-digit="8" onclick="pinPadDigit('8')">8</button>101 <button class="pin-pad-key" data-controller-focus data-pin-digit="9" onclick="pinPadDigit('9')">9</button>102 <button class="pin-pad-key pin-pad-action" data-controller-focus onclick="pinPadBackspace()" aria-label="Backspace">del</button>103 <button class="pin-pad-key" data-controller-focus data-pin-digit="0" onclick="pinPadDigit('0')">0</button>104 <button class="pin-pad-key pin-pad-action" data-controller-focus onclick="closePinPad()">done</button>105 </div>106 <button class="overlay-btn pin-pad-clear" data-controller-focus onclick="pinPadClear()">clear</button>107 </div>108 </div>109110 <div class="overlay-screen hidden" id="intro">111 <div class="overlay-content">112 <div class="flow-steps" aria-hidden="true">113 <span class="flow-step active">01 pin</span>114 <span class="flow-step">02 tap</span>115 <span class="flow-step">03 play</span>116 </div>117 <h1>portal</h1>118 <p id="intro-desc">enter the Portal security-key pin</p>119 <div class="dev-warning hidden" id="dev-connect-warning" role="alert">120 DEVELOPMENT DIRECT NODE<br />PASSKEY LOOKUP BYPASSED · NOT CLIENT AUTHORIZATION121 </div>122 <div class="overlay-form" id="intro-pin-form">123 <input class="overlay-input" id="intro-pin" data-controller-focus data-pin-target="intro-pin" type="password" inputmode="numeric" maxlength="64" placeholder="PIN" autocomplete="off" />124 <button class="overlay-btn controller-pin-btn" id="controller-pin-intro" data-controller-focus onclick="openPinPad('intro-pin', this)">D-pad PIN</button>125 </div>126 <div class="overlay-actions">127 <button class="overlay-btn primary" id="intro-connect" data-controller-focus onclick="introConnect()">connect</button>128 <button class="overlay-btn danger hidden" id="reset-enrollment-intro" data-controller-focus onclick="openEnrollmentReset()">reset enrollment</button>129 </div>130 <div class="overlay-status" id="intro-status"></div>131 <p class="controller-hint" id="intro-controller-hint">Controller optional · keyboard and mouse remain available</p>132 </div>133 </div>134135 <div class="topbar">136 <span class="brand">portal</span>137 <span class="dev-badge hidden" id="dev-connect-badge" title="Skips passkey identity lookup. This is not client authorization.">dev direct-node · not auth</span>138 <span class="sep">·</span>139 <div class="status-line" id="status-line">140 <span class="dot" id="status-dot"></span>141 <span id="status-text">idle</span>142 </div>143 <div class="topbar-right">144 <span class="controller-status" id="controller-status">controller: none</span>145 <span class="node-id" id="node-id-text"></span>146 <button class="link-btn" data-controller-focus onclick="togglePanel(true)">info</button>147 </div>148 </div>149150 <main class="main">151 <canvas id="frame-canvas"></canvas>152 <svg id="remote-pointer" class="remote-pointer" viewBox="0 0 24 32" aria-hidden="true">153 <path d="M2 1.5v25l6.2-6.1 4.2 9.7 4.4-1.9-4.2-9.5h8.8z" />154 </svg>155 <div class="placeholder" id="placeholder">no stream</div>156 </main>157158 <div class="bottombar">159 <div class="bottombar-start">160 <button class="action danger hidden" id="action-disconnect" data-controller-focus onclick="disconnect()">disconnect</button>161 </div>162 <div class="control-bar" id="control-bar">163 <button class="control-toggle" id="control-toggle" data-controller-focus onclick="toggleControl()">164 view only · input unavailable165 </button>166 <button type="button" class="control-toggle disabled" id="audio-toggle" data-controller-focus onclick="toggleAudioMute()" aria-disabled="true" aria-label="Audio unavailable. Not connected" title="Audio unavailable: not connected">167 vol off168 </button>169 </div>170 <div class="bottombar-end">171 <span class="pin-label" id="pin-label">pin</span>172 <input class="pin-input" id="pin-input" data-controller-focus data-pin-target="pin-input" type="password" inputmode="numeric" maxlength="64" placeholder="····" autocomplete="off" />173 <button class="action" id="controller-pin-main" data-controller-focus onclick="openPinPad('pin-input', this)">D-pad PIN</button>174 <span class="sep">·</span>175 <button class="action primary" id="action-connect" data-controller-focus onclick="connectHost()">connect</button>176 </div>177 </div>178179 <div class="panel-overlay" id="panel-overlay" onclick="togglePanel(false)"></div>180 <aside class="panel" id="panel" aria-label="Connection information">181 <div class="panel-header">182 <h2>client</h2>183 <button class="link-btn" id="panel-close" data-controller-focus onclick="togglePanel(false)">close</button>184 </div>185 <div class="panel-body">186 <div class="panel-section">187 <h3><span class="hash" aria-hidden="true">#</span> security key</h3>188 <div class="info-row"><span class="info-label">status</span><span class="info-value" id="fido-status">—</span></div>189 <div class="info-row"><span class="info-label">product</span><span class="info-value" id="fido-product">—</span></div>190 <div class="info-row"><span class="info-label">vid:pid</span><span class="info-value" id="fido-vidpid">—</span></div>191 <div class="info-row"><span class="info-label">ctap</span><span class="info-value" id="fido-versions">—</span></div>192 <div class="info-row"><span class="info-label">extensions</span><span class="info-value" id="fido-extensions">—</span></div>193 <div class="info-row"><span class="info-label">pin retries</span><span class="info-value" id="fido-pin">—</span></div>194 <button class="link-btn panel-action" data-controller-focus onclick="scanFido()">rescan</button>195 <button class="link-btn panel-action hidden" id="reset-enrollment" data-controller-focus onclick="openEnrollmentReset()">reset enrollment</button>196 </div>197 <div class="panel-section" id="stream-section">198 <h3><span class="hash" aria-hidden="true">#</span> stream</h3>199 <div class="info-row"><span class="info-label">codec</span><span class="info-value" id="stream-codec">—</span></div>200 <div class="info-row"><span class="info-label">transport</span><span class="info-value" id="stream-transport">—</span></div>201 <div class="info-row"><span class="info-label">path</span><span class="info-value" id="stream-path">—</span></div>202 <div class="info-row"><span class="info-label">RTT</span><span class="info-value" id="stream-rtt">— ms</span></div>203 <div class="info-row"><span class="info-label">network snapshot</span><span class="info-value" id="stream-network-session">unavailable</span></div>204 <div class="info-row metric-stack"><span class="info-label">media network</span><span class="info-value" id="stream-network-media">unavailable</span></div>205 <div class="info-row metric-stack"><span class="info-label">input network</span><span class="info-value" id="stream-network-input">unavailable</span></div>206 <div class="info-row metric-stack"><span class="info-label">audio network</span><span class="info-value" id="stream-network-audio">unavailable</span></div>207 <div class="info-row metric-stack"><span class="info-label">input ACK</span><span class="info-value" id="stream-network-input-ack">unavailable</span></div>208 <div class="info-row metric-stack"><span class="info-label">adaptive feedback</span><span class="info-value" id="stream-adaptive-feedback">unavailable</span></div>209 <div class="info-row metric-stack"><span class="info-label">adaptive decision</span><span class="info-value" id="stream-adaptive-decision">unavailable</span></div>210 <div class="info-row"><span class="info-label">control</span><span class="info-value" id="stream-control">—</span></div>211 <div class="info-row"><span class="info-label">audio</span><span class="info-value" id="stream-audio-state">unavailable</span></div>212 <div class="info-row"><span class="info-label">audio webview received</span><span class="info-value" id="stream-audio-webview-received-packets">0 packets</span></div>213 <div class="info-row"><span class="info-label">audio transport missing</span><span class="info-value" id="stream-audio-transport-missing-packets">0 packets</span></div>214 <div class="info-row"><span class="info-label">audio IPC dropped</span><span class="info-value" id="stream-audio-ipc-dropped-packets">0 packets</span></div>215 <div class="info-row"><span class="info-label">audio decoder dropped</span><span class="info-value" id="stream-audio-decoder-dropped-packets">0 packets</span></div>216 <div class="info-row"><span class="info-label">audio PCM handoff dropped</span><span class="info-value" id="stream-audio-pcm-handoff-dropped-blocks">0 blocks</span></div>217 <div class="info-row"><span class="info-label">audio ring overflow</span><span class="info-value" id="stream-audio-ring-overflow-ms">0.0 ms</span></div>218 <div class="info-row"><span class="info-label">audio ring recovery discarded</span><span class="info-value" id="stream-audio-ring-recovery-discarded-ms">0.0 ms</span></div>219 <div class="info-row"><span class="info-label">audio ring buffer</span><span class="info-value" id="stream-audio-ring-buffer-ms">0.0 ms</span></div>220 <div class="info-row"><span class="info-label">audio underruns</span><span class="info-value" id="stream-audio-underruns">0 events / 0.0 ms</span></div>221 <div class="info-row"><span class="info-label">audio output silence</span><span class="info-value" id="stream-audio-silence-ms">0.0 ms</span></div>222 <div class="info-row"><span class="info-label">transport fps</span><span class="info-value" id="stream-transport-fps">0.0</span></div>223 <div class="info-row"><span class="info-label">IPC send fps</span><span class="info-value" id="stream-ipc-send-fps">0.0</span></div>224 <div class="info-row metric-stack"><span class="info-label">transport interval p50/p95/max</span><span class="info-value" id="stream-transport-interval">—</span></div>225 <div class="info-row metric-stack"><span class="info-label">IPC send duration p50/p95/max</span><span class="info-value" id="stream-ipc-send-duration">—</span></div>226 <div class="info-row"><span class="info-label">Rust timing window/cap</span><span class="info-value" id="stream-rust-timing-window">—</span></div>227 <div class="info-row metric-stack"><span class="info-label">IPC frame queue</span><span class="info-value" id="stream-frontend-queue">—</span></div>228 <div class="info-row metric-stack"><span class="info-label">IPC resync</span><span class="info-value" id="stream-frontend-resync">—</span></div>229 <div class="info-row"><span class="info-label">webview fps</span><span class="info-value" id="stream-frontend-fps">0.0</span></div>230 <div class="info-row"><span class="info-label">decode fps</span><span class="info-value" id="stream-decode-fps">0.0</span></div>231 <div class="info-row"><span class="info-label">decoder output fps</span><span class="info-value" id="stream-decoder-output-fps">0.0</span></div>232 <div class="info-row"><span class="info-label">present fps</span><span class="info-value" id="stream-present-fps">0.0</span></div>233 <div class="info-row metric-stack"><span class="info-label">A−V skew p50/p95/max absolute</span><span class="info-value" id="stream-av-skew">—</span></div>234 <div class="info-row"><span class="info-label">decode queue</span><span class="info-value" id="stream-decode-queue">0</span></div>235 <div class="info-row"><span class="info-label">present queue</span><span class="info-value" id="stream-present-queue">0</span></div>236 <div class="info-row"><span class="info-label">decode p50 / p95</span><span class="info-value" id="stream-decode-latency">—</span></div>237 <div class="info-row"><span class="info-label">client present p50 / p95</span><span class="info-value" id="stream-present-latency">—</span></div>238 <div class="info-row"><span class="info-label">draw p50/p95/p99/max</span><span class="info-value" id="stream-draw-latency">—</span></div>239 <div class="info-row metric-stack"><span class="info-label">delivery interval p50/p95/p99/max</span><span class="info-value" id="stream-delivery-cadence">—</span></div>240 <div class="info-row metric-stack"><span class="info-label">decoder input interval p50/p95/p99/max</span><span class="info-value" id="stream-decoder-input-cadence">—</span></div>241 <div class="info-row metric-stack"><span class="info-label">decoder output interval p50/p95/p99/max</span><span class="info-value" id="stream-decoder-output-cadence">—</span></div>242 <div class="info-row metric-stack"><span class="info-label">present interval p50/p95/p99/max</span><span class="info-value" id="stream-present-cadence">—</span></div>243 <div class="info-row"><span class="info-label">total discarded</span><span class="info-value" id="stream-dropped">0 frames</span></div>244 <div class="info-row"><span class="info-label">transport sequence gap</span><span class="info-value" id="stream-transport-dropped">0 frames</span></div>245 <div class="info-row"><span class="info-label">transport object discarded</span><span class="info-value" id="stream-transport-object-dropped">—</span></div>246 <div class="info-row"><span class="info-label">late object discarded</span><span class="info-value" id="stream-transport-late-object-dropped">—</span></div>247 <div class="info-row"><span class="info-label">IPC discarded</span><span class="info-value" id="stream-frontend-dropped">0 frames</span></div>248 <div class="info-row"><span class="info-label">IPC queue discarded</span><span class="info-value" id="stream-frontend-queue-dropped">—</span></div>249 <div class="info-row"><span class="info-label">IPC resync discarded</span><span class="info-value" id="stream-frontend-resync-dropped">—</span></div>250 <div class="info-row"><span class="info-label">decoder discarded</span><span class="info-value" id="stream-decoder-dropped">0 frames</span></div>251 <div class="info-row"><span class="info-label">presenter overwrite</span><span class="info-value" id="stream-present-dropped">0 frames</span></div>252 <div class="info-row"><span class="info-label">received</span><span class="info-value" id="stream-received">0</span></div>253 <div class="info-row"><span class="info-label">decoder input</span><span class="info-value" id="stream-decoder-input">0</span></div>254 <div class="info-row"><span class="info-label">decoder output</span><span class="info-value" id="stream-decoder-output">0</span></div>255 <div class="info-row"><span class="info-label">presented</span><span class="info-value" id="stream-presented">0</span></div>256 </div>257 </div>258 </aside>259260 <script type="module" src="./main.js"></script>261</body>262</html>