Fix the SonarQube coverage gate (0% on new code) — a reporting gap, not a
testing gap: the JVM unit suite already exists but the source-only scan
never received a coverage report.
- app/build.gradle.kts: apply jacoco, enable debug unit-test coverage, add a
jacocoTestReport task (excludes generated/Hilt/Compose-singleton classes)
- sonar-project.properties: consume the JaCoCo XML; exclude pure-@Composable
UI from coverage (JVM unit tests can't execute composable bodies)
- .gitea/workflows/sonarqube.yml: run JDK 17 + Android SDK +
`testDebugUnitTest jacocoTestReport` before the scan
Also clear the three actionable code smells: remove an unused import
(AdminContentScreen), remove an unused parameter (AdminSupportScreen.
RespondDialog), and decompose LoginViewModel.submit() (cognitive complexity
20 -> under 15). The remaining 12 smells (snake_case DTO fields that mirror
the JSON wire contract; Compose/nav complexity) are marked Won't Fix in
SonarQube with rationale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
Add the app side of Android App Links (M9 follow-up, docs/android/APP_LINKS.md),
layered on the M9 Part 2 native SSO callback:
- Build-time `appLinkHost` Gradle property -> BuildConfig.APP_LINK_HOST +
manifestPlaceholders["appLinkHost"]. autoVerify needs a literal host, so the
generic multi-tenant build leaves it empty (placeholder falls back to the
reserved runic-gateway.invalid sentinel, making the filter inert); a
white-label build bakes one host with -PappLinkHost=play.myshard.com.
- Manifest: an autoVerify https `/mobile/callback` intent-filter beside the
unchanged custom-scheme one (the permanent fallback).
- SsoAuthManager: request the https App Link redirect_uri iff the baked host
matches the paired shard host; matchesAppLinkCallback() enforces a paired-host
trust check (host must equal the currently-paired base URL host) as
defense-in-depth. Both matchers feed the same complete()/exchange path.
- MainActivity routes custom-scheme and App Link callbacks identically.
+5 JVM tests (SsoAuthManagerTest -> 14). Built green (JDK 21,
-Pksp.incremental=false); white-label host substitution verified in the merged
manifest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
Replace the tag-triggered release.yml with 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 versionName in build.gradle.kts (versionCode derived
major*10000+minor*100+patch, monotonic), builds the SIGNED release APK, then
commits the bump [skip ci], tags vX.Y.Z, and creates the Gitea release with the
notes + APK + SHA256SUMS.
Uses REGISTRY_USER/REGISTRY_TOKEN (write:repository) to push the bump + create
the release, matching link/. main must allow that account to push (bump lands on
main; the [skip ci] + head-commit guard prevent a re-trigger loop). Signing
secrets (ANDROID_KEYSTORE_BASE64/_PASSWORD, ANDROID_KEY_ALIAS/_PASSWORD) unchanged.
Same self-hosted-runner handling as pr-checks.yml (apt JDK 17, sdkmanager, chmod).
Co-Authored-By: Claude <noreply@anthropic.com>
Release-hardening pass (PLAN.md §9 M6, §10, §12). No architecture, data-flow,
or endpoint changes; the app remains a pure API client.
App icons (default brand assets):
- New gateway-medallion launcher icon set (all densities, adaptive fg/bg, round,
Play Store icon) + an RG notification icon staged for M7 push.
- Replace the Image Asset wizard's default green-grid adaptive background with the
deep-indigo brand fill (@color/ic_launcher_background #1B1033); recomposite the
legacy square/round webps and the 512 Play icon over indigo so the whole set is
coherent (the green never shipped). Restore the SPDX headers the wizard stripped;
drop the orphaned placeholder foreground vector. No <monochrome> layer — the
full-colour medallion has no clean silhouette, so themed mode falls back to the
standard icon rather than a tinted blob.
Version-mismatch guard (§3):
- The connect probe now refuses a Runic Gateway backend whose API version this
build can't speak (e.g. a future v2) with a clear "app out of date" message,
instead of mis-rendering; lenient on a blank api (older backend). Decision logic
extracted to a pure ConnectionRepository.evaluateVersion() with unit tests.
Release build hardening (§7, §12):
- Enable R8 full-mode minify + resource shrink for release (~31 MB debug -> 4.2 MB
signed release). ProGuard keep-rules for kotlinx.serialization serializers + our
wire DTOs, Retrofit service interfaces, and a -dontwarn for Tink's compile-only
Error Prone annotations (EncryptedSharedPreferences).
- Release signingConfig reads keystore material from a gitignored keystore.properties
or env vars; absent -> unsigned (debug + PR gate unaffected). Keystore never in repo.
- versionName/versionCode overridable via -P so the release tag + CI run number
drive them (§10).
CI:
- release.yml: on a `v*` tag, build a SIGNED release APK (keystore from a base64
Gitea secret) and attach it + SHA256SUMS to a Gitea release; workflow_dispatch is
a signing dry run. Mirrors pr-checks.yml's self-hosted-runner handling (apt JDK 17,
explicit sdkmanager, in-step chmod +x gradlew).
Co-Authored-By: Claude <noreply@anthropic.com>
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 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
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>