From 53b33073ac417068f829ee1a4cb883318646701e Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Sun, 12 Jul 2026 01:34:25 +0200 Subject: [PATCH] docs(android): fix plan (dict source, Capacitor 8 / SDK 36 / JDK 21) and record scaffolding progress + native-build notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct ANDROID_PLAN.md: bundled offline dictionaries come from the scrabble-dictionary release (DICT_VERSION), not scrabble-solver; pin the toolchain to Capacitor 8 (compileSdk/targetSdk 36, minSdk 24, JDK 21 — @capacitor/android compiles at Java 21). Add a Progress section marking the scaffolding milestone done, and capture the native-Android build recipe + toolchain gotchas in .claude/CLAUDE.md so a fresh session resumes without re-deriving them. Co-Authored-By: Claude Opus 4.8 --- .claude/CLAUDE.md | 30 +++++++++++++++++++ ANDROID_PLAN.md | 73 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 86 insertions(+), 17 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 9588de4..451a7ad 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -28,6 +28,36 @@ on it**, and prefer the authoritative docs where they overlap. Add to this file - **pnpm corepack pre-flight flakes.** `pnpm exec` / `pnpm check` occasionally abort on a corepack pre-flight. Dodge it by invoking the tool directly: `node_modules/.bin/`. +## Native Android build (Capacitor) + +The native Android app (`ANDROID_PLAN.md`) is a Capacitor 8 wrapper of the `ui` SPA, scaffolded under +`ui/` (a Node project outside `go.work`). Hard-won bring-up facts — verify against current code: + +- **Capacitor 8 needs JDK 21, not 17.** `@capacitor/android` compiles at `VERSION_21`; a JDK 17 Gradle + run dies with `error: invalid source release: 21`. Install sudo-free via the Homebrew **formula** + `brew install openjdk@21` (the `temurin@21` **cask** wants sudo for a system `.pkg`, unusable + non-interactively) + a user symlink into `~/Library/Java/JavaVirtualMachines/` so + `/usr/libexec/java_home -v 21` finds it. JDK 17 stays fine for `sdkmanager`/`avdmanager`. +- **Cap 8 SDK pins:** compileSdk/targetSdk **36**, minSdk **24**, Gradle **8.14.3**, AGP **8.13.0** + (`ui/android/variables.gradle`). Install `platforms;android-36` — Android Studio's newer + `android-36.1` does NOT satisfy compileSdk 36. +- **SDK is Android Studio's** at `~/Library/Android/sdk` (no `cmdline-tools` by default → `brew install + --cask android-commandlinetools`, then `sdkmanager`/`avdmanager --sdk_root=$HOME/Library/Android/sdk`). + Gradle finds it via **`ANDROID_HOME`** (`local.properties` is gitignored, machine-specific). +- **Build recipe:** `cd ui && pnpm build && node_modules/.bin/cap sync android`; then `cd ui/android && + ANDROID_HOME=~/Library/Android/sdk JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew assembleDebug` + → `ui/android/app/build/outputs/apk/debug/app-debug.apk`. Prefer the local `ui/node_modules/.bin/cap` + (dodges the corepack flake). +- **Emulator smoke:** existing AVDs `Pixel_10` / `Pixel_4_Android_10_API_29` / `Pixel_Android_9`; + `emulator -avd `, `adb install -r `, `adb shell monkey -p ru.eruditgame.app -c + android.intent.category.LAUNCHER 1`, `adb exec-out screencap -p > x.png`. The boot wait needs `sleep` + → run it as a **background** Bash task (foreground `sleep` is blocked). +- **`sharp` is whitelisted** in `ui/pnpm-workspace.yaml` (`allowBuilds: sharp: true`) — `@capacitor/assets` + uses it for `pnpm android:assets` (launcher icon/splash); else pnpm 11 raises `ERR_PNPM_IGNORED_BUILDS`. +- **Bundled offline dicts come from the `scrabble-dictionary` release** (`scrabble-dawg-.tar.gz`, + keyed on the `DICT_VERSION` Gitea var — the same source the backend image + CI `curl`), NOT + `scrabble-solver/dawg` (those are the solver's pinned test fixtures). + ## Wire / schema evolution (client ↔ gateway ↔ backend) - **FBS is additive-only.** Add **trailing** fields ("added trailing — backward-compatible"). Never diff --git a/ANDROID_PLAN.md b/ANDROID_PLAN.md index 959ea6f..7bc9c99 100644 --- a/ANDROID_PLAN.md +++ b/ANDROID_PLAN.md @@ -45,6 +45,7 @@ installs away cleanly, and (c) opens offline-first as a soft guest with local pl | Payments in MVP | Hidden (deferred; reuse the distribution flag) | | appId (permanent) | **`ru.eruditgame.app`** | | App display name | **`Эрудит`** (Cyrillic) | +| Toolchain (locked) | **Capacitor 8** (`@capacitor/*` `^8`); **compileSdk/targetSdk 36, minSdk 24**; **JDK 21** (required — `@capacitor/android` compiles at Java 21; AGP 8.13 / Gradle 8.14.3); Android SDK `platforms;android-36` + `build-tools;36.x` | **Conventions:** all code/comments/commits/docs in English. Do NOT put stage/phase numbers in code, commits, or PR titles. Bake design into the main docs (`docs/ARCHITECTURE.md` etc.) — this repo @@ -57,23 +58,52 @@ client-only (no server change). --- +## Progress (as-built) + +Kept current as parts land so a fresh session resumes without re-deriving. Verify against code. + +- **A. Capacitor scaffolding — ✅ DONE & verified (2026-07-12).** Capacitor 8 native project generated + under `ui/android/` (tracked), `@capacitor/*` deps + `capacitor.config.ts`, hardware Back in + `ui/src/lib/native.ts` wired from `App.svelte`. `pnpm build` → `cap sync` → `./gradlew assembleDebug` + builds a debug APK that installs and **cold-launches to the Login screen** on the Pixel_10 emulator. + `svelte-check` 0 errors, `vitest` 584 passed. Build recipe + toolchain gotchas live in + `.claude/CLAUDE.md` → "Native Android build (Capacitor)". + **Remaining sub-item:** owner-supplied launcher icon (`ui/assets/icon.png`) → `pnpm android:assets`. +- **B–G — pending.** + +Open logistics (not code): the launcher icon (above); `~/.claude/bin/gitea-ci-watch.py` must exist +before the first push/PR; RuStore publication prerequisites gate release only. + +--- + ## Prerequisites to start DEVELOPMENT (on the other device) Have all of these before writing code: -- **JDK 17** (Temurin/OpenJDK). +- **JDK 21** (Temurin/OpenJDK) — **required** by Capacitor 8: `@capacitor/android` sets + `sourceCompatibility/targetCompatibility = VERSION_21`, so a JDK 17 Gradle run fails with + `invalid source release: 21`. (Homebrew: `brew install openjdk@21`.) - **Android SDK** — Android Studio (recommended: bundles the SDK manager + AVD emulator) *or* - cmdline-tools. Install: `platform-tools`, `platforms;android-34`, `build-tools;34.0.0`. + cmdline-tools. Capacitor 8 targets **compileSdk/targetSdk 36, minSdk 24**, so install: + `platform-tools`, `platforms;android-36`, `build-tools;36.0.0` (a newer build-tools such as + `36.1.0` is also accepted — AGP treats it as a minimum). Gradle 8.14.3 / AGP 8.13 arrive via the + wrapper `cap add android` generates. - **Node 20+ and pnpm** (via corepack), matching the repo's `ui` toolchain. - **Git**, plus this repo's Gitea access. If this device will push/PR, set up the `tea` CLI login and the CI watcher exactly as the owner's global setup (`~/.claude` tooling: `python3 ~/.claude/bin/gitea-ci-watch.py`, `tea` against `gitea.lan`). - **Both repositories side by side** per `go.work`: clone `developer/scrabble-game` **and** the - sibling `scrabble-solver` next to it. The sibling holds the committed dictionaries - (`scrabble-solver/dawg/{ru_scrabble,ru_erudit,en_sowpods}.dawg`) that offline-first bundles, and - the solver library the backend/CI use. -- **A test target**: a physical Android device with USB debugging **or** an emulator AVD (e.g. Pixel, - API 34). + sibling `scrabble-solver` next to it — the solver **library** the backend/CI consume via the + `go.work` replace. **The bundled dictionaries do NOT come from the solver.** The versioned, + production dictionary set is published by `developer/scrabble-dictionary` as a **release artifact** + `scrabble-dawg-.tar.gz` (one semver label per set); `backend/Dockerfile` and every CI + job already `curl` exactly that tarball, keyed on the shared Gitea variable `DICT_VERSION`. + Offline-first bundles the DAWGs from that same release (see D.1 / E), so the Android build needs + `DICT_VERSION` + network access to the release, **not** `scrabble-solver`. (`scrabble-solver/dawg/*.dawg` + are the solver's committed test fixtures — byte-identical to a build but pinned to the solver + commit, not the versioned production set.) +- **A test target**: a physical Android device with USB debugging **or** an emulator AVD (e.g. a + Pixel AVD; any recent API image). - **No backend needed to build the APK** — it points at production `erudit-game.ru`. A local gateway is only needed to exercise the version gate locally (optional). @@ -152,7 +182,7 @@ it and stay a local guest (do not overlay). Implementation seam: the reconciliat Ordered so each part is independently verifiable. The MVP now includes offline-first, so the sequence is: scaffold → native-correct → gate → offline-first → CI → docs → release. -### A. Capacitor scaffolding +### A. Capacitor scaffolding — ✅ DONE - `ui/package.json`: add deps `@capacitor/core`, `@capacitor/android`, `@capacitor/app`; devDeps `@capacitor/cli`, `@capacitor/assets`. Scripts: `"cap:sync": "cap sync android"`, @@ -339,9 +369,13 @@ and `ui/src/screens/NewGame.svelte:177-431`. This phase makes them reachable on with no network**. 1. **Bundle dictionaries in the APK.** - - New build step `ui/scripts/bundle-dicts.mjs`: copy `../scrabble-solver/dawg/{ru_scrabble,ru_erudit,en_sowpods}.dawg` - → `ui/dist/dict/@.dawg` (dictKey naming). Run only in the native pipeline - (after `pnpm build`, before `cap sync`). Web builds stay slim. + - New build step `ui/scripts/bundle-dicts.mjs`: copy the three DAWGs from the unpacked + **`scrabble-dictionary` release** (`scrabble-dawg-.tar.gz` → a dict dir passed via + `DICT_DIR`; **NOT** `../scrabble-solver/dawg`) → `ui/dist/dict/.dawg` (the + `dictKey(variant, version)` naming the loader's bundled tier fetches). Run only in the native + pipeline (after `pnpm build`, before `cap sync`). Web builds stay slim. `VITE_DICT_VERSION` must + equal the release's `DICT_VERSION` so the bundled `(variant, version)` matches what the loader + requests. - Vite `define`: add `__DICT_VERSION__` from `VITE_DICT_VERSION` (mirrors `__APP_VERSION__`); declare it in `vite-env.d.ts`. - **Loader tier** — `ui/src/lib/dict/loader.ts` `load()`: between the IndexedDB tier (line 73-85) @@ -394,15 +428,20 @@ silently establishes a server guest and lights up online play. New **manual** workflow `.gitea/workflows/android-build.yaml` (mirror `prod-deploy.yaml`'s `workflow_dispatch` + `confirm` gate; from `master`; not on PRs). Steps: -1. Checkout this repo **and the sibling `scrabble-solver`** — copy the checkout + Node/pnpm setup - block from `.gitea/workflows/ci.yaml` (the `ui` job). The sibling is needed for the bundled dicts. +1. Checkout this repo; copy the Node/pnpm setup block from `.gitea/workflows/ci.yaml` (the `ui` job). + Add the **"Fetch dictionary DAWGs"** step verbatim from `ci.yaml` (the `curl … + scrabble-dictionary/releases/download/${DICT_VERSION}/scrabble-dawg-${DICT_VERSION}.tar.gz` + + untar into `${GITHUB_WORKSPACE}/dawg`), keyed on the `DICT_VERSION` Gitea variable — **this**, not + the `scrabble-solver` sibling, is the source of the bundled dicts. (`scrabble-solver` is not needed + by the Android build: `ui` is a Node project outside `go.work`.) 2. `pnpm install --frozen-lockfile`. 3. `pnpm build` with the native env (below), `VITE_APP_VERSION` = `git describe --tags`, `VITE_DICT_VERSION` = the release dict version. -4. `node ui/scripts/bundle-dicts.mjs` (copy the `.dawg` into `ui/dist/dict/`). -5. Setup JDK 17 (`actions/setup-java@v4`, temurin 17). -6. Install Android SDK via `sdkmanager` (pin `platform-tools`, `platforms;android-34`, - `build-tools;34.0.0`); cache the SDK. **No emulator** — assemble only; on-device smoke is manual. +4. `DICT_DIR=${GITHUB_WORKSPACE}/dawg node ui/scripts/bundle-dicts.mjs` (copy the release `.dawg` + files into `ui/dist/dict/`). +5. Setup JDK 21 (`actions/setup-java@v4`, temurin 21). +6. Install Android SDK via `sdkmanager` (pin `platform-tools`, `platforms;android-36`, + `build-tools;36.0.0`); cache the SDK. **No emulator** — assemble only; on-device smoke is manual. 7. `npx cap sync android`. 8. Decode the keystore from `ANDROID_KEYSTORE_BASE64`; compute `versionCode`/`versionName` from the tag (scheme below).