A self-management web app for Immune Thrombocytopenia (ITP). It tracks platelet counts, follows a medication cycle, and forecasts monthly treatment costs.
Built with zero third-party dependencies: standard-library Python on the
back end, vanilla JavaScript and hand-drawn SVG on the front end. No pip install, no npm install, no CDN. Clone it and run it.
▶ Try the live demo (sample data, no sign-up)
py -3 server.py # → http://localhost:8000| Light | Dark |
|---|---|
ITP is an autoimmune condition in which the immune system destroys platelets, raising the risk of bleeding. Managing it means answering two questions every single day:
What is my platelet count doing? · What do I take today?
Existing tools answer neither well. Generic health apps don't understand platelet reference ranges, and none of them model the rotating dose cycles that treatments like avatrombopag actually use (e.g. "2 pills for 1 day, then 1 pill for 3 days, repeating"). Medication is also expensive and largely self-funded, so predicting monthly cost matters as much as the clinical picture.
This app is built around those needs, and around one design rule: logging a reading must take under five seconds.
Platelet tracking
- Readings plotted against the four clinical reference zones used in Chinese
hospitals (×10⁹/L): critical
<30, high-risk30-75, borderline75-125, normal≥125. Thresholds are configurable. - Interactive SVG chart: hover/touch crosshair with tooltip, y-axis gridlines, rolling average line, min/max annotations, and 7 / 30 / 90 / all-time ranges.
- Summary bar: latest, average, range, and net change over the window.
Medication cycles
- Compose a cycle from phases ("N pills × D days") that repeat from a start date, modelling real prescribing patterns instead of a fixed daily dose.
- Today's dose, position in the cycle (day X of Y), and a 7-day look-ahead strip.
- Mark each day taken or missed.
Dose-response analysis
- Estimates whether the medication is associated with higher platelet counts, accounting for the fact that the marrow responds over days rather than hours.
- Scans candidate lags of 0-21 days, correlating smoothed dose exposure against counts measured later, and reports the best-supported delay.
- Reports effect size, sample size and significance, and states plainly when the evidence is weak, absent, or when dose hasn't varied enough to test.
- Statistics are implemented from first principles in
analysis.pyand unit tested, including against synthetic data with a known planted lag.
Cost forecasting
- Computes pills required for the current calendar month directly from the cycle.
- Derives per-pill price from box price ÷ box size automatically.
- Adds doctor visits and CBC blood tests to produce an itemised monthly total and an annual projection.
Interface
- Light and dark themes, remembered between visits.
- Four tabs (Overview · Medication · Analysis · History), responsive down to phone width.
flowchart LR
subgraph Client["Browser (index.html)"]
UI["Vanilla JS UI<br/>hand-drawn SVG charts"]
end
subgraph Server["Python, standard library only"]
R["route()<br/>pure request handler"]
DB[("SQLite<br/>itp.db")]
end
UI -->|"JSON over fetch()"| R
R --> DB
L["server.py<br/>http.server, local"] --> R
W["wsgi.py<br/>WSGI, hosted"] --> R
The core design decision is that route() is a pure function:
route(method, path, body, auth_header) -> (status, headers, body)It touches no globals from the web layer, which means the same handler serves
both the local http.server runner and any WSGI host, and the entire HTTP
surface is testable without opening a socket.
| File | Purpose |
|---|---|
server.py |
Router, SQLite persistence, auth, static files, dev server |
analysis.py |
Dose-response statistics (correlation, lag scan, significance) |
wsgi.py |
WSGI entry point for hosted deployment |
schema.sql |
Database schema, applied idempotently at start-up |
seed_demo.py |
Generates sample data for a public demo deployment |
index.html |
Page structure |
static/app.css |
Design system: themes, layout, components |
static/app.js |
UI logic and the hand-drawn SVG charts |
tests/ |
81 unit tests (unittest, no dependencies) |
DEPLOY.md |
Step-by-step free hosting guide |
itp.db |
Your data. Git-ignored, never committed |
Requires Python 3.9+. Nothing else. (CI runs the test suite on 3.9, 3.11 and 3.13.)
git clone https://github.com/abdessamadr46/itp-tracker.git
cd itp-tracker
py -3 server.py # Windows; python3 server.py on macOS/LinuxOpen http://localhost:8000. The database is created automatically on first run.
All optional, via environment variables:
| Variable | Purpose | Default |
|---|---|---|
ITP_PASSWORD |
Require a password (HTTP Basic auth) | unset, open |
ITP_DB |
Path to the SQLite file | ./itp.db |
ITP_HOST |
Bind address | 127.0.0.1 |
ITP_PORT |
Port | 8000 |
py -3 -m unittest discover -s tests -vCovers schema and defaults, reading upsert/delete semantics, dose logging,
cycle-phase sanitisation, cost clamping, routing, authentication, static-file
serving (including a path-traversal check), port binding, the WSGI adapter, and
the statistical functions in analysis.py. Tests run against a temporary
database and never touch real data.
See DEPLOY.md for a free hosting walkthrough.
Two deployment shapes are supported:
- Private instance holding real readings. Set
ITP_PASSWORDso the data is never publicly readable. - Public demo holding generated sample data. Run
seed_demo.py, setITP_DEMO=1for the sample-data banner, and leaveITP_PASSWORDunset.
Real medical records should never be placed on a publicly reachable instance.
Beyond being a logging tool, the app functions as a small n-of-1 observational instrument: a single patient, densely sampled over time, with an intervention (dose) that varies and an outcome (platelet count) measured repeatedly.
Question. Within one patient's own record, is medication exposure associated with subsequent platelet counts, and at what delay?
Method. The medication cycle is expanded into a daily dose series and adjusted for logged adherence. Because platelet production responds over days, same-day comparison is not meaningful; instead, dose is smoothed into a rolling 7-day exposure window and correlated against counts at lags of 0-21 days. Pearson's r, the least-squares slope, and a two-tailed significance test at α = 0.05 are computed for each lag, and the lag with the largest |r| is reported with its sample size.
Why this framing. Chronic-illness self-tracking generates exactly this kind of irregular, single-subject time series, and it is usually left unanalysed. Treating one person's record as a legitimate unit of analysis connects to established work on n-of-1 trials and patient-reported outcomes in digital health.
Limitations, stated deliberately and surfaced in the app itself. Observational, single-subject data cannot establish causation; infection, other medication, and spontaneous remission are uncontrolled confounders. Searching 21 lags inflates the false-positive rate (multiple comparisons), and the smoothing window is a modelling choice that affects which lag wins. Dose is also strongly autocorrelated, since it changes slowly, so neighbouring lags produce near-identical correlations and the selected lag is only approximate. Sample sizes are small, so estimates are unstable. The app therefore reports effect size and n alongside every result, labels weak findings as weak, and refuses to report anything when dose has not varied. Presenting an uncertain result honestly was treated as a design requirement, not a caveat added afterwards.
Possible extensions. Pre-registering a single hypothesised lag would remove the multiple-comparisons problem; cross-correlation with confidence intervals, change-point detection around dose adjustments, or a mixed-effects model across multiple consenting users would all strengthen the inference.
Why no dependencies? A tool you rely on daily for medication shouldn't break because a transitive dependency published a bad release, and it should still run in five years. The constraint also forced clearer thinking: the charts are plotted with explicit coordinate math and Catmull-Rom smoothing rather than delegated to a library, and the whole app is a handful of files anyone can read end to end.
Trade-offs. SQLite and a single-process server suit one user well and would not suit many; a multi-user version would need a client/server database and real sessions. The front end is served as plain static files with no build step, which keeps it inspectable and dependency-free but would want a module bundler to grow much further.
Testing focus. Tests target the logic most likely to cause silent harm:
dose-cycle arithmetic and input sanitisation. Writing them surfaced a real
connection leak: with sqlite3.connect(...) commits the transaction but does
not close the connection. That is now fixed with an explicit context manager.
Verifying start-up behaviour separately turned up a portability bug: Python
enables SO_REUSEADDR by default, which on Windows lets a second process bind
a port already in use, so two servers silently compete for requests. Address
reuse is now disabled on Windows only, where the semantics differ from Unix.
- Dose-response analysis with lag detection and significance testing
- Export data as CSV / PDF for clinic appointments
- Symptom log (bruising, petechiae, fatigue) correlated against counts
- Multi-language interface (English / 中文)
- Multi-user accounts, so other ITP patients can use it
- Accessibility audit (keyboard navigation, screen readers, WCAG contrast)
- Confidence intervals and change-point detection around dose adjustments
This software is a personal record-keeping tool. It is not a medical device, it does not provide medical advice, and it has not been clinically validated. Do not use it to make treatment decisions. Always consult your haematologist.
All data stays in a local SQLite file that you control. The app makes no
outbound network requests, contains no analytics or telemetry, and sends nothing
to any third party. *.db is git-ignored so health data is never committed.