Implements M3 (docs/android/PLAN.md §4): the functional Kotlin auth pass.
- Native username/password (+ single-request TOTP) login over the existing
POST /auth/mobile/login; a 401 { totpRequired } reveals the code field, 429
surfaces a backoff message (§4.1).
- Token pair in EncryptedSharedPreferences (TokenStore behind SessionManager,
the single source of truth for the in-memory bearer + observable Session);
base URL stays in plain DataStore (§4.3).
- OkHttp AuthInterceptor (bearer) + TokenAuthenticator: one-shot, mutex-
serialized refresh-on-401 that replays the request, on its own bare client so
it can never recurse; single-use rotation; dead refresh signs out, transient
network keeps the session.
- Logout (POST /auth/mobile/logout, this session or all devices) tears down
locally even on failure.
- GET /auth/me re-validates the role on every resume; a surviving 401 signs out
(role stays advisory — backend is authority).
- Declarative access-level menu (visibleEntries: public/signed-in/player) with a
Sign in / Sign out toggle + a My Account screen.
- Custom-Tab hand-offs (androidx.browser) to the website for register / forgot-
password / SSO — no native screens (§4.2).
- Settings → Server switch now also clears the stored session (§3).
Biometric app-lock is deferred to M6 (tokens already encrypted at rest; it is
opt-in UX, not a v1 requirement — decided at M3).
JVM unit tests (18): auth-DTO decode (incl. totpRequired vs a plain credential
401), the SessionManager lifecycle over a fake store, and the menu access filter
+ role mapping. No backend/API change — a pure consumer of the existing mobile
bearer + /auth/me surface.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
Implements M2 of docs/android/PLAN.md §6.2 (functional pass): the public
shard surface over /api/v1/public/shard/*, plus the live SSE feed with
reconnect/backoff and graceful degradation (§7).
- Shard DTOs (status/economy/feed/online/presence/champs/guilds/governors/
houses) mirroring public/shard.controller.js; ignoreUnknownKeys keeps
additive backend fields safe, and the live *.update frames decode into the
same board DTOs.
- PublicApi: the /public/shard/* GETs (status, feed, economy, online,
presence, champs, guilds, governors + history, houses).
- ShardStreamClient: OkHttp SSE over /public/shard/stream. Unlike the browser
EventSource it reconnects itself — a cold Flow<ShardStreamEvent> with
growing backoff (reset on open), no read timeout for the idle keepalive,
and clean teardown on cancel so a dropped feed degrades to "offline".
- ShardRepository: typed ApiResult snapshot reads + the shared live feed and
frame decoders.
- Screens: a Shard hub (status/online count/economy/presence/staff + live
activity feed with a live indicator) linking to live boards for champion
spawns, guilds, governors (+ on-demand term history) and falling houses
(IDOC). Boards seed from a snapshot then merge SSE deltas in place via a
reusable LiveBoard, mirroring the website's merge semantics. Wired into the
shared navigation drawer (§5); all strings externalized (§2).
- Tests (28): DTO/frame decode, LiveBoard merge, event-text formatting, and
SSE frame parsing.
Co-Authored-By: Claude <noreply@anthropic.com>
The M1 PR-checks run hung ~16 min in `lintReportDebug` and was killed by
the runner (33m35s, marked failure) — every compile/test/assemble task
completed first; no task FAILED. Android lint's report phase needs more
than the 2 GB heap and GC-thrashes to a hang below it on the full app
codebase (it passed at 2 GB only while the M0 scaffold was trivial).
- gradle.properties: -Xmx2048m → -Xmx3g, cap MaxMetaspaceSize=1g so the
larger heap doesn't crowd container RAM.
- pr-checks.yml: run `testDebugUnitTest lintDebug assembleDebug` instead
of the aggregate `test lint assembleDebug`, so the release variant
isn't compiled+linted in parallel — halving peak memory and build time
while keeping the same coverage (unit tests are variant-agnostic).
Verified locally with the exact command (`--no-daemon`); lintReportDebug
+ lintDebug run and pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
Implements M1 (functional Kotlin pass, docs/android/PLAN.md §9): the
first-run base-URL connect flow, brand-seeded Material 3 theming from
/public/settings, a Retrofit/OkHttp/kotlinx-serialization client with a
runtime host-selection interceptor (the base URL is not compiled in),
the layered repository stack returning a typed ApiResult for graceful
degradation, and functional Compose screens for Home/Status, News
(+ post detail), Wiki (+ detail), CMS pages (block renderer), and the
contact form. One shared, declarative navigation drawer. No auth yet (M3).
DTOs + the Retrofit interface are hand-written and spec-aligned rather
than openapi-generated: the committed swagger-output.json is produced by
swagger-autogen and its component schemas are meta-descriptive (nested
{type, example} wrappers), not codegen-clean, so a hand-authored client
module is the pragmatic "checked-in generated module" the plan allows
(§2). Shapes were matched against the website controllers/models.
JVM unit tests cover URL normalization, host rewriting, ApiResult/UiState
mapping, and brand-color parsing. `lint test assembleDebug` green locally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
Record in the README that the M0 CI pipeline (lint + test + assembleDebug)
is verified green end-to-end on the self-hosted runner, and summarize the
runner-specific workflow accommodations (apt JDK, sdkmanager pipefail, gradlew
chmod) so contributors understand why they're there.
Co-Authored-By: Claude <noreply@anthropic.com>
The SDK install succeeds (reaches 100% unzip) but the step reported exit
141: with -o pipefail, 'yes' dying of SIGPIPE (128+13) once sdkmanager
closes the pipe poisons the pipeline status. Disable pipefail for that line
so only sdkmanager's exit gates success.
Co-Authored-By: Claude <noreply@anthropic.com>
PR #2 merged the M0 scaffold with the initial CI, which fails on this
self-hosted runner. Fix forward:
- JDK 17 via apt (openjdk-17-jdk-headless) + JAVA_HOME, replacing
actions/setup-java: the runner can't resolve api.adoptium.net (EAI_AGAIN).
- Explicit `sdkmanager` install of platform-tools + platforms;android-35 +
build-tools;35.0.0 so the build never relies on AGP auto-download.
- `chmod +x ./gradlew` in the run step (and set the 100755 git bit): this
runner's checkout drops file modes, so `./gradlew` alone hit
"Permission denied" (exit 126).
- ScaffoldSanityTest: drop the BuildConfig.DEBUG assertion, which fails under
testReleaseUnitTest (the `test` task runs both variants); assert VERSION_NAME.
- Remove the redundant android:label on MainActivity (RedundantLabel lint).
Verified locally against the Android Studio SDK (platform 35 + build-tools
35.0.0): `./gradlew lint test assembleDebug` passes and builds a debug APK.
Co-Authored-By: Claude <noreply@anthropic.com>
Stand up the Android-app repo per docs/android/PLAN.md M0: a buildable
Kotlin + Jetpack Compose (Material 3) single-activity skeleton wired for
Hilt, ready for the M1-M4 functional pass.
- Gradle 8.7 wrapper; AGP 8.6.1 / Kotlin 2.0.20, JDK 17, minSdk 29, target 35.
- Version catalog (gradle/libs.versions.toml) pins the full planned stack
(Compose, Hilt, Retrofit/OkHttp + kotlinx.serialization, DataStore,
security-crypto, Coil, Navigation) so later milestones reference by alias.
- RunicGatewayApp (@HiltAndroidApp) + MainActivity (Compose) + ui/theme/*.
- Strings externalized from day one; adaptive launcher icon; backup rules
exclude the token store / DataStore (no session material off-device).
- CI: .gitea/workflows/pr-checks.yml gates PRs with lint + test + assembleDebug
(JDK 17 + Android SDK on the self-hosted runner; debug builds auto-signed,
no secrets). Placeholder JVM unit test so the test gate runs.
- .gitattributes forces LF on gradlew so the wrapper runs on the Linux runner.
Verified locally: `gradle help`/`projects` configure the :app module and
resolve all six plugins cleanly (full assemble needs the Android SDK, done in CI).
app id: com.runicgateway.app (PLAN.md §13, pending runicgateway.app domain).
Co-Authored-By: Claude <noreply@anthropic.com>