4 Commits

Author SHA1 Message Date
cbaa18ea0b docs(android): release.yml is a conventional-commit auto-release engine
Update M6 docs to reflect release.yml mirroring link/'s engine (auto version +
changelog + tag + signed APK + Gitea release on merge to main), not a v* tag
trigger. Note REGISTRY_USER/REGISTRY_TOKEN + the main-push requirement, and the
derived monotonic versionCode. §9 status, §9 M6/M7 milestones, §12.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 04:25:05 -05:00
f79c2fa5a9 docs(android): record M6 (release mechanics) landed
Mark M6 landed (RunicGateway/Android-app#11): signed-APK release plumbing
(R8 minify + resource shrink, release signingConfig from a gitignored keystore,
release.yml on a v* tag), the §3 version-mismatch guard, and the default brand
app icons (deep-indigo medallion). Record the decision to descope the optional
biometric app-lock from v1 (tokens already encrypted at rest) across §4.3, the
M3 note, §9, and §13.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 03:37:37 -05:00
8798a0ff79 Merge pull request 'docs(android): record M5 (design pass) landed' (#18) from docs/android-m5-landed into main
Reviewed-on: #18
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-20 08:03:55 +00:00
4eae12448a docs(android): record M5 (design pass) landed
Mark the M5 shard-website design pass complete in docs/android/PLAN.md,
matching RunicGateway/Android-app#10: dark-only theme (deep blue-black
surfaces, slate accent, Cinzel display face), reusable pill/label/card/meter
components, and retained per-shard brand-accent seeding — restyling the
working M1–M4 screens with no architecture, data-flow, endpoint, or DTO
change. Updates the status header, the §9 build-progress record, and the
milestone list (M0–M5 landed; M6 release hardening next).

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-20 01:56:27 -05:00

View File

@@ -1,6 +1,6 @@
# Android App — Plan
Status: **M0M4 landed; M5 (design pass) next.** This document is the
Status: **M0M6 landed; the functional build, design pass, and release mechanics are complete (the auto-release engine cuts a tagged, signed APK on merge to `main`; M7 push notifications next).** This document is the
design contract for the `RunicGateway/Android-app` repo. It was written before implementation so the
API changes it depends on could be landed in `website/` and `docs/` first. The authoritative API
reference is the committed OpenAPI spec at `website/server/swagger/swagger-output.json` (regenerated
@@ -70,10 +70,11 @@ plain credential `401`), the `SessionManager` lifecycle over a fake store, and t
+ role mapping. **No backend/API change** — the app is a pure consumer of the existing mobile bearer +
`/auth/me` surface.
> **Biometric app-lock — deferred (decided at M3).** §4.3/§9 flag an *optional* biometric app-lock;
> it is **deferred to M6 (release hardening)**. Tokens are already encrypted at rest (Tink/AES-256-GCM),
> so an app-lock is a UX layer, not a security requirement, and adding it in the functional pass would
> widen scope without changing the data flow. Revisit as an opt-in setting during M6.
> **Biometric app-lock — descoped from v1 (decided at M6).** §4.3/§9 flagged an *optional* biometric
> app-lock, deferred from M3 to M6. At M6 it was **descoped from v1 entirely**: tokens are already
> encrypted at rest (Tink/AES-256-GCM), so an app-lock is a pure UX convenience, not a security
> requirement, and it changes no data flow. It is **not** in the first release; revisit only if it
> becomes a requested feature.
**M4 — player self-service & game data** (2026-07-19, `RunicGateway/Android-app#9`, functional Kotlin
pass): the signed-in player surface, all as a pure consumer of the existing bearer-gated API.
@@ -98,7 +99,51 @@ spec-aligned (as recorded for M1); 17 new JVM unit tests cover the account + pla
**No backend/API change** — the `/auth/me/*` and `/player/shard/*` surfaces the app consumes were the
§8 prerequisites, already landed.
**M5 (design pass) remains.**
**M5 design pass** (2026-07-20, `RunicGateway/Android-app#10`): the shard-website theme applied
across every screen, restyling the working M1M4 UI with **no architecture, data-flow, endpoint, or DTO
change** (§2.1). The design was produced in Claude Design (`Runic Gateway Screens.dc.html`) and
implemented in Compose. Because the functional screens already draw their color/type/shape from
`MaterialTheme` tokens (§2), the restyle lives mostly in the **theme layer** and propagates: a deep
blue-black surface stack (page `#0b0f14` / screen `#0e1318` / elevated `#11161d`), a slate-blue accent
(`#7f99bd`) with a light CTA fill (`#cdd9e8`), parchment serif body copy, and the engraved **Cinzel**
serif display face (bundled weight-axis variable font, SIL OFL) for headings and the top bar. The app is
now **dark-only** — the shard-website look is a single dark theme, so the light scheme is dropped and the
system light/dark setting is ignored; **per-shard brand-accent seeding is retained** (a site's published
accent still tints the primary/secondary roles, §3). A small set of reusable components — semantic status
pills, section labels, a gradient "feature" card, and slim stat meters — carries the motifs the design
repeats (home status, shard-online banner, champ/character/house status, character vitals & skills). The
launch theme and system bars are darkened so the first frame matches (no white flash). Verified by
`:app:assembleDebug` + `:app:testDebugUnitTest` (green); an on-device visual pass against the mockup is
the one open QA item noted on the PR.
**M6 — polish & release mechanics** (2026-07-20, `RunicGateway/Android-app#11`): the release
plumbing to ship v1 as a signed, sideloadable APK, with **no architecture, data-flow, or endpoint
change**. **Default brand app icons** — a gateway-medallion adaptive launcher icon (all densities +
round + Play Store icon) over the deep-indigo brand background (the Image Asset wizard's default
green grid was replaced, and the legacy square/round bitmaps + 512 Play icon recomposited to match);
plus an "RG" notification icon staged for M7. A **version-mismatch guard** (§3): the first-run connect
probe refuses a backend whose API version this build can't speak (a future `v2`) with a clear
"app out of date" message rather than mis-rendering (lenient on an older backend that omits `api`).
**Release build hardening** (§7, §12) — R8 full-mode minify + resource shrink (~31 MB debug → ~4 MB
signed release) with keep-rules for the kotlinx.serialization serializers, the wire DTOs, and the
Retrofit interfaces; a release `signingConfig` that reads keystore material from a **gitignored**
`keystore.properties` or env vars (absent → unsigned; the keystore is never committed); and
`versionName` is the committed source of truth (bumped by the release engine); `versionCode` is derived
from it (`major*10000+minor*100+patch`, monotonic); both stay `-P`-overridable for local builds (§10).
**CI `release.yml`** — mirrors `link/`'s language-agnostic release engine, adapted for Android: on every
push to `main` it derives the next version from conventional-commit subjects since the last `v*` tag
(`feat!`/BREAKING → major, `feat` → minor, `fix`/`perf` → patch; nothing releasable → no release),
generates a grouped changelog, bumps `build.gradle.kts`, builds the **signed** APK (keystore decoded from
a base64 Gitea secret), then commits the bump `[skip ci]`, tags `vX.Y.Z`, and creates the Gitea release
with notes + APK + `SHA256SUMS`. Uses `REGISTRY_USER`/`REGISTRY_TOKEN` (as `link/` does) to push the bump
and create the release, so `main` must allow that account to push.
HTTPS-only in release (M1), no token logging (logging is debug-gated, M3), and the Settings → Server
hard reset (M3) were already in place. Biometric app-lock is **descoped from v1** (see the note below).
**The functional build (M0M4), design pass (M5), and release mechanics (M6) are complete. The first
signed release now cuts automatically on the next release-worthy merge to `main` — once the signing +
`REGISTRY_*` secrets are set, `main` allows the CI account to push, and the on-device QA pass is done.
M7 push notifications are what remain.**
**Prerequisite progress (§8):** all v1 prerequisites are **done** (2026-07-19) — ✅ password reset
(item 2; website#75 + docs#8), ✅ role-agnostic `/auth/me/*` self surface (item 1; website#76 + docs#10),
@@ -268,8 +313,8 @@ completes them in a Custom Tab, then returns and signs in natively (§4.1):
- **Logout:** `POST /auth/mobile/logout` `{ refreshToken?, all? }` (requires bearer) — this session or
all sessions ("sign out everywhere").
- **Storage:** access + refresh tokens live in EncryptedSharedPreferences, never in plain prefs/logs.
The base URL may live in plain DataStore; tokens must not. Optional **biometric app-lock** (available
cleanly at API 29) gates access to the stored session — decide at M3.
The base URL may live in plain DataStore; tokens must not. An optional **biometric app-lock** was
considered here but **descoped from v1** (tokens are already encrypted at rest; see the M3 note).
- **Role for the menu** comes from the login response `user.role` and is re-validated via
`GET /auth/me` on app resume (roles can change server-side; admin access is re-checked every
request on the backend, so the app treats role as *advisory for menu rendering* and lets the server
@@ -436,11 +481,11 @@ No `link/` or `servuo-plugins/` changes are expected — the app is downstream o
## 9. Milestones
**Two passes (§2.1).** M0M4 are the **functional Kotlin pass** — every screen wired to its endpoints
and working end-to-end with placeholder/functional Compose UI, no design investment yet. **M5 is the
design pass**: once the functional Kotlin is done, Claude Design produces the front-end design and
Claude Code implements the final UI to it. Polish/release, push, and Play (M6M8) follow the designed
app.
**Two passes (§2.1).** M0M4 were the **functional Kotlin pass** — every screen wired to its endpoints
and working end-to-end with placeholder/functional Compose UI, no design investment yet. **M5 was the
design pass**: with the functional Kotlin done, Claude Design produced the front-end design and Claude
Code implemented the final UI to it. **Both passes are now complete** (M0M5 landed); polish/release,
push, and Play (M6M8) follow the designed app.
1. **M0 — Repo scaffold**: Gradle + Compose + Hilt skeleton, CI (build + lint + unit test), license
headers (GPL-3.0-or-later), CONTRIBUTING/AI-disclosure parity with the other repos.
@@ -452,8 +497,8 @@ app.
4. **M3 — Auth (§4)** *(functional pass)*: native password+TOTP login (429 handling), token storage,
refresh interceptor, logout, `/auth/me` role re-validation, the access-level menu. Custom-Tab
**hand-offs** to the website for register / invite / password-reset / SSO (no native screens for
those). Optional biometric app-lock. *Prerequisite:* the website password-reset flow (§8) is
already built.
those). (An optional biometric app-lock was considered here, then descoped from v1 at M6.)
*Prerequisite:* the website password-reset flow (§8) is already built.
5. **M4 — Player self-service & game data** *(functional pass)*: account management (via
`/auth/me/*`), game-account linking, own roster/characters/vendors/houses/sales — **text-only**
presentation (§6.3).
@@ -462,10 +507,15 @@ app.
UI to it** — Compose screens, Material 3 theming from the per-shard branding (§3), reusable
components, loading/error/empty states, the designed access-level menu. Restyles the existing
screens only; no changes to architecture, data flow, or endpoint contracts. Text-only game data
(§6.3) still holds — this is visual design of the data screens, not paperdoll art.
7. **M6 — Polish & first release**: settings (server switch = hard reset), version-mismatch guard,
release build hardening (HTTPS-only, no token logging). No offline cache in v1 (§7). **Ship v1 as a
signed APK attached to a Gitea release** (see §10).
(§6.3) still holds — this is visual design of the data screens, not paperdoll art. **Landed**
2026-07-20 (`RunicGateway/Android-app#10`): dark-only shard-website theme, Cinzel display face,
reusable pill/label/card/meter components; brand-accent seeding retained (see §9 build progress).
7. **M6 — Polish & release mechanics**: settings (server switch = hard reset, done M3),
version-mismatch guard, release build hardening (HTTPS-only, no token logging, R8 minify + resource
shrink, release signing). No offline cache in v1 (§7). **Ships v1 as a signed APK attached to a Gitea
release** via `release.yml`'s conventional-commit engine on merge to `main` (see §10, §12).
Biometric app-lock **descoped** (below).
**Landed** 2026-07-20 (`RunicGateway/Android-app#11`).
8. **M7 — Push notifications** (post-v1): add the self-hosted `ntfy` service to
`website/docker-compose.yml` (declarative, zero-interaction config), UnifiedPush integration in the
app, device registration, the subscriptions UI, and the content-free-tickle backend fan-out (see
@@ -565,11 +615,16 @@ repos use), on a bare `ubuntu:latest` container.
frequent: run the job under a prebuilt Android-SDK `container:` image so nothing installs per-run.)
- **PR gate** (`.gitea/workflows/pr-checks.yml`, on PR → `main`): `./gradlew lint test assembleDebug`.
Debug builds are auto-signed, so the gate needs no secrets. Mirrors `website/`'s pre-merge gate.
- **Release** (`.gitea/workflows/release.yml`, M6+): build a **signed release APK** and attach it to a
Gitea release (mirrors `link/`'s release job). The **keystore is a base64 Gitea Actions secret**
decoded in CI; store/key passwords are secrets. The keystore never lives in the repo. Keep the
signing identity stable from the first release (Play later requires consistency).
- Semantic `versionName` + monotonic `versionCode`; tag releases.
- **Release** (`.gitea/workflows/release.yml`, M6): mirrors `link/`'s release engine — on every push to
`main` it computes the next version from conventional commits since the last `v*` tag, generates a
changelog, bumps `build.gradle.kts`, builds a **signed release APK**, commits the bump `[skip ci]`,
tags `vX.Y.Z`, and creates the Gitea release with the notes + APK + `SHA256SUMS`. The **keystore is a
base64 Gitea Actions secret** decoded in CI (`ANDROID_KEYSTORE_BASE64`); store/key passwords + alias
are secrets too. The keystore never lives in the repo. `REGISTRY_USER`/`REGISTRY_TOKEN`
(`write:repository`) push the bump + create the release, so `main` must allow that account to push.
Keep the signing identity stable from the first release (Play later requires consistency).
- Semantic `versionName` (bumped by the engine) + derived monotonic `versionCode`
(`major*10000+minor*100+patch`); the engine tags each release.
## 13. Open questions (revisit as we go)
@@ -578,8 +633,8 @@ password+TOTP only, with registration/invite/reset/SSO **handled by the website*
reset built on backend + web first**, before app work (§8); minSdk 29, compile/target 35 (§2); no
telemetry in v1 (§2); strings externalized from day one, English-only bundled (§2); **text-only** game
data in v1, pretty paperdoll is future (§6.3); **no offline cache in v1** (§7); push via self-hosted
ntfy / UnifiedPush (§11); **biometric app-lock deferred to M6** (tokens already encrypted at rest, so
it is opt-in UX, not a v1 requirement — decided at M3).
ntfy / UnifiedPush (§11); **biometric app-lock descoped from v1** (tokens already encrypted at rest, so
it is a UX convenience, not a v1 requirement — deferred at M3, descoped at M6; revisit only if requested).
**Still open:**
- **App identity / domain.** Target application ID **`com.runicgateway.app`** — pending securing the