Skip to content

Mileage Android app (step 5 of the first slice) - #27

Draft
rorychatt wants to merge 43 commits into
mainfrom
feat/mileage-app
Draft

rorychatt wants to merge 43 commits into
mainfrom
feat/mileage-app

Conversation

@rorychatt

@rorychatt rorychatt commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Draft. Step 5 of the Mileage first slice (docs/spec/mobile.md 9): the Android app in web/apps/mileage-app (Expo SDK 58, React Native 0.88.0). No file under crates/ is changed by this pull request.

What is in it

  • The app: system-browser PKCE sign-in (tokens in the Keystore), the in-app location disclosure and permission steps, manual Start, a foreground-service recording (expo-location + expo-task-manager, no background-location permission), an SQLCipher outbox with a Keystore key, upload by bulk_create in fixed batches with deterministic idempotency keys (429, 503 and network loss handled), Stop = mileage_trip.end with a retry on 409 incomplete (first wait 8 s, then doubling), Submit, Discard with a reason, trips and trip detail, Account (own device rename and removal, sign out), and a screen for the trips of another sign-in. English strings in one catalogue with plural forms and Intl formatting.
  • @obop/ui-native (components and generated native tokens with parity tests), web/apps/storybook (the components in the web Storybook through react-native-web, Q42; touch, safe areas, fonts and TalkBack are not shown there), an OAuth helper in @obop/client.
  • docs/spec/mobile.md 8.7 (what was built, the decisions, what was verified and what was not), 9.1 (the real-phone test, step by step), docs/ISSUES.md L20 to L41 (L29, L30, L38, L40 and L41 were corrected in the third round, L29, L30, L34 and L38 in the fourth, L30 and L38 in the fifth).
  • scripts/emulator/ (the route, the feeder, a read-only server-side check of a trip; run with vp run emulator:route|drive|check).

First review round (45 confirmed findings)

  • The outbox belongs to one person on one instance; a sign-in as someone else finds the earlier trips foreign (nothing uploaded or shown, nothing deleted until the driver chooses). Sign out and Remove this phone ask to keep or delete the trips that are not completely on the server. A sign-in that ends during a trip leaves the Recording screen with Stop and a Sign in button. Recording is checked (precise location, location switch, service restart; minSdk 26). Plural forms and Intl with fallbacks (Hermes has no Intl.PluralRules). Declined findings are in docs/ISSUES.md with a reason each.

Second review round (a delta check of a15cb10: 38 confirmed findings, 17 medium and 21 low; the code is in 38e04db)

  • Sign-out is not undone by work in flight (findings 4 and 16). The session has a generation number that changes with every sign-in change; a refresh answered after a sign-out or a new sign-in is dropped and writes nothing, Keystore writes are serialised, and a pass of the outbox that was running stops before its next call when the sign-in changed (the sync controller then starts a new one under the new sign-in). client() also refuses while the outbox is foreign.
  • A refresh that gets no answer within 60 s (30 s was the helper's default) ends the sign-in as "unsure" with its own explanation, tells the instance in the background, and never presents the possibly spent token again (13, 22). Every call now has a deadline (the owner question, the calls about this phone, the body of an answer; the list is in ISSUES L29).
  • Only the plain 400 "no mileage_trip X" means that the server has no trip; a 404 keeps every point, says that the phone cannot tell what it means, and offers Try again and no Discard (14, 10, 24). A discard of a trip whose create was refused now finishes on the phone (5).
  • A client that the instance forgot (a replaced database) is found by one read of the authorization page (400) and registered again once (7). The "instance was not told" notice follows its flag and is set before the screens are told (1, 6). The choice to sign in from the recording ends with a sign-in (18, 26). The Recording screen says nothing about missing positions before it has read them (27). The Failed screen no longer claims that recording goes on (17).
  • Comments and documents corrected: the forged redirect (L32; it still ends a pending sign-in), the 409 delay (8 s), the screen lists (Foreign), the 1.2 rows, the order and bounds of Remove this phone, unsentOf wording, the OR-34 citation of the emulator scripts, the hand-over file. New ISSUES rows: L39 (the same person at a new address of the same instance is foreign; needs a backend change, the instance id is not exposed; the test hand-over warns to keep the PC's address), L40, L41.
  • Each code fix has a test that fails when the fix is reverted (run by reverting each in turn: session generation, sign-out bump, timeout, invalid_client, 404 as gone, discard of a refused create, the engine identity, the sync re-run, canDiscard for unknown, liveness, keepSigningIn, clientUnknown, the body-read bound, the deadline helper). Glue that imports Expo has no unit test (L38).

Third review round (a delta check of 8ca73db: 18 confirmed findings, 3 medium (one defect) and 15 low; the code is in 9ec6d87 and b74a10d)

  • A late answer of the trip list or of one trip is no longer written under the next sign-in (1, 2, 3, the medium ones). refreshRemote and refreshTrip capture the sign-in before the call and drop the answer when it changed; they were the one place where the server's rows are inserted into the outbox and the one that the guard of the second round did not cover. No other place in core/ or native/ awaits the network and then writes to the store without the check.
  • withDeadline now races the call against the deadline as well as aborting its signal, so a call that ignores the signal (a wait for a token, a half-open connection) is bound too; it ends as a network error (10). A sign-in the phone ended itself after a refresh without an answer now says so on the Sign in screen when the background revoke fails as well, with the notice of the offline sign-out (12). A refresh refused with invalid_client re-checks the sign-in after the config write; the obop server never sends invalid_client to a refresh (exchange_refresh answers invalid_grant), so that branch is stated as defence in depth for other servers, and the probe of the authorization page is the real recovery (8, 17). The comment of the backoff factors, the 404 of the create step, "Stop alone does not finish a discarded trip" and the order of Sign out (the request to the instance is started first, the phone does not wait for it) are corrected in the code and in the documents (5, 7, 15, 16, 18). L40 now names the sign-in page slot of the probe (9).
  • Tests that fail when the guard is taken out, found by taking each out in turn: the serialised Keystore writes, refreshing = null in signOut, the finally guard, the guard at the top of the catch of refresh (refusal and timeout), the guard before the repeat of a failed write, the loop-top check of a pass, the guards of refreshRemote and refreshTrip, the race of withDeadline, the notice after a failed revoke, the create-step 404, "Stop alone leaves the flag" (4, 11). The third round added 17 tests; 15 of them fail when their guard is taken out, one is a control that has no guard ("a list under an unchanged sign-in is merged") and one could not fail ("leaves nothing unhandled ..."; it was replaced in the fourth round). Still without a test (L38): the wiring of the notice in native/runtime.ts and the order of the steps of signOut.
  • The hand-over file and 9.1: steps 11 to 15 now run the check before the Confirm, then Confirm, then the check again; the unmeasured corner-cutting figure and "the same text" are gone from the hand-over file; the new APK figures (6, 13). 8.7 "Not verified" no longer lists what the third run did and names what it did not reach (14).

Fourth review round (a closure and delta check of f56489e: 18 confirmed findings, 2 medium (both about what the documents say a run showed) and 16 low; the code is in 938f9eb, the documents in ab7b960)

  • What the emulator runs showed was overstated in two places, and is corrected (1, 2, 7, 12). The held trip list of the fourth run lasted 3 min 54 s (held at 23:32:37.2, delivered at 23:36:31.7) and a trip call waits 120 seconds, so the phone had already given up the call: that run shows nothing about the guard of refreshRemote, and 8.7 and this description no longer offer it as a device check. It was repeated with a hold of 57 seconds (below). The order of the steps of Sign out is read from signOut and leaveSession; the proxy log shows only that a revoke request arrived at the test proxy and was held (the instance never saw it; the fifth round corrected this line), that the request carried the token follows from revokeSignIn, and the notice that appeared between two screen reads fits the 5 second bound of the Sign out path (REVOKE_TIMEOUT_MS in native/runtime.ts; the background end uses END_REVOKE_MS in core/session.ts); none of it shows the order. Step 7b says what 8.7 does record (two force-stops of a recording trip on the emulator) and what it does not (the swipe from recents, a locked minute afterwards, a maker's battery manager).
  • withDeadline (3, 4, 8, 11): the network error is made before the signal is aborted, so a call that rejects in its abort listener cannot win with its own error (the older test now expects the ObopNetworkError); the stalled.catch line, which did nothing, and its test, which could not fail, are removed; two tests were added (no timer is left after an answer or an error; a call that ends after it was given up leaves nothing unhandled). Every call site (ownerOf, probeAuthorize, the three device calls) goes through client.call or an async wrapper and is unchanged in behaviour.
  • Wording (5, 6, 9, 10): ISSUES L30 says that isGone tests the message and not the status; L29 says that the trip calls rely on the abort alone and are not raced (not changed: the installed expo/fetch cancels the request on abort); the create-step 404 no longer says that "nothing of the trip is on the server" (the phone holds no server id there, and a trip made by a create whose answer was lost may still be on the server; the behaviour is unchanged and the case is written down in L30); "a newer sign-in has begun" says "has finished" in the five places.
  • A lost Keystore key (15): documented in L34, the hint of the Failed screen (now: if Try again keeps failing, the key may be lost, the trips cannot be read, and Clear storage is the way out, which deletes the trips that were not sent), 8.7 and 9.1. databaseKey() is not changed: refusing to replace a key when mileage.db exists needs a file-existence check (a new native module and build) and cannot be tested without importing Expo, so it is in L38.
  • The hand-over (9.1 and the file of the session) (13, 14, 16, 17, 18): throwaway passwords, plain http, rita is a full administrator with the second factor off, the firewall step for a network that is Public or a firewall that is off (it is off on the build PC), the same router and subnet, the full path of adb, Battery Saver, the clocks (the phone shows local time, the back office and the check script UTC), keep the app on screen until Ended, and everything the steps change with how to undo it.
  • Mutations (each applied alone to the source, vp test on the test file, the file restored; git diff -- web was empty before and after; the harness is a throwaway script and is not committed). All 19 failed as they should, with these tests:
Guard taken out Failing tests
Keystore write chain replaced by a plain write the write of a sign-out is not overtaken by the slow write of a refresh; a failed write of a refresh is not repeated after a sign-out
refreshing = null in signOut a refresh asked for after a sign-out and a new sign-in does not join the old one; the end of an old refresh does not free the slot of the new one
finally guard of refreshing the end of an old refresh does not free the slot of the new one
guard at the top of the catch of refresh a refusal after a sign-out does not end the next sign-in; a refresh that times out after a sign-out does not end the next sign-in or tell the instance
guard before the repeat of a failed write a failed write of a refresh is not repeated after a sign-out
re-check after the config write of invalid_client a refresh refused as invalid_client does not end a sign-in that finished while the config was written
onEndUntold never called a sign-in that ends on its own after a timeout says so when the instance could not be told either
onEndUntold ignores a finished newer sign-in it says nothing when the instance was told, or when a newer sign-in has finished
loop-top check of a pass a sign-in that changes during the last call of a trip stops the pass before the next trip is touched
guard of refreshRemote a list that was asked for under one sign-in is not merged under the next
guard of refreshTrip the answer for one trip that was asked for under one sign-in is not written under the next
create step keeps the server's code for a 404 a 404 on the create keeps the server's own code and the driver may discard on the phone
a flagged trip has no work Stop alone leaves the flag of a trip the server discarded; two older Stop tests
withDeadline without the race aborts the signal ... and the call ends with a network error; ends a call that ignores the signal; leaves nothing unhandled when a call that was given up ends later
withDeadline never aborts the signal aborts the signal ...; ends a call that ignores the signal
withDeadline aborts before it rejects aborts the signal ... and the call ends with a network error
clearTimeout removed does not abort a call that has finished; leaves no timer running when the call ends
clearTimeout only after an answer, not after an error leaves no timer running when the call ends
the end of a call that was given up left without a handler leaves nothing unhandled when a call that was given up ends later; leaves no timer running (the mutant hangs on an error)

Count: of the 18 tests of the third and fourth rounds that are in the code now, 17 fail under at least one of these mutations; the control "a list under an unchanged sign-in is merged" cannot (it has no guard). Still without a test: the create-step 404 after a create whose answer was lost (L30) and the wiring in native/ (L38).

Fifth review round (a closure check of 0b31461: no high or medium finding and no blocker for the real phone; 13 low findings, about 7 distinct; docs and one test comment only, no change to the app: the APK is the same 54,531,555 bytes, SHA-256 c00d1a4a..., of 938f9eb; the head is 6d10c6f)

  • The held revoke (1, 9, 10): 8.7, L38 and this description now say what the proxy log of the held revoke shows (one line: a request to /oauth/revoke arrived at the test proxy and was held, never forwarded, body not logged), what follows from the code (the token was in it: revokeSignIn sends nothing without one) and what the session read on the screen (the notice between two reads, which fits the 5 second bound of the Sign out path). The constants are named: REVOKE_TIMEOUT_MS (native/runtime.ts) for Sign out, END_REVOKE_MS (core/session.ts) for the background end after an unsure refresh.
  • L30 (2, 6, 7): the orphan trip case is not narrow because a retry replays the key. Dispatcher::call answers the 404 of a hidden command (a lost group, a removed module) before bulk_create looks up the idempotency key, so the replay does not help then. It is narrow because it needs a create that was applied with its answer lost and then the command hidden before any retry got through. Behaviour not changed.
  • The count (3, 5, 8): the comment in the loser test of withDeadline said that no change made it fail; it fails when the race is removed and when the late end of the call is left without a handler, and the comment now says so. 8.7, L38 and this description say the same: 18 tests of the third and fourth rounds are in the code, 17 fail under a mutation, and the control "a list under an unchanged sign-in is merged" cannot.
  • Step 7b (4): only the first force-stop run records a gap-free seq (1 to 22); the review run records 15 points and 779.08 m, no seq check; 8.7 does not measure what a force-stop loses.
  • The timeline (11), checked against the logs: run 4 delivered the held list at 23:36:31.7 (3 min 54 s after 23:32:37.2), not at about 23:36:40 (that is the next list request); run 5 held the list 0.13 s after the hold was set, not a second, and the proxy does not check whether the connection is open.
  • The address (12): <LAN> is the PC's address on the network that the phone is on, found by the first three numbers of the phone's own address; the PC that built the APK is on Ethernet and lists two more private addresses (vEthernet) that a phone cannot reach.
  • The commands (13): every text in the commands of 9.1 and the hand-over file is single-quoted (PowerShell expands $ in double quotes: "Mil3age$test99" is 7 characters, 'Mil3age$test99' is 14; the admin create of the build checked on a scratch database: single quotes create the user, double quotes are refused as shorter than 12 characters); the removal list says that the passwords typed into admin create are in PowerShell's history file.
  • Verified this round: vp check clean and vp test 209 tests in 13 files in web/apps/mileage-app; git diff 938f9eb -- web is one comment in deadline.test.ts; git diff origin/main...HEAD -- crates is empty; the APK is untouched. CI (run 40865428046, first attempt, no rerun): web, e2e, rust, lockfiles and all passed.

Verified (emulator, Android 16 x86_64, a local instance with a throwaway database; written down in 8.7)

  • Fourth round, on the APK of 938f9eb (the fifth run): sign in as dave, Start, 10 positions, Stop: the trip uploaded and the server refused the end for lack of distance (4 of 13 points counted; it looks like L37), Discard finished it; a second trip of 12 positions fed (9 points stored): Ended, 1.2 km, Submit, In review, and emulator:check ok on every line (9 points, seq 1 to 9 once each, point_count 9 = max_seq 9, the distance 1,208.22 m against 1,201 m = 0.60 %). The trip list held across a sign-out and a sign-in as someone else, with a hold of 57 seconds (inside the 120 seconds of a trip call): the release came after rita had signed in, and Home and the Trips screen under rita showed none of dave's trips. It does not show that the app received the answer (the proxy writes a held answer to the client's connection without checking whether it is still open; nothing on the phone says it arrived) or that the build without the guard would have shown them (the unit test does). Not run on this APK: the Confirm and the monthly total, the new Failed text on the screen, refreshTrip, the race of withDeadline. The instance binary was built before main was merged in (crates/ is unchanged by the PR).
  • Third round, on the APK of 9ec6d87, with the proxy extended to hold the trip list, cut the revoke and hold the revoke: a refresh with no answer and a revoke that fails (the Sign in screen showed both notices, the instance listed the device as active); a sign-out with a held revoke (the proxy log has one line: a revoke request arrived at the test proxy, which held it, so the instance never saw it; that the request carried the refresh token follows from revokeSignIn, and the Sign in screen showed the notice between the two screen reads, 1 to 3 and 4 to 7 seconds after the arrival, which fits the 5 second bound REVOKE_TIMEOUT_MS; the log does not show the bound, no screenshot of the reads was saved, and the order of the steps was not seen, it is read from the code). The trip list held for 3 min 54 s across a sign-out and a sign-in as rita shows nothing about the guard: it outlasted the 120 second deadline, so the phone had already given the call up (the fifth run repeats it). Not run on a device: the race of withDeadline, refreshTrip, an interrupted pass at the last call of a trip, a failed Keystore write.
  • Second round, on the APK of 38e04db, with a test proxy that held a refresh answer or answered 404 or 403 to the trip calls: sign-out while a refresh is in flight (the held answer then released: still signed out, also after a restart); a refresh with no answer for 60 s (clean "unsure" end, old token not reused, the kept trip uploaded after signing in again); offline sign-out with nothing unsent (the notice shows); discard of a trip whose create was refused with 403 (finishes locally); a 404 for a trip that exists (kept, no Discard, Try again made it in_review); sign-in after the instance database was replaced (400, register again, 200), twice; the Failed screen with the new text (nothing deleted, the trips back after the key file was restored).
  • First round, earlier APKs: sign-in, approximate location, a force-stop in the middle of a trip, two unsent trips offline with the Sign out dialog, a sign-in as another person (foreign), a revoked sign-in in the middle of a trip (58 points, seq 1 to 58, no gap, 3,989.65 m against 4,000 m = -0.26%, tolerance 1.0%), delete at sign-out, location switched off while recording; Submit, a reviewer's Confirm and the monthly total, Discard, Account rename and remove.
  • main (5f8c3a9, 01ec24f) was merged in at f56489e; the only conflict was docs/DECISIONS.md (the branch's OR-34 and D87 rows kept, main's OR-35, D88, D89 and paragraph added after them); no file under crates/ differs from main, and web/ and the lock files are byte-identical to before the merge, so the APK below is still the build of that code. CI on f56489e: rust, lockfiles, web, e2e and all passed (run 40848210960). Local at the merged head: vp check clean in apps/mileage-app, ui-native, trip-core, client, storybook and backoffice; vp test apps/mileage-app 208 tests in 13 files (191 before the third round), ui-native 11, trip-core 26, client 151 (10 integration tests skipped without a server binary), backoffice 280. At ab7b960 (fourth round): vp check clean and vp test apps/mileage-app 209 tests in 13 files (the one that could not fail is gone, two for withDeadline are new); the other packages are untouched. CI on ab7b960 (fourth round): rust, lockfiles, web, e2e and all passed (run 40856861906). main was merged in again at 0b31461 (47005a6: PR 31, OR-35 k and K42). The only conflict was docs/DECISIONS.md, in two hunks: OR-34 and D87 keep the branch's rows, OR-35, D88 and D89 are main's rows byte for byte (checked by script); docs/ISSUES.md, docs/ROADMAP.md and docs/MODULES.md merged without conflict. web/, crates/ and the lock files are unchanged by the merge, so the APK above is still the build of that code. CI on 0b31461: e2e, lockfiles, web, rust and all passed on the second attempt (run 40858426480). The first attempt failed in rust on an_exhausted_pool_answers_503_with_retry_after_never_a_401_and_the_server_recovers (crates/obop/tests/pool.rs:520: obop_db_pool_in_use{pool="main"} was 1, expected 0), a test in code this PR does not touch (crates/ is identical to main, whose own run at 47005a6 is green); the rerun of the failed jobs passed. It is not in the flake list of K22.
  • The APK of the real-phone test has changed (the source changed in the fourth round): D:\Android\phone-test\obop-mileage-test.apk, 54,531,555 bytes, SHA-256 c00d1a4a1f2936209fd98550ac998bf5482a520de3e10d27aa4dde8af52dbb60, built from commit 938f9eb (vp run android:build:release, OBOP_ALLOW_CLEARTEXT=1, OBOP_DEFAULT_SERVER=http://192.168.1.132:18080; both ABIs; the Hermes bundle holds the new Failed text). The commits after it change documents and one comment in a test file (git diff --stat 938f9eb..HEAD -- web/apps/mileage-app is empty). The earlier figures (54,531,407 bytes with 5181b20e..., built from 9ec6d87, and 54,530,159 bytes with 8f515f85...) are no longer valid for that path. Which run was on which APK is in 8.7.

Not verified: anything on a real phone (10-minute lock, GPS noise, a maker's battery manager, the battery figure), an arm64 device, a phone without Play services, TalkBack and the focus moves, the Settings screen for precise location, "No new position" notices and the false alert of finding 27 (unit-tested only), the invalid_client clearing (it cannot happen against this instance), offline sign-out with unsent trips or with Delete on the new APK, the 30 s bounds and the race of withDeadline on a slow network, the Failed screen with a recording trip, a lost Keystore key on a phone (the next start replaces it and the trips cannot be read; L34), the upload after Stop with the phone locked or the app in the background (JavaScript timers pause; read from the library source), refreshTrip on a device, a failed Keystore write, the 429/503 answers on the real instance, a release or store build. The list is in 8.7.

Not merged, not final. The Gradle build is the mobile-toolchain exception of OR-34; the build of a test APK needs OBOP_ALLOW_CLEARTEXT=1 and is not for anyone else.

Merge of main

2026-11-02, approved by the owner: origin/main (33f48c6) is merged into this branch as a normal merge commit, head 93a5379 (parents e4c3aea and 33f48c6; no rebase, no force push). Two documents conflicted and nothing else: CLAUDE.md (the Native apps paragraph keeps this branch's description of the built Android app and takes OR-36 from main) and docs/DECISIONS.md (OR-34 from this branch with OR-35 to OR-37 from main; the D87 row, the "Ratified on" paragraph and the D87 status line carry both the SDK 58 record and the OR-36 ratification). This PR changes no Rust, so crates/ and Cargo.lock are identical to main. No ids collide (L20 to L41 here, D8 to D15 and K43 to K46 on main). No phone and no emulator were used for the merge.

Merge of main (2026-11-02)

Head ae55abd = merge of 93a5379 and main 36fa622 (PR 37 Microsoft sign-in, PR 41, PR 42). One conflict, in docs/DECISIONS.md (the "Ratified on 2026-10-01 ..." paragraph): the branch's SDK 58 clause and main's OR-38 clause are both kept. No Rust, manifest or lockfile of main is changed by the merge; no duplicate ids or headings in the merged documents. CI green (all jobs). Still a draft; no phone or emulator was used for this merge.

🤖 Generated with Claude Code

rorychatt and others added 30 commits October 28, 2026 17:08
…of 2026-10-28

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The owner's amendment of OR-34 e: SDK 58 if the packages pass the 7 day
cooldown of D23 when step 5 starts. expo 58.0.16 (2026-10-19) and the
packages of its bill of materials pass; all 778 locked entries are older
than 7 days. react and react-dom stay 19.3.0 (the bill of materials and
the renderer of react-native 0.88.0), the web uses 19.3.1.

check:sdk compares with the bill of materials of the installed expo
(offline); check:sdk:latest asks Expo's API, which wants patch releases
younger than the cooldown. The dates are in docs/spec/mobile.md 8.7.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…eb Storybook

The small themed React Native component set of the Mileage app (Screen,
Button, Row, Field, Banner, plus Text), the native tokens generated from
web/packages/ui/src/colors.ts and a small table with a parity test
against tokens.css, and web/apps/storybook, which draws the components
through react-native-web (Q42 as answered by the owner, OR-34 g).

Storybook 10.6.3, react-native-web 0.21.4 and vite 8.3.4 pass the D23
cooldown. esbuild's install script stays off (allowBuilds: false). The
Docker web stage leaves the two new members out of its install, as it
does the app.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…oken exchange and refresh) to @obop/client

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The pure-TypeScript core of step 5: the encrypted-store port (SqlDb), the outbox store,
the upload engine (batches of at most 1000 with fixed ranges and deterministic idempotency
keys, 429 and 503 handling, incomplete retry, reconcile-first end/submit/discard), the typed
calls, and 51 tests that run the real SQL on node:sqlite against a scripted server.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…r with tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ices

Home, permissions with the in-app disclosure, Recording, Trips, Trip detail and Account over
@obop/ui-native; system-browser sign-in with PKCE; the encrypted SQLite outbox behind the
location task; English catalogue; the SQLCipher and cleartext-for-test config.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the emulator route and the 8.7 notes

The Gradle helper sets CMAKE_POLICY_VERSION_MINIMUM so expo-sqlite (SQLCipher) configures under
CMake 4. The manifest blocks permissions that libraries add and the app never uses. route.mjs is
the synthetic reference route of the emulator run; docs/spec/mobile.md 8.7 states the route and
its tolerance before the run, and the decisions of step 5.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…on answers, stale first fix

- RECEIVE_BOOT_COMPLETED is declared: expo-task-manager schedules a persisted job at the first
  location and Android refused it, so the release build crashed at the first fix.
- end, submit and discard answer with the changed fields and the id, not the trip: the app now
  reads the trip after each action (the API test uses the real answer shape).
- A cached fix from before the start is not stored (STALE_FIX_MS, 10 s): the first trip had a
  point 900 m and six minutes away as point 1 and a distance of 4,880 m for a 3,989 m route.
- drive.mjs gets a third offline phase for the second full run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-phone steps

- docs/spec/mobile.md 8.7: what was verified on the emulator (sign-in, permission
  steps, recording with the screen off, offline Stop and drain, distance within
  the stated tolerance, submit, reviewer confirm and the monthly total, discard,
  force-stop recovery, Account rename, remove and sign out), the findings (the
  failed first run and its stale first fix, the missing boot permission, the
  action answers), and what is not verified; corrects the "41 points" statement.
- docs/spec/mobile.md 9.1: the real-phone test, step by step, with the tolerance
  proposed before the drive.
- scripts/emulator/check-trip.mjs: a read-only check from the server's side.
- ui-native Row: a long value no longer squeezes the title to nothing (found on
  the Account screen); a Storybook story with the long value.
- docs/ISSUES.md: L20 to L28.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ISSUES.md: K22 keeps the r29 multi_process flake from the docs branch, K23 is the closed row of main.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…dmap, the spec

Corrects statements that still described the SDK 57 spike (PR 27 review, findings 12, 18,
19, 34, 42), the Q43 wording about expo-location/next (20), the setting.get paragraph and
ISSUES L20 (21), the corner-cutting estimate of 9.1 (39) and the ISSUES section heading (41).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…efresh, OAuth timeouts

- engine: a trip the server discarded or no longer has finishes on the phone or stops with a
  worded code, and can always be discarded from the phone (6, 7); the end and the purge of a
  trip are one transaction and a sweep removes routes a kill left behind (30); the wait after
  an incomplete end doubles (24).
- session: the rotated refresh token is held in memory before the write (22, 27); a client the
  instance may have forgotten is registered again (2, pure rule with tests).
- client oauth: a timeout on every call (5); the state is checked before an error (28).
- tests: the session rig follows the real single-use rule of the token endpoint (22); the fake
  server answers a missing trip like the module (400, no code).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ess in core

The outbox records whose trips it holds (server origin and the person the
instance reports as owner of the device). A sign-in as someone else, or at
another address, finds unsent trips foreign: nothing is uploaded, shown or
deleted until the driver chooses. The address check moves to core with tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ording health, plurals

Sign out and Remove this phone say how many trips and points exist only on the
phone and keep them for the same account or delete them after a second
confirmation. A foreign outbox gets its own screen. A trip that is open
when the sign-in ends keeps a Stop control. Start refuses approximate
location and switched-off location; Recording starts the service again when
the app returns, warns when no position arrives, and scrolls. The catalogue
has plural forms and Intl numbers and dates. Selected state for the purpose
buttons. The native stories move to the Storybook app so ui-native needs no
Storybook dependency.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…6; vp run tasks for the emulator scripts

classify() has a table test; a portal answer (invalid_response) and a 408 are
repeated later instead of being a standing refusal; the sync controller's
timer is tested with a wait that differs from the idle interval, and over the
real client and engine. Boundary tests: the stale fix, the 20,000 point cap,
the backoff cap, the token skew. A gate decides before any route that an open
trip is never hidden behind the sign-in screen. The app's minimum Android is
26, where expo-location starts its foreground service.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ends on Storybook

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed state as aria-checked; record the review round in the docs

The first build of the review round crashed at start on the emulator:
'undefined cannot be used as a constructor' from new Intl.PluralRules. The
plural rules fall back to English and the number and date formatting to plain
text. ISSUES L28 to L36, the spec (8.7 design of the outbox and of the
recording checks, 9.1 steps, minimum Android 26).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d say 'not completely on the server'

The emulator run showed '1 trip ... exist only on this phone', and the count includes the
points of a trip that was already uploaded in part while recording.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…a far-off first fix)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rect the redirect comment

A call that was answered with headers and then stalled in the body held the sign-in for as long
as the connection lived; the whole call, body included, now has the deadline. OAuthError.timedOut
tells a given-up call from a refusal, so that the session can treat a refresh that timed out as
an unknown outcome. The comment on parseAuthorizationResponse no longer claims that a forged
error redirect cannot end a pending sign-in: the state check only changes the word it ends with.

PR 27 delta findings 2, 9, 13, 15, 22.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…is not "gone"; discard of a refused create

Session: a generation counter changes with every sign-in change (sign-in, sign-out, end, restore).
A refresh whose answer arrives after that is dropped and writes nothing, Keystore writes go one
after the other, and a refresh that times out ends the sign-in as "unsure" (the old token is not
presented again; the instance is told in the background). An invalid_client clears the stored
use so that the next sign-in registers again.

Engine: a pass remembers the sign-in it started under and stops before its next call when it
changed ("interrupted"); the sync controller then runs again at once under the new one. Only
the plain 400 "no mileage_trip X" means that the server has no trip; a 404 keeps every point and
the problem code is "unknown", which cannot be discarded. A discard of a trip whose create was
refused finishes on the phone. Comments and tests agree on the 409 delay (8 s in production).

PR 27 delta findings 4, 5, 7, 11, 13, 14, 16, 20, 21, 25, 27, 28.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ice, register again, bound the calls

runtime.ts gives the engine the session's generation and refuses a client when the outbox is not
the signed-in person's. The "instance was not told" flag is read by a hook that follows changes
and is set before the screens are told. The sign-in screen says when a sign-in ended because a
refresh timed out. App.tsx forgets the choice to sign in from the recording once a sign-in has
finished. The Recording screen says nothing about missing positions before it has read them. A
client the instance has forgotten for another reason than idleness is found with one read of the
authorization page (400) and registered again once; the owner question and the device calls have
a 30 second deadline. New texts: the unknown 404 of a trip, the unsure end, a truthful database
hint. Comments on leaveSession and the owner key corrected.

PR 27 delta findings 1, 6, 7, 8, 13, 17, 18, 22, 26, 27, 29, 33, 34, 38.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…md 8.7 and 9.1, screen lists

ISSUES: L29 (the 60 s refresh timeout and the "unsure" end, every call has a deadline), L30 (only
the plain 400 is "gone"; a 404 is "unknown"), L32 (a forged redirect still ends a pending sign-in),
L37 (four points, about 8,583 km), L38 (more untested glue), new L39 (a new address of the same
instance is foreign; backend change named), L40 (the client forgotten by a replaced database), L41
(no notice on the Recording screen for a trip the server lost).

mobile.md: the 1.2 rows that 8.7 contradicted, the 8 seconds of the first 409 wait, the Foreign
screen in the screen list (also CLAUDE.md and D87), 22 tests of oauth.ts, seven stories (the
seventh not looked at), the real order and bounds of Sign out and Remove this phone, what is kept
when trips are kept, the identity key and the address warning in 9.1, UTC time of the revocation,
the rebuilt instance, the Hermes plural crash as finding 5, the APK of 38e04db (size and SHA-256),
the third emulator run with what was and was not run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s no longer cite OR-34 (b)

Not part of the APK (the comments of two test scripts and of router.ts; the build of 38e04db is the
APK). OR-34 (b) names Metro, the Expo CLI, Gradle and Xcode; the emulator scripts are vp run
tasks and claim no exception.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- refreshRemote and refreshTrip capture the sign-in before the call and drop the answer when it
  changed, so a list of the person who signed out never lands in the next person's outbox.
- withDeadline races the call against the deadline as well as aborting its signal, so a call that
  ignores the signal (a token wait, a half-open connection) is bound too; it ends as a network error.
- A sign-in the phone ended on its own after a refresh without an answer now tells the sign-in screen
  when the background revoke fails too (onEndUntold); the notice text covers both cases.
- A refresh refused as invalid_client re-checks the sign-in after writing the config.
- Tests that fail on revert for the serialised Keystore writes and the six guards of round 2
  (refreshing cleared in signOut, the finally guard, both catch guards, the loop-top check), for
  both new guards and for the deadline race (15 new tests).
- Comments: the backoff factors, Stop alone does not finish a trip the server discarded, a 404 at
  the create step keeps the server's code, the real order of Sign out.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
rorychatt and others added 11 commits October 29, 2026 00:28
…s a discarded trip flagged

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- L29: the notice when the background revoke of an unsure end fails too; the deadlines are raced.
- L30: what each step does with a 404, and the create step as the exception.
- L38: which guards now have tests that fail on revert, and what still has none.
- L40: the per-address sign-in page slot of the look at the page; the invalid_client branch is
  defence in depth, the obop server never sends it to a refresh.
- L41: Stop alone does not finish a trip the server discarded; Try again after Stop and Discard do.
- 8.7: the real order of Sign out, the fourth emulator run, the mutation record, the Not verified list
  corrected; 9.1: the new APK (size, SHA-256, commit), steps 11 to 15 in an order that can be followed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One conflict, docs/DECISIONS.md: this branch rewrote the rows OR-34 and D87 (SDK 58, the
cooldown), main added OR-35 after OR-34 and D88, D89 and their paragraph after D87. Both sides are
kept: the branch's OR-34 and D87, then main's added rows and paragraph, unchanged. ISSUES.md,
mobile.md (main's M8 update of the batch-rate paragraph), DEPLOY.md, CLAUDE.md merged without
conflicts; the L rows (L1 to L41) and the K rows (K1 to K41) do not clash.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tch, honest tests; wording fixes

- core/deadline.ts: the network error is made before the signal is aborted, so a call that
  rejects in its abort listener cannot win with its own error; the stalled.catch line and its
  wrong comment are gone (it was a no-op); the test that could not fail is replaced by one that
  checks that no timer is left after an answer or an error, and one for a call that ends after
  it was given up.
- 'a newer sign-in has begun' says 'has finished' (the guard is on completeSignIn).
- The create-step 404 comments no longer say that nothing of the trip is on the server.
- The Failed screen hint says what to do when Try again keeps failing (the key is lost).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, test count, hand-over steps

- 8.7: the held list of the fourth run outlasted the 120 s trip deadline and shows nothing about
  the guard; the fifth run repeats it with a 57 s hold; the order of Sign out is read from the
  code, the proxy log shows the 5 s bound only; which tests are revert tests (17 of 18); which
  APK each run was on; the force-stops that 8.7 does record (7b).
- ISSUES L29/L30/L34/L38: the trip calls rely on the abort alone, isGone tests the message only,
  the create-step 404 rationale, a lost Keystore key and the Failed screen, what has no test.
- 9.1: adb path, throwaway passwords, plain http, rita is an administrator, firewall for a
  Public network or no firewall, same router and subnet, Battery Saver, clocks (phone local,
  server UTC), keep the app on screen until Ended, what to undo afterwards.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts: docs/DECISIONS.md (two hunks). OR-34 and D87 keep the branch's rows;
OR-35, D88 and D89 take main's rows byte for byte (47005a6). docs/ISSUES.md,
docs/ROADMAP.md and docs/MODULES.md merged without conflict. web/, crates/ and the lock
files are unchanged by the merge.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The comment said that no change to the function was found that makes the
test fail. It fails when the race is removed and when the late end of a
call that was given up has no handler (fifth review round, findings 3, 5
and 8). Comment only: the test file is not in the APK.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… commands

Fifth review round, docs only: say what the proxy log of the held revoke
shows and what follows from the code (1, 9, 10), the real reason why the
orphan trip case is narrow (2, 6, 7), one count and the named control
(3, 5, 8), what step 7b's force-stop runs record (4), the timeline of the
two held list runs (11), how to pick the PC's address (12), and single
quotes for every command that carries a password, with the history file
in the removal list (13).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
9.1 and 8.7 said the commits after 938f9eb change documents only; the fifth
round also changed a comment in deadline.test.ts. No source file that goes
into the APK changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
rorychatt and others added 2 commits November 2, 2026 10:09
Two textual conflicts, both in documents, resolved keeping both sides:

- CLAUDE.md: the "Native apps" paragraph keeps this branch's description of
  the built Android app and takes OR-36 (every native app works on both
  Android and iOS) from main.
- docs/DECISIONS.md: OR-34 (this branch: SDK 58 pinned, Storybook check run)
  with OR-35 to OR-37 from main; D87 row, the "Ratified on" paragraph and the
  D87 status line merged word by word so that both the SDK 58 record and the
  OR-36 ratification stand; the D88/D89 paragraph and D88 row come from main.

Ids checked for collisions (ISSUES L20 to L41 against main's D8 to D15 and
K43 to K46, Q37 to Q43, D87 to D90): none.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One conflict, in docs/DECISIONS.md (the "Ratified on 2026-10-01 ..."
paragraph): the branch's "amended on 2026-10-28 to SDK 58, which the app
pins" and main's "and on 2026-11-02 D91 as written ... (OR-38)" are both
kept. No Rust, manifest or lockfile of main is changed by the merge.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant