# ADR 0035 — The four p2p-intel limits: two closed, one sidestepped, one open Status: accepted Date: 2026-09-01 ## Context The p2p-intel README named four limits rather than glossing them: 1. In-app alerts need the app open — no OS notification. 2. Scans are manual; `poll_interval_seconds` is validated but unused. 3. Timestamps are zero, pending a host clock. 4. The Makepad widget is compiled but never executed. This records what happened to each, because two of them turned out to be cheap, one had a route nobody had tried, and one is genuinely somebody else's work. Rounding all four up to "addressed" would have been the easy write-up and the wrong one. ## Decision ### 2 and 3 are closed outright **The poll timer** is `cx.start_interval`, which Makepad delivers as an ordinary event on the UI thread. No runtime, no thread, nothing to join at shutdown. The interval reads through `Config::effective_poll_seconds`, so the fifteen-second floor still applies and a config file cannot be used to hammer a rate-limited endpoint — pinned by `a_too_fast_poll_cannot_be_used_to_bypass_the_rate_limit`. **The clock** is `SystemTime::now()` in the app and `offline_store::offline_now_ms()` in the exchange pages. Staleness is shown in the header, because a price from four minutes ago is not a price. One deliberate ambiguity is recorded rather than hidden: `last_scan_ms == 0` is the "never scanned" sentinel, so a scan whose timestamp genuinely is 0 reads as *never*. That only happens when the host clock is broken, which is exactly when the UI should not claim the data is current. `a_scan_at_the_unix_epoch_still_reads_as_never` pins it as a decision. ### 4 is sidestepped, and the distinction matters The blocker is real and unchanged: `windowing_backend.rs` in the Makepad fork knows only X11 and Wayland, defaults to X11, and dies with no display. Six `ui.rs` tests across this repo are `#[ignore]`d for it. **Xvfb is a real X11 server that draws into memory.** So the app gets the display it insists on, and `tools/test-p2p-app-smoke.sh` runs the real binary, parses the real widget tree, creates a real GL context, and renders. This is worth having and is not what it is often mistaken for: - It **can** catch a malformed `script_mod!` block, a missing widget id, a shader that will not compile, and a startup panic. It earned its place the day it was written by catching `Row = { … }`, which is not valid in this fork's script language. `cargo build` is silent about it, because the block is parsed at *runtime* — a broken widget tree compiles perfectly and then renders nothing. The smoke test greps for `[E]` in the log precisely because a non-zero exit code would not have appeared. - It **cannot** drive widgets. `makepad_test`'s `Selector::id(..).click()` needs the harness to own the event loop. That is fork work and it is not done, so the `#[ignore]`d interaction tests stay ignored. The gate was verified to fail: reintroducing the bad syntax made it exit 1 with the parser error, and reverting made it pass. ### 1 is open, and now says so at runtime There is **no OS notification backend in this repository**. No `robius-notification` crate exists beside `robius-sms`, `robius-contacts` and `robius-ussd`, and Makepad's platform layer exposes none on any target. Building one means `NotificationCompat` plus a channel on Android, `UNUserNotification` on iOS, and a D-Bus call to `org.freedesktop.Notifications` on Linux. That is a robius-sized crate. What was built is the seam: `NotificationSink`, with `UnavailableNotifications` as the only implementation. It is named for what it is, because a `DefaultNotifier` that silently discarded every alert would read like a working feature at every call site. The part that earns its place is `is_delivering()` and `unavailable_reason()`. The dashboard renders the reason, so the user is *told* that alerts stop when the window closes rather than assuming they are covered and missing a spread. A no-op that reports success is exactly the "declared versus delivered" failure ADR 0017 exists to prevent; this reports its own absence. ## Consequences - Adding a real notification backend touches one `impl` and no alert logic. `the_seam_is_object_safe_so_a_backend_can_be_swapped_in` fails at compile time if that stops being true. - CI gains two jobs: the Xvfb smoke test, and a build of `nigig-mpesa` and `nigig-pay` with a check that their exchange pages have not drifted apart. - The honest scoreboard is two closed, one usefully sidestepped, one open with a seam and a runtime disclosure. The README says the same. ## Alternatives rejected **Shipping a no-op notifier that returns `true`.** It would have let the README claim four of four. It would also mean a user who closed the window believed they were still being alerted. The whole point of this tool is that its numbers can be trusted. **Claiming Xvfb closes the headless-backend gap.** It closes the "never executed" half. Saying it closed the interaction half would misrepresent what the six ignored tests need, and the next person would waste a day finding out.