openvibers/openvibe.live Commits
Dashboard on a phone: Bench moves to the Chat tab, cards fit the screen (#709)
Changes
@@@ -111,7 +111,15 @@}@media (max-width: 768px) {-- .dash-grid { grid-template-columns: 1fr; }++ /* minmax(0, 1fr), not 1fr: a 1fr track grows to its widest content, and one long line in the Chat tab made every++ card there 466 px wide on a 390 px phone (the page zoomed out; signed-in sweep, 11-09). */++ .dash-grid { grid-template-columns: minmax(0, 1fr); }++ .dash-grid > * { min-width: 0; }++ /* A card's address rows (the chat overlay, the scenes): the address on its own line, then Copy and Preview side by++ side (they were two unlabeled full-width grey bars). */++ .dash-card .key-display { flex-direction: row; }++ .dash-card .key-display .form-input { flex: 1 1 100%; min-width: 0; }++ .dash-card .key-display .btn { flex: 1 1 0; width: auto; min-height: 40px; justify-content: center; }}@media (max-width: 360px) {.dash-card { padding: 14px; border-radius: 14px; }@@@ -516,3 +524,9 @@.dash-schedule-list a { color: var(--text-secondary); text-decoration: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.dash-schedule-list a:hover { color: var(--text-primary); text-decoration: underline; }.dash-schedule-actions { display: flex; flex-wrap: wrap; gap: 8px; }++++/* Bench on stream (Chat tab): labels over their fields, the button at the start of the row. */++.dash-bench-form { display: grid; gap: 6px; }++.dash-bench-form label { margin-top: 6px; font-size: 0.85rem; font-weight: 600; color: var(--text-secondary); }++.dash-bench-form .btn { justify-self: start; min-height: 40px; margin-top: 10px; }++#dash-bench-status:empty { display: none; }@@@ -15,26 +15,6 @@</div><div class="ch-tab-panel dash-tab-panel active" id="dash-panel-home" role="tabpanel"><div class="dash-grid" id="dash-grid-home">-- <section class="dash-card" id="dash-bench-stream" aria-labelledby="dash-bench-title">-- <h3 id="dash-bench-title">Bench on stream</h3>-- <form id="dash-bench-form">-- <label for="dash-bench-kind">Kind</label>-- <select class="form-input" id="dash-bench-kind" name="kind" required>-- <option value="design.page">Design page</option><option value="design.component">Design component</option>-- <option value="meme.caption">Meme caption</option><option value="chat.reply">Chat reply</option>-- <option value="games.fun">Games fun</option><option value="vision.pair">Vision pair</option>-- <option value="codes.review">Codes review</option><option value="surf.desk">Surf desk</option>-- <option value="speech.tts">Speech TTS</option><option value="translate.ja">Translate Japanese</option>-- <option value="search.answer">Search answer</option>-- </select>-- <label for="dash-bench-prompt">Prompt</label>-- <textarea class="form-input" id="dash-bench-prompt" name="prompt" maxlength="4000" required></textarea>-- <label for="dash-bench-duration">Vote time (seconds)</label>-- <input class="form-input" id="dash-bench-duration" name="duration" type="number" min="15" max="600" value="60" required>-- <button class="btn" type="submit">Start round</button>-- </form>-- <p id="dash-bench-status" role="status"></p>-- </section></div></div><div class="ch-tab-panel dash-tab-panel" id="dash-panel-chatai" role="tabpanel"><div class="dash-grid" id="dash-grid-chatai"></div></div><div class="ch-tab-panel dash-tab-panel" id="dash-panel-moderation" role="tabpanel"><div class="dash-grid" id="dash-grid-moderation"></div></div>@@@ -65,10 +45,10 @@<div class="key-display"><input type="text" id="dash-obs-chat-overlay-url" readonly class="form-input"><button class="btn btn-small" onclick="copyDashObsOverlayUrl()" title="Copy overlay URL">-- <i class="fa-solid fa-copy"></i>++ <i class="fa-solid fa-copy" aria-hidden="true"></i> Copy</button><button class="btn btn-small" onclick="openDashObsOverlay()" title="Open overlay">-- <i class="fa-solid fa-arrow-up-right-from-square"></i>++ <i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Preview</button></div></div>@@@ -86,6 +66,30 @@<ul class="dash-scenes" id="dash-scenes"></ul></div>++ <!-- Bench on stream (bench-stream-dashboard.js): a round chat votes on, so it sits in the Chat tab beside the++ scenes. It was static markup in the Home grid, so it came first on every dashboard, above the profile. -->++ <section class="dash-card" id="dash-bench-stream" aria-labelledby="dash-bench-title">++ <h3 id="dash-bench-title"><i class="fa-solid fa-scale-balanced" aria-hidden="true"></i> Bench on stream</h3>++ <p class="muted" style="margin-bottom:12px">Two AI models answer your prompt on stream, and chat votes for the better one with !1, !2 or !tie.</p>++ <form id="dash-bench-form" class="dash-bench-form">++ <label for="dash-bench-kind">Kind</label>++ <select class="form-input" id="dash-bench-kind" name="kind" required>++ <option value="design.page">Design page</option><option value="design.component">Design component</option>++ <option value="meme.caption">Meme caption</option><option value="chat.reply">Chat reply</option>++ <option value="games.fun">Games fun</option><option value="vision.pair">Vision pair</option>++ <option value="codes.review">Codes review</option><option value="surf.desk">Surf desk</option>++ <option value="speech.tts">Speech TTS</option><option value="translate.ja">Translate Japanese</option>++ <option value="search.answer">Search answer</option>++ </select>++ <label for="dash-bench-prompt">Prompt</label>++ <textarea class="form-input" id="dash-bench-prompt" name="prompt" maxlength="4000" rows="3" placeholder="A landing page for a neighbourhood bakery" required></textarea>++ <label for="dash-bench-duration">Vote time (seconds)</label>++ <input class="form-input" id="dash-bench-duration" name="duration" type="number" min="15" max="600" value="60" required>++ <button class="btn btn-primary" type="submit">Start round</button>++ </form>++ <p id="dash-bench-status" role="status"></p>++ </section>++<!-- API Tokens (Bot / Integration) --><div class="dash-card" id="dash-card-tokens"><h3><i class="fa-solid fa-key"></i> API Tokens</h3>@@@ -12,7 +12,7 @@ let activeStreams = [];modules (e.g. moderation) register their loader on window._dashTabLoaders. */const _DASH_TAB_CARDS = {home: ['dash-card-profile', 'dash-card-schedule', 'dash-card-ai-derivation', 'dash-card-offline'],-- chatai: ['dash-card-vibe', 'dash-aibot-card', 'dash-card-overlay', 'dash-card-scenes', 'dash-card-chatlogs'],++ chatai: ['dash-card-vibe', 'dash-aibot-card', 'dash-card-overlay', 'dash-card-scenes', 'dash-bench-stream', 'dash-card-chatlogs'],moderation: [], // injected by dashboard-moderation.js into #dash-grid-moderationcontrols: ['dash-card-bot-embed', 'dash-card-tokens'],money: ['dash-card-bucks', 'dash-card-goals', 'dash-card-tips-page', 'dash-powerchat-card'],@@@ -245,9 +245,9 @@ function updateDashScenes() {name.append(i, ` ${label}`);const row = document.createElement('div'); row.className = 'key-display';const input = document.createElement('input'); input.type = 'text'; input.readOnly = true; input.className = 'form-input'; input.value = dashSceneUrl(kind); input.setAttribute('aria-label', `${label} scene URL`);-- const copy = document.createElement('button'); copy.className = 'btn btn-small'; copy.title = `Copy the ${label} URL`; copy.innerHTML = '<i class="fa-solid fa-copy"></i>';++ const copy = document.createElement('button'); copy.className = 'btn btn-small'; copy.title = `Copy the ${label} URL`; copy.innerHTML = '<i class="fa-solid fa-copy" aria-hidden="true"></i> Copy';copy.addEventListener('click', () => navigator.clipboard.writeText(input.value).then(() => toast(`${label} URL copied`, 'success')));-- const open = document.createElement('a'); open.className = 'btn btn-small'; open.href = input.value; open.target = '_blank'; open.rel = 'noopener'; open.title = `Preview ${label}`; open.innerHTML = '<i class="fa-solid fa-arrow-up-right-from-square"></i>';++ const open = document.createElement('a'); open.className = 'btn btn-small'; open.href = input.value; open.target = '_blank'; open.rel = 'noopener'; open.title = `Preview ${label}`; open.innerHTML = '<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Preview';row.append(input, copy, open);li.append(name, row);return li;nnew file mode 100644@@@ -0,0 +1,36 @@++'use strict';++/**++ * The streamer dashboard on a phone (signed-in sweep, 2026-11-09):++ * - "Bench on stream" was static markup in the Home grid, so it came first on every dashboard, above the profile. It is++ * a round chat votes on: it now lives in the Chat tab, after Scenes, relocated like every other card.++ * - The phone grid was `1fr`, which grows to its widest content: one long line made every Chat-tab card 466 px wide on a++ * 390 px phone and the page zoomed out. It is `minmax(0, 1fr)` now.++ * - Address rows (chat overlay, scenes) showed two unlabeled full-width grey bars; they read Copy and Preview, side by side.++ *++ * node test/dashboard-phone-layout.test.js++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const root = path.join(__dirname, '..');++const html = fs.readFileSync(path.join(root, 'public/fragments/dashboard.html'), 'utf8');++const js = fs.readFileSync(path.join(root, 'public/js/dashboard.js'), 'utf8');++const css = fs.readFileSync(path.join(root, 'public/css/features/dashboard.css'), 'utf8');++++const home = html.slice(html.indexOf('id="dash-grid-home"'), html.indexOf('id="dash-panel-chatai"'));++assert.ok(!home.includes('dash-bench-stream'), 'Bench is not static markup in the Home grid');++const staging = html.slice(html.indexOf('id="dash-staging"'));++assert.ok(staging.includes('id="dash-bench-stream"'), 'Bench is authored in the staging grid like the other cards');++assert.ok(/chatai:\s*\[[^\]]*'dash-card-scenes', 'dash-bench-stream'/.test(js), 'Bench is relocated into the Chat tab after Scenes');++assert.ok(/home:\s*\['dash-card-profile'/.test(js), 'the profile leads Home');++assert.ok(/chat votes for the better one with !1, !2 or !tie/.test(staging), 'the card says what a round is');++++const phone = css.slice(css.indexOf('@media (max-width: 768px)'));++assert.ok(/\.dash-grid \{ grid-template-columns: minmax\(0, 1fr\); \}/.test(phone), 'the phone column can shrink below its content');++assert.ok(/\.dash-card \.key-display \{ flex-direction: row; \}/.test(phone), 'Copy and Preview share a row on a phone');++++assert.ok(/fa-copy" aria-hidden="true"><\/i> Copy'/.test(js) && /aria-hidden="true"><\/i> Preview'/.test(js), 'scene buttons are labelled');++assert.ok(/copyDashObsOverlayUrl\(\)[^>]*>\s*<i class="fa-solid fa-copy" aria-hidden="true"><\/i> Copy/.test(html), 'the overlay copy button is labelled');++++console.log('dashboard on a phone: Bench in the Chat tab, columns that shrink, labelled address buttons');
@@ -111,7 +111,15 @@
@@ -111,7 +111,15 @@
}
}
@media (max-width: 768px) {
@media (max-width: 768px) {
- .dash-grid { grid-template-columns: 1fr; }
+ /* minmax(0, 1fr), not 1fr: a 1fr track grows to its widest content, and one long line in the Chat tab made every
+ card there 466 px wide on a 390 px phone (the page zoomed out; signed-in sweep, 11-09). */
+ .dash-grid { grid-template-columns: minmax(0, 1fr); }
+ .dash-grid > * { min-width: 0; }
+ /* A card's address rows (the chat overlay, the scenes): the address on its own line, then Copy and Preview side by
+ side (they were two unlabeled full-width grey bars). */
+ .dash-card .key-display { flex-direction: row; }
+ .dash-card .key-display .form-input { flex: 1 1 100%; min-width: 0; }
+ .dash-card .key-display .btn { flex: 1 1 0; width: auto; min-height: 40px; justify-content: center; }
}
}
@media (max-width: 360px) {
@media (max-width: 360px) {
.dash-card { padding: 14px; border-radius: 14px; }
.dash-card { padding: 14px; border-radius: 14px; }
@@ -516,3 +524,9 @@
@@ -516,3 +524,9 @@
.dash-schedule-list a { color: var(--text-secondary); text-decoration: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-schedule-list a { color: var(--text-secondary); text-decoration: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-schedule-list a:hover { color: var(--text-primary); text-decoration: underline; }
.dash-schedule-list a:hover { color: var(--text-primary); text-decoration: underline; }
.dash-schedule-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.dash-schedule-actions { display: flex; flex-wrap: wrap; gap: 8px; }
+
+/* Bench on stream (Chat tab): labels over their fields, the button at the start of the row. */
+.dash-bench-form { display: grid; gap: 6px; }
+.dash-bench-form label { margin-top: 6px; font-size: 0.85rem; font-weight: 600; color: var(--text-secondary); }
+.dash-bench-form .btn { justify-self: start; min-height: 40px; margin-top: 10px; }
+#dash-bench-status:empty { display: none; }
@@ -15,26 +15,6 @@
@@ -15,26 +15,6 @@
</div>
</div>
<div class="ch-tab-panel dash-tab-panel active" id="dash-panel-home" role="tabpanel"><div class="dash-grid" id="dash-grid-home">
<div class="ch-tab-panel dash-tab-panel active" id="dash-panel-home" role="tabpanel"><div class="dash-grid" id="dash-grid-home">
- <section class="dash-card" id="dash-bench-stream" aria-labelledby="dash-bench-title">
- <h3 id="dash-bench-title">Bench on stream</h3>
- <form id="dash-bench-form">
- <label for="dash-bench-kind">Kind</label>
- <select class="form-input" id="dash-bench-kind" name="kind" required>
- <option value="design.page">Design page</option><option value="design.component">Design component</option>
- <option value="meme.caption">Meme caption</option><option value="chat.reply">Chat reply</option>
- <option value="games.fun">Games fun</option><option value="vision.pair">Vision pair</option>
- <option value="codes.review">Codes review</option><option value="surf.desk">Surf desk</option>
- <option value="speech.tts">Speech TTS</option><option value="translate.ja">Translate Japanese</option>
- <option value="search.answer">Search answer</option>
- </select>
- <label for="dash-bench-prompt">Prompt</label>
- <textarea class="form-input" id="dash-bench-prompt" name="prompt" maxlength="4000" required></textarea>
- <label for="dash-bench-duration">Vote time (seconds)</label>
- <input class="form-input" id="dash-bench-duration" name="duration" type="number" min="15" max="600" value="60" required>
- <button class="btn" type="submit">Start round</button>
- </form>
- <p id="dash-bench-status" role="status"></p>
- </section>
</div></div>
</div></div>
<div class="ch-tab-panel dash-tab-panel" id="dash-panel-chatai" role="tabpanel"><div class="dash-grid" id="dash-grid-chatai"></div></div>
<div class="ch-tab-panel dash-tab-panel" id="dash-panel-chatai" role="tabpanel"><div class="dash-grid" id="dash-grid-chatai"></div></div>
<div class="ch-tab-panel dash-tab-panel" id="dash-panel-moderation" role="tabpanel"><div class="dash-grid" id="dash-grid-moderation"></div></div>
<div class="ch-tab-panel dash-tab-panel" id="dash-panel-moderation" role="tabpanel"><div class="dash-grid" id="dash-grid-moderation"></div></div>
@@ -65,10 +45,10 @@
@@ -65,10 +45,10 @@
<div class="key-display">
<div class="key-display">
<input type="text" id="dash-obs-chat-overlay-url" readonly class="form-input">
<input type="text" id="dash-obs-chat-overlay-url" readonly class="form-input">
<button class="btn btn-small" onclick="copyDashObsOverlayUrl()" title="Copy overlay URL">
<button class="btn btn-small" onclick="copyDashObsOverlayUrl()" title="Copy overlay URL">
- <i class="fa-solid fa-copy"></i>
+ <i class="fa-solid fa-copy" aria-hidden="true"></i> Copy
</button>
</button>
<button class="btn btn-small" onclick="openDashObsOverlay()" title="Open overlay">
<button class="btn btn-small" onclick="openDashObsOverlay()" title="Open overlay">
- <i class="fa-solid fa-arrow-up-right-from-square"></i>
+ <i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Preview
</button>
</button>
</div>
</div>
</div>
</div>
@@ -86,6 +66,30 @@
@@ -86,6 +66,30 @@
<ul class="dash-scenes" id="dash-scenes"></ul>
<ul class="dash-scenes" id="dash-scenes"></ul>
</div>
</div>
+ <!-- Bench on stream (bench-stream-dashboard.js): a round chat votes on, so it sits in the Chat tab beside the
+ scenes. It was static markup in the Home grid, so it came first on every dashboard, above the profile. -->
+ <section class="dash-card" id="dash-bench-stream" aria-labelledby="dash-bench-title">
+ <h3 id="dash-bench-title"><i class="fa-solid fa-scale-balanced" aria-hidden="true"></i> Bench on stream</h3>
+ <p class="muted" style="margin-bottom:12px">Two AI models answer your prompt on stream, and chat votes for the better one with !1, !2 or !tie.</p>
+ <form id="dash-bench-form" class="dash-bench-form">
+ <label for="dash-bench-kind">Kind</label>
+ <select class="form-input" id="dash-bench-kind" name="kind" required>
+ <option value="design.page">Design page</option><option value="design.component">Design component</option>
+ <option value="meme.caption">Meme caption</option><option value="chat.reply">Chat reply</option>
+ <option value="games.fun">Games fun</option><option value="vision.pair">Vision pair</option>
+ <option value="codes.review">Codes review</option><option value="surf.desk">Surf desk</option>
+ <option value="speech.tts">Speech TTS</option><option value="translate.ja">Translate Japanese</option>
+ <option value="search.answer">Search answer</option>
+ </select>
+ <label for="dash-bench-prompt">Prompt</label>
+ <textarea class="form-input" id="dash-bench-prompt" name="prompt" maxlength="4000" rows="3" placeholder="A landing page for a neighbourhood bakery" required></textarea>
+ <label for="dash-bench-duration">Vote time (seconds)</label>
+ <input class="form-input" id="dash-bench-duration" name="duration" type="number" min="15" max="600" value="60" required>
+ <button class="btn btn-primary" type="submit">Start round</button>
+ </form>
+ <p id="dash-bench-status" role="status"></p>
+ </section>
+
<!-- API Tokens (Bot / Integration) -->
<!-- API Tokens (Bot / Integration) -->
<div class="dash-card" id="dash-card-tokens">
<div class="dash-card" id="dash-card-tokens">
<h3><i class="fa-solid fa-key"></i> API Tokens</h3>
<h3><i class="fa-solid fa-key"></i> API Tokens</h3>
@@ -12,7 +12,7 @@ let activeStreams = [];
@@ -12,7 +12,7 @@ let activeStreams = [];
modules (e.g. moderation) register their loader on window._dashTabLoaders. */
modules (e.g. moderation) register their loader on window._dashTabLoaders. */
const _DASH_TAB_CARDS = {
const _DASH_TAB_CARDS = {
home: ['dash-card-profile', 'dash-card-schedule', 'dash-card-ai-derivation', 'dash-card-offline'],
home: ['dash-card-profile', 'dash-card-schedule', 'dash-card-ai-derivation', 'dash-card-offline'],
- chatai: ['dash-card-vibe', 'dash-aibot-card', 'dash-card-overlay', 'dash-card-scenes', 'dash-card-chatlogs'],
+ chatai: ['dash-card-vibe', 'dash-aibot-card', 'dash-card-overlay', 'dash-card-scenes', 'dash-bench-stream', 'dash-card-chatlogs'],
moderation: [], // injected by dashboard-moderation.js into #dash-grid-moderation
moderation: [], // injected by dashboard-moderation.js into #dash-grid-moderation
controls: ['dash-card-bot-embed', 'dash-card-tokens'],
controls: ['dash-card-bot-embed', 'dash-card-tokens'],
money: ['dash-card-bucks', 'dash-card-goals', 'dash-card-tips-page', 'dash-powerchat-card'],
money: ['dash-card-bucks', 'dash-card-goals', 'dash-card-tips-page', 'dash-powerchat-card'],
@@ -245,9 +245,9 @@ function updateDashScenes() {
@@ -245,9 +245,9 @@ function updateDashScenes() {
name.append(i, ` ${label}`);
name.append(i, ` ${label}`);
const row = document.createElement('div'); row.className = 'key-display';
const row = document.createElement('div'); row.className = 'key-display';
const input = document.createElement('input'); input.type = 'text'; input.readOnly = true; input.className = 'form-input'; input.value = dashSceneUrl(kind); input.setAttribute('aria-label', `${label} scene URL`);
const input = document.createElement('input'); input.type = 'text'; input.readOnly = true; input.className = 'form-input'; input.value = dashSceneUrl(kind); input.setAttribute('aria-label', `${label} scene URL`);
- const copy = document.createElement('button'); copy.className = 'btn btn-small'; copy.title = `Copy the ${label} URL`; copy.innerHTML = '<i class="fa-solid fa-copy"></i>';
+ const copy = document.createElement('button'); copy.className = 'btn btn-small'; copy.title = `Copy the ${label} URL`; copy.innerHTML = '<i class="fa-solid fa-copy" aria-hidden="true"></i> Copy';
copy.addEventListener('click', () => navigator.clipboard.writeText(input.value).then(() => toast(`${label} URL copied`, 'success')));
copy.addEventListener('click', () => navigator.clipboard.writeText(input.value).then(() => toast(`${label} URL copied`, 'success')));
- const open = document.createElement('a'); open.className = 'btn btn-small'; open.href = input.value; open.target = '_blank'; open.rel = 'noopener'; open.title = `Preview ${label}`; open.innerHTML = '<i class="fa-solid fa-arrow-up-right-from-square"></i>';
+ const open = document.createElement('a'); open.className = 'btn btn-small'; open.href = input.value; open.target = '_blank'; open.rel = 'noopener'; open.title = `Preview ${label}`; open.innerHTML = '<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Preview';
row.append(input, copy, open);
row.append(input, copy, open);
li.append(name, row);
li.append(name, row);
return li;
return li;
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+'use strict';
+/**
+ * The streamer dashboard on a phone (signed-in sweep, 2026-11-09):
+ * - "Bench on stream" was static markup in the Home grid, so it came first on every dashboard, above the profile. It is
+ * a round chat votes on: it now lives in the Chat tab, after Scenes, relocated like every other card.
+ * - The phone grid was `1fr`, which grows to its widest content: one long line made every Chat-tab card 466 px wide on a
+ * 390 px phone and the page zoomed out. It is `minmax(0, 1fr)` now.
+ * - Address rows (chat overlay, scenes) showed two unlabeled full-width grey bars; they read Copy and Preview, side by side.
+ *
+ * node test/dashboard-phone-layout.test.js
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const root = path.join(__dirname, '..');
+const html = fs.readFileSync(path.join(root, 'public/fragments/dashboard.html'), 'utf8');
+const js = fs.readFileSync(path.join(root, 'public/js/dashboard.js'), 'utf8');
+const css = fs.readFileSync(path.join(root, 'public/css/features/dashboard.css'), 'utf8');
+
+const home = html.slice(html.indexOf('id="dash-grid-home"'), html.indexOf('id="dash-panel-chatai"'));
+assert.ok(!home.includes('dash-bench-stream'), 'Bench is not static markup in the Home grid');
+const staging = html.slice(html.indexOf('id="dash-staging"'));
+assert.ok(staging.includes('id="dash-bench-stream"'), 'Bench is authored in the staging grid like the other cards');
+assert.ok(/chatai:\s*\[[^\]]*'dash-card-scenes', 'dash-bench-stream'/.test(js), 'Bench is relocated into the Chat tab after Scenes');
+assert.ok(/home:\s*\['dash-card-profile'/.test(js), 'the profile leads Home');
+assert.ok(/chat votes for the better one with !1, !2 or !tie/.test(staging), 'the card says what a round is');
+
+const phone = css.slice(css.indexOf('@media (max-width: 768px)'));
+assert.ok(/\.dash-grid \{ grid-template-columns: minmax\(0, 1fr\); \}/.test(phone), 'the phone column can shrink below its content');
+assert.ok(/\.dash-card \.key-display \{ flex-direction: row; \}/.test(phone), 'Copy and Preview share a row on a phone');
+
+assert.ok(/fa-copy" aria-hidden="true"><\/i> Copy'/.test(js) && /aria-hidden="true"><\/i> Preview'/.test(js), 'scene buttons are labelled');
+assert.ok(/copyDashObsOverlayUrl\(\)[^>]*>\s*<i class="fa-solid fa-copy" aria-hidden="true"><\/i> Copy/.test(html), 'the overlay copy button is labelled');
+
+console.log('dashboard on a phone: Bench in the Chat tab, columns that shrink, labelled address buttons');