detached f20ef21

fix(webui): restore webrtc diagnostics page build

commit
f20ef211e0de86178d00a5c2f79e6757dc20cc16
author
brice · · 2026-02-08 12:50 UTC
parent
c75e65d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +175 -1
M webui/package.json +3 -1
@@ -4,14 +4,16 @@
44 "private": true,
55 "type": "module",
66 "scripts": {
7- "build": "VITE_PAGE=homepage vite build && VITE_PAGE=error vite build && VITE_PAGE=config vite build",
7+ "build": "VITE_PAGE=homepage vite build && VITE_PAGE=error vite build && VITE_PAGE=config vite build && VITE_PAGE=webrtc vite build",
88 "build:homepage": "VITE_PAGE=homepage vite build",
99 "build:error": "VITE_PAGE=error vite build",
1010 "build:config": "VITE_PAGE=config vite build",
11+ "build:webrtc": "VITE_PAGE=webrtc vite build",
1112 "dev": "VITE_PAGE=homepage vite build --watch",
1213 "dev:homepage": "VITE_PAGE=homepage vite build --watch",
1314 "dev:error": "VITE_PAGE=error vite build --watch",
1415 "dev:config": "VITE_PAGE=config vite build --watch",
16+ "dev:webrtc": "VITE_PAGE=webrtc vite build --watch",
1517 "check": "svelte-check",
1618 "lint": "eslint src --fix",
1719 "fmt": "prettier --write ."
@@ -4,14 +4,16 @@
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "scripts": {6 "scripts": {
7- "build": "VITE_PAGE=homepage vite build && VITE_PAGE=error vite build && VITE_PAGE=config vite build",7+ "build": "VITE_PAGE=homepage vite build && VITE_PAGE=error vite build && VITE_PAGE=config vite build && VITE_PAGE=webrtc vite build",
8 "build:homepage": "VITE_PAGE=homepage vite build",8 "build:homepage": "VITE_PAGE=homepage vite build",
9 "build:error": "VITE_PAGE=error vite build",9 "build:error": "VITE_PAGE=error vite build",
10 "build:config": "VITE_PAGE=config vite build",10 "build:config": "VITE_PAGE=config vite build",
11+ "build:webrtc": "VITE_PAGE=webrtc vite build",
11 "dev": "VITE_PAGE=homepage vite build --watch",12 "dev": "VITE_PAGE=homepage vite build --watch",
12 "dev:homepage": "VITE_PAGE=homepage vite build --watch",13 "dev:homepage": "VITE_PAGE=homepage vite build --watch",
13 "dev:error": "VITE_PAGE=error vite build --watch",14 "dev:error": "VITE_PAGE=error vite build --watch",
14 "dev:config": "VITE_PAGE=config vite build --watch",15 "dev:config": "VITE_PAGE=config vite build --watch",
16+ "dev:webrtc": "VITE_PAGE=webrtc vite build --watch",
15 "check": "svelte-check",17 "check": "svelte-check",
16 "lint": "eslint src --fix",18 "lint": "eslint src --fix",
17 "fmt": "prettier --write ."19 "fmt": "prettier --write ."
A webui/src/pages/webrtc.ts +160 -0
@@ -0,0 +1,160 @@
1+import "../styles/app.css";
2+
3+const activeStreams: MediaStream[] = [];
4+
5+function now(): string {
6+ return new Date().toLocaleTimeString();
7+}
8+
9+function appendLog(output: HTMLPreElement, message: string): void {
10+ output.textContent = `${output.textContent}[${now()}] ${message}\n`;
11+ output.scrollTop = output.scrollHeight;
12+}
13+
14+function setStatus(status: HTMLElement, value: string): void {
15+ status.textContent = value;
16+}
17+
18+function stopAllStreams(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): void {
19+ for (const stream of activeStreams.splice(0)) {
20+ for (const track of stream.getTracks()) {
21+ track.stop();
22+ }
23+ }
24+
25+ video.pause();
26+ video.srcObject = null;
27+ setStatus(status, "idle");
28+ appendLog(output, "Stopped all active tracks");
29+}
30+
31+async function probeDevices(output: HTMLPreElement): Promise<void> {
32+ if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.enumerateDevices !== "function") {
33+ appendLog(output, "enumerateDevices is unavailable");
34+ return;
35+ }
36+
37+ const devices = await navigator.mediaDevices.enumerateDevices();
38+ const summary = devices
39+ .map((device) => `${device.kind} ${device.label || "(label hidden until permission)"}`)
40+ .join(" | ");
41+
42+ appendLog(output, `Devices: ${summary || "none"}`);
43+}
44+
45+async function requestUserMedia(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): Promise<void> {
46+ if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.getUserMedia !== "function") {
47+ appendLog(output, "getUserMedia is unavailable");
48+ return;
49+ }
50+
51+ try {
52+ setStatus(status, "requesting camera + microphone");
53+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
54+ activeStreams.push(stream);
55+ video.srcObject = stream;
56+ await video.play();
57+ setStatus(status, "camera + microphone granted");
58+ appendLog(output, "camera + microphone permission granted");
59+ await probeDevices(output);
60+ } catch (err) {
61+ setStatus(status, "camera + microphone blocked");
62+ appendLog(output, `camera + microphone request failed: ${String(err)}`);
63+ }
64+}
65+
66+async function requestDisplayMedia(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): Promise<void> {
67+ if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.getDisplayMedia !== "function") {
68+ appendLog(output, "getDisplayMedia is unavailable");
69+ return;
70+ }
71+
72+ try {
73+ setStatus(status, "requesting screen capture");
74+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
75+ activeStreams.push(stream);
76+ video.srcObject = stream;
77+ await video.play();
78+ setStatus(status, "screen capture granted");
79+ appendLog(output, "screen capture permission granted");
80+ } catch (err) {
81+ setStatus(status, "screen capture blocked");
82+ appendLog(output, `screen capture request failed: ${String(err)}`);
83+ }
84+}
85+
86+function mount(): void {
87+ document.title = "WebRTC Diagnostics";
88+ document.body.innerHTML = `
89+ <main class="page-shell" style="padding: 24px; max-width: 920px; margin: 0 auto; display: grid; gap: 16px;">
90+ <header style="display: grid; gap: 8px;">
91+ <h1 style="margin: 0; font-size: 1.8rem;">WebRTC Diagnostics</h1>
92+ <p style="margin: 0; opacity: 0.85;">Use this page to validate camera, microphone, and screen permission behavior.</p>
93+ </header>
94+
95+ <section style="display: flex; flex-wrap: wrap; gap: 10px;">
96+ <button id="btn-av" type="button">Request camera + microphone</button>
97+ <button id="btn-screen" type="button">Request screen capture</button>
98+ <button id="btn-stop" type="button">Stop all streams</button>
99+ </section>
100+
101+ <section style="display: grid; gap: 8px;">
102+ <strong>Status: <span id="status">idle</span></strong>
103+ <video id="preview" autoplay playsinline muted style="width: 100%; max-height: 320px; border-radius: 10px; background: #0c1522;"></video>
104+ </section>
105+
106+ <section style="display: grid; gap: 8px;">
107+ <strong>Event Log</strong>
108+ <pre id="output" style="margin: 0; padding: 12px; border-radius: 10px; background: #0d1727; color: #dbe8f4; min-height: 180px; max-height: 280px; overflow: auto;">[${now()}] Ready\n</pre>
109+ </section>
110+ </main>
111+ `;
112+
113+ const status = document.getElementById("status");
114+ const preview = document.getElementById("preview");
115+ const output = document.getElementById("output");
116+ const btnAV = document.getElementById("btn-av");
117+ const btnScreen = document.getElementById("btn-screen");
118+ const btnStop = document.getElementById("btn-stop");
119+
120+ if (!(status instanceof HTMLElement)) {
121+ return;
122+ }
123+ if (!(preview instanceof HTMLVideoElement)) {
124+ return;
125+ }
126+ if (!(output instanceof HTMLPreElement)) {
127+ return;
128+ }
129+ if (!(btnAV instanceof HTMLButtonElement)) {
130+ return;
131+ }
132+ if (!(btnScreen instanceof HTMLButtonElement)) {
133+ return;
134+ }
135+ if (!(btnStop instanceof HTMLButtonElement)) {
136+ return;
137+ }
138+
139+ btnAV.addEventListener("click", () => {
140+ requestUserMedia(preview, output, status).catch((err: unknown) => {
141+ appendLog(output, `unexpected user media error: ${String(err)}`);
142+ });
143+ });
144+
145+ btnScreen.addEventListener("click", () => {
146+ requestDisplayMedia(preview, output, status).catch((err: unknown) => {
147+ appendLog(output, `unexpected display media error: ${String(err)}`);
148+ });
149+ });
150+
151+ btnStop.addEventListener("click", () => {
152+ stopAllStreams(preview, output, status);
153+ });
154+
155+ window.addEventListener("beforeunload", () => {
156+ stopAllStreams(preview, output, status);
157+ });
158+}
159+
160+mount();
@@ -0,0 +1,160 @@
1+import "../styles/app.css";
2+
3+const activeStreams: MediaStream[] = [];
4+
5+function now(): string {
6+ return new Date().toLocaleTimeString();
7+}
8+
9+function appendLog(output: HTMLPreElement, message: string): void {
10+ output.textContent = `${output.textContent}[${now()}] ${message}\n`;
11+ output.scrollTop = output.scrollHeight;
12+}
13+
14+function setStatus(status: HTMLElement, value: string): void {
15+ status.textContent = value;
16+}
17+
18+function stopAllStreams(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): void {
19+ for (const stream of activeStreams.splice(0)) {
20+ for (const track of stream.getTracks()) {
21+ track.stop();
22+ }
23+ }
24+
25+ video.pause();
26+ video.srcObject = null;
27+ setStatus(status, "idle");
28+ appendLog(output, "Stopped all active tracks");
29+}
30+
31+async function probeDevices(output: HTMLPreElement): Promise<void> {
32+ if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.enumerateDevices !== "function") {
33+ appendLog(output, "enumerateDevices is unavailable");
34+ return;
35+ }
36+
37+ const devices = await navigator.mediaDevices.enumerateDevices();
38+ const summary = devices
39+ .map((device) => `${device.kind} ${device.label || "(label hidden until permission)"}`)
40+ .join(" | ");
41+
42+ appendLog(output, `Devices: ${summary || "none"}`);
43+}
44+
45+async function requestUserMedia(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): Promise<void> {
46+ if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.getUserMedia !== "function") {
47+ appendLog(output, "getUserMedia is unavailable");
48+ return;
49+ }
50+
51+ try {
52+ setStatus(status, "requesting camera + microphone");
53+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
54+ activeStreams.push(stream);
55+ video.srcObject = stream;
56+ await video.play();
57+ setStatus(status, "camera + microphone granted");
58+ appendLog(output, "camera + microphone permission granted");
59+ await probeDevices(output);
60+ } catch (err) {
61+ setStatus(status, "camera + microphone blocked");
62+ appendLog(output, `camera + microphone request failed: ${String(err)}`);
63+ }
64+}
65+
66+async function requestDisplayMedia(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): Promise<void> {
67+ if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.getDisplayMedia !== "function") {
68+ appendLog(output, "getDisplayMedia is unavailable");
69+ return;
70+ }
71+
72+ try {
73+ setStatus(status, "requesting screen capture");
74+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
75+ activeStreams.push(stream);
76+ video.srcObject = stream;
77+ await video.play();
78+ setStatus(status, "screen capture granted");
79+ appendLog(output, "screen capture permission granted");
80+ } catch (err) {
81+ setStatus(status, "screen capture blocked");
82+ appendLog(output, `screen capture request failed: ${String(err)}`);
83+ }
84+}
85+
86+function mount(): void {
87+ document.title = "WebRTC Diagnostics";
88+ document.body.innerHTML = `
89+ <main class="page-shell" style="padding: 24px; max-width: 920px; margin: 0 auto; display: grid; gap: 16px;">
90+ <header style="display: grid; gap: 8px;">
91+ <h1 style="margin: 0; font-size: 1.8rem;">WebRTC Diagnostics</h1>
92+ <p style="margin: 0; opacity: 0.85;">Use this page to validate camera, microphone, and screen permission behavior.</p>
93+ </header>
94+
95+ <section style="display: flex; flex-wrap: wrap; gap: 10px;">
96+ <button id="btn-av" type="button">Request camera + microphone</button>
97+ <button id="btn-screen" type="button">Request screen capture</button>
98+ <button id="btn-stop" type="button">Stop all streams</button>
99+ </section>
100+
101+ <section style="display: grid; gap: 8px;">
102+ <strong>Status: <span id="status">idle</span></strong>
103+ <video id="preview" autoplay playsinline muted style="width: 100%; max-height: 320px; border-radius: 10px; background: #0c1522;"></video>
104+ </section>
105+
106+ <section style="display: grid; gap: 8px;">
107+ <strong>Event Log</strong>
108+ <pre id="output" style="margin: 0; padding: 12px; border-radius: 10px; background: #0d1727; color: #dbe8f4; min-height: 180px; max-height: 280px; overflow: auto;">[${now()}] Ready\n</pre>
109+ </section>
110+ </main>
111+ `;
112+
113+ const status = document.getElementById("status");
114+ const preview = document.getElementById("preview");
115+ const output = document.getElementById("output");
116+ const btnAV = document.getElementById("btn-av");
117+ const btnScreen = document.getElementById("btn-screen");
118+ const btnStop = document.getElementById("btn-stop");
119+
120+ if (!(status instanceof HTMLElement)) {
121+ return;
122+ }
123+ if (!(preview instanceof HTMLVideoElement)) {
124+ return;
125+ }
126+ if (!(output instanceof HTMLPreElement)) {
127+ return;
128+ }
129+ if (!(btnAV instanceof HTMLButtonElement)) {
130+ return;
131+ }
132+ if (!(btnScreen instanceof HTMLButtonElement)) {
133+ return;
134+ }
135+ if (!(btnStop instanceof HTMLButtonElement)) {
136+ return;
137+ }
138+
139+ btnAV.addEventListener("click", () => {
140+ requestUserMedia(preview, output, status).catch((err: unknown) => {
141+ appendLog(output, `unexpected user media error: ${String(err)}`);
142+ });
143+ });
144+
145+ btnScreen.addEventListener("click", () => {
146+ requestDisplayMedia(preview, output, status).catch((err: unknown) => {
147+ appendLog(output, `unexpected display media error: ${String(err)}`);
148+ });
149+ });
150+
151+ btnStop.addEventListener("click", () => {
152+ stopAllStreams(preview, output, status);
153+ });
154+
155+ window.addEventListener("beforeunload", () => {
156+ stopAllStreams(preview, output, status);
157+ });
158+}
159+
160+mount();
M webui/vite.config.ts +12 -0
@@ -42,6 +42,18 @@ const pages = {
4242 filename: "config.html",
4343 },
4444 },
45+ webrtc: {
46+ entry: "src/pages/webrtc.ts",
47+ output: "webrtc.min.js",
48+ global: "DumberWebRTC",
49+ html: {
50+ name: "webrtc",
51+ title: "WebRTC Diagnostics",
52+ script: "webrtc.min.js",
53+ css: "style.css",
54+ filename: "webrtc.html",
55+ },
56+ },
4557 } as const;
4658
4759 // Get page from VITE_PAGE env var, default to building all
@@ -42,6 +42,18 @@ const pages = {
42 filename: "config.html",42 filename: "config.html",
43 },43 },
44 },44 },
45+ webrtc: {
46+ entry: "src/pages/webrtc.ts",
47+ output: "webrtc.min.js",
48+ global: "DumberWebRTC",
49+ html: {
50+ name: "webrtc",
51+ title: "WebRTC Diagnostics",
52+ script: "webrtc.min.js",
53+ css: "style.css",
54+ filename: "webrtc.html",
55+ },
56+ },
45 } as const;57 } as const;
46 58
47 // Get page from VITE_PAGE env var, default to building all59 // Get page from VITE_PAGE env var, default to building all

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close