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

104 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Handoff — PON Go Android
Context for the next session/developer. Pairs with [`README.md`](README.md) and
[`docs/MCMS_API.md`](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.