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:
@@ -956,7 +956,7 @@ const oscStateUiCache = {
|
||||
* ---------------------------------------------------------------------------------------------------
|
||||
*/
|
||||
const OSC_TAB_KINDS = ['bible', 'sync', 'whiteboard', 'notes', 'media', 'alpha',
|
||||
'announcements', 'graphics', 'maps', 'prayer', 'ask'];
|
||||
'announcements', 'graphics', 'maps', 'browser', 'prayer', 'ask'];
|
||||
const OSC_TAB_SLOTS = 16;
|
||||
|
||||
/*
|
||||
@@ -2250,6 +2250,29 @@ function startOscListener() {
|
||||
sendOscFeedback(msg, info, 'show_maps', !!result, result ? null : 'Renderer invoker returned false');
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* The Browser tab (browser-tab.js). /bibleapp/browser shows it, like /bibleapp/maps. reload and home
|
||||
* are the booth's recovery buttons for a live web page in front of the room: a page that hung, or
|
||||
* a tap that wandered off to another site, is put right from Companion without walking to the TV.
|
||||
*/
|
||||
if (norm === '/bibleapp/browser' || norm === '/bibleapp/showbrowser') {
|
||||
if (!tokenOk) { sendOscFeedback(msg, info, 'show_browser', false, 'Token validation failed'); return; }
|
||||
const result = invokeRenderer('showBrowser');
|
||||
sendOscFeedback(msg, info, 'show_browser', !!result, result ? null : 'Renderer invoker returned false');
|
||||
return;
|
||||
}
|
||||
if (norm === '/bibleapp/browser/reload') {
|
||||
if (!tokenOk) { sendOscFeedback(msg, info, 'browser_reload', false, 'Token validation failed'); return; }
|
||||
const result = invokeRenderer('browserReload');
|
||||
sendOscFeedback(msg, info, 'browser_reload', !!result, result ? null : 'Renderer invoker returned false');
|
||||
return;
|
||||
}
|
||||
if (norm === '/bibleapp/browser/home') {
|
||||
if (!tokenOk) { sendOscFeedback(msg, info, 'browser_home', false, 'Token validation failed'); return; }
|
||||
const result = invokeRenderer('browserHome');
|
||||
sendOscFeedback(msg, info, 'browser_home', !!result, result ? null : 'Renderer invoker returned false');
|
||||
return;
|
||||
}
|
||||
if (norm === '/bibleapp/chapter/prev' || norm === '/bibleapp/prev_chapter') {
|
||||
if (!tokenOk) { sendOscFeedback(msg, info, 'chapter_prev', false, 'Token validation failed'); return; }
|
||||
const result = invokeRenderer('chapterPrev');
|
||||
@@ -3882,6 +3905,7 @@ const server = http.createServer(async (req, res) => {
|
||||
'switchToMode', 'switchTabId', 'switch_tab_left', 'switch_tab_right',
|
||||
'new_bible_tab', 'new_whiteboard_tab', 'new_notes_tab',
|
||||
'close_tab', 'close_active_tab', 'showMaps',
|
||||
'showBrowser', 'browserReload', 'browserHome',
|
||||
// Presentation
|
||||
'splash_show', 'splash_hide', 'splashToggle', 'presentOff',
|
||||
'laser', 'spotlight', 'redLetterToggle',
|
||||
@@ -4320,7 +4344,7 @@ const server = http.createServer(async (req, res) => {
|
||||
// Safe subset of control:perform the remote is allowed to trigger.
|
||||
// clear_all + the four scroll actions are handled in app.js; they were unreachable from the
|
||||
// phone until they were listed here.
|
||||
const REMOTE_ACTIONS = new Set(['chapterPrev','chapterNext','gotoPassage','splash_show','splashToggle','presentOff','laser','spotlight','redLetterToggle','showMaps','switchToMode','switchTabId','tool','undo','redo','clear_current','clear_all','switch_tab_left','switch_tab_right','applyProfile','new_bible_tab','scroll_top','scroll_bottom','scroll_up_5','scroll_down_5','close_tab','mirror_scroll','mirror_draw','splash_hide','mirror_tap']);
|
||||
const REMOTE_ACTIONS = new Set(['chapterPrev','chapterNext','gotoPassage','splash_show','splashToggle','presentOff','laser','spotlight','redLetterToggle','showMaps','showBrowser','browserReload','browserHome','switchToMode','switchTabId','tool','undo','redo','clear_current','clear_all','switch_tab_left','switch_tab_right','applyProfile','new_bible_tab','scroll_top','scroll_bottom','scroll_up_5','scroll_down_5','close_tab','mirror_scroll','mirror_draw','splash_hide','mirror_tap']);
|
||||
|
||||
// Public (no PIN): lets the phone page show a friendly on/off state.
|
||||
// `v` is the app version: the phone remembers the version it booted against and reloads
|
||||
@@ -7702,10 +7726,14 @@ const server = http.createServer(async (req, res) => {
|
||||
res.end(`Server Error: ${error.code}`);
|
||||
}
|
||||
} else {
|
||||
// Success - Set CSP headers to allow Google OAuth and pdf.js CDN/worker
|
||||
// Success - Set CSP headers to allow Google OAuth and pdf.js CDN/worker.
|
||||
// The Browser tab (browser-tab.js) adds three things. frame-src http(s): its <webview> is a frame as
|
||||
// far as CSP is concerned, and it shows whatever page the operator set. style-src / font-src https:
|
||||
// the booth follower shows that page as a script-free srcdoc copy, which INHERITS this policy, so the
|
||||
// site's own stylesheets and web fonts must be allowed to load there. script-src is unchanged.
|
||||
const headers = {
|
||||
'Content-Type': contentType,
|
||||
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://apis.google.com https://www.gstatic.com https://accounts.google.com https://cdn.jsdelivr.net; worker-src 'self' blob: https://cdn.jsdelivr.net; frame-src 'self' blob: https://accounts.google.com; connect-src 'self' https://www.googleapis.com https://accounts.google.com https://bible-api.com https://api.esv.org https://api.scripture.api.bible https://cdn.jsdelivr.net https://greekwordnet.chs.harvard.edu https://complete-study-bible.p.rapidapi.com https://*.rapidapi.com; img-src 'self' data: https: blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com;"
|
||||
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://apis.google.com https://www.gstatic.com https://accounts.google.com https://cdn.jsdelivr.net; worker-src 'self' blob: https://cdn.jsdelivr.net; frame-src 'self' blob: https://accounts.google.com https: http:; connect-src 'self' https://www.googleapis.com https://accounts.google.com https://bible-api.com https://api.esv.org https://api.scripture.api.bible https://cdn.jsdelivr.net https://greekwordnet.chs.harvard.edu https://complete-study-bible.p.rapidapi.com https://*.rapidapi.com; img-src 'self' data: https: blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com https:; font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com https: data:;"
|
||||
};
|
||||
// Disable caching for core app assets to ensure rebuilt changes are picked up
|
||||
if (/^text\/(?:html|css|javascript)$/.test(contentType)) {
|
||||
|
||||
Reference in New Issue
Block a user