feat: add public Android staging flow
This commit is contained in:
parent
4d5efea419
commit
3a35af6782
|
|
@ -11,8 +11,10 @@ PHX_URL_PORT=4010
|
|||
# Android debug builds compile this origin into BuildConfig. The Docker
|
||||
# emulator uses adb reverse to expose the local Compose proxy on loopback.
|
||||
WNH_DEBUG_BASE_URL=http://localhost:4010
|
||||
# Release builds require a public HTTPS origin.
|
||||
WNH_BASE_URL=https://whoneedhelp.imalto.site
|
||||
# Staging/release builds require a public HTTPS origin. Keep the value
|
||||
# environment-specific; scripts/ensure-local-public-origin.sh can derive it
|
||||
# from the three PHX_* values in the ignored .env.
|
||||
WNH_BASE_URL=
|
||||
# These local values preserve the existing five-second client freshness window
|
||||
# and fifteen-second request timeout. They are build inputs, not measured
|
||||
# production capacity recommendations.
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ thank-you link; money goes directly between users outside the platform.
|
|||
- Native Android WebView client with the same authenticated LiveView, map,
|
||||
private chat, and a user-started location foreground service. Its persistent
|
||||
notification exposes Stop, it continues while the Activity is minimized, and
|
||||
it retains only the current point. The reproducible Docker target currently
|
||||
exports a debug APK; production signing and store publication are not
|
||||
configured.
|
||||
it retains only the current point. Reproducible Docker targets export
|
||||
distinct local and public-staging debug APKs; production signing and store
|
||||
publication are not configured.
|
||||
- Local, advisory Codex category review through the user's ChatGPT-authenticated
|
||||
Codex CLI. It receives a PII-free export and never writes to the database.
|
||||
- One immutable release image with `web`, `worker`, and one-shot `migrate`
|
||||
|
|
|
|||
|
|
@ -111,3 +111,32 @@ COPY --from=android-sdk \
|
|||
COPY --from=android-sdk \
|
||||
/workspace/android/app/build/reports/lint-results-debug.html \
|
||||
/lint-results-debug.html
|
||||
|
||||
FROM android-base AS android-staging-sdk
|
||||
|
||||
USER gradle
|
||||
WORKDIR /workspace/android
|
||||
|
||||
COPY --chown=gradle:gradle . .
|
||||
|
||||
ARG WNH_BASE_URL
|
||||
ARG WNH_TRACKING_MIN_TIME_MS
|
||||
ARG WNH_TRACKING_HTTP_TIMEOUT_MS
|
||||
|
||||
RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||
--mount=type=cache,target=/home/gradle/.android,uid=1000,gid=1000 \
|
||||
gradle --no-daemon \
|
||||
"-PWNH_BASE_URL=${WNH_BASE_URL}" \
|
||||
"-PWNH_DEBUG_BASE_URL=${WNH_BASE_URL}" \
|
||||
"-PWNH_TRACKING_MIN_TIME_MS=${WNH_TRACKING_MIN_TIME_MS}" \
|
||||
"-PWNH_TRACKING_HTTP_TIMEOUT_MS=${WNH_TRACKING_HTTP_TIMEOUT_MS}" \
|
||||
testDebugUnitTest lintStaging assembleStaging
|
||||
|
||||
FROM scratch AS staging-artifact
|
||||
|
||||
COPY --from=android-staging-sdk \
|
||||
/workspace/android/app/build/outputs/apk/staging/app-staging.apk \
|
||||
/who-need-help-staging.apk
|
||||
COPY --from=android-staging-sdk \
|
||||
/workspace/android/app/build/reports/lint-results-staging.html \
|
||||
/lint-results-staging.html
|
||||
|
|
|
|||
|
|
@ -41,6 +41,25 @@ Release builds do not have a default server. Supply the real HTTPS deployment:
|
|||
The build rejects a missing, HTTP, credentialed, query-bearing, or
|
||||
fragment-bearing release URL.
|
||||
|
||||
## Public staging build
|
||||
|
||||
The installable `staging` build type uses the explicit public HTTPS
|
||||
`WNH_BASE_URL`, disables cleartext traffic, and has its own
|
||||
`org.whoneedhelp.mobile.staging` application ID. Configure a missing local value
|
||||
from the existing `PHX_HOST`, `PHX_SCHEME`, and `PHX_URL_PORT`, then build:
|
||||
|
||||
```sh
|
||||
./scripts/ensure-local-public-origin.sh
|
||||
./scripts/android-staging-build.sh
|
||||
sha256sum android/dist-staging/who-need-help-staging.apk
|
||||
```
|
||||
|
||||
This variant uses Android's generic debug signing key so it can be installed
|
||||
for staging verification. It is not a production-signed artifact and must not
|
||||
be published as a release. The manifest accepts same-origin HTTPS deep links,
|
||||
but verified Android App Links additionally require the final signing
|
||||
certificate fingerprint in the deployment's `/.well-known/assetlinks.json`.
|
||||
|
||||
## Reproducible Docker build
|
||||
|
||||
From the repository root:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@ val trackingMinTimeMs = providers.gradleProperty("WNH_TRACKING_MIN_TIME_MS").orE
|
|||
val trackingHttpTimeoutMs =
|
||||
providers.gradleProperty("WNH_TRACKING_HTTP_TIMEOUT_MS").orElse("0")
|
||||
|
||||
fun manifestOrigin(value: String): URI? =
|
||||
runCatching { URI(value) }
|
||||
.getOrNull()
|
||||
?.takeIf { uri ->
|
||||
(uri.scheme == "http" || uri.scheme == "https") && !uri.host.isNullOrBlank()
|
||||
}
|
||||
|
||||
val debugManifestOrigin = manifestOrigin(debugBaseUrl.get())
|
||||
val releaseManifestOrigin = manifestOrigin(releaseBaseUrl.get())
|
||||
|
||||
android {
|
||||
namespace = "org.whoneedhelp.mobile"
|
||||
compileSdk = 37
|
||||
|
|
@ -42,6 +52,25 @@ android {
|
|||
"\"${debugBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||
)
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
manifestPlaceholders["deepLinkScheme"] = debugManifestOrigin?.scheme ?: "https"
|
||||
manifestPlaceholders["deepLinkHost"] =
|
||||
debugManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
||||
}
|
||||
|
||||
create("staging") {
|
||||
initWith(getByName("debug"))
|
||||
applicationIdSuffix = ".staging"
|
||||
versionNameSuffix = "-staging"
|
||||
buildConfigField(
|
||||
"String",
|
||||
"BASE_URL",
|
||||
"\"${releaseBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||
)
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
manifestPlaceholders["deepLinkScheme"] = releaseManifestOrigin?.scheme ?: "https"
|
||||
manifestPlaceholders["deepLinkHost"] =
|
||||
releaseManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
||||
matchingFallbacks += listOf("debug")
|
||||
}
|
||||
|
||||
release {
|
||||
|
|
@ -53,6 +82,9 @@ android {
|
|||
"\"${releaseBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||
)
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
manifestPlaceholders["deepLinkScheme"] = releaseManifestOrigin?.scheme ?: "https"
|
||||
manifestPlaceholders["deepLinkHost"] =
|
||||
releaseManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
|
|
@ -74,7 +106,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
tasks.matching { it.name == "preReleaseBuild" }.configureEach {
|
||||
tasks.matching { it.name == "preReleaseBuild" || it.name == "preStagingBuild" }.configureEach {
|
||||
doFirst {
|
||||
val value = releaseBaseUrl.orNull.orEmpty()
|
||||
val uri = runCatching { URI(value) }.getOrNull()
|
||||
|
|
@ -88,7 +120,8 @@ tasks.matching { it.name == "preReleaseBuild" }.configureEach {
|
|||
uri.fragment != null
|
||||
) {
|
||||
throw GradleException(
|
||||
"Release builds require -PWNH_BASE_URL=https://your-real-deployment.example"
|
||||
"Staging and release builds require "
|
||||
+ "-PWNH_BASE_URL=https://your-real-deployment.example"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,20 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="false">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:host="${deepLinkHost}"
|
||||
android:scheme="${deepLinkScheme}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".TrackingService"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package org.whoneedhelp.mobile;
|
||||
|
||||
final class LaunchUrlResolver {
|
||||
private LaunchUrlResolver() {}
|
||||
|
||||
static String initialUrl(
|
||||
TrustedOrigin trustedOrigin,
|
||||
String deepLinkUrl,
|
||||
String debugInitialUrl,
|
||||
boolean debugBuild
|
||||
) {
|
||||
String candidate = incomingUrl(
|
||||
trustedOrigin,
|
||||
deepLinkUrl,
|
||||
debugInitialUrl,
|
||||
debugBuild
|
||||
);
|
||||
|
||||
return candidate == null ? trustedOrigin.startUrl() : candidate;
|
||||
}
|
||||
|
||||
static String incomingUrl(
|
||||
TrustedOrigin trustedOrigin,
|
||||
String deepLinkUrl,
|
||||
String debugInitialUrl,
|
||||
boolean debugBuild
|
||||
) {
|
||||
if (deepLinkUrl != null && trustedOrigin.matches(deepLinkUrl)) {
|
||||
return deepLinkUrl;
|
||||
}
|
||||
|
||||
if (
|
||||
debugBuild &&
|
||||
debugInitialUrl != null &&
|
||||
trustedOrigin.matches(debugInitialUrl)
|
||||
) {
|
||||
return debugInitialUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -127,15 +127,29 @@ public final class MainActivity extends ComponentActivity {
|
|||
}
|
||||
|
||||
private String initialUrl() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
String candidate = getIntent().getStringExtra("initial_url");
|
||||
return LaunchUrlResolver.initialUrl(
|
||||
trustedOrigin,
|
||||
getIntent().getDataString(),
|
||||
getIntent().getStringExtra("initial_url"),
|
||||
BuildConfig.DEBUG
|
||||
);
|
||||
}
|
||||
|
||||
if (candidate != null && trustedOrigin.matches(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
|
||||
String candidate = LaunchUrlResolver.incomingUrl(
|
||||
trustedOrigin,
|
||||
intent.getDataString(),
|
||||
intent.getStringExtra("initial_url"),
|
||||
BuildConfig.DEBUG
|
||||
);
|
||||
|
||||
if (candidate != null && webView != null) {
|
||||
webView.loadUrl(candidate);
|
||||
}
|
||||
|
||||
return trustedOrigin.startUrl();
|
||||
}
|
||||
|
||||
private static String safeLogPath(Uri uri) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
package org.whoneedhelp.mobile;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class LaunchUrlResolverTest {
|
||||
private final TrustedOrigin origin =
|
||||
TrustedOrigin.parse("https://help.example", false);
|
||||
|
||||
@Test
|
||||
public void releaseAcceptsOnlySameOriginDeepLinks() {
|
||||
assertEquals(
|
||||
"https://help.example/requests/123",
|
||||
LaunchUrlResolver.incomingUrl(
|
||||
origin,
|
||||
"https://help.example/requests/123",
|
||||
null,
|
||||
false
|
||||
)
|
||||
);
|
||||
assertNull(
|
||||
LaunchUrlResolver.incomingUrl(
|
||||
origin,
|
||||
"https://attacker.example/requests/123",
|
||||
null,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void releaseIgnoresDebugInitialUrlExtra() {
|
||||
assertEquals(
|
||||
"https://help.example",
|
||||
LaunchUrlResolver.initialUrl(
|
||||
origin,
|
||||
null,
|
||||
"https://help.example/requests/123",
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void debugCanUseValidatedInitialUrlExtra() {
|
||||
assertEquals(
|
||||
"https://help.example/requests/123",
|
||||
LaunchUrlResolver.initialUrl(
|
||||
origin,
|
||||
null,
|
||||
"https://help.example/requests/123",
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -119,8 +119,10 @@ and a block prevents new discovery, joins, and messages.
|
|||
## Success criteria for the hackathon
|
||||
|
||||
- A new user can complete the primary journey locally in two browser sessions.
|
||||
- The debug Android client can authenticate against the local stack and use
|
||||
the request map, matched chat, and foreground sharing on an emulator.
|
||||
- The debug Android client can authenticate against the local stack, while the
|
||||
separately installable staging variant can use an explicit public HTTPS
|
||||
origin. Both use the request map, matched chat, and user-started foreground
|
||||
sharing on an emulator.
|
||||
- The same immutable image runs as both web and worker roles.
|
||||
- Two web replicas and two worker replicas run concurrently in normal
|
||||
development.
|
||||
|
|
|
|||
|
|
@ -16,25 +16,28 @@ results from product limits and unknown production properties.
|
|||
| Consent-driven live tracking | Implemented and cross-client verified | On API 37, Android started `TrackingService` as a location foreground service with a persistent Stop notification. After Home minimized the Activity, an emulator coordinate change reached PostGIS. Notification Stop removed the service, notification, active session, and raw position. | Browsers stop with the page. Android has no `ACCESS_BACKGROUND_LOCATION`, unattended start, or route history. |
|
||||
| Privacy settings | Implemented and browser-verified | The profile exposed hidden, approximate public, exact for active match, and explicit exact-public options. Blocking and current-position cleanup have automated tests. | Exact public location remains a user opt-in; legal privacy and retention text still requires jurisdiction-specific review before launch. |
|
||||
| Reputation and anti-abuse | Implemented at MVP level | Handover codes, two-party completion, double-blind reviews, unique-counterpart ranking, optional movement/proximity evidence, reports, blocks, abuse signals, and moderator audit paths have automated tests. | The system is not bot-proof and does not claim identity verification. No punitive numeric policy is enabled without measured and approved thresholds. |
|
||||
| Social profiles | Manual links implemented; GitHub verification implemented and automated-tested | Manual links cannot set verification fields. The optional GitHub flow uses state, PKCE, a user-bound one-time session, unique provider ownership, and an audit record; 146 tests pass, including callback replay/state checks. No access-token field exists and the controller receives only normalized identity attributes. | The staging operator has not supplied GitHub OAuth credentials, so the real external provider redirect/callback remains disabled and has not been browser-verified. Other providers remain manual/unverified. |
|
||||
| Social profiles | Manual links implemented; GitHub verification implemented and automated-tested | Manual links cannot set verification fields. The optional GitHub flow uses state, PKCE, a user-bound one-time session, unique provider ownership, and an audit record; 149 tests pass, including callback replay/state checks. No access-token field exists and the controller receives only normalized identity attributes. | The staging operator has not supplied GitHub OAuth credentials, so the real external provider redirect/callback remains disabled and has not been browser-verified. Other providers remain manual/unverified. |
|
||||
| Voluntary thanks | Implemented as an external optional link | A helper can expose an optional link after completion; the UI states that the platform does not process the payment. | The platform does not provide payments, escrow, refunds, tax reporting, or payment guarantees. |
|
||||
| Android client | Debug client implemented and emulator-verified | The native package `org.whoneedhelp.mobile.debug` launches the same authenticated LiveView app. Login, map, chat, permission prompts, minimized foreground-service location updates, notification Stop, and server cleanup were exercised on API 37. | Production signing, a public production origin, Play Store publication, unattended/background-permission tracking, and iOS are not implemented. |
|
||||
| Android client | Local and public-staging clients implemented and emulator-verified | The native packages `org.whoneedhelp.mobile.debug` and `org.whoneedhelp.mobile.staging` launch the same authenticated LiveView app. Public HTTPS login, map, two-way chat, permission prompts, minimized foreground-service location updates, notification Stop, deep-link routing, and server cleanup were exercised on API 37. | Production signing, Play Store publication, verified Android App Links, unattended/background-permission tracking, and iOS are not implemented. |
|
||||
| Multiple web/worker instances | Implemented and locally verified | Docker Compose and kind each ran 2 web and 2 worker replicas. The project probes cross-node Phoenix PubSub using different BEAM nodes. Kubernetes web/worker pods were Ready with zero restarts at the final observation. | Local PostGIS is a single instance. Production database HA, backups, and recovery are operator work and are not claimed complete. |
|
||||
|
||||
## Reproducible checks
|
||||
|
||||
- `./scripts/test.sh`: 146 tests, 0 failures after verified GitHub linking
|
||||
- `./scripts/test.sh`: 149 tests, 0 failures after the Android public-staging and
|
||||
realtime tracking-evidence changes
|
||||
on Elixir 1.20.2 and Erlang/OTP 29.0.3.
|
||||
- `mix compile --force --warnings-as-errors` and
|
||||
`mix format --check-formatted`: passed after the OAuth change.
|
||||
`mix format --check-formatted`: passed against the same final source.
|
||||
- The generated Activity migration was rolled back by exactly one step and
|
||||
migrated forward again against `who_need_help_test`; both directions passed.
|
||||
- The Activity-report migration was also rolled back and migrated forward. The
|
||||
observed database constraint changed from exactly one of 3 urgent-help
|
||||
targets to exactly one of 5 urgent-help/Activity targets.
|
||||
- `mix format --check-formatted`: passed in the final run.
|
||||
- Android Docker build target: `testDebugUnitTest`, `lintDebug`, and
|
||||
`assembleDebug` passed; the final lint report contains no errors or warnings.
|
||||
- Android local Docker build target: `testDebugUnitTest`, `lintDebug`, and
|
||||
`assembleDebug` passed. The public-staging target ran the unit tests plus
|
||||
`lintStaging` and `assembleStaging`; both final lint reports contain no errors
|
||||
or warnings.
|
||||
- Release Android guard: a release build without `WNH_BASE_URL` failed as
|
||||
intended; release configuration accepts only an explicit HTTPS origin.
|
||||
- Helm lint, template rendering, server-side dry-run, rollout waits, readiness
|
||||
|
|
@ -49,14 +52,23 @@ Local generated evidence (ignored by Git):
|
|||
- `output/playwright/final-privacy-profile.png`
|
||||
- `output/android/final-image-smoke.png`
|
||||
- `output/android/foreground-notification.png`
|
||||
- `output/android/staging-deep-link-safety.png`
|
||||
|
||||
Android artifact:
|
||||
|
||||
- `android/dist/who-need-help-debug.apk`
|
||||
- Final post-upgrade SHA-256:
|
||||
`063f3d8d877009ee229a403692e4b16517244dd9e7dddab4c2b3c202c5def4b8`
|
||||
- Observed manifest values: version `0.1.0-debug`, minimum SDK 24, target and
|
||||
compile SDK 37.
|
||||
- SHA-256:
|
||||
`4520aa0b50eaf53bb7052e7f4c096a73d456f45781380187586c79d7267c21c5`
|
||||
- `android/dist-staging/who-need-help-staging.apk`
|
||||
- SHA-256:
|
||||
`08d990a9268a382052be5f2d3ba5afba2aa65a9ad5f9ae1ad4bff720f8999a6e`
|
||||
- Observed staging manifest values: package
|
||||
`org.whoneedhelp.mobile.staging`, version `0.1.0-staging`, minimum SDK 24,
|
||||
target and compile SDK 37, cleartext traffic disabled, `singleTop`
|
||||
`MainActivity`, and an exact HTTPS host of `whoneedhelp.imalto.site`.
|
||||
- `apksigner verify` accepted the staging artifact's v2 generic debug
|
||||
signature. That signing identity is only for installable staging verification
|
||||
and is not a production release identity.
|
||||
|
||||
## Configuration finding
|
||||
|
||||
|
|
@ -174,6 +186,60 @@ Local ignored browser evidence:
|
|||
|
||||
- `.playwright-cli/page-2026-07-18T18-09-44-303Z.png`
|
||||
|
||||
## Public Android and cross-client observation
|
||||
|
||||
The public-staging APK was built with the explicit ignored `.env`
|
||||
`WNH_BASE_URL`, installed on the API 37 emulator, and connected to
|
||||
`https://whoneedhelp.imalto.site`. Android package inspection found exactly one
|
||||
matching Activity for the configured HTTPS host and no matching Activity for
|
||||
`attacker.example`. A standard implicit `ACTION_VIEW` launch of `/safety`
|
||||
opened the native `MainActivity` and displayed the public Safety rules.
|
||||
|
||||
The manifest intentionally sets `android:autoVerify="false"`: this is a
|
||||
same-origin HTTPS deep link, not a claimed verified Android App Link. Enabling
|
||||
verification requires the final production application ID and signing
|
||||
certificate fingerprint to be published in the deployment's
|
||||
`/.well-known/assetlinks.json`.
|
||||
|
||||
A cross-client scenario then used headed Chrome as the requester and the
|
||||
public-staging Android app as the helper. The observed behavior was:
|
||||
|
||||
- the Android app authenticated against the public HTTPS origin and rendered
|
||||
the MapLibre request;
|
||||
- chat messages travelled in both directions, including an Android-bound
|
||||
browser message appearing without reload;
|
||||
- Android granted foreground location permissions, started
|
||||
`TrackingService`, and showed its persistent Stop notification;
|
||||
- after Home minimized the Activity, an emulator location change from
|
||||
`50.46009833, 30.5334` to `50.4611, 30.5344` reached PostGIS and produced
|
||||
`121.97` metres of observed movement;
|
||||
- the headed browser received the helper marker in real time and removed it
|
||||
when tracking stopped;
|
||||
- notification Stop removed the Android service and notification, ended the
|
||||
exact tracking session, left zero active sessions, and deleted its raw current
|
||||
position.
|
||||
|
||||
The browser scenario exposed one UI defect: movement evidence was persisted and
|
||||
the marker updated, but the evidence badge remained stale until reload. The
|
||||
tracking PubSub event now carries the already-derived movement/proximity
|
||||
timestamps, and the LiveView updates its in-memory assignment from that same
|
||||
event without an extra query. Domain and two-client LiveView regression tests
|
||||
assert the event and badge change.
|
||||
|
||||
That fix was deployed with image digest
|
||||
`sha256:1b991c1e07d98babb5151c010250cd0b9458195cf3de5a91972456ee72d75673`.
|
||||
Both web and both worker containers used the digest, the cross-node PubSub probe
|
||||
passed, local and public readiness returned HTTP 200, and no application
|
||||
error/warning was found in the checked post-rollout logs. The before/after
|
||||
database counts remained `2 users / 1 matched request / 7 messages / 14
|
||||
categories / 1 assignment / 4 ended tracking sessions / 0 active tracking
|
||||
sessions / 0 activities / 0 reports / 0 social identities`.
|
||||
|
||||
The exact temporary E2E users, tokens, request, assignment, messages, tracking
|
||||
session, and audit records were removed in one scoped transaction. Queries by
|
||||
their exact identifiers found no remaining rows and the original database
|
||||
counts were restored. The headed browser windows and emulator were left open.
|
||||
|
||||
## Operations and metrics verification
|
||||
|
||||
On 2026-07-18, the Compose database was archived with PostgreSQL 18
|
||||
|
|
|
|||
|
|
@ -80,11 +80,11 @@ defmodule WhoNeedHelp.Tracking do
|
|||
end
|
||||
end)
|
||||
|
||||
with {:ok, position} <- result do
|
||||
with {:ok, {position, evidence}} <- result do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WhoNeedHelp.PubSub,
|
||||
"tracking:#{assignment.id}",
|
||||
{:position_updated, user.id, public_position(position)}
|
||||
{:position_updated, user.id, public_position(position), evidence}
|
||||
)
|
||||
|
||||
{:ok, position}
|
||||
|
|
@ -223,8 +223,14 @@ defmodule WhoNeedHelp.Tracking do
|
|||
assignment =
|
||||
maybe_mark_helper_movement(assignment, user.id, session.movement_observed_at, now)
|
||||
|
||||
maybe_mark_proximity(assignment, session, position, now)
|
||||
{:ok, position}
|
||||
assignment = maybe_mark_proximity(assignment, session, position, now)
|
||||
|
||||
evidence = %{
|
||||
helper_movement_observed_at: assignment.helper_movement_observed_at,
|
||||
proximity_observed_at: assignment.proximity_observed_at
|
||||
}
|
||||
|
||||
{:ok, {position, evidence}}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -59,9 +59,13 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
load(socket, Help.get_request!(socket.assigns.request.id), socket.assigns.tracking_active)}
|
||||
end
|
||||
|
||||
def handle_info({:position_updated, user_id, position}, socket) do
|
||||
def handle_info({:position_updated, user_id, position, evidence}, socket) do
|
||||
positions = Map.put(socket.assigns.positions, user_id, position)
|
||||
{:noreply, assign_positions(socket, positions)}
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_positions(positions)
|
||||
|> assign_assignment_evidence(evidence)}
|
||||
end
|
||||
|
||||
def handle_info({:tracking_stopped, user_id}, socket) do
|
||||
|
|
@ -389,6 +393,20 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
|||
|> assign(:markers, Jason.encode!(markers))
|
||||
end
|
||||
|
||||
defp assign_assignment_evidence(%{assigns: %{assignment: nil}} = socket, _evidence),
|
||||
do: socket
|
||||
|
||||
defp assign_assignment_evidence(socket, evidence) do
|
||||
assignment = socket.assigns.assignment
|
||||
|
||||
assign(socket, :assignment, %{
|
||||
assignment
|
||||
| helper_movement_observed_at:
|
||||
evidence.helper_movement_observed_at || assignment.helper_movement_observed_at,
|
||||
proximity_observed_at: evidence.proximity_observed_at || assignment.proximity_observed_at
|
||||
})
|
||||
end
|
||||
|
||||
defp structured_details(request, locale) do
|
||||
request.category
|
||||
|> WhoNeedHelp.Catalog.structured_fields()
|
||||
|
|
|
|||
23
scripts/android-staging-build.sh
Executable file
23
scripts/android-staging-build.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env"
|
||||
|
||||
"$ROOT/scripts/ensure-local-public-origin.sh"
|
||||
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
: "${WNH_BASE_URL:?Set WNH_BASE_URL in .env}"
|
||||
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}"
|
||||
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
||||
|
||||
exec docker build \
|
||||
--build-arg "WNH_BASE_URL=$WNH_BASE_URL" \
|
||||
--build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
|
||||
--build-arg "WNH_TRACKING_HTTP_TIMEOUT_MS=$WNH_TRACKING_HTTP_TIMEOUT_MS" \
|
||||
--target staging-artifact \
|
||||
--output "type=local,dest=$ROOT/android/dist-staging" \
|
||||
"$ROOT/android"
|
||||
71
scripts/ensure-local-public-origin.sh
Executable file
71
scripts/ensure-local-public-origin.sh
Executable file
|
|
@ -0,0 +1,71 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Missing $ENV_FILE. Copy .env.example to .env first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v perl >/dev/null 2>&1; then
|
||||
echo "Required command is unavailable: perl" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
if [ -n "${WNH_BASE_URL:-}" ]; then
|
||||
chmod 600 "$ENV_FILE"
|
||||
echo "WNH_BASE_URL already exists; no configuration was changed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
: "${PHX_HOST:?PHX_HOST is missing from .env}"
|
||||
: "${PHX_SCHEME:?PHX_SCHEME is missing from .env}"
|
||||
: "${PHX_URL_PORT:?PHX_URL_PORT is missing from .env}"
|
||||
|
||||
if [ "$PHX_SCHEME" != https ]; then
|
||||
echo "A public Android origin requires PHX_SCHEME=https." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$PHX_HOST" in
|
||||
"" | *[!A-Za-z0-9.-]*)
|
||||
echo "PHX_HOST is not a supported public DNS hostname." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$PHX_URL_PORT" in
|
||||
"" | *[!0-9]*)
|
||||
echo "PHX_URL_PORT must be a numeric HTTPS port." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$PHX_URL_PORT" = 443 ]; then
|
||||
public_origin="https://$PHX_HOST"
|
||||
else
|
||||
public_origin="https://$PHX_HOST:$PHX_URL_PORT"
|
||||
fi
|
||||
|
||||
tmp_env=$(mktemp "${ENV_FILE}.public-origin.XXXXXX")
|
||||
trap 'rm -f "$tmp_env"' EXIT HUP INT TERM
|
||||
chmod 600 "$tmp_env"
|
||||
|
||||
WNH_PUBLIC_ORIGIN=$public_origin perl -0pe '
|
||||
END {
|
||||
print "\nWNH_BASE_URL=$ENV{WNH_PUBLIC_ORIGIN}\n";
|
||||
}
|
||||
' "$ENV_FILE" >"$tmp_env"
|
||||
|
||||
mv "$tmp_env" "$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
trap - EXIT HUP INT TERM
|
||||
unset public_origin
|
||||
|
||||
echo "Configured WNH_BASE_URL from the existing HTTPS Phoenix origin."
|
||||
|
|
@ -59,6 +59,7 @@ defmodule WhoNeedHelp.TrustSafetyTest do
|
|||
test "tracking derives movement and proximity from browser accuracy envelopes", context do
|
||||
{:ok, request} = Help.create_request(context.requester_scope, context.attrs)
|
||||
{:ok, assignment} = Help.accept_request(context.helper_scope, request.id)
|
||||
:ok = Tracking.subscribe(assignment.id)
|
||||
{:ok, _} = Tracking.start_session(context.helper_scope, assignment)
|
||||
{:ok, _} = Tracking.start_session(context.requester_scope, assignment)
|
||||
|
||||
|
|
@ -69,6 +70,11 @@ defmodule WhoNeedHelp.TrustSafetyTest do
|
|||
"accuracy_meters" => 5.0
|
||||
})
|
||||
|
||||
assert_receive {:position_updated, helper_id, _position,
|
||||
%{helper_movement_observed_at: nil, proximity_observed_at: nil}}
|
||||
|
||||
assert helper_id == context.helper.id
|
||||
|
||||
{:ok, _} =
|
||||
Tracking.update_position(context.helper_scope, assignment, %{
|
||||
"latitude" => 50.4510,
|
||||
|
|
@ -76,6 +82,12 @@ defmodule WhoNeedHelp.TrustSafetyTest do
|
|||
"accuracy_meters" => 5.0
|
||||
})
|
||||
|
||||
assert_receive {:position_updated, helper_id, _position,
|
||||
%{helper_movement_observed_at: movement_at, proximity_observed_at: nil}}
|
||||
|
||||
assert helper_id == context.helper.id
|
||||
assert %DateTime{} = movement_at
|
||||
|
||||
{:ok, _} =
|
||||
Tracking.update_position(context.requester_scope, assignment, %{
|
||||
"latitude" => 50.4511,
|
||||
|
|
|
|||
|
|
@ -208,11 +208,17 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
|
|||
send(
|
||||
requester_view.pid,
|
||||
{:position_updated, helper.id,
|
||||
%{latitude: 50.452, longitude: 30.526, accuracy: 6, captured_at: DateTime.utc_now()}}
|
||||
%{latitude: 50.452, longitude: 30.526, accuracy: 6, captured_at: DateTime.utc_now()},
|
||||
%{
|
||||
helper_movement_observed_at: DateTime.utc_now(:second),
|
||||
proximity_observed_at: nil
|
||||
}}
|
||||
)
|
||||
|
||||
assert render(requester_view) =~ "50.452"
|
||||
assert render(requester_view) =~ "30.526"
|
||||
assert render(requester_view) =~ "helper movement supported"
|
||||
refute render(requester_view) =~ "no movement evidence"
|
||||
|
||||
send(requester_view.pid, {:tracking_stopped, helper.id})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user