Two defects found on device by phase 8's AC-5 walk, both the same trap phase 2 hit with card elevation: Material takes these values as DEFAULT ARGUMENTS, not from the theme, so mapping the token is not enough on its own. 1. Every ShardCard drew in Material's grey, not the shard's panel color. CardDefaults.cardColors() takes its container from surfaceContainerHighest - FilledCardTokens.ContainerColor, checked in the material3 1.3.0 artifact's bytecode - and shardColorScheme mapped surfaceContainer, High and Low but not Highest. All 26 ShardCard sites across 20 files were affected. Themed instances showed it worst: on Fantasy the page went brown and the cards stayed grey. This is NOT an M12 regression. The untouched app draws the same grey cards and has since M5; M12 only made it obvious by theming everything around them. Fixing it therefore changes the untouched app too - cards move from Material's grey to --panel-flat - which is the milestone's second deliberate change to a shard that has set nothing, alongside phase 2's card shadow. AC-1 is updated to record that rather than absorb it: every other role is still asserted byte-for-byte against the verbatim pre-M12 scheme, and the two that moved are named, given their new values, and checked to have actually differed before. surfaceContainerLowest is mapped alongside it for consistency with surfaceContainerLow. It has no reader in this app - the phase 8 sweep checked every Material component the app draws against the roles the mapping leaves at Material defaults, and surfaceContainerHighest was the only live one. The drawer scrim reads the unmapped `scrim`, which stays Material's black deliberately. 2. The drawer's selected row ignored --radius-pill. NavigationDrawerItem takes `shape` as a default argument (CircleShape); the three call sites set `colors` but never `shape`, so on Fantasy every other radius went square while the selected row stayed fully round. Verified on device against a Fantasy-themed local instance: the three ShardCards on the shard screen now paint --panel-flat, and the selected drawer row is the 4px rectangle the preset asks for. 477 unit tests green (476 + 1), lintDebug clean. Co-Authored-By: Claude <noreply@anthropic.com>
Runic Gateway — Android app
A native Android client for a Runic Gateway shard's public site + player self-service. It is
purely an API client of the website backend — it never talks to the link/ sidecar or the game
shard directly, and it ships none of the shard/sidecar wiring. It surfaces the same content and
player features as the website's browser client, minus every administrative/management console.
The authoritative design contract is docs/android/PLAN.md
in the RunicGateway/docs repo. The authoritative API reference is the committed OpenAPI spec at
website/server/swagger/swagger-output.json.
Status
M0 — repo scaffold. Gradle + Compose + Hilt skeleton with CI (lint + unit test + debug build). The functional Kotlin pass (M1–M4) and the design pass (M5) follow — see the plan's milestones (§9).
Stack
| Concern | Choice |
|---|---|
| Language / UI | Kotlin + Jetpack Compose (Material 3) |
| Navigation | Navigation-Compose, single-activity |
| HTTP | Retrofit + OkHttp, kotlinx.serialization |
| Async | Coroutines + Flow |
| DI | Hilt |
| Prefs / base URL | Jetpack DataStore |
| Tokens at rest | EncryptedSharedPreferences |
| Images | Coil |
| Min SDK | Android 10 (API 29) |
| Target / compile SDK | 35 |
Dependency and plugin versions are pinned in gradle/libs.versions.toml.
Build
Requires JDK 17 and the Android SDK (ANDROID_HOME / local.properties).
./gradlew assembleDebug # build a debug APK -> app/build/outputs/apk/debug/
./gradlew test # JVM unit tests
./gradlew lint # Android lint
./gradlew installDebug # install on a connected device/emulator
The app self-configures its server URL on first run (PLAN.md §3), so a single build works against any shard's website — there is no compiled-in API host.
CI
.gitea/workflows/pr-checks.yml gates PRs into main with ./gradlew lint test assembleDebug on the
org's self-hosted runner (JDK 17 + Android SDK). Debug builds are auto-signed, so the gate needs no
secrets. This pipeline is verified green end-to-end on the runner (M0). A signed release APK
attached to a Gitea release comes at M6.
The workflow carries a few runner-specific accommodations (each explained in comments in the file), because this self-hosted runner differs from a stock GitHub runner:
- JDK 17 is installed via
apt(notactions/setup-java) — the runner can't resolveapi.adoptium.net, while the Ubuntu mirrors are reachable. - SDK packages are installed explicitly via
sdkmanager, withset +o pipefailsoyesdying ofSIGPIPEdoesn't fail the step. gradlewischmod +x'd in the run step — the runner's checkout does not preserve the git executable bit, so./gradlewalone fails with "Permission denied".
Contributing
See CONTRIBUTING.md. AI-assisted contributions must be disclosed (org
policy): tick the PR box naming the tool and add a Co-Authored-By trailer to AI-authored commits.
Licensed GPL-3.0-or-later.