Sound was dead in the app because status 0 is not a failure; and back is a swipe to the RIGHT

Oliver on the first shell build: *"I don't hear any of the games sounds.. daniel the umpire is
synthesized which is horrible and no music is playing on start"* — two independent causes.

- **Opaque fetch.** Capacitor serves the bundle through a custom WKURLSchemeHandler, and `fetch`
  reports those responses as `status: 0, ok: false` with the whole body present and decoding
  perfectly. All three loads in `audio.js` guarded with `if (!r.ok) throw`, so inside the app every
  recorded sound failed at the door: the umpire fell through to the synthesized `say()`, no bed
  started, no sample played. The synthesized sfx kept working, which made it read as a mixing bug
  rather than a loader one. Fixed once, in the shared `bytes()` helper every audio fetch now routes
  through — a status of 0 is the ABSENCE of an HTTP status, so trust it only when there is one and
  a real 404 still falls back. Any future `!r.ok` on a bundle-relative fetch is this same bug.
- **The ring switch.** WKWebView's WebAudio runs in the SoloAmbient session, which the hardware
  silent switch mutes outright — a silent game on a phone that lives on silent, with nothing on
  screen to explain it. AppDelegate now sets AVAudioSession `.playback`, the games category.

Confirmed in the shell: the bed reaches PLAYING, the context is `running` with no gesture at all
(autoplay really is allowed in WKWebView, as LATER predicted), and `say()` never fires. On the web,
a genuinely missing file still falls back — checked both.

And the swipe flips: **right goes back**, the way iOS's own pop gesture goes (*"that's the iOS
way"*). Left now does nothing. Noted in PROJECT.md that on a platform gesture the platform's
convention beats the literal wording of the brief, and that is worth saying up front.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-20 17:18:47 +02:00
1 parent d71a26831a
commit 76ea1d0d79
6 files changed
+67 -20

No files matched your search

+34 -9
View File
@@ -2582,15 +2582,40 @@ inside a `<script type="module">` is a hard 404 the moment the same file is serv
purpose — iOS already routes these through the system's own vibration setting.
**Untested on hardware.** The simulator has no Taptic Engine, so every weight and gap below is a
guess on paper and needs its own phone round before any of it is assumed good.
- **SWIPE LEFT GOES BACK** in the menus. Every screen but the splash wears exactly one chevron, and
that button already knows where back is for its own screen — so the gesture finds the visible
`.chev` and clicks it rather than keeping a second map of the wizard that would drift the first
time a stage is added. Threshold 60 px, horizontal-dominant so the stats and board screens still
scroll. The `touchend` listener is deliberately **not passive**: a swipe that starts and ends
inside one of the big poster cards also produces that card's click, and `preventDefault` is what
stops the screen answering forward in the same frame the chevron walked it back.
**iOS convention is the opposite** (the system pop gesture drags rightward from the left edge).
Built to the brief as asked; flipping it is the sign on one comparison in `main.js`.
- **SWIPE RIGHT GOES BACK** in the menus — the direction iOS's own pop gesture goes. (Built leftward
first, to the letter of the brief; Oliver flipped it the same hour: *"that's the iOS way"*. The
lesson is cheap and worth keeping: on a platform gesture, the platform's convention beats the
literal wording of the request — say so up front rather than shipping it and flagging it after.)
Every screen but the splash wears exactly one chevron, and that button already knows where back is
for its own screen — so the gesture finds the visible `.chev` and clicks it rather than keeping a
second map of the wizard that would drift the first time a stage is added. Threshold 60 px,
horizontal-dominant so the stats and board screens still scroll. The `touchend` listener is
deliberately **not passive**: a swipe that starts and ends inside one of the big poster cards also
produces that card's click, and `preventDefault` is what stops the screen answering forward in the
same frame the chevron walked it back.
- **SOUND, and it was broken twice over on the first shell build.** Oliver, minutes after it ran:
*"I don't hear any of the games sounds.. daniel the umpire is synthesized which is horrible and no
music is playing on start"*. Two independent causes, both now fixed, both worth knowing before the
next plugin:
1. **`fetch` responses from the app bundle are OPAQUE.** Capacitor serves `capacitor://localhost`
through a custom `WKURLSchemeHandler`, and `fetch` reports those as `status: 0, ok: false` —
with the full body present and decoding perfectly. `audio.js` guarded all three of its loads
with a plain `if (!r.ok) throw`, so inside the app EVERY recorded sound failed at the door: the
umpire fell through to the synthesized `say()`, no bed ever started, no sample ever played.
The synthesized sfx kept working, which made it read as a mixing bug rather than a loader one.
Fixed once, in the shared `bytes()` helper every audio fetch now routes through: **a status of
0 is the absence of an HTTP status, not a failure** — trust the status only when there is one,
so a real 404 still falls back. **Any future `!r.ok` guard on a bundle-relative fetch is this
same bug.** (`stats.js` is fine: its board calls go to the live host over real HTTPS.)
2. **The ring switch muted everything on device.** WKWebView's WebAudio runs in the SoloAmbient
session, which the hardware silent switch kills outright — a completely silent game on a phone
that lives on silent, with nothing on screen to explain it. `AppDelegate` now sets
`AVAudioSession .playback`, the games-and-media category. That is a shell-only fix; the web
build has no say in it, and it is a good part of why the shell is worth having for sound.
Confirmed in the shell after both fixes: the bed reaches `PLAYING`, the context is `running`
**with no gesture at all** (autoplay really is allowed in WKWebView, as LATER predicted), and the
`say()` fallback never fires. On the web, a genuinely missing file still falls back — checked.
- **The app has no server behind it.** Inside the shell the page is served from the bundle
(`capacitor://localhost`), so `fetch('api/scores')` and the netplay socket would have resolved
against the app's own files. `config.NATIVE`/`config.API` and one branch in `net.js` point the