feat(m1): Connect & browse — first-run flow, public content, contact #6

Merged
whitlocktech merged 2 commits from feat/m1-connect-browse into main 2026-07-19 23:10:48 +00:00
Member

M1 — Connect & browse (functional Kotlin pass)

Implements docs/android/PLAN.md M1 (§9): every public screen wired to its endpoint and working end-to-end with functional (not yet designed) Compose UI. No auth yet — that's M3. The M5 design pass restyles these screens without touching the architecture, data flow, or contracts established here (§2.1).

What's here

  • First-run connect flow (§3). A mandatory "Connect to your shard" screen validates the entered base URL by probing GET /public/status, confirms it's a Runic Gateway backend (version identity), and persists it to DataStore. HTTPS required in release; HTTP allowed only in debug for local dev. Settings → Server switch does a hard reset back to this screen.
  • Runtime base URL (not compiled in). Retrofit is built against a sentinel host; a HostSelectionInterceptor retargets every relative call onto the selected site, preserving any base-path prefix. A UserAgentInterceptor sends an identifiable UA so the site's scanner guard (§8) doesn't reject the app.
  • Layered client mirroring the backend's discipline: screen → ViewModel → repository → PublicApi (Retrofit) → DTO. Repositories return a typed ApiResult (Ok / HttpError / NetworkError) so the UI degrades gracefully (§7) — clean loading / error+retry / empty states, no crash on a down backend.
  • Brand-seeded theming (§3, §5). Material 3 color seeded from the per-shard brand.accent in /public/settings; site-relative asset paths resolved against the base URL.
  • Screens: Home/Status (mode + version + branding), News hub with category tabs + post detail (HTML body, Coil images), Wiki index with search + detail (tags, backlinks), CMS pages with a block renderer (heading / rich_text / image / quote / cta / divider / two_column), and the Contact form (validation, mailto-fallback, 429/502 handling). One shared, declarative navigation drawer (§5).
  • Tests: JVM unit tests for URL normalization, host rewriting, ApiResult/UiState mapping, and brand-color parsing.

Note: hand-written vs generated API client

PLAN §2 anticipates generating DTOs from swagger-output.json. In practice the committed spec is produced by swagger-autogen and its component schemas are meta-descriptive (nested {type, example} wrappers), so openapi-generator would emit unusable DTOs from it. I therefore hand-wrote spec-aligned DTOs + the Retrofit interface — the "checked-in generated module" the plan explicitly allows (§2) — matching shapes against the website controllers/models. Every DTO ignores unknown keys so additive backend fields never break decoding. If we want true codegen later, the spec's component schemas need to be authored as real OpenAPI models first.

Verification

./gradlew lint test assembleDebug green locally (JDK 17 via the Android Studio JBR + platform-35 SDK). CI runs the same gate.

Out of scope (later milestones)

Public shard widgets + SSE (M2), auth/token handling (M3), player self-service (M4), the design pass (M5).


AI-assisted contribution disclosure: authored with Claude Code (Claude Opus). AI-authored commit carries a Co-Authored-By: Claude trailer. I've reviewed the changes for correctness.

🤖 Generated with Claude Code

## M1 — Connect & browse *(functional Kotlin pass)* Implements [`docs/android/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/android/PLAN.md) **M1** (§9): every public screen wired to its endpoint and working end-to-end with functional (not yet designed) Compose UI. No auth yet — that's M3. The M5 design pass restyles these screens without touching the architecture, data flow, or contracts established here (§2.1). ### What's here - **First-run connect flow (§3).** A mandatory "Connect to your shard" screen validates the entered base URL by probing `GET /public/status`, confirms it's a Runic Gateway backend (version identity), and persists it to DataStore. HTTPS required in release; HTTP allowed only in debug for local dev. Settings → Server switch does a hard reset back to this screen. - **Runtime base URL (not compiled in).** Retrofit is built against a sentinel host; a `HostSelectionInterceptor` retargets every relative call onto the selected site, preserving any base-path prefix. A `UserAgentInterceptor` sends an identifiable UA so the site's scanner guard (§8) doesn't reject the app. - **Layered client** mirroring the backend's discipline: `screen → ViewModel → repository → PublicApi (Retrofit) → DTO`. Repositories return a typed `ApiResult` (`Ok` / `HttpError` / `NetworkError`) so the UI degrades gracefully (§7) — clean loading / error+retry / empty states, no crash on a down backend. - **Brand-seeded theming (§3, §5).** Material 3 color seeded from the per-shard `brand.accent` in `/public/settings`; site-relative asset paths resolved against the base URL. - **Screens:** Home/Status (mode + version + branding), News hub with category tabs + post detail (HTML body, Coil images), Wiki index with search + detail (tags, backlinks), CMS pages with a **block renderer** (`heading / rich_text / image / quote / cta / divider / two_column`), and the Contact form (validation, mailto-fallback, 429/502 handling). One shared, declarative navigation drawer (§5). - **Tests:** JVM unit tests for URL normalization, host rewriting, `ApiResult`/`UiState` mapping, and brand-color parsing. ### Note: hand-written vs generated API client PLAN §2 anticipates generating DTOs from `swagger-output.json`. In practice the committed spec is produced by **swagger-autogen** and its component schemas are *meta-descriptive* (nested `{type, example}` wrappers), so `openapi-generator` would emit unusable DTOs from it. I therefore hand-wrote spec-aligned DTOs + the Retrofit interface — the "checked-in generated module" the plan explicitly allows (§2) — matching shapes against the website controllers/models. Every DTO ignores unknown keys so additive backend fields never break decoding. If we want true codegen later, the spec's component schemas need to be authored as real OpenAPI models first. ### Verification `./gradlew lint test assembleDebug` green locally (JDK 17 via the Android Studio JBR + platform-35 SDK). CI runs the same gate. ### Out of scope (later milestones) Public shard widgets + SSE (M2), auth/token handling (M3), player self-service (M4), the design pass (M5). --- **AI-assisted contribution disclosure:** authored with **Claude Code** (Claude Opus). AI-authored commit carries a `Co-Authored-By: Claude` trailer. I've reviewed the changes for correctness. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-07-19 22:10:25 +00:00
feat(m1): connect & browse — first-run flow, public content, contact
Some checks failed
PR Checks / android-build (pull_request) Failing after 33m35s
9019ded556
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
whitlocktech approved these changes 2026-07-19 22:14:14 +00:00
wtclaude added 1 commit 2026-07-19 22:51:13 +00:00
ci(android): raise Gradle heap + scope PR gate to debug variant
All checks were successful
PR Checks / android-build (pull_request) Successful in 10m13s
01481ef2d5
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
whitlocktech merged commit 81f10fbca4 into main 2026-07-19 23:10:48 +00:00
whitlocktech deleted branch feat/m1-connect-browse 2026-07-19 23:10:48 +00:00
Sign in to join this conversation.
No description provided.