No personal data
The interface explains the required export and what will happen before asking for a file.
Target interaction specification
Was the air around me different from the city background at the same place and hour?
Air may compare personal and ambient PM2.5 only when metric, place, and time align. Missing or unresolved matches must remain missing, every ambient value must expose its source and age, and the product must not turn an exposure difference into a medical conclusion.
Case-study thesis
This is the behavior to build toward. The implementation receipts below remain the record of what exists today.
The interaction problem is joining two imperfect worlds without hiding the gaps: a wearable that knows what was near the body and a city model that knows the background around a place.
State model
If two states would look identical to the person, the implementation is not finished.
The interface explains the required export and what will happen before asking for a file.
Valid, rejected, duplicate, and outlier rows are counted visibly.
An hour has a measurement but no trustworthy location, so comparison waits.
Personal and ambient PM2.5 share the same hour and place context, with both sources visible.
A personal hour has no ambient counterpart and remains visibly outside the comparison.
A travel stay supplies location context for a GPS-less window and recomputes affected hours.
The person can inspect raw personal value, ambient source, model or station, location basis, and timestamp.
Unavailable ambient data, failed fetch, or stale background is represented directly rather than interpolated without disclosure.
Edge cases
Definition of done
HAAM product edition · What I breathed, next to the city
I carry an Atmotube. HAAM Air joins those personal PM2.5 hours to ECMWF CAMS city background, with travel stays filling the gaps where GPS drops. Compare is PM2.5 only. Hours without a city background are omitted.

compare metric
PM2.5Not AQI. The join skips hours without a city background.
city background
CAMSECMWF CAMS via Open-Meteo. Source labelled on the hour.
Atmotube export
CSVParser handles aliases and outliers. Fixture tests in air-core.
personal tool
Liveair.haam.co is the published web dashboard. Observed use is personal.
The job
A wearable PM2.5 number without a matched city hour is hard to read. I wanted the two series on the same clock, with the source labelled, and with travel stays filling GPS-less windows.
Medical interpretation would have been a different product. AQI as the compare metric would have mixed indexes. Multi-tenant SaaS was never the job.
The web dashboard is canonical. Expo and Streamlit exist as companions.
The compare
Signed-in CSVs go to Blob on the live host. Stays go to Supabase. Ambient fetch is Open-Meteo. Secrets stay out of Git.
01
Atmotube CSV aliases and outliers are handled in air-core. Fixture tests cover parse without live APIs.
02
GPS points fall into place boxes. Travel stays fill windows where the wearable has no fix.
03
Each personal hour looks up CAMS background for that place. Hours without ambient are omitted from compare.
04
The dashboard shows the two PM2.5 series hour by hour, with CAMS or optional station labelled as the city source.
Design decisions
air.haam.co
Metric
AQI would have mixed pollutants and indexes. I kept one particle measure so personal and city hours are actually the same quantity.
Place
Travel windows without a fix still need a city. The stays flow is how those hours get a place box instead of disappearing from the join.
Canonical surface
Parser, places, and join live in air-core so the web app is not a one-off script. Expo and Streamlit are companions. OpenAQ station overlay is optional and not claimed as production-verified.
The working stack
Core
CSV parse, place boxes, hourly join. pnpm test against fixtures.
Ambient
ECMWF city PM2.5 background. Optional OpenAQ overlay undocumented as production.
Account
Signed-in CSVs and stay windows on the live host. Secrets gitignored.
Host
Published personal tool. Web is canonical.
In evidence
01Personal Atmotube hours sit next to CAMS city hours
02The join is tested without live APIs
03Travel stays can fill GPS-less gaps
04Hours without city background are omitted from compare
Where it stops
There is no medical interpretation and no live AQI compare. Observed use is personal, not a public multi-user study.
Expo and Streamlit are companions. Accessibility is not recorded in this pass. Do not call it a city-wide air-quality product.
HAAM’s role
PM2.5-only compare, stays interview, HAAM chrome, air-core join, web app, and the live host wiring. Direct work, 2026.
How we learned
Research paired hour-by-hour Atmotube PM2.5 readings with the CAMS city background. Looking at both together makes it possible to ask when personal exposure diverges from the broader environment instead of treating either sensor stream as the whole story.
Optional analytics. Google Analytics and Clarity load only if allowed; form, email, and chat content are excluded.