Add the Browser tab: a live web page with no browser chrome

A new tab (globe, after Maps) shows one web page full-bleed in the content area —
no address bar, bookmarks or scrollbars — built for It Is Written but usable with
any page. It loads at startup and keeps running in the background.

Stage (browser-tab.js, browser-guest-preload.cjs):
- An Electron <webview> per Browser tab, in its own sandboxed process and persistent
  session. The pages live in one persistent layer inside .tab-content: every tab
  switch rebuilds .tab-content and moving a webview reloads it, so app.js now clears
  around that layer (clearTabContentKeepingBrowser).
- Tapping a text field brings up the Prayer tab's keyboard (createPrayerKeyboard with
  a sink) and types into the page with trusted input; Enter submits; the keyboard puts
  itself away when the page moves focus on (e.g. after a search). The OS touch
  keyboard is suppressed with inputmode="none" while a field is focused.
- Tapping a scripture reference opens it in a new Bible tab, in the translation
  printed beside it when that one is switched on. References are found with the app's
  own scanner: scripture-scan.js gains scanAll() (every match with its offsets),
  translationAfter() and isTranslation(); scanText/parseSlide are unchanged.
- Input inside a webview never reaches the app, so the page reports activity (idle
  splash) and five-finger touches (blank the screen) itself.
- main.cjs: webviewTag on; every webview is forced sandboxed, context-isolated, no Node,
  http(s) only, with the guest preload. No new windows (opened in place), no permission
  prompts, no downloads, no pinch-zoom, no alert/confirm/prompt boxes, and "Leave
  site?" can never block Reload or Start page. The session reports plain Chrome.
- server.js CSP: frame-src allows http(s) (a webview is a frame to CSP); style-src and
  font-src allow https (the follower's srcdoc copy inherits this policy).

Everywhere a tab kind is listed:
- app.js: create at startup and on Reset, icon, visibility defaults, + menu, fixed
  mode, no ink layer, tool handling, OSC kind + information line (the page's search
  term), phone remote scroll / tap / page buttons, OSC show / reload / home actions,
  live setting changes, translation for tapped references.
- quick-dock, tool-wheel, presentation-tools: no drawing tools on the tab.
- Phone remote: tab label, one finger scrolls the page (taps still tap), hint text;
  mirror-stream.js reports the page's scroll for the phone's local preview.
- Follower screen: the stage publishes a script-free copy of the page's DOM (only while
  a follower is linked), its scroll and form fields; the follower shows it in
  sandboxed, double-buffered frames; the relay keeps the page for late joiners.
- OSC: /bibleapp/browser, /bibleapp/browser/reload, /bibleapp/browser/home;
  tab_kind/browser feedback. Admin and phone allow-lists updated.
- Settings: Tab Visibility, a Browser Tab section (start page, page size, scripture
  taps, return to start page), Service Profiles carry its settings, OSC references.
- Admin console: show the tab, reload / start page, start page and page size.
- BROWSER_TAB.md documents it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:25:15 -04:00
co-authored by Claude Opus 5.5
parent a0c3921edd
commit e7e527197f
20 changed files with 1780 additions and 60 deletions
+100 -2
View File
@@ -829,7 +829,7 @@
<div style="color:#777; margin-bottom:14px; font-size:13px; line-height:1.9;">
<code style="color:#8cc88c;">/bibleapp/feedback/state/tab_active/&lt;n&gt;</code> &mdash; <code>1</code> while the <strong>n-th tab on the row</strong> is showing (1&ndash;16).<br>
<code style="color:#8cc88c;">/bibleapp/feedback/state/tab_kind/&lt;kind&gt;</code> &mdash; <code>1</code> while a tab of that <strong>kind</strong> is showing:<br>
<span style="margin-left:18px;">bible &middot; sync &middot; whiteboard &middot; notes &middot; media &middot; alpha &middot; announcements &middot; graphics &middot; maps &middot; prayer &middot; ask</span>
<span style="margin-left:18px;">bible &middot; sync &middot; whiteboard &middot; notes &middot; media &middot; alpha &middot; announcements &middot; graphics &middot; maps &middot; browser &middot; prayer &middot; ask</span>
</div>
<label style="display:flex; align-items:center; gap:8px; margin:0;">
<input type="checkbox" id="oscFeedbackTabStates"> Send per-tab on/off states for Companion button colours
@@ -943,6 +943,57 @@
</div>
</div>
<!-- Browser Tab Settings -->
<div class="section">
<h2>Browser Tab</h2>
<p style="color: #999; margin-bottom: 18px;">
The Browser tab shows one web page, full screen in the tab: no address bar, bookmarks or scrollbars.
It loads when the app starts and keeps running while other tabs are showing, so it is ready the moment
you open it. Scroll it with a finger; tap a text box on the page and the app's own keyboard comes up to
type into it; tap a scripture reference on the page to open it in a new Bible tab. The booth follower
screen shows the same page, clean.
</p>
<div class="form-group">
<label for="browserTabUrl">Start page</label>
<input type="url" id="browserTabUrl" placeholder="https://it-is-written-waters-church.vercel.app/" onchange="saveBrowserTabSettings()"
style="width: 100%; max-width: 560px; padding: 10px; background: #1f1f1f; color: #e0e0e0; border: 1px solid #444; border-radius: 6px;">
<div class="help-text">The page the tab opens. Leave blank for It Is Written. Changing it takes the tab to the new page.</div>
</div>
<div class="form-group">
<label for="browserTabZoom">Page size</label>
<select id="browserTabZoom" onchange="saveBrowserTabSettings()"
style="width: 100%; max-width: 360px; padding: 10px; background: #1f1f1f; color: #e0e0e0; border: 1px solid #444; border-radius: 6px;">
<option value="auto">Auto — fit the page to the screen</option>
<option value="75">75%</option>
<option value="100">100%</option>
<option value="125">125%</option>
<option value="150">150%</option>
<option value="175">175%</option>
<option value="200">200%</option>
<option value="250">250%</option>
<option value="300">300%</option>
</select>
<div class="help-text">Auto lays the page out like a tablet and scales it to fill the width — on the portrait TV that is
about 200%. Pick a size if a site looks too small or too big.</div>
</div>
<div class="form-group">
<label style="display:flex; align-items:center; gap:8px;">
<input type="checkbox" id="browserTabScriptureTaps" onchange="saveBrowserTabSettings()"> Tap a scripture reference on the page to open it in a new Bible tab
</label>
<div class="help-text">Opens in the translation printed beside the reference (e.g. NIV) when that translation is switched on
here, otherwise in the default. Taps anywhere else work on the page as normal.</div>
</div>
<div style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
<span style="color:#8cc88c; font-size:13px;">✓ Saves automatically</span>
<button type="button" class="btn btn-secondary" onclick="browserTabBackToStart()">Return to the start page</button>
</div>
<div class="help-text" style="margin-top:8px;">
The tab remembers where it was (a search, how far down) while the app is running, so a trip to Settings does
not lose it. <strong>Return to the start page</strong> clears that, as does the refresh button on the TV.
From the booth: OSC <code>/bibleapp/browser/home</code> and <code>/bibleapp/browser/reload</code>.
</div>
</div>
<!-- Ask Tab (AI) Settings -->
<div class="section">
<h2>Ask Tab (AI Assistant)</h2>
@@ -1002,6 +1053,14 @@
<div><strong>/bibleapp/close</strong> - Close active tab</div>
<div><strong>/bibleapp/tab/left</strong> - Switch to previous tab</div>
<div><strong>/bibleapp/tab/right</strong> - Switch to next tab</div>
<div><strong>/bibleapp/maps</strong> - Show the Maps &amp; Timelines tab</div>
<div><strong>/bibleapp/browser</strong> - Show the Browser tab</div>
</div>
<div style="margin-bottom: 10px;"><strong style="color: #4a9eff;">Browser tab:</strong></div>
<div style="margin-left: 10px; margin-bottom: 10px;">
<div><strong>/bibleapp/browser/reload</strong> - Reload the page (if it hung or looks wrong)</div>
<div><strong>/bibleapp/browser/home</strong> - Take the page back to its start page (Settings › Browser Tab)</div>
</div>
<div style="margin-bottom: 10px;"><strong style="color: #4a9eff;">Tools:</strong></div>
@@ -2027,6 +2086,7 @@
{ mode: 'announcements', label: 'Announcements Tab', description: 'Image-based announcements display' },
{ mode: 'graphics', label: 'Graphics Tab', description: 'Image-based graphics display' },
{ mode: 'maps', label: 'Maps & Timelines Tab', description: 'Image gallery for maps and timelines' },
{ mode: 'browser', label: 'Browser Tab', description: 'A live web page, full screen in the tab with no browser bars (set it up under Browser Tab below)' },
{ mode: 'whiteboard', label: 'Whiteboard Tab', description: 'Blank whiteboard for drawing' },
{ mode: 'notes', label: 'Notes Tab', description: 'PDF notes with annotations' },
{ mode: 'prayer', label: 'Prayer Tab', description: 'Text editor for prayers and notes' },
@@ -2791,6 +2851,43 @@
}).join('<br>');
}
try { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', loadSplashTriggers); else loadSplashTriggers(); } catch (_) {}
// ---- Browser Tab (browser-tab.js reads these from the same-origin localStorage) ----
const BROWSER_TAB_DEFAULT_URL = 'https://it-is-written-waters-church.vercel.app/';
function loadBrowserTabSettings() {
try {
document.getElementById('browserTabUrl').value = localStorage.getItem('browserTabUrl') || '';
const z = localStorage.getItem('browserTabZoom') || 'auto';
const sel = document.getElementById('browserTabZoom');
sel.value = Array.from(sel.options).some(o => o.value === z) ? z : 'auto';
document.getElementById('browserTabScriptureTaps').checked = localStorage.getItem('browserTabScriptureTaps') !== '0';
} catch (_) {}
}
function saveBrowserTabSettings() {
try {
const urlEl = document.getElementById('browserTabUrl');
let url = String(urlEl.value || '').trim();
// "example.org/page" is what people type; the tab needs a full web address.
if (url && !/^https?:\/\//i.test(url)) url = 'https://' + url.replace(/^\/+/, '');
if (url) {
try { new URL(url); } catch (_) { showStatus('That start page is not a web address.', 'error'); return; }
}
const before = localStorage.getItem('browserTabUrl') || '';
if (url) localStorage.setItem('browserTabUrl', url); else localStorage.removeItem('browserTabUrl');
urlEl.value = url;
// A new start page means the tab should go there, not back to where it was on the old one.
if (url !== before) { try { sessionStorage.removeItem('browserTabSession'); } catch (_) {} }
localStorage.setItem('browserTabZoom', document.getElementById('browserTabZoom').value || 'auto');
localStorage.setItem('browserTabScriptureTaps', document.getElementById('browserTabScriptureTaps').checked ? '1' : '0');
showStatus('Browser tab saved — ' + (url || BROWSER_TAB_DEFAULT_URL), 'success');
} catch (e) { showStatus('Could not save Browser tab settings: ' + e.message, 'error'); }
}
// Settings replaces the app in this same window, so the session the tab keeps is visible from here.
function browserTabBackToStart() {
try { sessionStorage.removeItem('browserTabSession'); } catch (_) {}
showStatus('The Browser tab will open its start page when you go back to the app.', 'success');
}
try { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', loadBrowserTabSettings); else loadBrowserTabSettings(); } catch (_) {}
window.addEventListener('storage', (ev) => { try { if (ev && ev.key === 'splashHistory') renderSplashHistory(); } catch (_) {} });
// ---- Follower screen (booth mirror → ProPresenter / ATEM) ----
@@ -3058,7 +3155,8 @@
'redLetterEnabled', 'redLetterOverrides', 'popupVisibility',
'translationVisibility', 'wordPopupsEnabled', 'spotlightHalfPx',
'userLaserColor', 'toolWheelEnabled', 'toolWheelStyle', 'toolSnapBackSec',
'idleSplashSeconds', 'fiveFingerSplash', 'ppsyncSeparateAnnotations'].forEach(function (k) {
'idleSplashSeconds', 'fiveFingerSplash', 'ppsyncSeparateAnnotations',
'browserTabUrl', 'browserTabZoom', 'browserTabScriptureTaps'].forEach(function (k) {
var v = localStorage.getItem(k);
if (v !== null) settings[k] = v;
});