chore(android): de-anchor ANDROID_PLAN.md from the docs, re-enable the APK workflow
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
Reword every code comment and doc that referenced ANDROID_PLAN.md or its stage anchors (§E, G-step-0, O1) so the living docs no longer depend on the plan file: .claude/CLAUDE.md, deploy/README.md, docs/ARCHITECTURE.md, docs/TESTING.md, ui/README.md, ui/android/.gitignore, ui/e2e/native.spec.ts, ui/src/lib/netstate.ts. Re-enable the manual signed-APK workflow (android-build.yaml.disabled -> android-build.yaml; the CI host already has the Android SDK; workflow_dispatch-only, so it does not auto-run on this PR). Add docs/ICONS.md — the single-master icon/logo format reference (web, PWA, Android, future iOS, store listings).
This commit is contained in:
+4
-4
@@ -30,7 +30,7 @@ on it**, and prefer the authoritative docs where they overlap. Add to this file
|
|||||||
|
|
||||||
## Native Android build (Capacitor)
|
## Native Android build (Capacitor)
|
||||||
|
|
||||||
The native Android app (`ANDROID_PLAN.md`) is a Capacitor 8 wrapper of the `ui` SPA, scaffolded under
|
The native Android app 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:
|
`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
|
- **Capacitor 8 needs JDK 21, not 17.** `@capacitor/android` compiles at `VERSION_21`; a JDK 17 Gradle
|
||||||
@@ -234,9 +234,9 @@ The native Android app (`ANDROID_PLAN.md`) is a Capacitor 8 wrapper of the `ui`
|
|||||||
`--pg1-user=scrabble`.
|
`--pg1-user=scrabble`.
|
||||||
- **Offline mode** — the robot brain, move generator/validator/scorer and DAWG reader are **JS ports**
|
- **Offline mode** — the robot brain, move generator/validator/scorer and DAWG reader are **JS ports**
|
||||||
of the Go engine, bundled client-side and **parity-pinned** by golden tests. Multi-phase; native
|
of the Go engine, bundled client-side and **parity-pinned** by golden tests. Multi-phase; native
|
||||||
offline-first bundles the dictionaries (see `ANDROID_PLAN.md`).
|
offline-first bundles the dictionaries.
|
||||||
- **VK ID web login** — raw OAuth 2.1 against `id.vk.com`, a **separate VK "Web" app** from the Mini
|
- **VK ID web login** — raw OAuth 2.1 against `id.vk.com`, a **separate VK "Web" app** from the Mini
|
||||||
App, server-side confidential code exchange.
|
App, server-side confidential code exchange.
|
||||||
- **Email relay** — Selectel SMTP + confirm / link / unlink / deletion codes + alerts.
|
- **Email relay** — Selectel SMTP + confirm / link / unlink / deletion codes + alerts.
|
||||||
- **Native Android** — see `ANDROID_PLAN.md` (Capacitor bundle model, client-version gate,
|
- **Native Android** — Capacitor bundle model, client-version gate, offline-first, RuStore; the
|
||||||
offline-first, RuStore).
|
design lives in `docs/ARCHITECTURE.md` (§2 gate, §3 identity, §13 native build).
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
#
|
#
|
||||||
# Signing degrades gracefully: with the ANDROID_KEYSTORE_* secrets present the APK is signed; without
|
# Signing degrades gracefully: with the ANDROID_KEYSTORE_* secrets present the APK is signed; without
|
||||||
# them build.gradle produces an UNSIGNED release APK (so a dry run still proves the whole pipeline).
|
# them build.gradle produces an UNSIGNED release APK (so a dry run still proves the whole pipeline).
|
||||||
# The keystore is a publication prerequisite — see deploy/README.md (Android build/release runbook) and
|
# The keystore is a publication prerequisite — see deploy/README.md (Android build/release runbook).
|
||||||
# ANDROID_PLAN.md §E. The toolchain is self-provisioned here (JDK 21 + a cached Android SDK), so the
|
# The toolchain is self-provisioned here (JDK 21 + a cached Android SDK), so the runner host needs
|
||||||
# runner host needs nothing pre-installed.
|
# nothing pre-installed.
|
||||||
name: android-build
|
name: android-build
|
||||||
run-name: "android build ${{ github.sha }}"
|
run-name: "android build ${{ github.sha }}"
|
||||||
|
|
||||||
+1
-1
@@ -241,7 +241,7 @@ that survives losing the host.
|
|||||||
## Android app build & release (RuStore)
|
## Android app build & release (RuStore)
|
||||||
|
|
||||||
The standalone Android app is built by a **manual** workflow, never automatically, and is separate from the
|
The standalone Android app is built by a **manual** workflow, never automatically, and is separate from the
|
||||||
web/prod rollout — a signed APK uploaded to RuStore by hand. Full plan: [`../ANDROID_PLAN.md`](../ANDROID_PLAN.md).
|
web/prod rollout — a signed APK uploaded to RuStore by hand. The build/release runbook follows.
|
||||||
|
|
||||||
- **Trigger:** `Actions → android-build → Run workflow` from **`master`**, input `confirm=build` (mirrors
|
- **Trigger:** `Actions → android-build → Run workflow` from **`master`**, input `confirm=build` (mirrors
|
||||||
`prod-deploy`). **Tag the release first** (`git tag vX.Y.Z` on `master`) — the workflow refuses anything
|
`prod-deploy`). **Tag the release first** (`git tag vX.Y.Z` on `master`) — the workflow refuses anything
|
||||||
|
|||||||
@@ -1541,8 +1541,8 @@ hidden in the MVP (`VITE_PAYMENTS_DISABLED`). The APK is built by a **manual** `
|
|||||||
the dicts, and assembles a **release APK** artifact — signed when the `ANDROID_KEYSTORE_*` secrets are
|
the dicts, and assembles a **release APK** artifact — signed when the `ANDROID_KEYSTORE_*` secrets are
|
||||||
present, unsigned otherwise. `versionCode`/`versionName` are deterministic from the release tag `vMA.MI.PA`
|
present, unsigned otherwise. `versionCode`/`versionName` are deterministic from the release tag `vMA.MI.PA`
|
||||||
(`versionCode = MA*1_000_000 + MI*1_000 + PA`, `versionName = "MA.MI.PA"`). The runner is a host-executor
|
(`versionCode = MA*1_000_000 + MI*1_000 + PA`, `versionName = "MA.MI.PA"`). The runner is a host-executor
|
||||||
with a host-provisioned Android SDK; RuStore upload is manual. Full plan + runbook:
|
with a host-provisioned Android SDK; RuStore upload is manual. Runbook:
|
||||||
[`../ANDROID_PLAN.md`](../ANDROID_PLAN.md), [`../deploy/README.md`](../deploy/README.md).
|
[`../deploy/README.md`](../deploy/README.md).
|
||||||
|
|
||||||
## 14. CI & branches
|
## 14. CI & branches
|
||||||
|
|
||||||
|
|||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
# Icon & logo asset map
|
||||||
|
|
||||||
|
> Single reference for every icon/logo format the project ships, and the
|
||||||
|
> single-master workflow a rebrand should follow. Goal: one vector master →
|
||||||
|
> regenerate every target, miss nothing. The iOS / iPadOS rows are **future**
|
||||||
|
> (no iOS shell exists yet) but are listed so a rebrand covers them in one pass.
|
||||||
|
|
||||||
|
## Current state — three drifting sources
|
||||||
|
|
||||||
|
The «Э» mark is emitted today by three independent pipelines that visually drift
|
||||||
|
(the favicon is a rounded bordered tile, the maskable is a full-bleed square, the
|
||||||
|
Android launcher is an upscale). The rebrand collapses them onto one master.
|
||||||
|
|
||||||
|
1. **Web favicon set** — `assets/icons/build/generate.js` composes SVG from font
|
||||||
|
glyph outlines (`assets/icons/build/glyphs.json`) and rasterises with the `ui`
|
||||||
|
package's Playwright chromium →
|
||||||
|
`ui/public/{favicon.svg, favicon.ico, apple-touch-icon.png, og-image.png}`.
|
||||||
|
2. **PWA icons** — `ui/public/{icon-192, icon-512, icon-maskable-512}.png` are
|
||||||
|
hand-placed and covered by **no** generator (the main drift source).
|
||||||
|
3. **Android** — `capacitor-assets generate --android` (`pnpm android:assets`)
|
||||||
|
expands `ui/assets/icon.png` into the launcher + splash matrix under
|
||||||
|
`ui/android/app/src/main/res/`.
|
||||||
|
|
||||||
|
## Source masters (author once; commit)
|
||||||
|
|
||||||
|
The minimum set the designer supplies so every target below regenerates:
|
||||||
|
|
||||||
|
| Master | Size | Format | Background | Need | Drives |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Vector mark | — | SVG | transparent | required | `favicon.svg`; the canonical shape |
|
||||||
|
| App icon | 1024×1024 | PNG | opaque | required | Android legacy, PWA raster, iOS, store icons |
|
||||||
|
| Adaptive foreground | 1024×1024 | PNG | transparent | recommended | Android/iOS adaptive foreground — mark inside the ~66 % safe area (outer third is masked) |
|
||||||
|
| Adaptive background | 1024×1024 | PNG | opaque | recommended | Android/iOS adaptive background layer |
|
||||||
|
| Maskable | 512×512 | PNG | opaque | recommended | PWA `maskable` (mark inside the maskable safe zone) |
|
||||||
|
| Splash | 2732×2732 (+ a `-dark` variant) | PNG | opaque | optional | Android/iOS splash / launch screen |
|
||||||
|
| OG card | 1200×630 | PNG | opaque | optional | link-preview card (mark **plus** wordmark text) |
|
||||||
|
|
||||||
|
Capacitor reads the adaptive/splash masters from `ui/assets/`:
|
||||||
|
`icon.png`, `icon-foreground.png`, `icon-background.png`, `splash.png`,
|
||||||
|
`splash-dark.png`. Without the foreground/background pair it insets the single
|
||||||
|
`icon.png` into the adaptive safe zone (today's poorer look).
|
||||||
|
|
||||||
|
## Generated targets (full coverage)
|
||||||
|
|
||||||
|
### Web — `ui/public/`, via `node assets/icons/build/generate.js`
|
||||||
|
|
||||||
|
| File | Size | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| `favicon.svg` | vector | canonical vector output |
|
||||||
|
| `favicon.ico` | 32×32 | PNG-in-ICO; answers the blind `/favicon.ico` probe |
|
||||||
|
| `apple-touch-icon.png` | 180×180 | opaque full-bleed; iOS Safari home-screen (iOS masks its own corners) |
|
||||||
|
| `og-image.png` | 1200×630 | link-preview card; has the wordmark, referenced by absolute URL |
|
||||||
|
|
||||||
|
### PWA — `ui/public/`, referenced by `manifest.webmanifest`
|
||||||
|
|
||||||
|
| File | Size | `purpose` |
|
||||||
|
|---|---|---|
|
||||||
|
| `icon-192.png` | 192×192 | `any` |
|
||||||
|
| `icon-512.png` | 512×512 | `any` |
|
||||||
|
| `icon-maskable-512.png` | 512×512 | `maskable` |
|
||||||
|
|
||||||
|
### Android — `ui/android/app/src/main/res/`, via `pnpm android:assets`
|
||||||
|
|
||||||
|
Generated per density mdpi→xxxhdpi (exact px tool-managed); **do not hand-edit**:
|
||||||
|
|
||||||
|
- `mipmap-*/ic_launcher.png` — legacy square (48 / 72 / 96 / 144 / 192)
|
||||||
|
- `mipmap-*/ic_launcher_round.png` — legacy round
|
||||||
|
- `mipmap-*/ic_launcher_foreground.png` + `ic_launcher_background.png` — adaptive layers
|
||||||
|
- `mipmap-anydpi-v26/ic_launcher.xml` + `ic_launcher_round.xml` — adaptive descriptors
|
||||||
|
- `drawable*/splash.png` — splash (portrait/landscape × light/night × density)
|
||||||
|
|
||||||
|
### iOS / iPhone / iPad — FUTURE, via `capacitor-assets generate --ios`
|
||||||
|
|
||||||
|
No iOS shell today; listed so the rebrand covers it. Same masters produce
|
||||||
|
`App/App/Assets.xcassets/AppIcon.appiconset`:
|
||||||
|
|
||||||
|
- Modern (Xcode 14+): a single **1024×1024** marketing icon; Xcode derives the rest.
|
||||||
|
- Full asset-catalog px (if a full set is emitted): 20, 29, 40, 58, 60, 76, 80,
|
||||||
|
87, 120, 152, 167, 180, 1024 — iPhone {40, 58, 60, 80, 87, 120, 180}, iPad
|
||||||
|
{20, 29, 40, 76, 152, 167}, App Store marketing {1024}.
|
||||||
|
- Splash: `splash-2732x2732.png` (+ dark) → the launch storyboard.
|
||||||
|
- iOS icons are **opaque, no alpha, no rounded corners** (the OS applies the mask).
|
||||||
|
|
||||||
|
### Store-listing art — uploaded by hand (outside the repo)
|
||||||
|
|
||||||
|
| Store | Asset | Spec |
|
||||||
|
|---|---|---|
|
||||||
|
| RuStore (now) | app icon | 512×512 PNG |
|
||||||
|
| RuStore | screenshots | phone, per the RuStore listing spec |
|
||||||
|
| RuStore | promo / feature graphic | per the RuStore listing spec |
|
||||||
|
| App Store (future) | marketing icon | 1024×1024 |
|
||||||
|
| App Store (future) | screenshots | per device class |
|
||||||
|
| Google Play (future) | icon / feature graphic | 512×512 / 1024×500 |
|
||||||
|
|
||||||
|
## Regenerate
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd assets/icons && node build/generate.js # web favicon set + og card -> ui/public/
|
||||||
|
cd ui && pnpm android:assets # Android launcher + splash
|
||||||
|
cd ui && node_modules/.bin/capacitor-assets generate --ios # future, once the iOS shell exists
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open items for the rebrand
|
||||||
|
|
||||||
|
- Decide how the new vector master feeds `assets/icons/` — retarget the generator
|
||||||
|
onto the supplied vector, or drop in the output files and keep the generator for
|
||||||
|
the OG card only.
|
||||||
|
- Bring `icon-192 / icon-512 / icon-maskable-512` under a generator (today
|
||||||
|
hand-placed — the main drift source).
|
||||||
|
- Add `ui/assets/icon-foreground.png` + `icon-background.png` for a proper adaptive
|
||||||
|
icon (today a single inset `icon.png`).
|
||||||
+1
-1
@@ -40,7 +40,7 @@ tests or touching CI.
|
|||||||
in the **offline guest lobby** (not `/login`), plays a local vs_ai move from the **bundled** dawg tier
|
in the **offline guest lobby** (not `/login`), plays a local vs_ai move from the **bundled** dawg tier
|
||||||
(`playwright.config.ts` bundles the dicts into `dist-e2e/dict/`), starts a hotseat game, and drives
|
(`playwright.config.ts` bundles the dicts into `dist-e2e/dict/`), starts a hotseat game, and drives
|
||||||
`window.__native.reconcile()` to prove online lights up — the device-local game stays listed in the
|
`window.__native.reconcile()` to prove online lights up — the device-local game stays listed in the
|
||||||
**unified lobby** (closing G-step-0) — and Profile hides the Telegram/VK link buttons.
|
**unified lobby** — and Profile hides the Telegram/VK link buttons.
|
||||||
The **version-gate** spec (`e2e/update.spec.ts`) drives the `__update` hook to prove both tiers — the
|
The **version-gate** spec (`e2e/update.spec.ts`) drives the `__update` hook to prove both tiers — the
|
||||||
hard *Update / Play offline* notice (and that "Play offline" drops into the offline lobby) and the soft
|
hard *Update / Play offline* notice (and that "Play offline" drops into the offline lobby) and the soft
|
||||||
dismissable *update available* nudge in the lobby; `retry.test.ts` covers the `FailedPrecondition →
|
dismissable *update available* nudge in the lobby; `retry.test.ts` covers the `FailedPrecondition →
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ The **native (Capacitor) build** additionally reads `VITE_DICT_VERSION` (the bun
|
|||||||
the offline path requests, matching the packaged DAWGs), `VITE_PAYMENTS_DISABLED=1` (hide in-app purchases
|
the offline path requests, matching the packaged DAWGs), `VITE_PAYMENTS_DISABLED=1` (hide in-app purchases
|
||||||
in the RuStore MVP), `VITE_RUSTORE_URL` (the update overlay's store target; empty until published) and
|
in the RuStore MVP), `VITE_RUSTORE_URL` (the update overlay's store target; empty until published) and
|
||||||
`VITE_APP_VERSION` (`git describe --tags` → the `X-Client-Version` gate header) — all wired by
|
`VITE_APP_VERSION` (`git describe --tags` → the `X-Client-Version` gate header) — all wired by
|
||||||
`.gitea/workflows/android-build.yaml`; see `ANDROID_PLAN.md`.
|
`.gitea/workflows/android-build.yaml`.
|
||||||
|
|
||||||
The build has **two entries**: the game SPA (`index.html`, served at `/app/` and
|
The build has **two entries**: the game SPA (`index.html`, served at `/app/` and
|
||||||
`/telegram/`) and a lightweight landing page (`landing.html`, served at `/`).
|
`/telegram/`) and a lightweight landing page (`landing.html`, served at `/`).
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ captures/
|
|||||||
.idea/navEditor.xml
|
.idea/navEditor.xml
|
||||||
|
|
||||||
# Keystore files
|
# Keystore files
|
||||||
# Release signing material must never be committed (see ANDROID_PLAN.md — keystore loss/leak risk).
|
# Release signing material must never be committed (keystore loss/leak risk).
|
||||||
*.jks
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
|
|
||||||
|
|||||||
@@ -67,14 +67,14 @@ test.describe('native offline-first', () => {
|
|||||||
// returns: reconciliation silently mints + adopts a server guest and clears the auto-offline, so
|
// returns: reconciliation silently mints + adopts a server guest and clears the auto-offline, so
|
||||||
// online lights up — the offline tint drops, the seeded online game (vs Ann) appears and Stats
|
// online lights up — the offline tint drops, the seeded online game (vs Ann) appears and Stats
|
||||||
// re-enables. The local game STAYS visible in the unified lobby alongside the server games — a
|
// re-enables. The local game STAYS visible in the unified lobby alongside the server games — a
|
||||||
// reconciled guest keeps its device-local games (this closes G-step-0).
|
// reconciled guest keeps its device-local games.
|
||||||
await page.getByRole('button', { name: /Back|Назад/i }).click();
|
await page.getByRole('button', { name: /Back|Назад/i }).click();
|
||||||
await expect(page.locator('button.tab').nth(2)).toBeVisible();
|
await expect(page.locator('button.tab').nth(2)).toBeVisible();
|
||||||
await expect(page.locator('.rowwrap').filter({ hasText: '🤖' })).toBeVisible(); // the local vs_ai game, listed while offline
|
await expect(page.locator('.rowwrap').filter({ hasText: '🤖' })).toBeVisible(); // the local vs_ai game, listed while offline
|
||||||
await page.evaluate(() => (window as unknown as { __native: { reconcile(): void } }).__native.reconcile());
|
await page.evaluate(() => (window as unknown as { __native: { reconcile(): void } }).__native.reconcile());
|
||||||
await expect(page.locator('header.nav.offline')).toHaveCount(0);
|
await expect(page.locator('header.nav.offline')).toHaveCount(0);
|
||||||
await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible({ timeout: 15000 });
|
await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible({ timeout: 15000 });
|
||||||
await expect(page.locator('.rowwrap').filter({ hasText: '🤖' })).toBeVisible(); // still listed online — the reconciled guest keeps it (G-step-0)
|
await expect(page.locator('.rowwrap').filter({ hasText: '🤖' })).toBeVisible(); // still listed online — the reconciled guest keeps it
|
||||||
await expect(page.locator('button.tab').nth(1)).toBeEnabled();
|
await expect(page.locator('button.tab').nth(1)).toBeEnabled();
|
||||||
|
|
||||||
// The native sign-in surface is guest + email only: on Profile the Telegram + VK LINK buttons are
|
// The native sign-in surface is guest + email only: on Profile the Telegram + VK LINK buttons are
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// The pure net-state machine (O1 of the offline-model redesign). A single reducer replaces the old
|
// The pure net-state machine. A single reducer replaces the old
|
||||||
// two-layer connection.svelte.ts / offline.svelte.ts split: connectivity and the client-version gate
|
// two-layer connection.svelte.ts / offline.svelte.ts split: connectivity and the client-version gate
|
||||||
// collapse into one detected `state`, with an explicit hysteresis buffer so a brief network blip does
|
// collapse into one detected `state`, with an explicit hysteresis buffer so a brief network blip does
|
||||||
// not thrash the chrome. It is a pure function of (previous snapshot, event, config) — no timers, no
|
// not thrash the chrome. It is a pure function of (previous snapshot, event, config) — no timers, no
|
||||||
|
|||||||
Reference in New Issue
Block a user