who_need_help/docs/verification.md

26 KiB

Who Need Help — implementation verification

Observed through 2026-07-19 in the local workspace. This report separates observed results from product limits and unknown production properties.

Verified MVP capabilities

Requirement Status Observed evidence Limit
Urgent medicine-help flow Implemented and tested Request creation, discovery, matching, start, handover, two-party completion, and review rules are covered by the Phoenix test suite and exercised in the local UI. The product coordinates pickup of an already purchased or reserved legal item; it is not a pharmacy, medical, or emergency service.
Urgent roadside help Implemented and tested Fuel, car wheel, bicycle, motorcycle, vehicle-breakdown, and secured-incident categories are seeded as a translated hierarchy. Server and LiveView tests exercise category paths, required fields, allowed values, boolean normalization, unknown-field rejection, and request creation. Roadside requests require no immediate danger; this is not emergency response or professional recovery.
Extensible categories Implemented and tested Categories and validated text/select/boolean fields are stored in PostgreSQL. Proposal, vote, approve, reject, and merge paths have automated tests. Coffee, cinema, hiking, and other social activities remain separate from urgent-help safety and ranking rules.
Separate Activity mode Implemented and tested Coffee, cinema, walk, and hiking categories use a separate activity lifecycle. Domain and two-client LiveView tests cover creation, join request, organizer approval, capacity enforcement, public/pending/chat privacy, exact-location disclosure to approved users, group chat, blocking, completion, and zero impact on helper reputation. Activity and message reports expose only the linked group conversation to an audited moderator; moderators can hide and restore reported activities. This does not guarantee participant identity or physical safety.
Map and discovery Implemented and browser-verified The committed isolated Chromium suite rendered request and Activity maps, waited for MapLibre idle, and completed with no console, page, or request failures against a local PNG raster fixture. An earlier headed session rendered the configured OpenStreetMap tiles. A production operator must configure a tile provider appropriate for its policy and traffic.
Private matched chat Implemented and cross-client verified A message sent from the helper browser appeared in the requester's browser without reload. An earlier Android emulator run also sent a message that appeared in the requester browser in real time. There is no unsolicited general-purpose inbox.
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; 163 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 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 failure/rollout-verified The isolated Compose profile passed BEAM crashes and sequential replacement with 3 web/2 worker replicas, all five nodes joined, PubSub passed, and 743/743 readiness requests succeeded. The project-owned kind cluster replaced all 2 web/2 worker pod UIDs under maxUnavailable=0; all four replacement pods joined and PubSub passed. Local PostGIS is a single instance. Production database HA, backups, and recovery are operator work and are not claimed complete.

Reproducible checks

  • The isolated Phoenix suite completed on 2026-07-19 with 163 tests and 0 failures after cursor pagination, database aggregation, and the full localization changes on Elixir 1.20.2 and Erlang/OTP 29.0.3.
  • mix compile --force --warnings-as-errors and mix format --check-formatted: passed against the same final source.
  • ./scripts/quality.sh passed ShellCheck 0.11.0, Hadolint 2.14.0 at warning threshold, actionlint 1.7.12, all four Compose renders, Helm lint, Trivy source/rendered-manifest scanning, xref, Credo high-priority checks, Sobelow strict/private checks, Hex audit, 163 Phoenix tests, both npm audits, and the production-image vulnerability scan. The rendered Helm manifest and Debian 13.6 release image each reported zero HIGH/CRITICAL findings under the configured gates.
  • Dialyzer passed with three path- and warning-specific documented filters and zero unused filters. Two findings are opaque-type warnings at Ecto Multi/MapSet call sites; the third is the generated Gettext backend's three-form plural-rule opaque-value warning; the filters are visible in .dialyzer_ignore.exs rather than hidden by a broad pattern. This is not a claim that third-party dependency compilation is warning-free.
  • 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 local Docker build target: testDebugUnitTest, lintDebug, assembleDebug, and assembleDebugAndroidTest passed. The isolated API 37 runner then passed 5/5 instrumentation tests for denied/granted location permission, deep-link lifecycle, foreground location upload, notification Stop, and disconnected-Stop retry state. The public-staging target ran the unit tests plus lintStaging and assembleStaging; both previously recorded 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 checks, and cross-node PubSub verification passed in the local kind cluster.
  • The hardened kind rollout then ran PostGIS as UID/GID 70 and Mailpit as UID/GID 10001. Both used read-only root filesystems, dropped all Linux capabilities, disabled privilege escalation, and used RuntimeDefault seccomp. Both web and both worker replicas were Ready; their migration-wait init containers also used read-only root filesystems and dropped capabilities. The cross-node probe observed all four BEAM nodes, and live/readiness returned ok/ready. Database counts remained 0 users / 0 help requests.
  • The isolated Compose resilience drill observed a restart count increase for one crashed web and worker BEAM process, replaced every replica sequentially, observed all 5 cluster nodes, passed PubSub, and completed a real Oban retry on attempt 2 after one recorded error. Its exact job row and all fixture domain rows were absent afterward; 743 readiness samples had no final failure.
  • The reproducible kind rolling drill advanced both Deployments from revision 17 to 18, replaced all four pod UIDs, observed all 4 BEAM nodes, passed PubSub, and left the database-count diff empty. All 305 readiness samples ultimately returned 200; two samples needed three total reconnect attempts during local single-node NodePort endpoint replacement.
  • The committed browser suite passed its 1/1 bootstrap and all 8/8 Chromium specs against a fresh PostGIS volume with two web and two worker replicas on 2026-07-19. The retained successful-run artifact directory is output/e2e/20260719012404-847451. It covered axe checks across four pages and two themes, keyboard navigation, three responsive widths, Activity/moderation/privacy, the two-user medicine handover and blind reviews, public boundaries, local raster maps, and an offline LiveSocket transport failure/reconnect. It also verified persistent Russian/Ukrainian public language selection and the selected Ukrainian locale inside an authenticated LiveView. The uniquely named Compose volume and networks were removed.
  • The default localization domain contains 508 current messages and the errors domain contains 40. Russian and Ukrainian catalogs have no empty or fuzzy current entries. Category names, descriptions, structured-field labels and select/boolean values use locale maps with an English fallback; the compatibility description column remains as a fallback for pre-existing categories.
  • The isolated database-scale harness migrated an empty PostgreSQL 18.4 database to the exact pre-index version, seeded the configured canonical sample, captured JSON EXPLAIN (ANALYZE, BUFFERS) plans before and after the generated cursor-index migration, and removed its project and volume. The final 50,000-row-per-large-table run selected every asserted cursor index; two consecutive request pages matched the first 50 ordered rows with no gap or duplicate. Exact observations and their non-SLO limitations are recorded in docs/performance.md.
  • Browser verification used headed Chrome. The authenticated matched-request page rendered its chat, MapLibre marker, and live-location controls with no console errors or warnings after the foreground-service rollout.

Local generated evidence (ignored by Git):

  • output/playwright/final-request-map-chat.png
  • output/playwright/final-privacy-profile.png
  • output/android/final-image-smoke.png
  • output/android/foreground-notification.png
  • output/android/staging-deep-link-safety.png
  • output/android-instrumentation/20260718225400-2694340/results.txt

Android artifact:

  • android/dist/who-need-help-debug.apk
  • 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

WNH_DEBUG_BASE_URL is a required Android build input, because the trusted same-origin value is compiled into the debug APK. Its concrete local value is read from the repository's ignored .env by scripts/android-build.sh and is passed to Docker with --build-arg. android/Dockerfile only declares ARG WNH_DEBUG_BASE_URL; it contains no URL default.

The example http://10.0.2.2:4010 remains only as unit-test data for origin matching. It is not a runtime or build default. The ignored .env is also excluded from the root Docker build context.

Database observations

The post-roadside local Compose observation found 2 users, 1 help request, 7 messages, and 1 assignment before and after deployment. Category seeding changed only the category count from 1 to 9: 7 selectable categories and 2 inactive grouping nodes. These are local scenario data; the database is not assumed empty.

The Activity migration then preserved those same user/help/message/assignment counts, added empty activities, activity_participants, and activity_messages tables, and expanded the category tree to 14 rows: 9 help rows and 5 Activity rows. A validated custom-format dump was created immediately before that migration at output/backups/compose-before-activity-20260718-203930.dump; this ignored local artifact is not a substitute for the still-required isolated restore drill.

The final kind observation found 0 users, 0 help requests, and 0 messages. That state was queried before changing the database workload. The migration from the old emptyDir Deployment created a custom-format dump, validated it with pg_restore --list, changed PostGIS to a persistent StatefulSet, and restored the dump before the Helm upgrade. The final kind database reported PostgreSQL 18.4 and PostGIS 3.6.4.

The local kind Secret contains independently generated values. Only their decoded lengths were inspected: the PostgreSQL password is 64 characters and each application secret is 128 characters. Secret values were not printed or written to tracked files.

Dependency-upgrade observations

  • The running Compose and kind releases reported Elixir 1.20.2 and Erlang/OTP 29.
  • Compose reported Traefik 3.7.8 and Mailpit 1.30.4; Compose and kind both reported PostgreSQL 18.4 and PostGIS 3.6.4.
  • The production asset build reported Tailwind CSS 4.3.3 and daisyUI 5.6.18; esbuild is configured at 0.28.1.
  • The isolated Node build stage reported Node.js 24.18.0, npm 12.0.1, npm outdated --json returned {}, and npm ci reported zero known vulnerabilities.
  • The Android build used Android Gradle Plugin 9.3.0, Gradle 9.6.1, Command-line Tools 22.0, and its embedded Android CLI 1.0.15857036. Unit tests, lint, and debug assembly passed; the lint report says “No errors or warnings.” The rebuilt emulator image contains the API 37 ps16k AVD and APK, and its emulator 36.6.11.0 binary returned its version successfully at runtime.
  • The final complete scripts/kind-up.sh run was idempotent and finished with both 2-replica Deployments Ready and a successful cross-replica PubSub probe.

The migration 20260718114233 and reverse block lookup index blocks_blocked_id_blocker_id_index were present. An actual EXPLAIN ANALYZE for that reverse lookup selected the index. The table contained no block rows, so this confirms query shape, not production performance.

Exact production capacity, minimum CPU/RAM, and scaling thresholds are unknown: there is no representative production dataset, traffic model, or target-environment measurement. The isolated measurements in docs/performance.md include the earlier paced public/readiness/heartbeat comparison and a later 3-web/2-worker run with real authentication, LiveView, chat writes, tracking writes, and explicit position deletion. The latter completed 1,880 authenticated chains and 240 heartbeat sockets without a functional failure and restored every tracked application table count after fixture cleanup. Neither short run found a saturation point or represents production traffic, so the Helm chart does not invent resource limits or an HPA policy.

Public staging observation

On 2026-07-18, whoneedhelp.imalto.site was published through the existing Ubuntu Nginx gateway and its OpenVPN path to the local Compose proxy. HTTP redirected to HTTPS; the homepage, fingerprinted assets, and both health endpoints returned HTTP 200. A headed Chrome session rendered the public page with zero console errors or warnings. A complete WebSocket Upgrade request to /live/websocket returned 101 Switching Protocols.

The observed Let's Encrypt certificate had the correct DNS:whoneedhelp.imalto.site SAN and an expiry of 2026-10-16. Nginx and the Certbot renewal timer were active. This verifies the current staging path; it does not make the workstation or gateway a production availability environment.

After the Activity rollout, headed Chrome rendered the updated public navigation, followed the Activity link to the authenticated route, and received the expected login redirect and flash with zero console errors or warnings.

After commit 307794c, staging was rebuilt with the optional GitHub OAuth boundary. Both web and both worker replicas started, the cross-node PubSub probe passed, public home/readiness returned HTTP 200, and the before/after database counts remained 2 users / 1 request / 7 messages / 14 categories / 1 assignment / 0 activities / 0 reports / 0 social identities.

A headed Chrome E2E run then registered a uniquely named example.invalid account, consumed its confirmation link from local Mailpit, authenticated, opened /profile, and observed:

  • the GitHub network option and the explicit provider-disabled explanation;
  • no Verify button while both OAuth environment variables were absent;
  • a manually added Telegram link labelled unverified;
  • removal of that owner-controlled manual link;
  • a direct /auth/social/github request returning to the profile with the controlled provider is not configured flash;
  • zero browser console errors or warnings.

The exact E2E user was checked for every foreign-key relationship to users. It had only its one authentication token and no domain records after the manual link was removed. That exact user was then deleted through Ecto, its browser session became invalid, the generated Mailpit message was deleted individually, and the original database counts were observed again. The browser windows were left open.

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 custom-format pg_dump. The published archive and SHA-256 sidecar were validated before an isolated restore. The restore drill created a database from template0, restored with pg_restore --exit-on-error, read 23 public application tables and 1106 restored rows, reported PostGIS 3.6.4, observed all 8 current migrations, and removed the exact temporary database. A follow-up catalog query returned zero remaining restore-drill databases. Source counts before and after remained 2 users / 1 help request / 7 messages / 14 categories / 1 assignment / 0 activities / 0 reports / 0 social identities.

The same rollout started 2 healthy web replicas and 2 worker replicas. Local live/readiness, public HTTPS readiness, and the public root returned HTTP 200. The cross-node PubSub probe passed across all four connected BEAM nodes. The protected metrics route returned HTTP 401 both locally and through public HTTPS without credentials, returned valid Prometheus text with the generated ignored local token, and was scraped directly from each web container. Helm 4.2.3 lint and template rendering passed.

Before the runtime image change, every BEAM container logged that libsctp.so.1 was unavailable. The rebuilt Debian trixie release contains the libsctp1 package and the exact shared library; no SCTP, application error, or application warning appeared in the post-rollout web/worker logs checked during this verification window.

Final full-stack regression

After isolating the load-test Compose project from staging, the complete regression was repeated against the final source on 2026-07-18:

  • ./scripts/test.sh rebuilt the test image and passed all 150 Phoenix tests; mix format --check-formatted and mix compile --warnings-as-errors also passed.
  • The staging and load-test Compose configurations both rendered successfully. Helm 4.2.3 lint and template rendering passed with the checked-in kind values.
  • The public root, /healthz/live, and /healthz/ready returned HTTP 200. The two health responses were served by different web nodes during the observation. The four-node web/worker cluster also passed the cross-node Phoenix PubSub probe.
  • Headed Chrome loaded the public home and Safety pages, including all fingerprinted assets, and redirected an anonymous /requests visit to login with the expected flash. The browser reported zero console errors and zero warnings. Existing browser windows were left open.
  • Both Android Docker builds repeated their unit-test, lint, and APK assembly stages. The debug and staging artifacts retained the documented SHA-256 hashes, and both lint reports had no nonzero error or warning summary. apksigner verified the staging APK's v2 generic debug signature; aapt confirmed package org.whoneedhelp.mobile.staging, SDK 24 through 37, disabled cleartext, and the exact HTTPS staging host.
  • The protected metrics endpoint returned HTTP 401 without a token and valid Prometheus text with the generated ignored token. The checked 15-minute web/worker/proxy log window contained no application error, warning, or SCTP finding.
  • The staging database remained at 2 users, 1 help request, 1 assignment, 7 messages, 14 categories, 4 ended tracking sessions, 0 active tracking sessions, 0 activities, 0 reports, and 0 social identities.

Known work before a public production launch

  • Replace the temporary staging origin with the production-owned domain and production-sign the Android app.
  • Operate PostgreSQL/PostGIS with backups, recovery testing, and the required availability model.
  • Load-test representative data and traffic, then set measured pool, resource, autoscaling, and action-limit policies.
  • Repeat representative authenticated write, chat, tracking, and reconnect load scenarios before deriving production capacity or autoscaling policy.
  • Publish jurisdiction-specific emergency contacts, privacy, retention, prohibited-items, and voluntary-payment guidance after legal review.
  • Create and configure a GitHub OAuth App, then exercise the real external provider redirect/callback in a headed browser. Until then staging keeps the provider disabled and manual links remain unverified.