| android | ||
| artwork | ||
| crates | ||
| docs | ||
| playstore | ||
| scripts | ||
| site | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| clippy.toml | ||
| GEMINI.md | ||
| LICENSE | ||
| README.md | ||
| ROADMAP.md | ||
| roadmap.toml | ||
Under Pressure
An Android application for logging blood pressure by hand and reading it back as trends, distributions, and a summary suitable for handing to a clinician. The Play listing calls it Blood pressure tracker and trends.
Status:
v1.0.0(2026-08-18). Production launch, carried to the wall this repository cannot cross: the release gate refuses to name the public track without a measuredPASSverdict, and against a live credential it refused; the Play application record no API can create is still a Console session away. Update 2026-08-24: that session happened. The record exists asapp.underpressure, versionCode 16 is live on the closed track, every Console row is confirmed, and production waits on Google's 12-tester / 14-day rule (docs/play/TESTERS.md). Update 2026-09-04:v1.1.0(versionCode 17) went live on the closed track: the v2 icon, a second cited guideline, backups with restore, six themes, a pulse chart in Trends, and a plain-language pass (deltas 72 to 79). Update 2026-09-05:v1.2.0(versionCode 18) went to the closed track: seven full-palette themes and an About room (deltas 80 and 81);v1.2.1(versionCode 19) followed the same day with one line fixed in About. Update 2026-09-14:v1.2.2(versionCode 20) answered the three Console items listed against 1.2.1; nothing on screen changed.v1.2.3(versionCode 21) followed the same day: the edge-to-edge helper that made the retired calls is gone, and the bar icons follow the theme picked in the app (delta 82). A reading is typed on a custom keypad in a modal sheet, stored in SQLite through the UniFFI bindings, and listed under a day header carrying that day's mean. Rows swipe to edit or to a soft delete with a five-second undo. Two reminder slots fire per weekday, exactly where the platform allows it and inexactly where it does not. They survive a reboot and a system time change, and open the log sheet from the notification; the Today screen shows a fourteen-day coverage strip. A first-run screen states what the app is before it asks for anything. Trends reads the log back as statistics and five charts under a shared 14d / 30d / all period selector: a zone scatter over the guideline edition's own regions, a paired timeline with a gated trend line, the pulse over time, AM/PM dumbbells, and a coverage calendar whose empty days are tappable. Every chart is a ComposeCanvas, animates in once, and carries both a one-sentence spoken description and a Read-as-table form.The app will also hold a protocol series for you: seven days, a morning and an evening each, two readings an occasion, with day one recorded and deliberately left out of the average. A progress rail draws all twenty-eight positions, day one hatched rather than hidden, and a single card names the one thing to do next. It is a structure the app keeps, not a regimen it prescribes: the series' shape is read from the published guideline and cited, the rest and spacing waits are the app's own adjustable defaults and say so, and the screen that runs it is structurally incapable of naming a reading's value.
The previous release added the clinician summary: a period of readings written out as a document: the window and its provenance, then one block per source with the mean and its standard deviation, the range, the pulse pressure, the distribution across the guideline's bands, a morning/evening grid, coverage, and the change over the period. It shares as an A4 PDF and as a CSV through the system share sheet, and it renders light whatever theme the app is in, because the artefact gets printed. The summary is transport: it formats readings you entered and hands them over. It does not interpret, conclude, recommend, or flag, and that is held by scans over the package rather than by a review of its wording.
The release before this made the log portable. Everything you have entered leaves as a CSV row per reading, a versioned JSON envelope, or a summarised markdown roll-up, saved to a file you name or handed to another app through the share sheet. It comes back the same way: the first-run screen offers Import a CSV, and a file this app wrote restores on a clean install. A bad row is skipped and named with the line it was on; a bad file is refused whole, because refusing three hundred readings over one hand-edited row is not a restore. More is now a menu with rooms rather than one screen. And
pressure-cli, a stub until now, files a raw export and splices the roll-up into a local health record, dry-run unless you pass--apply.The release before this put the log on the home screen and, if you ask it to, into Health Connect. The widget comes in two sizes: a small one carrying your last reading with its band strip, the band's name and a Log button that opens the entry sheet directly, and a wide one adding the fortnight's coverage strip, the days covered and the seven-day mean. It updates the moment you log, edit or delete something, with no need to open the app, and on a device where you never have, it says so plainly instead of drawing zeroes. It uses this app's own colours, never the wallpaper's, because a tinted band strip would quietly say something false about a reading.
Writing to Health Connect is off until you turn it on, and there is a separate, deliberate button for the readings already in the logbook. Turning it back off stops future writes and leaves what was already written where it is. The screen says so, next to the switch. Cuffless wearable readings are not written at all: the platform's vocabulary is upper arm, wrist or unknown, and writing an estimate as a wrist measurement would tell it something untrue about where the number came from. No pulse is written either, and a scan of the source fails the build if that changes.
This release adds no feature. It closes every defect the six earlier accessibility sweeps found and wrote down (fifteen of them, where the plan that scheduled this work had said six) and then sweeps the whole application for the first time: all fifteen screens at twice the system font size, in both colour schemes, with frame timings for each. Tick labels no longer run together into one long number. The paired timeline was profiled before it was touched, and draws a hundred days in 48 ms where it took 133–150. Four labels under the protocol rail that read as one phrase now carry a separator. The time-entry dialog is the app's own, because the platform's froze its AM/PM control at a fixed size whatever the font scale. Blood-pressure pairs are spoken as "120 over 80" rather than read out as their glyphs. A store failure now says what its type means instead of showing a message written for a developer, at twelve places, where the note recording the problem had said three. Four defects are shipped knowingly rather than fixed, each with the measurement behind that decision and the condition that would reopen it.
Every screen is built to the design system in
docs/design/rather than styled afterwards, and the design's rules are enforced: colour, motion, haptics, type floors, contrast and voice each have a test that fails the build, and every one of them has been seen failing.
What it is
Domain logic is a plain Rust library crate (pressure-core). The interface is
Kotlin and Jetpack Compose. Data lives on the device only: no account, no backend,
no network call anywhere in the application.
A companion desktop CLI (pressure-cli) ingests the application's export into a
separate, local-only health record store.
Why the split
pressure-core never imports anything Android. That single constraint buys three
things:
- Roughly half the project's real complexity (persistence, statistics, guideline
classification) is verifiable with
cargo testin under a second, no emulator. - The same crate can be consumed by a desktop client, so phone and desktop compute identical numbers from one definition.
- The writer and the reader of the export format are the same code, making format drift impossible rather than merely unlikely.
Layout
under-pressure/
├── artwork/app-icon/ # Launcher, themed-icon, and Play Store masters
├── Cargo.toml # workspace
├── crates/
│ ├── pressure-core/ # model, storage, stats, classification, export
│ ├── pressure-cli/ # desktop ingest
│ └── uniffi-bindgen/ # binding generator, pinned to the crate's uniffi
├── android/
│ ├── app/ # Compose UI, screens, charts
│ └── gradle/ # cargo-ndk + uniffi-bindgen wiring
├── scripts/ # check_so_alignment.sh (the Play 16 KB gate), deploy-site.sh
├── site/ # the marketing site: Leptos rendered to static HTML, deployed by scripts/deploy-site.sh
└── docs/
├── SESSION_NOTES.md
├── design/ # the design system every UI milestone builds to
├── plans/ # per-milestone plans and their audit trails
└── superpowers/specs/
uniffi-bindgen is a third workspace member the design's two-crate tree does not
show. uniffi publishes no standalone CLI, and the generator has to be built at
exactly the version pressure-core depends on, so it lives here as build tooling.
The site
https://underpressure.aldermetric.com is built from this repository. site/ renders Leptos
components to static HTML (cargo run --release in site/), reads the disclaimer, the
privacy policy, the band words, the guideline edition, the Android requirement and the
changelog versions from the files here, and is deployed by scripts/deploy-site.sh into
~/docker-containers/underpressure-site/ behind the existing Traefik. site/README.md holds
the commands. The privacy policy Play links to lives at /privacy there.
playstore/app-content/privacy-policy-url.txt names it, and Alan set the Play Console field to
match on 2026-09-02; the old alangaudet.dev/privacy also still answers 200. That field has no
API write path in either direction, so it is a by-hand Console step and cannot be read back.
Documentation
| Document | What it holds |
|---|---|
docs/superpowers/specs/2026-07-25-under-pressure-design.md |
The approved design. §12 holds per-milestone scope |
docs/superpowers/specs/2026-08-25-under-pressure-site-design.md |
The marketing site's design; §8 is the privacy-URL migration order |
ROADMAP.md |
Milestone ladder and the **Next:** pointer |
docs/SESSION_NOTES.md |
Current state and handoff |
AGENTS.md |
Agent context, build commands, hard constraints |
Two constraints worth knowing before reading any code
The FFI surface never panics. A Rust panic crossing UniFFI aborts the process,
which Android records as a native crash, which counts against Android Vitals, which
is a direct Play Store search-ranking input. Every exported function returns
Result; unwrap, expect, slice indexing, and overflowing arithmetic are
lint-denied on any path reachable from ffi.rs. Design §3.2.
Readings from different sources never share a statistic. Cuffless wearable blood pressure is an estimate, not a cuff-equivalent measurement. Merging the two would corrupt the clinician summary. This is enforced by the statistics API's type signatures rather than by convention. Design §4.5.
Licence
BSD 3-Clause. See LICENSE.