12 Commits
Author SHA1 Message Date
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
Waters ChurchandClaude Opus 5 a06cf35cbb A stumble should not colour the desk red for the rest of the service
Build 121. Three things a single 16 s stall did that it should not have.

captionOnlyFinals is a running total for the whole service, and the verdict
tested it for more than zero — so one stale sentence pinned the light red until
the next restart. MEASURED: a campus Mac stalled once at 14:25, recovered
immediately to a 421 ms lag with an empty queue, and still showed red half an
hour later, with the operator quite reasonably reporting it as behind. The
totals still travel for the record; the verdict now asks when it last happened,
inside a two-minute window. A light that cannot go back to green teaches the
desk to ignore it.

That sentence also had its translations skipped. Dropping them is how the app
catches up — the listener has moved on and the time is better spent on what is
behind it. But that reasoning needs a backlog, and the queue was EMPTY: the room
lost a line of the sermon to a catch-up that had nothing to catch up on. It now
skips only when something is actually waiting, and says plainly when it chooses
to translate a late sentence anyway.

The stall itself was not the app. Two Screen Sharing sessions were taking as
much of the machine as the app was, with WindowServer on top — watching a campus
Mac during a service measurably degrades it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 14:32:25 -04:00
Waters ChurchandClaude Opus 5 39dea80433 Do not step down twice for the same backlog
Build 120.

Swapping the speech model did nothing about the queue that caused the swap. The
first of those chunks went straight to a model that had just loaded, took the
full 30 s worker timeout, and tripped the absolute rule — so the ladder fell
again. MEASURED on a campus Mac: large-v3-turbo to small to base in 34 seconds,
landing on a model measurably SLOWER than the one it started from, which is the
opposite of what stepping down is for.

Two changes. A successful step-down now discards the queued chunks: they are
already past the caption-only cutoff, so they cannot be translated in time and
are worth nothing to the room, and it says how many it dropped. And no further
step-down is allowed for twenty seconds, so the new model is judged on sentences
it was actually given rather than on the wreckage of the old one.

Twenty seconds covers a cold first inference while still letting a genuinely
hopeless model fall through the ladder within a sentence or two. Checked against
the real cascade from the log, including that a hung model long after a swap is
still caught.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:54:26 -04:00
Waters ChurchandClaude Opus 5 6f91bc1357 Step down on landing late, not on a deep queue
Build 118. Build 117 got the first half right and the second half wrong.

117 compared the time taken against the audio the chunk held, which was correct,
and then required a queue five deep as proof the machine was losing. Watching a
campus M4 Pro go red disproved that: the queue sat at 2-3 while the lag held at
15 s and the verdict was already red. A machine can be persistently late without
ever stacking work up — it finishes each sentence just slightly too slowly, for
ever, and the backlog never gets deep enough to notice.

The second test is now how far behind the speaker the sentence actually landed,
which is what the operator is complaining about in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:32:37 -04:00
Waters ChurchandClaude Opus 5 92e86e415f Step down when losing ground, not when slower than a constant
Build 117.

A campus M4 Pro sat in the red with nobody listening and no language being
translated — so transcription alone. Measured over SSH: sentences 4.2-4.7 s
against roughly 4-5 s of audio, queue 46 deep, lag 152 s and still climbing.

The mechanism built to catch exactly this could not fire. It triggers past 8 s,
on the reasoning that 8 s means slower than people talk. That held when a chunk
could be 8 s long; chunks are capped at HARD_CUT_MAX, 5.5 s. A 4.4 s sentence is
already losing and never comes near 8 s, so the ladder never stepped down and
the backlog grew without bound.

It now compares against the audio the chunk actually held. Two details are
measured rather than chosen: the margin is 0.85, because the Mini's 4.4 s
against 4.5 s was NOMINALLY faster than real time while still losing — the work
outside that timer (segmenting, the preview model sharing the GPU, the
scheduler) loses on its own. And it requires a backlog of five, so one slow
sentence on a healthy machine touches nothing. Checked against eight cases
including the Mini's real numbers and a healthy M2 Max.

The old absolute rule stays for a model that is genuinely hung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:24:05 -04:00
Waters ChurchandClaude Opus 5 73ec7f1436 Report the app version on /healthz
Build 116.

/healthz has always carried a 'build' field, but it is a hash of the listener
page's assets — a cache-busting token, not a version. So the only way to tell
what a campus Mac was actually running was to install something and watch the
hash move, which is how the last three updates were confirmed. With an updater
in the field, what version a machine is on is the first question anyone asks.

It now answers plainly: version and buildNumber, passed to the server child from
the running bundle. Checking a campus is a single unauthenticated request.

It found something immediately: this development Mac reported appBuild 112 while
the source tree was at 115, because deploying loose files over an installed
bundle leaves its package.json behind. A packaged install reports correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:04:46 -04:00
Waters ChurchandClaude Opus 5 4aa30699c9 Install the update itself, instead of asking for a drag
Build 115. Downloading and then telling the operator to quit, mount and drag
was the friction the updater was supposed to remove.

An app cannot replace itself while running, so the swap is done by a small
script that outlives the process: it waits for us to exit, moves the old bundle
aside, copies the new one in, clears the quarantine flag and relaunches.

Moved aside, not deleted. If the copy fails half way — a full disk, a truncated
download, the volume going away — the old app goes straight back and is
reopened. The failure this is written against is a campus Mac with no working
app and a service in an hour, which is far worse than not updating. Tested in a
sandbox across all three paths: a clean swap, a copy that fails mid-way (old app
restored, no leftovers), and a disk image with no app in it (aborts before
touching the installed one).

It refuses while translation is live: quitting stops every listener's captions
and no update is worth that. Stop translating first.

No sudo — /Applications is admin-writable and the bundle belongs to the
operator. If that is ever untrue the copy fails and the rollback runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 12:50:05 -04:00
Waters ChurchandClaude Opus 5 6753a94e1c Say what is loading the machine when it falls behind
Build 114.

Translation cost is roughly linear in the number of active languages, through
one NLLB process — measured at five languages in 1.9 s. So when a machine is
running behind, the most useful single fact is how many languages it is
carrying, and that number sat one function away from the health verdict and was
never shown.

A campus reported being constantly in the red. The cause was eight languages
with 'let listeners pick any' on, against a pipeline tuned and measured against
one, and finding that took an afternoon of reading logs. The operator page now
says it in the sentence that reports the problem.

Quiet when it cannot help: nothing is added below two languages, and the
'listeners may add more' clause only appears when that setting is actually on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 12:36:30 -04:00
Waters ChurchandClaude Opus 5 bf8a8cc2d3 Build 113
Carries the in-app updater, so this is the last build that has to be installed
by hand: from here a campus Mac can fetch the next one itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 12:19:14 -04:00
Waters ChurchandClaude Opus 5 070c0dfb82 Pull updates from the church's own forge, from inside the app
Settings gains an Updates block: it asks the church Gitea for the newest
release, compares the tag against the running build, and if there is a newer
one shows what changed and offers to download the installer.

Deliberately not electron-updater. That wants a static feed published beside
the artifacts, and this build is ad-hoc signed with no Team ID, so a silent
in-place swap would meet Gatekeeper on a machine nobody is sitting at. And
nothing here happens on its own: a background check that announces itself three
minutes into a sermon is a hazard, so the operator presses the button, and
presses a second one to download. The file lands in Downloads and is revealed;
installing is a drag across when the room is empty.

Tags are v<version>-<build>, compared numerically field by field so 3.9.0 does
not read as newer than 3.23.2 and build 113 beats build 112.

Two things only real testing found. Gitea answers with whatever address it was
reached on, so the release API returned asset URLs on 10.3.8.90 while the forge
is configured by hostname — the same-host guard refused a perfectly good
installer. The URL is now rebuilt on the configured origin, which keeps the
property that matters (this only ever fetches from the server the operator
configured) and survives a forge that knows itself by more than one name. And a
private repository answers 404 rather than 401, so 'no releases published yet'
told the operator the opposite of the truth; without a token that case now names
the remedy instead.

The token is this machine's credential: it stays out of PORTABLE_APP, so it does
not ride along in an exported settings file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 12:09:08 -04:00
Waters ChurchandClaude Opus 5 5d8d3e3527 Add a one-shot diagnostic for the falling-behind case
The operator page turns red on two things — the app going quiet, or lagMs past
12 s with captionOnlyFinals above zero, which is the pipeline running slower
than people talk. Neither says WHY from the outside, and the machine that is
struggling is usually not the machine anyone is sitting at.

tools/diag.sh reads the log and asks the running app for its status, and prints
the four things that separate the causes: what hardware it is, whether it is
swapping, which models it actually chose at startup (a GPU with no fp16 or no
WebGPU at all changes that silently), and how long sentences are really taking.
Read-only throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 11:43:39 -04:00