# Changelog — PON Go (Android)

All notable changes to this app. The **★ Port to iOS** section in each entry calls
out changes that affect shared REST behaviour / security / device-safety and most
likely have an equivalent in the sibling iOS app (`../mcms_ios`) — the two share the
same MCMS field paths and request semantics (see `docs/MCMS_API.md`).

---

## [Unreleased] — Security hardening & code-review fixes (2026-05-31)

Outcome of a full code review. No `versionCode` bump yet — bump before release.
Release build now uses R8; **run `./gradlew :app:assembleDebug` and a `release`
build to verify** (the dev machine for this change had no Android SDK).

### Security

- **Removed untrusted-TLS support entirely.** Deleted `ServerTrustPolicy.AllowSelfSignedForHost`
  and its all-accepting `X509TrustManager` (it accepted *any* certificate for the
  configured host — a full MITM hole on a tool that can reset gear and push firmware).
  Removed the "Allow self-signed certificate" toggle from Settings. Only system-CA
  trust remains, plus the still-available `ServerTrustPolicy.PinPublicKeySha256` for
  pinning a known box's key. *(`core/Networking.kt`, `core/ApiConfiguration.kt`, `ui/AuthScreens.kt`)*
- **Enforced HTTPS.** `normalizedBaseUrl` now defaults a missing scheme to `https`,
  rejects any non-`https` URL, and rejects embedded credentials. Added
  `res/xml/network_security_config.xml` (cleartext forbidden, system CAs only — blocks
  the user-installed-CA MITM vector) + `usesCleartextTraffic="false"`. *(`core/ApiConfiguration.kt`, `AndroidManifest.xml`)*
- **Hardened stored credentials.** Passwords are no longer persisted at rest:
  "Remember me" stores the **email only**; the password field is never pre-filled and
  is kept out of the saved-instance `Bundle` (plain `remember`, not `rememberSaveable`).
  Credentials are cleared on sign-out and when "Remember me" is unchecked; any legacy
  stored password is purged. `allowBackup="false"`. *(`ui/AppState.kt`, `ui/AuthScreens.kt`, `AndroidManifest.xml`)*
- **CSRF cookie lookup** now resolves the host via OkHttp's parser (matching the cookie
  jar) so the `X-CSRFToken` lookup can't silently miss on host-normalisation edge cases.
- Unexpected (non-`ApiError`) throwables no longer leak raw library text to the UI. *(`ui/Types.kt`)*

### Correctness

- **Per-endpoint API versioning.** Endpoint paths are now fixed at `v1`/`v3` per
  `docs/MCMS_API.md` §4 instead of one global `apiVersion` (which was wrong for *every*
  config: lists/states need `v3`, single-doc/login/logs need `v1` — no single value
  worked). Removed the global `apiVersion` config field, the `version` override plumbing,
  and the API-version Settings input. Versions for undocumented endpoints (controllers,
  `olts/configs`, reset, `ponmgr/version`) are inferred and marked `// inferred`. *(`core/McmsEndpoint.kt`, `core/ApiClient.kt`)*
- **`"warning"` status treated as success.** The `{status,data}` unwrap now accepts
  `warning` as well as `success` (a `warning` means the write committed; only a schema
  mismatch is reported) — previously a committed firmware/config write was reported as
  failed. *(`core/ApiClient.kt`)*
- **Pagination fails loud, not silent.** `getAll` now throws instead of returning a
  truncated fleet when a full page yields no usable cursor (e.g. a projection drops
  `_id`). *(`core/ApiClient.kt`)*
- **`Response` no longer leaks on coroutine cancellation** (resume-with-onCancellation
  closes a dropped response — prevents connection-pool exhaustion on navigate-away). *(`core/ApiClient.kt`)*
- **Firmware compatibility now fails closed.** `FirmwareFile.isCompatible` returns
  compatible only when vendor + equipment id are known and the file positively matches
  (was: unknown/missing metadata ⇒ "compatible"). Staging a non-compatible image now
  requires an explicit extra acknowledgment in the dialog. Mis-flashing can brick an ONU. *(`core/Firmware.kt`, `ui/OnuScreens.kt`)*
- **JSON parse is tolerant.** A non-JSON 2xx body yields a typed `ApiError.Decoding`
  instead of crashing. A required non-empty `User-Agent` header is now sent (§3.1). *(`core/ApiClient.kt`)*

### Concurrency / lifecycle

- **Network I/O off the main thread** (`Dispatchers.IO`) — fixes ANR/jank on large
  fleets; the `onAuthExpired` Compose-state write stays on the main thread. *(`core/ApiClient.kt`)*
- **Stale-result generation guards** in every screen `load()`/`reload()`: a slow earlier
  load can no longer overwrite newer data, and `CancellationException` is rethrown (a
  cancelled load is no longer shown as an error). Fixes the ONU-detail tab-switch race. *(`ui/*Screens.kt`)*
- **`AppState` hosted in a `ViewModel`** so rotation / dark-mode no longer rebuilds the
  connection and bounces the user to login. *(`MainActivity.kt`)*
- Thread-safe `SessionCookieJar` (synchronised; OkHttp calls it from background threads). *(`core/Networking.kt`)*
- Re-entry guard on `reset()`/`upgrade()`; list-screen "Retry" buttons wired up; the
  dashboard keeps its last-good data when a background refresh fails. *(`ui/*Screens.kt`)*

### Build / Android

- Release build enables R8 `isMinifyEnabled` + `isShrinkResources` with correct
  kotlinx.serialization keep rules (so config (de)serialization survives shrinking). *(`app/build.gradle.kts`, `app/proguard-rules.pro`)*
- Dropped the unused `navigation-compose` dependency. Added the missing `.gitignore`. *(`app/build.gradle.kts`, `.gitignore`)*

### ★ Port to iOS (`mcms_ios`)

These touch shared REST/security/safety behaviour and very likely have an iOS analogue —
review and port:

1. **Per-endpoint v1/v3 versioning** — if iOS uses a single global API version it has the
   same "no value works for everything" defect. Pin versions per endpoint per `docs/MCMS_API.md` §4.
2. **`"warning"` = success** in the `{status,data}` unwrap — a committed write must not be
   reported as a failure.
3. **Firmware `isCompatible` fail-closed** (+ extra acknowledgment for a non-compatible
   image) — this is a device-brick safety fix, not cosmetic.
4. **Remove the self-signed "escape hatch."** HANDOFF notes the Android trust-all path
   "matches the iOS self-signed escape hatch," so iOS almost certainly has the same MITM
   hole. Remove it; rely on ATS + system trust, or pin the leaf key for a known box.
5. **Credential hardening** — store email only (never the password) in the Keychain;
   clear on sign-out / when remember-me is off; don't pre-fill the password field.
6. **Reject `http://` at config time** even though iOS ATS blocks cleartext at the
   transport layer — fail early with a clear message rather than a confusing transport error.
7. **Pagination fail-fast** when a full page has no cursor, rather than silently truncating.
8. **Send a non-empty `User-Agent`** (an empty UA is rejected by MCMS, §3.1).

iOS-specific equivalents worth a glance but not 1:1 ports: cancel-safe request teardown
(don't leak the response on task cancellation), and not surfacing stale results when a
newer load is in flight.
