PONGo_android/HANDOFF.md
Jon Vanvik 41098831ae Security hardening + code-review fixes
Outcome of a full multi-agent code review. Touches TLS, credential storage,
API versioning, networking correctness, concurrency/lifecycle, and the release
build. See CHANGELOG.md for the full list and the items to port to the iOS app.

Security
- Remove untrusted-TLS support: delete ServerTrustPolicy.AllowSelfSignedForHost
  and its trust-all X509TrustManager; drop the self-signed Settings toggle.
- Enforce HTTPS in normalizedBaseUrl (reject http + embedded credentials);
  add network_security_config (no cleartext, system CAs only) + allowBackup=false.
- Never persist passwords at rest: remember-me stores email only; no password
  pre-fill; clear creds on sign-out / when remember-me is off.
- Match CSRF host lookup to OkHttp's host parsing; stop leaking raw exception text.

Correctness
- Fix API versioning: hardcode v1/v3 per endpoint (docs Sec.4) instead of one
  broken global apiVersion; remove the version plumbing + Settings field.
- Treat "warning" status as success (committed write); fail-fast on a missing
  pagination cursor; close Response on cancellation; tolerant JSON parse; User-Agent.
- Firmware isCompatible fails closed + extra acknowledgment for a non-compatible image.

Concurrency / lifecycle
- Run network I/O on Dispatchers.IO; stale-result generation guards + rethrow
  CancellationException in all loads; host AppState in a ViewModel (survive config
  change); thread-safe cookie jar; reset/upgrade re-entry guard; wire list Retry.

Build
- Enable R8 minify+shrink with kotlinx.serialization keep rules; drop unused
  navigation-compose; add the missing .gitignore.

NOTE: built/verified by static review only (no Android SDK on the dev machine) —
run :app:assembleDebug and a release build before shipping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:37:48 +02:00

5.9 KiB
Raw Blame History

Handoff — PON Go Android

Context for the next session/developer. Pairs with README.md and docs/MCMS_API.md.

Architecture

  • core/ — pure Kotlin, no Android UI deps. Mirrors the iOS networking core.
    • JSON: MCMS docs are heterogeneous Mongo documents, so we keep raw kotlinx.serialization JsonElement trees and read fields via the accessors in Json.kt (el["key"], .string/.int/.double/.array/.obj) — the analogue of the iOS JSONValue. Models (Models.kt) are thin wrappers over a JsonElement with computed properties.
    • ApiClient (OkHttp): builds versioned requests, injects Referer + X-CSRFToken (writes), unwraps the {status,data} envelope, maps status codes to ApiError, paginates via the next cursor. Call.await() bridges OkHttp to coroutines with cancellation.
    • Repositories return model wrappers; McmsConnection wires it all; auth/cookies in Networking.kt (SessionCookieJar + SessionStore), TLS policy in ServerTrust, secrets in KeychainStore (EncryptedSharedPreferences).
  • ui/ — Jetpack Compose. Navigation is state-based in MainActivity (a List<Screen> back stack + BackHandler), not Navigation-Compose — so detail screens and the dashboard drill-downs can take in-memory objects directly. Each screen has a plain state-holder class (…Model) exposing mutableStateOf fields + suspend load()/refresh() — the analogue of the iOS @Observable view models. No androidx ViewModel/DI.

Build / verify loop (this matters)

The dev machine had no kotlinc/gradle on PATH, but Android Studio provides everything. Two loops:

  • Full build (authoritative):
    export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
    ./gradlew :app:assembleDebug --no-daemon --console=plain
    
    Needs local.properties (sdk.dir=…, git-ignored). First run downloads deps to ~/.gradle; afterwards it's ~2060 s. This is what proves the whole app (UI + resources + manifest + icons) compiles. release now enables R8 minify+shrink (keep rules in proguard-rules.pro) — smoke-test a release build before shipping, since R8 can strip serializers if the keep rules ever drift.
  • Quick core-only check (no Gradle), when iterating on core/: the bundled compiler at …/Android Studio.app/Contents/plugins/Kotlin/kotlinc/bin/kotlinc with -Xplugin=…/kotlinc/lib/kotlinx-serialization-compiler-plugin.jar and a classpath of Maven jars (kotlinx-serialization-json/core, okhttp, okio-jvm, kotlinx-coroutines-core-jvm). Compiles every core/*.kt except KeychainStore.kt (androidx). Faster than a Gradle round-trip.

API gotchas (all handled in code — don't regress)

  • Single-encode URLs. Ids stay RAW in McmsEndpoint; ApiClient encodes the path once via HttpUrl.addPathSegments. Pre-encoding double-encoded MAC colons (:%3A%253A) → MCMS "invalid id".
  • Endpoint versions are fixed per endpoint (v1/v3), NOT global. Lists/states are v3; single-doc/login/logs/firmware-files are v1 (§4) — the two are not interchangeable, so there is no global API-version setting. Hardcoded in McmsEndpoint.path(); endpoints not in the §4 table are marked // inferred.
  • 401 bounces to login; 403 does NOT (permission/CSRF — requiresReauthentication is 401-only).
  • Writes return {"status":"success"} (or "warning") with no data — the unwrap tolerates both. "warning" means the write committed (schema mismatch only) and is treated as success, NOT an error (§3.5).
  • ONU registration lives on OLT-STATE buckets (OltRepository.registrationMap), not ONU-STATE. Friendly names are ONU.Name / OLT.Name (NOT NETCONF.Name).
  • CPE DHCP comes from the web helper GET /api/cpe/onu/<id>/ — a versionless, bare [code, cpe…] array (use ApiClient.getRaw).
  • Firmware = Procedure 7: GET ONU-CFG → write filename/version into the inactive bank → full-doc PUT (FirmwareUpgrade.apply). Inactive-bank only, so service stays up until the ONU reboots. FirmwareFile.isCompatible now fails CLOSED (unknown vendor/model ⇒ not compatible-by-default) and staging a non-compatible image needs an extra acknowledgment. Still worth testing on a spare ONU.
  • Field paths were confirmed against live JSON; see docs/MCMS_API.md and the model accessors.

Trimmed vs iOS (intentional, easy to restore)

  • ONU detail tabs are Overview / CPE / Logs / Config — no separate Alarms tab. Current alarms live on ONU-STATE "Alarm" buckets if you want to add it.
  • Config shows pretty-printed JSON text, not the collapsible tree the iOS app has.
  • Dashboard ONU-state rows aren't tappable (the Health rows are). The iOS app drills ONU-state → filtered ONU list.
  • Toggles use rememberSaveable (survive rotation, not process death). Persist in prefs if you want parity with iOS @AppStorage.
  • TLS is HTTPS-only with system-CA trust; self-signed/cleartext support was removed (security — see CHANGELOG.md, not "easy to restore"). res/xml/network_security_config.xml forbids cleartext and user-installed CAs. To reach a private-cert box, install its CA in the device system store or pin its key via ServerTrustPolicy.PinPublicKeySha256. The trust policy lives in ServerTrust.

Repo split

This folder is self-contained (has its own docs/MCMS_API.md). To make it a git repo: git init here; the included .gitignore already excludes build/, .gradle/, local.properties, .idea/. Commit the Gradle wrapper (gradlew, gradle/wrapper/*). The full dev-guide PDFs (323-1961-30x) live in the iOS repo if you ever need the deep source.

Possible next steps

Restore the trimmed items above; add OLT firmware (Procedure 8 bulk task); per-ONU live-alarms view; charts for PM history; pull-to-refresh on lists; dark-theme polish; a proper app theme from the brand colours.