3 Commits
Author SHA1 Message Date
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
Waters ChurchandClaude Opus 5 826c471ed7 On a domain, show the code and nothing else
Build 123. 122 taught the banner and the QR about the public address but left
the interface picker underneath them untouched, so the panel still listed
10.6.5.55 and 10.1.7.39 with one marked IN USE — numbers that no longer point
anywhere anybody is being sent, directly under a banner naming the domain.

Those rows exist to choose which interface the code encodes, which is a
question only a LAN setup has: with a public address set the code points at the
domain whichever row is picked. They are hidden now, and a LAN-only campus is
unaffected.

Printable poster is replaced by Save QR image. The poster people actually print
is made in their own design tool, and what they need from the app is the code
as a file — a 512px SVG, so it scales to any size without going soft. The
built-in poster page is untouched at /poster for anyone who wants it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 13:48:18 -04:00
Waters ChurchandClaude Opus 5 817fb743de Show the domain, not the LAN address, once one is set
Build 122. The operator page honoured Public address and the main app window
did not, so the two disagreed about the one fact they both exist to display:
translate.watersmedia.org was configured and working through nginx, while the
window, the QR code, the poster and Copy address all still handed out
http://10.1.7.39:3100.

engine.js had no mention of publicUrl at all. It now reads it from the server
config it was already fetching, and prefers it everywhere the congregation is
pointed.

The operator link deliberately does not follow. /operator returns 404 to
anything carrying X-Forwarded-For, so over a proxy the domain is exactly where
that page cannot be reached; it stays on the LAN address, which is the whole
point of being able to run the desk from a phone.

The QR warnings are suppressed when a public address is set. They talk about
Wi-Fi being down and leases moving, which are facts about an address nobody is
being given any more; the panel now names the public address and reminds the
desk that this Mac still has to be reachable from the proxy.

The share panel also re-renders when the server config arrives. It is fetched
asynchronously and lost a race at startup, so the first paint had no publicUrl
to read even once this was wired up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 13:40:48 -04:00
4 changed files with 119 additions and 7 deletions
+95 -4
View File
@@ -3508,14 +3508,50 @@ function shareChoice() {
};
}
function shareBase() {
/* This Mac on the church network. The operator page lives here and NOWHERE
else: it returns 404 to anything arriving through a proxy, by design. */
function lanBase() {
const c = shareChoice();
if (!c || c.none) return '';
return 'http://' + c.addr.address + ':' + state.local.port;
}
/*
* WHAT THE CONGREGATION OPENS.
*
* A church that has put this on a domain has said so in the operator page's
* Public address, and from that moment the LAN address is the wrong thing to
* print: it is not what is on the poster, not what the QR should encode, and
* not what someone off the building's wifi can reach. The app window went on
* showing the IP anyway, because this half of it never read that setting --
* the operator page honoured it and the main window did not, so the two
* disagreed about the one fact they both exist to display.
*/
function publicBaseConfigured() {
const u = opConfig && typeof opConfig.publicUrl === 'string' ? opConfig.publicUrl.trim() : '';
return u ? u.replace(/\/+$/, '') : '';
}
function shareBase() {
return publicBaseConfigured() || lanBase();
}
function renderQrWarning() {
const el = $('qrWarn');
/*
* On a domain the warnings below are about an address nobody is being
* given any more. Saying "Wi-Fi is down, phones cannot reach the code"
* while the code points at a proxy would send the desk after the wrong
* thing entirely.
*/
const pub = publicBaseConfigured();
if (pub) {
el.hidden = false;
el.textContent = 'The code points at ' + pub + ', set as the public address. ' +
'This Mac still has to be reachable from there; its own address is ' +
(lanBase() || 'unavailable') + '.';
return;
}
const c = shareChoice();
if (!c) { el.hidden = true; return; }
@@ -3566,6 +3602,26 @@ function renderShare() {
const list = state.local.addresses || [];
const box = $('addrList');
box.textContent = '';
/*
* WHICH INTERFACE THE QR USES IS A QUESTION ONLY A LAN SETUP HAS.
*
* These rows let the operator pick the address the code points at. Once a
* public address is configured the code points at the domain whatever they
* pick, so the list is a row of numbers that do nothing — and the one
* marked IN USE is not the address anyone is being given. Hide it.
*/
const addrBox = $('addrList');
addrBox.hidden = !!publicBaseConfigured();
if (addrBox.hidden) {
/* Still draw the code itself — it is the only thing left in this panel. */
const only = shareBase();
$('qr').src = only
? 'http://127.0.0.1:' + state.local.port + '/qr.svg?url=' + encodeURIComponent(only + '/')
: '';
renderQrWarning();
renderOperatorUrl();
return;
}
/* Mark the row the QR is actually using, not the row that was saved — when
a lease moves those are two different things and the saved one is gone. */
@@ -3607,8 +3663,40 @@ $('copyAddr').addEventListener('click', () => {
.catch((err) => log('could not copy: ' + err.message, 'err'));
});
$('openPoster').addEventListener('click', () => {
window.open('http://127.0.0.1:' + state.local.port + '/poster', '_blank');
/*
* The QR itself, saved as a file, because the poster people actually print
* 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', async () => {
const base = shareBase();
if (!base) { log('no address to encode yet', 'err'); return; }
/*
* 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();
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');
@@ -3665,7 +3753,8 @@ function renderBuildTag() {
}
function renderOperatorUrl() {
const base = shareBase();
/* Deliberately lanBase, not shareBase: over a proxy /operator is a 404. */
const base = lanBase();
$('opPageUrl').textContent = base ? base + '/operator' : 'no network';
}
@@ -3968,6 +4057,7 @@ async function loadOperator() {
try {
const j = await opApi('/api/config');
opConfig = j.config;
renderShare(); /* publicUrl may have just arrived */
clearTimeout(opLoadTimer); opLoadTimer = null; // it answered; stop retrying
opRetryMs = 0;
renderOpLanguages();
@@ -4022,6 +4112,7 @@ async function opSave(patch, okMsg) {
try {
const j = await opApi('/api/config', { method: 'POST', body: JSON.stringify(patch) });
opConfig = j.config;
renderShare(); /* publicUrl may have just arrived */
if (okMsg) log(okMsg, 'ok');
return j;
} catch (e) { log('could not save: ' + e.message, 'err'); throw e; }
+1 -1
View File
@@ -433,7 +433,7 @@
<div id="addrList" class="addrlist"></div>
<div class="railbtns">
<button class="btn btn-primary" id="copyAddr">Copy address</button>
<button class="btn" id="openPoster">Printable poster</button>
<button class="btn" id="saveQr">Save QR image</button>
</div>
<h3 class="subhead">Operator page</h3>
+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": 121,
"buildNumber": 124,
"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": "121",
"buildVersion": "124",
"win": {
"target": [
{