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
+21 -1
View File
@@ -361,7 +361,7 @@
// ---------------------------------------------------------------- catalogue data
var TAB_MODES = [
{ m: 'bible', t: 'Bible' }, { m: 'announcements', t: 'Announcements' },
{ m: 'graphics', t: 'Graphics' }, { m: 'maps', t: 'Maps & Timelines' },
{ m: 'graphics', t: 'Graphics' }, { m: 'maps', t: 'Maps & Timelines' }, { m: 'browser', t: 'Browser' },
{ m: 'whiteboard', t: 'Whiteboard' }, { m: 'notes', t: 'Notes' },
{ m: 'prayer', t: 'Prayer' }, { m: 'ask', t: 'Ask (AI)' }, { m: 'ppsync', t: 'ProPresenter Sync' },
{ m: 'media', t: 'Media' }, { m: 'alpha', t: 'Alpha' }
@@ -502,6 +502,11 @@
btn('◀ Previous tab', 'sec', function () { pushAction('Previous tab', 'switch_tab_left'); }),
btn('Next tab ▶', 'sec', function () { pushAction('Next tab', 'switch_tab_right'); })
]));
// The Browser tab's live web page: put it right without walking to the TV.
s2._body.appendChild(row('Browser tab page', 'Reload it if it hung; Start page if a tap wandered off the site', [
btn('Reload', 'sec', function () { pushAction('Reload the Browser tab page', 'browserReload'); }),
btn('Start page', 'sec', function () { pushAction('Browser tab back to its start page', 'browserHome'); })
]));
s2._body.appendChild(row('Open a new tab', 'Added to the end of the tab bar', [
btn('Bible', 'sec', function () { pushAction('New Bible tab', 'new_bible_tab'); }),
btn('Whiteboard', 'sec', function () { pushAction('New whiteboard tab', 'new_whiteboard_tab'); }),
@@ -602,6 +607,21 @@
pushSetting('Bible text size ' + v + 'px', 'bibleFontSize', String(v));
})
]));
var bUrl = document.createElement('input'); bUrl.type = 'url'; bUrl.placeholder = 'https://it-is-written-waters-church.vercel.app/';
bUrl.style.width = '320px';
b.appendChild(row('Browser tab start page', 'Blank = It Is Written. The tab goes there when queued.', [
bUrl, btn('Queue', '', function () {
var u = String(bUrl.value || '').trim();
if (u && !/^https?:\/\//i.test(u)) u = 'https://' + u;
if (u) { try { new URL(u); } catch (_) { return toast('Not a web address', true); } }
pushSetting('Browser tab start page ' + (u || '(It Is Written)'), 'browserTabUrl', u);
})
]));
var bZoom = sel([{ v: 'auto', t: 'Auto (fit the screen)' }, { v: '75', t: '75%' }, { v: '100', t: '100%' }, { v: '125', t: '125%' },
{ v: '150', t: '150%' }, { v: '175', t: '175%' }, { v: '200', t: '200%' }, { v: '250', t: '250%' }, { v: '300', t: '300%' }], 'auto');
b.appendChild(row('Browser tab page size', 'Auto is about 200% on the portrait TV', [
bZoom, btn('Queue', '', function () { pushSetting('Browser tab page size ' + bZoom.value, 'browserTabZoom', bZoom.value); })
]));
b.appendChild(row('Word popups', 'Double-tap a word to open its card', [
btn('On', 'sec', function () { pushSetting('Word popups ON', 'wordPopupsEnabled', 'true'); }),
btn('Off', 'sec', function () { pushSetting('Word popups OFF', 'wordPopupsEnabled', 'false'); })