Picked up #53 (Priority/High, the best use of an attached phone). It
does not reproduce on current main; what it did turn up is a real,
platform-independent defect in the same component, which is what this PR
fixes.
Commits
commit
issue
what
fe1fbef fix(player): give the seek bar's interval one owner
Found by yonlu reading the component during the #122–#127 player audit,
split out because it is neither Android-specific nor #53's symptom.
handleInput() called stopProgress() and mutated no reactive
state, so Lit scheduled no update, updated() never ran, and the tail
of updated() that restarts the interval never executed. Only a change event or the next backend report could bring it back — so any input that never commits froze the interpolation: a drag cancelled
outside the element, a pointer taken by a scroll, a touch on the track
treated as a scrub. All ordinary gestures on a phone. While playing, the
1 Hz report papered over it within a second; with reports not arriving
it was permanent.
updated() owns the interval now, so one place decides whether it runs,
and handleChange no longer starts it directly.
The risk the fix introduces is the interesting part. A flag set on input can strand, and that would turn a stall of up to a second into a
permanent one — the exact failure being removed. change is the
ordinary end of a drag; pointerup/pointercancel/touchend/ touchcancel on the document are the ends that are not, attached with
the drag and dropped with it (and on disconnect), because the pointer is
routinely released outside the element it started in.
Second half, same fix: a report arriving mid-drag used to overwrite seekValue and pull the thumb out from under the finger once a second.
It is skipped while dragging, and its seq is deliberately left
unrecorded so the first report after the drag still counts as fresh.
Verification
The guards are exercised — all three
Not just "the suite passes". Reverting the component and re-running:
× keeps ticking after a drag that never commits
× leaves the thumb where the finger is while a drag is live
Tests 2 failed | 38 passed (40)
The third guards the stranding failure mode the fix introduces, so it
cannot fail against the old code. Removing the endDrag() from handleChange to simulate a stranded flag:
× takes the backend back as the authority once the drag commits
Tests 1 failed | 39 passed (40)
On the device (Chrome 113, the engine at risk)
Mid-drag — holds its value, interval stopped, reports ignored:
A real adb input swipe across the bar also leaves it ticking
(14 → 16 → 20).
The suite
make lint (3 configs, 0 issues), make test (3 configs), make ui-test (952 tests, was 949), make e2e (187 passed), tsc --noEmit in both packages, make css-check, make bindings-check, make skill-check.
Formalities: everything except make ui-test and make skill-check.
This branch changes one TypeScript component, one test file and two
documents — no Go, no SQL, no bindings, no CSS — so lint, test, e2e,
css-check and bindings-check could not have been affected. Run and
reported anyway so the claim is checkable.
seek-bar.ts and player-store.ts are byte-identical to v0.3.1,
the release on the reporter's phone — every change in that area since is
in backend/player/, which is where the #122–#127 audit landed, and
those shipped in v0.4.0.
Four scenarios on that phone against main, reading the component's own
state rather than a screenshot: mount-while-playing (28/240),
open-then-play (7/240), tap on the track (50 → 55), and an activity
recreation mid-playback (same pid, 30 → 35). All track correctly.
Reverting onlybackend/player/ to v0.3.1 reproduces a real defect
on the same phone — six seconds into a 20-second file with no database
row, played after a 240-second one, the bar reads 01:27 of 240.
That is #125's stale trackLengthMs, fixed at HEAD. But its shape is
not quite #53's: the fraction stays roughly right while the absolute
numbers lie, so it presents as a wrong clock rather than a handle pinned
at zero. That difference is exactly why this is a question for the
reporter and not a close.
#162 — make android-emulator's boot wait can be satisfied by an
unrelated attached device (from the #159 session).
Also worth knowing
#159's fix got its real-device outing here: wails3 task android:run:device was used repeatedly against a phone carrying the
released app, and app.yellowjacket v0.3.1 / firstInstallTime 2026-08-17 23:35:56 is unchanged. That is the task that used to
uninstall it.
The documentation commit carries the thing that cost the most time: the
runtime call does not go over HTTP on Android. The WebView cannot
deliver a fetch() POST body to shouldInterceptRequest, so v3 routes
runtime calls through the addJavascriptInterface bridge — which means .playwright/init-events.js does not transfer to the device, and its /wails/runtime POST fails with missing object value, an error that
reads like a wrong payload shape and is actually no body at all. The
working recipe is now in android-tier.md, and it is what made the four
measurements above possible.
Picked up #53 (`Priority/High`, the best use of an attached phone). It
does **not** reproduce on current `main`; what it did turn up is a real,
platform-independent defect in the same component, which is what this PR
fixes.
## Commits
| commit | issue | what |
|---|---|---|
| `fe1fbef` fix(player): give the seek bar's interval one owner | **#164** | `updated()` owns the interval; the drag is `@state`; reports skipped mid-drag; 3 tests |
| `67eeb75` docs(android): the device can be driven, not just looked at | — | how to call a binding on the phone, and the #53 measurements |
**#53 is deliberately not closed.** Full measurement posted on it,
`Status/Need More Info`, unassigned — see below.
## The fix (#164)
Found by yonlu reading the component during the #122–#127 player audit,
split out because it is neither Android-specific nor #53's symptom.
`handleInput()` called `stopProgress()` and mutated **no reactive
state**, so Lit scheduled no update, `updated()` never ran, and the tail
of `updated()` that restarts the interval never executed. Only a
`change` event or the next backend report could bring it back — so **any
`input` that never commits froze the interpolation**: a drag cancelled
outside the element, a pointer taken by a scroll, a touch on the track
treated as a scrub. All ordinary gestures on a phone. While playing, the
1 Hz report papered over it within a second; with reports not arriving
it was permanent.
`updated()` owns the interval now, so one place decides whether it runs,
and `handleChange` no longer starts it directly.
**The risk the fix introduces is the interesting part.** A flag set on
`input` can strand, and that would turn a stall of up to a second into a
permanent one — the exact failure being removed. `change` is the
ordinary end of a drag; `pointerup`/`pointercancel`/`touchend`/
`touchcancel` on the *document* are the ends that are not, attached with
the drag and dropped with it (and on disconnect), because the pointer is
routinely released outside the element it started in.
Second half, same fix: a report arriving mid-drag used to overwrite
`seekValue` and pull the thumb out from under the finger once a second.
It is skipped while dragging, and its `seq` is **deliberately left
unrecorded** so the first report after the drag still counts as fresh.
## Verification
### The guards are exercised — all three
Not just "the suite passes". Reverting the component and re-running:
```
× keeps ticking after a drag that never commits
× leaves the thumb where the finger is while a drag is live
Tests 2 failed | 38 passed (40)
```
The third guards the stranding failure mode the fix introduces, so it
cannot fail against the *old* code. Removing the `endDrag()` from
`handleChange` to simulate a stranded flag:
```
× takes the backend back as the authority once the drag commits
Tests 1 failed | 39 passed (40)
```
### On the device (Chrome 113, the engine at risk)
Mid-drag — holds its value, interval stopped, reports ignored:
```
{"seekValue":100,"timerID":-1,"prevSeq":31,"elapsed":"01:40"}
```
On release — adopts the backend's real position and resumes ticking:
```
1s: {"seekValue":38,"timerID":75,"elapsed":"00:38"}
5s: {"seekValue":41,"timerID":83,"elapsed":"00:41"}
```
A real `adb input swipe` across the bar also leaves it ticking
(14 → 16 → 20).
### The suite
`make lint` (3 configs, 0 issues), `make test` (3 configs), `make
ui-test` (**952** tests, was 949), `make e2e` (187 passed), `tsc
--noEmit` in both packages, `make css-check`, `make bindings-check`,
`make skill-check`.
**Formalities:** everything except `make ui-test` and `make skill-check`.
This branch changes one TypeScript component, one test file and two
documents — no Go, no SQL, no bindings, no CSS — so lint, test, e2e,
css-check and bindings-check could not have been affected. Run and
reported anyway so the claim is checkable.
## Why #53 is not closed here
`seek-bar.ts` and `player-store.ts` are **byte-identical** to `v0.3.1`,
the release on the reporter's phone — every change in that area since is
in `backend/player/`, which is where the #122–#127 audit landed, and
those shipped in **v0.4.0**.
Four scenarios on that phone against `main`, reading the component's own
state rather than a screenshot: mount-while-playing (28/240),
open-then-play (7/240), tap on the track (50 → 55), and an activity
recreation mid-playback (same pid, 30 → 35). All track correctly.
Reverting **only** `backend/player/` to v0.3.1 reproduces a real defect
on the same phone — six seconds into a 20-second file with no database
row, played after a 240-second one, the bar reads **`01:27` of 240**.
That is #125's stale `trackLengthMs`, fixed at HEAD. But its *shape* is
not quite #53's: the fraction stays roughly right while the absolute
numbers lie, so it presents as a wrong clock rather than a handle pinned
at zero. That difference is exactly why this is a question for the
reporter and not a close.
## Filed on the way past
- **#164** — the defect this PR fixes.
- **#162** — `make android-emulator`'s boot wait can be satisfied by an
unrelated attached device (from the #159 session).
## Also worth knowing
`#159`'s fix got its real-device outing here: `wails3 task
android:run:device` was used repeatedly against a phone carrying the
released app, and `app.yellowjacket` v0.3.1 / `firstInstallTime
2026-08-17 23:35:56` is unchanged. That is the task that used to
uninstall it.
The documentation commit carries the thing that cost the most time: **the
runtime call does not go over HTTP on Android.** The WebView cannot
deliver a `fetch()` POST body to `shouldInterceptRequest`, so v3 routes
runtime calls through the `addJavascriptInterface` bridge — which means
`.playwright/init-events.js` does not transfer to the device, and its
`/wails/runtime` POST fails with `missing object value`, an error that
reads like a wrong payload shape and is actually no body at all. The
working recipe is now in `android-tier.md`, and it is what made the four
measurements above possible.
Closes #164
`handleInput()` called `stopProgress()` and mutated no reactive state,
so Lit scheduled no update, `updated()` never ran, and the tail of
`updated()` that restarts the interval never executed. Only a `change`
event or the next backend report could bring it back — so an `input`
that never commits froze the interpolation: a drag cancelled outside
the element, a pointer taken by a scroll, or a touch on the track
treated as a scrub, all ordinary gestures on a phone. While playing the
1 Hz report papered over it within a second; with reports not arriving
it was permanent.
The drag is `@state` now and `updated()` decides whether the interval
runs, so there is one place that knows. `handleChange` no longer starts
it directly for the same reason.
A flag set on `input` can strand, which would turn a stall of up to a
second into a permanent one — the failure this removes. `change` is the
ordinary end; `pointerup`/`pointercancel`/`touchend`/`touchcancel` on
the document are the ends that are not, attached with the drag and
dropped with it, because the pointer is routinely released outside the
element it started in.
The other half is that a report arriving mid-drag used to overwrite
`seekValue` and pull the thumb out from under the finger once a second.
It is skipped while dragging, and its seq is deliberately left
unrecorded so the first report after the drag still counts as fresh.
Three tests, all exercised against the fault: two fail on the old
component, and the third fails if the drag flag is left set — which is
the failure mode the fix introduces and the listeners exist to prevent.
Verified on the device too (Chrome 113): mid-drag the bar holds its
value and ignores reports, and on release it adopts the backend's real
position and resumes ticking.
Closes#164
The runtime call does not go over HTTP on Android — the WebView cannot
deliver a fetch() POST body to shouldInterceptRequest, so v3 routes
runtime calls through the addJavascriptInterface bridge. Two things
follow that cost an hour each before the v3 source was read:
`.playwright/init-events.js` does not transfer to the device (its
outbound half hooks fetch, and a POST to /wails/runtime answers
"missing object value" — which reads like a wrong payload and is the
interceptor getting no body at all), and hooking fetch from an eval is
too late on any platform because the bundle captured its reference at
module scope.
The recipe that does work goes in, along with how to get audio onto the
phone (scoped storage silently swallows a push into
/sdcard/Android/data/<pkg>/files, and the fixtures are 2 seconds long,
which is useless for watching a seek bar) and the permission dialog a
reinstall raises, which looks exactly like the app failing to start.
NOTES.md takes the #53 measurements: that its frontend is byte-identical
to the v0.3.1 the phone carries, that the symptom does not reproduce on
main in four scenarios, and that reverting only backend/player/ to
v0.3.1 reproduces #125 instead — with the shim that makes that a
ten-minute experiment rather than a full checkout.
logan
merged commit c19a806298 into main2026-08-20 21:31:17 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Picked up #53 (
Priority/High, the best use of an attached phone). Itdoes not reproduce on current
main; what it did turn up is a real,platform-independent defect in the same component, which is what this PR
fixes.
Commits
fe1fbeffix(player): give the seek bar's interval one ownerupdated()owns the interval; the drag is@state; reports skipped mid-drag; 3 tests67eeb75docs(android): the device can be driven, not just looked at#53 is deliberately not closed. Full measurement posted on it,
Status/Need More Info, unassigned — see below.The fix (#164)
Found by yonlu reading the component during the #122–#127 player audit,
split out because it is neither Android-specific nor #53's symptom.
handleInput()calledstopProgress()and mutated no reactivestate, so Lit scheduled no update,
updated()never ran, and the tailof
updated()that restarts the interval never executed. Only achangeevent or the next backend report could bring it back — so anyinputthat never commits froze the interpolation: a drag cancelledoutside the element, a pointer taken by a scroll, a touch on the track
treated as a scrub. All ordinary gestures on a phone. While playing, the
1 Hz report papered over it within a second; with reports not arriving
it was permanent.
updated()owns the interval now, so one place decides whether it runs,and
handleChangeno longer starts it directly.The risk the fix introduces is the interesting part. A flag set on
inputcan strand, and that would turn a stall of up to a second into apermanent one — the exact failure being removed.
changeis theordinary end of a drag;
pointerup/pointercancel/touchend/touchcancelon the document are the ends that are not, attached withthe drag and dropped with it (and on disconnect), because the pointer is
routinely released outside the element it started in.
Second half, same fix: a report arriving mid-drag used to overwrite
seekValueand pull the thumb out from under the finger once a second.It is skipped while dragging, and its
seqis deliberately leftunrecorded so the first report after the drag still counts as fresh.
Verification
The guards are exercised — all three
Not just "the suite passes". Reverting the component and re-running:
The third guards the stranding failure mode the fix introduces, so it
cannot fail against the old code. Removing the
endDrag()fromhandleChangeto simulate a stranded flag:On the device (Chrome 113, the engine at risk)
Mid-drag — holds its value, interval stopped, reports ignored:
On release — adopts the backend's real position and resumes ticking:
A real
adb input swipeacross the bar also leaves it ticking(14 → 16 → 20).
The suite
make lint(3 configs, 0 issues),make test(3 configs),make ui-test(952 tests, was 949),make e2e(187 passed),tsc --noEmitin both packages,make css-check,make bindings-check,make skill-check.Formalities: everything except
make ui-testandmake skill-check.This branch changes one TypeScript component, one test file and two
documents — no Go, no SQL, no bindings, no CSS — so lint, test, e2e,
css-check and bindings-check could not have been affected. Run and
reported anyway so the claim is checkable.
Why #53 is not closed here
seek-bar.tsandplayer-store.tsare byte-identical tov0.3.1,the release on the reporter's phone — every change in that area since is
in
backend/player/, which is where the #122–#127 audit landed, andthose shipped in v0.4.0.
Four scenarios on that phone against
main, reading the component's ownstate rather than a screenshot: mount-while-playing (28/240),
open-then-play (7/240), tap on the track (50 → 55), and an activity
recreation mid-playback (same pid, 30 → 35). All track correctly.
Reverting only
backend/player/to v0.3.1 reproduces a real defecton the same phone — six seconds into a 20-second file with no database
row, played after a 240-second one, the bar reads
01:27of 240.That is #125's stale
trackLengthMs, fixed at HEAD. But its shape isnot quite #53's: the fraction stays roughly right while the absolute
numbers lie, so it presents as a wrong clock rather than a handle pinned
at zero. That difference is exactly why this is a question for the
reporter and not a close.
Filed on the way past
make android-emulator's boot wait can be satisfied by anunrelated attached device (from the #159 session).
Also worth knowing
#159's fix got its real-device outing here:wails3 task android:run:devicewas used repeatedly against a phone carrying thereleased app, and
app.yellowjacketv0.3.1 /firstInstallTime 2026-08-17 23:35:56is unchanged. That is the task that used touninstall it.
The documentation commit carries the thing that cost the most time: the
runtime call does not go over HTTP on Android. The WebView cannot
deliver a
fetch()POST body toshouldInterceptRequest, so v3 routesruntime calls through the
addJavascriptInterfacebridge — which means.playwright/init-events.jsdoes not transfer to the device, and its/wails/runtimePOST fails withmissing object value, an error thatreads like a wrong payload shape and is actually no body at all. The
working recipe is now in
android-tier.md, and it is what made the fourmeasurements above possible.
Closes #164