Closes the six remediation items from the 2026-07-29 iOS completion audit plus the
whole P2 wave and Android access-token parity.
The audit's headline was that the client was code-complete but stuck at the device
door: no DEVELOPMENT_TEAM, no entitlements, so it had never run on real hardware
once, and it had fallen two months behind the server (Android had the git panel,
iOS had none) while neither native client could connect at all once WEBTERM_TOKEN
was set.
Package tests 310 -> 452, app bundle 296 -> 550 (iPhone and iPad, zero known
issues), integration 10 -> 32, Android 687 -> 691. ClientTLS went 55.76% -> 89.49%
and is now actually in the coverage gate, which it never was. Device build now
succeeds on the free personal team.
src/ and public/ are untouched — the git-panel endpoints already existed
server-side; iOS simply never consumed them.
# Conflicts:
# android/.gitignore
# android/README.md
# android/api-client/src/main/kotlin/wang/yaojia/webterm/api/routes/Endpoints.kt
# android/app/src/main/java/wang/yaojia/webterm/screens/PairingScreen.kt
# android/app/src/main/java/wang/yaojia/webterm/viewmodels/PairingViewModel.kt
# android/app/src/main/java/wang/yaojia/webterm/wiring/AppEnvironment.kt
# android/transport-okhttp/src/main/kotlin/wang/yaojia/webterm/transport/OkHttpClientFactory.kt
# docs/PROGRESS_LOG.md
Android could not connect at all once the server set WEBTERM_TOKEN. Hand-written
Cookie header on every request and on the WS upgrade (no CookieJar, matching the
frozen decision), POST /auth pairing probe, Keystore-backed storage, and a 401
upgrade as a terminal state with no reconnect loop.
Four gaps that made "ship it" and "test it on hardware" impossible.
RELEASE SIGNING. assembleRelease was silently producing
app-release-unsigned.apk because there was no signingConfigs block at all —
the failure mode where you only notice at install time. Credentials now
resolve from keystore.properties, then local.properties, then
WEBTERM_RELEASE_* env; nothing is created or committed, and
keystore.properties.example is the template.
The degradation is ENFORCED, not documented. The signingConfig is only
created when credentials actually resolve — an empty config is precisely what
makes AGP emit an unsigned artifact quietly — and a guard on packageRelease
throws with an actionable message, distinguishing "not configured" from
"credentials found but the keystore file is missing: <path>". It hooks the
PACKAGE task on purpose: R8 has already run and reported by then, so a
developer without a keystore still gets the full shrink result rather than an
early abort. Verified: minifyReleaseWithR8 completes, packageRelease fails
loudly, and no unsigned APK is written.
VERSIONING. Was still the scaffold 1 / "0.1.0". Now 1 / "0.1.0-alpha01",
where the suffix is a factual claim about device verification rather than
decoration: alpha means device QA is essentially unstarted, beta means the
A34/A35 checklist blocks pass on hardware, and plain 0.1.0 means the whole
checklist is ticked. versionCode stays 1 because no artifact has ever left
the build machine.
MINIFICATION. release had isMinifyEnabled = false and an empty rules file —
so nothing had ever exercised R8 on this app, and the first release build
would have been the experiment. Now minify + resource shrinking, with a
deliberately short rules file: the actual artifacts were checked, and
kotlinx.serialization, Tink, Hilt, FCM, OkHttp, Compose and CameraX all ship
their own consumer rules, so none are re-declared. The termux packages are
kept whole because the JitPack artifacts ship no rules and :terminal-view
binds to non-API internals — it writes the public TerminalView.mEmulator
field and calls TerminalRenderer.getFontWidth()/getFontLineSpacing(). Getting
that wrong yields a build that crashes only in production.
lintVitalRelease also passes now, the two pre-existing lint ERRORS having
been fixed earlier this session.
ANDROID TEST ENABLEMENT. :app had no androidTest source set and no
instrumentation runner, which is the mechanical reason A34/A35 have no code
at all. The runner (Hilt-aware) and dependencies are wired, and a
:macrobenchmark module exists. The tests themselves are a separate change.
Verified: ./gradlew test :app:assembleDebug :app:assembleDebugAndroidTest
:app:assembleBenchmark :macrobenchmark:assembleBenchmark
:app:lintVitalRelease koverVerify -> BUILD SUCCESSFUL; 901 JVM tests, 0
failures on a forced re-execution with test-results deleted and
--no-build-cache.