The publishing guide covered the CI pipeline but never mentioned that
production access is gated behind a closed test: 12 testers opted in
continuously for the preceding 14 days, for personal developer accounts
created after 13 Nov 2023.
Record what the window actually requires -- install under the matching
account, real devices, an unbroken 14 days, and the feature-usage and
feedback evidence the application form asks for.
Flag the track mismatch: android-release.yml uploads to the internal
track, which does not count toward the requirement. Builds have to
reach the closed track, either by promoting each release in the Console
or by repointing tracks: in the workflow.
Plans were split across three trees: plans/, web/plans/, and
android/plans/. Merge them into plans/ at the repo root.
Sweep the shipped Android experience pass and the two reports for it,
carrying the residual forward into plans/todo.md: the device QA
checklist (open only because this environment has no device or
emulator), the clipped web PWA icons defect, and the untested native
assumptions.
Merge android/plans/todo.md in the same pass, dropping items the
experience pass already delivered -- haptics, back button, safe-area
insets, launcher icon, and splash -- each verified against
MainActivity.java, web/src/app.css, and the res/ tree. Correct two
stale claims while merging: web/ is a plain directory in this monorepo,
not a git submodule, and the serialize-javascript override is held by
@rollup/plugin-terser rather than workbox-build.
The retired native Kotlin/Compose port plan moves as-is; it is
superseded by the Capacitor wrapper but not yet swept.
Replace pnpm-lock.yaml with package-lock.json and drop pnpm-workspace.yaml.
The three security overrides move to package.json#overrides, which npm reads
natively; allowBuilds for esbuild is unnecessary because npm runs dependency
build scripts by default.
android/build:web drove the web build through corepack pnpm, so it would have
broken once web/pnpm-lock.yaml was gone. It now uses npm --prefix.
Fresh resolution picks up patched versions the old lockfile had pinned below,
so npm audit reports no advisories where pnpm audit reported eight.
XML forbids "--" inside a comment, and both comments referenced the CSS
custom property by its "--background" name, so mergeDebugResources
failed to parse them.
Keeps the reasoning next to the change: the brainstorm that scoped it,
the phase plan, and an implementation report carrying the device QA
checklist for everything that cannot be verified without a phone.
Records what a maintainer cannot infer from the wrapper: why VIBRATE is
declared when the web build needs no equivalent, when the wake lock is
held and why it re-acquires on visibilitychange, how back maps to
overlay history, and why the textZoom pin and the in-app size setting
ship together.
Also documents where launcher art comes from, that regenerating needs
Roboto Condensed converted out of the .woff fontsource ships, and the
clipping bug still present in source.svg.
The "Why no INTERNET permission?" section is unchanged — the offline
guarantee still holds.
The APK shipped the Capacitor template's artwork: the launcher icon,
round icon, adaptive foreground and splash were all the stock blue mark
on a teal grid plate, and the adaptive background was the template's
white. The brand icon existed only under web/static/icons, so the
published app showed another project's logo in the launcher and on cold
start.
Regenerated from web/static/icons/source.svg — legacy, round, adaptive
foreground and a monochrome layer for Android 13+ themed icons, at all
five densities. The adaptive background becomes a vector carrying the
rose → amber gradient.
source.svg sets font-size="240", which renders "Lô tô" wider than its
512px canvas, and the committed PNGs under web/static/icons were
rendered without Roboto Condensed so a wider fallback pushed the text
past the edge — they are visibly clipped on both sides. The Android art
is rendered at a corrected size with the real font. The web PWA icons
still carry the original bug.
The splash becomes a layer-list over a themed colour, with a night
variant, replacing eleven density-specific PNGs; API 31+ uses
windowSplashScreenBackground / windowSplashScreenAnimatedIcon instead.
The app has a full dark theme but the splash always flashed light.
Drops the now-unused template robot vector and the white
@color/ic_launcher_background.
Several behaviours diverged between the web build and the Capacitor
wrapper, none of them reproducible on a desktop browser.
- Declare VIBRATE. PlayerBoard calls navigator.vibrate() on every cell
tap; a WebView app must hold the permission itself, where a browser
holds it on the page's behalf. Taps were silent in the APK.
- Hold a screen wake lock while numbers remain to be called. Auto-call
advances on a timer with no touch input, so a round could run 15
minutes untouched — long enough for the display to sleep and the
WebView to throttle the interval. Android drops the lock whenever the
page hides, so wake-lock.js re-acquires on visibilitychange.
- Handle back through OnBackPressedCallback. Android 16 (targetSdk 36)
no longer calls onBackPressed() nor dispatches KEYCODE_BACK. Each open
overlay pushes one history entry, so "the WebView can go back" means
"an overlay is open": back closes the bingo modal or settings sheet,
and only at the root does it confirm before quitting. Browsers get the
overlay behaviour too.
- Inset content by env(safe-area-inset-*) with viewport-fit=cover.
targetSdk 36 forces edge-to-edge, so the WebView painted under the
status bar and gesture nav. Also fixes the iOS PWA notch.
- Pin the WebView's textZoom to 100 and add a "Cỡ chữ bảng" setting.
The player card is a fixed 9-column grid that clips at large system
font scales. Pinning the zoom overrides an accessibility control, so
the app now owns an equivalent one; the two belong together.
- Point the volume rocker at the media stream, so it works before the
first clip plays.
strings.xml also gains the exit-dialog strings and picks up the
diacritics in app_name.
Both subprojects ship from the same commit, so ordinary CI is now a single
ci.yml; only the tag-driven release stands apart. The web app is built twice
per run — once per base path — and every consumer downloads the artifact
instead of rebuilding, replacing three redundant base-"" builds on main.
Nothing deploys unless the test job is green, and android-release runs the
suite before signing (ci.yml does not fire on tags, so it was the only gap).
Shared toolchain setup moves into composite actions, which puts the web build
and the APK on the same Node version for the first time. The Firebase PR path
was still on npm ci against a stale web/package-lock.json that could resolve a
different tree than pnpm-lock.yaml; drop the lockfile and the npm path with it.
Also: least-privilege permissions widened per job, persist-credentials off on
every checkout, concurrency groups that cancel superseded PRs but never a live
deploy, npm caching for android, and the Firebase action pinned by commit SHA
to match how the release actions were already pinned.
Adds a root README covering both subprojects and the CI matrix, folds the two
identical Apache-2.0 copies into a single root LICENSE, and updates the web
docs that named workflow files by their pre-move paths.
GitHub only runs workflows from .github/workflows at the root, so the six
workflows inherited from the two projects have to live there rather than under
web/ and android/. Each one gets a path filter so web-only changes do not
trigger Android builds, a working directory for its subproject, and a prefixed
filename to keep the two sets apart.
Gradle now sits at android/android, the Firebase action reads its config from
web/ via entryPoint, and the Android checkout no longer needs submodules.
web/ and android/ now live in one repository, so the pinned submodule at
android/loto is redundant. Point Capacitor's webDir and the build script at
../web directly, and drop the pin-bumping workflow from the docs.