Author SHA1 Message Date
christianandClaude Opus 5 acfca90836 The English switch did nothing, so English could not be turned off
Build 125. Reported from the desk: every language on the listener page can be
switched off except English.

Exactly right, and it was not subtle. renderFeatured() unshifted the SOURCE
language into the list on every render, whatever the operator had chosen:

    if (list.indexOf(app.cfg.sourceLanguage) === -1)
      list.unshift(app.cfg.sourceLanguage);

So English was not merely on, it was always FIRST — the leading button on the
page — on a desk whose saved list was ["spa_Latn","por_Latn","por_PT","spa_ES"]
with English deliberately off and allowAll false. The operator had turned it
off, the config recorded that it was off, and the page put it back anyway.

The reasoning behind it was sound: people do want the English captions for
accessibility rather than translation. But that is a choice for the desk, not
for this function, and the switch to make it is already there in Settings next
to every other language. English is in the catalogue (languages.js:19) and the
operator window reads that same file, so turning it back on is one click.

A control that silently refuses is worse than no control. The operator's list
is now the list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 15:17:15 -04:00
christianandClaude Opus 5 7291ff48f7 Save QR image stranded the window on a picture of itself
Build 124. Reported from the booth: the button did not download anything and
there was no way back off the QR.

Both halves are the same fault. The window is served from the asset port and
the code comes off the congregation port, so the link the button built was
CROSS-ORIGIN — and a cross-origin <a download> has its download attribute
ignored and NAVIGATES instead. So the file never saved, and the window went to
the image. The app has no back button, no reload item and no address bar, so
that is a one-way trip: a headless booth Mac showing a full-screen QR with the
operator locked out of the desk. It fetches the code and downloads a blob now,
which is same-origin and actually saves.

The second change is the one that matters more. Nothing was stopping the app
window being replaced by any navigation at all, and the only reason this had
never bitten before is that nothing had tried. will-navigate now refuses to
replace the app page, logs what it refused, and sends anything external to the
browser. Our own pages are unaffected — the operator console and the poster
open as their own windows through setWindowOpenHandler, which is a different
path entirely.

Found because the build number went to the wrong field: buildNumber is the
source of truth and build.buildVersion is synced from it by predist, which
caught it and rebuilt correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 14:14:45 -04:00
4 changed files with 64 additions and 14 deletions
+22 -4
View File
@@ -3668,17 +3668,35 @@ $('copyAddr').addEventListener('click', () => {
* is made in their own design tool. The built-in poster page is still there
* at /poster for anyone who wants it; it is just not what this button does.
*/
$('saveQr').addEventListener('click', () => {
$('saveQr').addEventListener('click', async () => {
const base = shareBase();
if (!base) { log('no address to encode yet', 'err'); return; }
const a = document.createElement('a');
a.href = 'http://127.0.0.1:' + state.local.port + '/qr.svg?url=' +
/*
* Fetch the code and hand the browser a BLOB rather than linking straight at
* the server. This window is served from the asset port and the QR comes off
* the congregation port, so a plain <a download> is CROSS-ORIGIN — the
* download attribute is ignored and the window navigates to the image
* instead. With no back button, no reload item and no address bar, that
* strands the operator on a full-screen QR with no way out; build 123 did
* exactly that to a booth Mac. A blob: URL is same-origin, so it downloads.
*/
const url = 'http://127.0.0.1:' + state.local.port + '/qr.svg?url=' +
encodeURIComponent(base + '/');
try {
const r = await fetch(url, { cache: 'no-store' });
if (!r.ok) throw new Error('the server answered ' + r.status);
const href = URL.createObjectURL(await r.blob());
const a = document.createElement('a');
a.href = href;
a.download = 'church-translate-qr.svg';
document.body.appendChild(a);
a.click();
a.remove();
log('saved the QR for ' + base + '/', 'ok');
setTimeout(() => URL.revokeObjectURL(href), 10000);
log('saved the QR for ' + base + '/ to Downloads', 'ok');
} catch (err) {
log('could not save the QR: ' + err.message, 'err');
}
});
$('openOperator').addEventListener('click', () => {
window.open('http://127.0.0.1:' + state.local.port + '/operator', '_blank');
+21
View File
@@ -662,6 +662,27 @@ function createWindow() {
: '/index.html';
win.loadURL(assetOrigin + page);
/*
* NOTHING replaces the app in this window. There is no back button, no
* reload item and no address bar, so a window that leaves the app page is a
* window the operator cannot get back from — which is precisely how build
* 123 stranded a booth Mac on a full-screen QR image. Our own pages still
* open as their OWN windows through setWindowOpenHandler below; this only
* refuses to replace the app itself, and sends anything external to the
* browser where it belongs.
*/
win.webContents.on('will-navigate', (e, url) => {
if (url === assetOrigin + page) return; /* reloading ourselves is fine */
e.preventDefault();
console.error('[main] refused to navigate the app window to ' + url);
let local = false;
try {
const u = new URL(url);
local = (u.hostname === '127.0.0.1' || u.hostname === 'localhost');
} catch (_) { local = false; }
if (!local) shell.openExternal(url).catch(() => {});
});
win.webContents.on('did-finish-load', () => console.log('[main] page loaded'));
win.webContents.on('did-fail-load', (_e, code, desc, url) =>
console.error('[main] page FAILED to load: ' + code + ' ' + desc + ' ' + url));
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "church-translate-engine",
"productName": "Church Translate",
"version": "3.23.2",
"buildNumber": 123,
"buildNumber": 125,
"private": true,
"description": "Live sermon translation — self-contained: captures audio, transcribes, translates, and serves the congregation page itself",
"main": "main.js",
@@ -106,7 +106,7 @@
]
},
"asar": false,
"buildVersion": "123",
"buildVersion": "125",
"win": {
"target": [
{
+14 -3
View File
@@ -163,10 +163,21 @@
function renderFeatured() {
var wrap = $('featured');
wrap.textContent = '';
/*
* THE OPERATOR'S LIST IS THE LIST. This used to unshift the source
* language in unconditionally, on the reasoning that plenty of people want
* the English captions for accessibility rather than translation — which
* is true, and still available, but it is not this code's call to make.
* The operator has a switch for every language including English, and for
* the source language alone that switch did nothing: English was put back
* on every render, so a desk that had deliberately turned it off still
* offered it as the first button on the page. A control that silently
* refuses is worse than no control.
*
* Wanting English captions is now one click in Settings, in the same list
* as every other language, and it stays where it is put.
*/
var list = (app.cfg.featured || []).slice();
/* The source language belongs in the list too — plenty of people want the
English captions for accessibility, not translation. */
if (list.indexOf(app.cfg.sourceLanguage) === -1) list.unshift(app.cfg.sourceLanguage);
list.forEach(function (code) {
var b = langButton(code);
if (b) wrap.appendChild(b);