Files
hansdezwartandClaude Opus 5 3a7eb8c612 Save the match every second instead of waiting to be told
A force-quit deep into a turn came back at the clock's last-turn-switch value,
losing the whole interrupted turn. The tell was the delay counter returning at
a full 12: the newest saved state was the one tap() writes, and tap() sets
turnAt and calls saveGame() in the same breath, so it carries held ~ 0.
Restoring it reproduces the moment of the turn switch exactly.

The two triggers meant to save at quit time — visibilitychange and pagehide —
do not fire in Android's WebView. MainActivity's onPause() forwards to
web.onPause() under a comment claiming that is what makes visibilitychange
fire. It isn't: document visibility follows the WebView's view and window
visibility, not that call. The forwarding is still right for stopping timers
and audio, but the saved match should never have depended on it.

This is not new. It has almost certainly been broken since 1.0, and the release
check could not catch it, because for the first 12 seconds of a turn the delay
absorbs everything and render() never touches st.reserve. A clock that has lost
its place is indistinguishable from one that is correctly sitting still, so
every force-quit test that quit soon after a turn change passed. RELEASING.md
now says to let the turn run 40 seconds first, which is the only version of
that check worth running.

So the page stops waiting to be told. A timer writes the match once a second
while a clock is running — a timer rather than a hook in loop(), because
requestAnimationFrame stops when the page is hidden, which is exactly when this
matters. blur joins the listeners too: a different signal, and one that does
fire when the task switcher opens. At most a second can now be lost, which is
well inside the delay nobody is charged for anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:07:18 +02:00

166 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Releasing a new version
The clock lives in two places: the web app in `public_html/`, which you host
yourself, and the Android app on F-Droid, which is built from a git tag.
**Everything F-Droid needs comes from a tag in this repository.** After the first
release was accepted there is nothing to do in F-Droid's own repo — their bot
watches the tags here and writes the build entry itself. Don't open a merge
request against `fdroiddata` for a new version.
Worked example below bumps 1.0 to 1.1. Substitute your own numbers.
## 1. Make the change
Both versions share one copy of the app. Edit `public_html/index.html` and the
Android build picks it up automatically — the Gradle build reads `public_html/`
directly rather than keeping a second copy, so the two can't drift.
If you changed anything visible, bump the service worker cache or installed web
users will keep the old page:
```
# public_html/sw.js
const CACHE = "bgclock-v18"; # any new value; it only has to differ
```
## 2. Bump the version
In `android/app/build.gradle.kts`:
```kotlin
versionCode = 2 // an integer, +1 every release, never reused
versionName = "1.1" // what people see
```
`versionCode` is what Android uses to decide one version is newer than another.
It must go up every single release, even for a one-character fix.
## 3. Write the changelog
A new file named after the **versionCode**, not the version name:
```
fastlane/metadata/android/en-US/changelogs/2.txt
```
Maximum 500 characters. This is what shows up in F-Droid's "What's New".
## 4. Build it and put it on your phone
```bash
export ANDROID_HOME="$HOME/Android/Sdk" # already in ~/.bashrc
cd android
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
./gradlew --stop # the daemon holds ~500 MB otherwise
```
Play a real game before tagging. Worth checking specifically: start a match,
make some moves, force-quit from the task switcher and reopen — everything
should come back, paused. That is the check that proves the app's storage is
working, and it fails silently rather than loudly.
**Let the turn run at least 40 seconds before you quit.** This is the whole
check, and doing it any faster proves nothing: for the first 12 seconds of a
turn the delay absorbs everything and the reserve clock is *supposed* to sit
still, so a clock that has lost its place looks exactly like one that hasn't.
Quit well past the delay and the time must come back lower than it was at the
turn switch — and the delay counter must not be sitting at a full 12. That is
how a broken save hid from this check from 1.0 to 1.3.
## 5. Commit, tag, push
```bash
git add -A
git commit -m "…"
git tag -a v1.1 -m "Backgammon Clock 1.1"
git push
git push origin v1.1
```
Keep tags in the `v1.1` shape for consistency, but nothing depends on it:
`UpdateCheckMode: Tags` with no pattern matches every tag whatever it's called,
and F-Droid's bot resolves whichever tag it finds to a commit hash before it
writes the build entry.
Never move or delete a tag that has been pushed — F-Droid may already have built
it. If a release is wrong, bump the version and release again. (The recipe pins
a hash, so a moved tag can't retroactively change a version they already built;
it would just leave your repo disagreeing with what's on people's phones.)
**If you ever write a build entry by hand**, its `commit:` must be a full 40-char
commit hash — never a tag, never a branch. Tags are mutable, so a tag there means
the thing F-Droid builds and signs can change under them, and they reject it on
sight. This came up on the very first submission and is easy to get wrong,
because `fdroid lint` doesn't check it under `UpdateCheckMode: Tags` — the
pipeline goes green and a human catches it days later. Get the hash with:
```bash
git rev-parse v1.1^{commit} # ^{commit}, or an annotated tag gives you the
# tag object's hash instead of the commit's
```
This doesn't come up in a normal release: the bot writes the entry, and the bot
writes hashes.
## 6. Wait
Their bot notices the new tag, adds a build entry, and the build server picks it
up on its next cycle. Expect a day or two. Nothing to do.
## Deploying the web version
Separate from all of the above, and manual: upload the six files in
`public_html/``index.html`, `manifest.webmanifest`, `sw.js`, `icon-192.png`,
`icon-512.png`, `icon-maskable-512.png` — to the web host. `README.md` and
`RELEASING.md` stay out of it; they live in the repo root for that reason.
## If the screenshots need redoing
```bash
python3 tools/screenshots.py
```
That rewrites all five in
`fastlane/metadata/android/en-US/images/phoneScreenshots/` at 1170×2532. They
are rendered from `public_html/index.html` with headless Chromium rather than
taken on a phone, so anything that changes the look of the app is a reason to
re-run it.
**Near-exact, not exact.** Two runs of identical code differ by a few bytes in
shots 1 and 4: a 4×13 pixel sliver at the delay bar's fill edge, where the
`scaleX` boundary lands mid-pixel and the rasteriser rounds it differently from
one run to the next. Max channel difference 11, invisible. So a `git diff` after
re-running is not evidence that anything changed — compare the pixels before
believing it, and `git checkout` the files back if that sliver is all it is.
The script sets each scene by seeding `localStorage` before the app boots, so
the app renders its own saved state rather than having the DOM poked from
outside. Editing a scene means editing the `SCENES` table at the top.
Three traps, all of which fail quietly, and all of which the script already
handles — they are written down here because they cost an afternoon each:
- The capture lands whenever the virtual-time budget runs out, not when your
script finishes — so freeze the render loop (`requestAnimationFrame = () => 0`)
once the frame you want is on screen.
- CSS transitions don't advance under `--virtual-time-budget`, so a panel caught
mid-transition photographs in its *old* colour. Disable transitions in the
screenshot build.
- Snap-confined Chromium cannot write into hidden directories — not `~/.cache`,
not any dot-directory, not the private `/tmp` it is handed. It fails with a
bare "Permission denied" and no hint as to why. Both the HTML it reads and the
PNG it writes have to sit in a plainly-named directory.
## Reference
- App ID: `nl.hansdezwart.bgclock` — fixed forever; changing it makes a new app.
- F-Droid listing text: `fastlane/metadata/android/en-US/` (title ≤50 chars,
summary ≤80, description ≤4000, changelog ≤500). Only these HTML tags work in
the description: `b big blockquote br cite em i li ol small strike strong sub
sup tt u ul`.
- The original submission: https://gitlab.com/fdroid/fdroiddata/-/merge_requests/45478
- F-Droid signs the APK with their key, not yours. Reproducible builds were
declined at submission and can't be enabled later for this app ID.