Files
glucose-dashboard/docs/requirements-reporting.md
willemjanandClaude Sonnet 5 3ef9a7666a add reporting foundation: multipage nav, auth gate, date-range fetch
First increment of the clinical reporting feature (see
docs/requirements-reporting.md and docs/design-reporting.md): moves the
existing live view to pages/1_Live.py, adds a password gate since the
dashboard URL will be shared with the diabetes team, adds
fetch_entries_range/fetch_treatments_range for multi-day report queries,
and adds reporting.py with the core clinical calculations (GMI, %CV,
5-band time-in-range, hypo/hyper event detection per the Battelino et al.
2019 persistence rule). No report pages wired up yet — that's next.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-26 20:00:48 +02:00

199 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Requirements: Clinical reporting page
Status: **draft — for review and editing**
Owner: willemjan
Related: [requirements-profile-tuning.md](requirements-profile-tuning.md) (separate feature, separate app)
## 1. Purpose & background
The previous FreeStyle Libre 2 setup (via LibreView) produced clinical reports
that the hospital/diabetes team wants to keep seeing: a multi-month time-in-range
(TIR) overview, day-by-day glucose graphs, hypoglycemia event analysis, and an
estimated HbA1c. The current DIY closed-loop setup (Medtrum TouchCare Nano 200U +
Lyumjev + Dexcom One+ via xDrip+ + AndroidAPS) stores everything in Nightscout,
which has no equivalent built-in report.
This feature adds a **reporting page to the existing `glucose-dashboard` Streamlit
app**, generating the closest practical equivalent from Nightscout data — read-only,
no new infrastructure, deployed through the existing CI/CD pipeline.
## 2. Scope
**In scope:**
- A new set of pages in the existing Streamlit app, reading `entries` (glucose) and
`treatments` (insulin, carbs) from Nightscout.
- Multi-day statistical summaries, an AGP-style percentile chart, day-by-day
glucose graphs, hypo/hyper event analysis, and an estimated HbA1c (GMI) on separate pages where not closely related.
- Generating PDF output of user selected pages, so the user can send the report to the hospital.
**Out of scope (this feature):**
- Anything that writes to Nightscout (see the separate profile-tuning spec).
- Non-Nightscout data sources.
- The oref0-autotune integration.
## 3. Clinical definitions & standards used
These are the standard definitions this report is built against (international
consensus, mainly based on dutch requirements, not invented for this project) the ranges are described in the table below.
| Term | Definition | Source |
|---|---|---|
| Hypoglycemia Level 1 | < 3.9 mmol/L (<70 mg/dL) | Battelino et al. 2019, *Diabetes Care* 42(8):1593–1603 |
| Hypoglycemia Level 2 | < 3.0 mmol/L (<54 mg/dL) | same |
| Hyperglycemia Level 1 | > 10.0 mmol/L (>180 mg/dL) | same |
| Hyperglycemia Level 2 | > 13.9 mmol/L (>250 mg/dL) | same |
| Event persistence rule | A hypo/hyper **event** starts once glucose has been continuously past the threshold for **≥15 minutes**, and ends once it has recovered past the threshold for **≥15 minutes**. A brief single reading crossing back doesn't end the event. | Battelino et al. 2019 |
| Time-in-range targets (adult, general) | TIR (3.9–10.0) >70% · TBR<3.9 <4% · TBR<3.0 <1% · TAR>10.0 <25% · TAR>13.9 <5% | Battelino et al. 2019 |
| GMI (estimated HbA1c) | `GMI% = 3.31 + 0.02392 × mean(mg/dL)` = `3.31 + 0.4311 × mean(mmol/L)` | Bergenstal et al. 2018, *Diabetes Care* 41(11):2275–2280 |
| Minimum valid report | ≥14 days of data with ≥70% sensor-active time | consensus standard |
Note: this dashboard already uses `TARGET_LOW = 3.9` / `TARGET_HIGH = 10.0` in
[`data_processing.py`](../data_processing.py) — that matches Level 1 thresholds
above, so the existing constants carry over directly; Level 2 thresholds are new.
GMI is explicitly **not** a substitute for a lab HbA1c (can differ by ~0.5
percentage points due to red blood cell/hemoglobin factors) — the report should
label it "estimated HbA1c (GMI)", not "HbA1c", to avoid confusing the hospital
team.
## 4. Functional requirements
Each FR below is meant to be independently implementable and testable, so they
can be delivered one at a time through the CI/CD pipeline.
### FR1 — Report period selector
The user can choose the reporting period: **14 days** (clinical default), 30
days, **90 days** (matches the old Libre 2 quarterly view), or a custom date
range. The period should be settable for each page separately since 90 days are relevant for A1C calculations and time in range but not so much for daily overview where 14 days is more relavant to the clinic. The selection drives every other requirement below.
*Acceptance:* selecting a period reloads the specific report page for that exact date
range; the 90-day option must not time out or hang (see NFR1 on data volume).
### FR2 — AGP-style summary statistics
A summary panel showing, for the selected period:
- % of time CGM was active (data completeness)
- Average glucose (mmol/L)
- Estimated HbA1c (GMI)
- Glucose variability (%CV = stdev/mean × 100)
- Time-in-range breakdown across the 5 standard bands (<3.0, 3.0–3.9, 3.9–10.0,
10.0–13.9, >13.9), each shown as a percentage and as hours:minutes/day
- Each stat shown against its consensus target (pass/fail or color-coded)
*Acceptance:* stats match a hand-calculated reference on a small fixture dataset
(see §7); TIR/TBR/TAR percentages sum to 100%.
### FR3 — AGP percentile chart
A single chart showing the 5th/25th/50th(median)/75th/95th percentile glucose
bands, aggregated by time-of-day across the selected period onto one 24-hour
composite axis (the standard AGP visualization).
*Acceptance:* median line at a given hour matches the median of all readings at
that hour-of-day across the period, computed independently in a test.
### FR4 — Day-by-day glucose graphs
One glucose trace per calendar day in the selected period (reusing the existing
target-range-band chart style from [`app.py`](../app.py)'s `build_chart`, adapted
for a single day per panel), so the hospital team can review specific days the
way the Libre 2 report's daily log did.
*Acceptance:* each day's panel shows exactly that day's readings; days with no
data are shown as empty/skipped rather than causing an error.
### FR5 — Hypoglycemia event analysis
Detect discrete hypo events per the persistence rule in §3, and report:
- Total event count (Level 1 and Level 2 separately)
- Duration of each event, and summary stats (average, longest, total time low)
- Events per week
- Time-of-day distribution (e.g., overnight vs. daytime split)
*Acceptance:* event count/duration matches a hand-traced fixture (a synthetic
glucose series with known dips of known duration, including edge cases: a dip
just under 15 minutes that should NOT count, and two dips separated by a brief
recovery that should count as one continuous event per the persistence rule).
### FR6 — Hyperglycemia event analysis
Same as FR5, symmetric, for Level 1/Level 2 highs. (Not explicitly requested by
the user but a natural complement and confirmed to implement by the user.)
### FR7 — Estimated HbA1c trend
GMI computed per rolling period (e.g., per month within a 90-day report),
plotted as a small trend, so the hospital team can see whether control is
improving/worsening over the quarter, not just the endpoint value.
### FR8 — Candidate additional statistics
The user mentioned having seen "other interesting stats" in other reports
without specifying which. Candidates to consider:
- Day-of-week pattern (e.g., weekends vs. weekdays)
- Overnight (00:00–06:00) vs. daytime TIR split
- Basal:bolus insulin ratio
- Insulin pump site-change and sensor-change adherence (interval between changes,
reusing existing `classify_treatments` output)
- Glucose trends after site-change
- Pump site age vs insuline usage vs glucose levels. Corrected for carb intake to see if age of the pump-site and/or insuline age influence insulin usage
- Total daily insulin dose trend
### FR9 — Data completeness / quality indicator
Flag periods with low sensor-active time (<70%) or visible data gaps, since a
report built on incomplete data is clinically unreliable — the report should
surface this rather than silently computing misleading stats.
### FR10 — Export / print-friendly view (open question)
The Libre 2 reports were shown to or printed for the hospital team.
The pages needs a print-optimized layout, a PDF export, and a
"share the dashboard URL".
## 5. Non-functional requirements
- **NFR1 — Data volume**: the existing `fetch_entries`/`fetch_treatments` in
[`nightscout_client.py`](../nightscout_client.py) scale their `count` parameter
linearly with `hours`, which doesn't scale cleanly to 90 days (~26,000 5-minute
readings). This feature needs either a proper date-range fetch (`find[date]`
query params) or paginated fetching — a new capability, not a reuse of the
existing rolling-window functions as-is.
- **NFR2 — Caching**: historical data for a fixed past period doesn't change; cache
TTL should be much longer than the existing 60-second rolling-view cache
(`app.py`'s `load_nightscout_data`), e.g. cached per exact date-range until
explicitly refreshed.
- **NFR3 — Consistency**: reuse the existing `PALETTE` dict and `st.context.theme`
detection pattern from `app.py` rather than introducing new colors; reuse
`TARGET_LOW`/`TARGET_HIGH` from `data_processing.py`.
- **NFR4 — Language**: Dutch UI strings and docstrings, consistent with the rest
of the app.
- **NFR5 — Multipage navigation**: this is the first feature needing more than one
page — introduces `st.Page`/`st.navigation` (anticipated in the project's own
`homelab-cicd-werkinstructie.md`).
- **NFR6 - Password protection on the dashboard pages**: as dashboard urls will be sharable and reachable by the diabetes team, access should be restricted to authorized users.
## 6. Data requirements
- Glucose entries over an arbitrary date range (new: `fetch_entries` needs a
date-range mode, not just "last N hours").
- Treatments over the same range (same scaling concern).
- No new Nightscout permissions needed — this feature only reads, so the
existing read-only token is sufficient.
## 7. Testing & acceptance criteria
- Each new pure function (event detection, GMI calculation, percentile
aggregation, %CV) gets unit tests in `data_processing.py` or a new
`reporting.py` module, following the existing `tests/test_data_processing.py`
fixture-based pattern — including edge cases called out per FR above (empty
data, exactly-15-minute boundary events, 100% in-range, etc.).
- Any new Nightscout fetch logic gets tests following the existing
`tests/test_nightscout_client.py` `monkeypatch`-on-`requests` pattern.
- A UAT checklist (produced during the design phase) will map each FR's
"Acceptance" line above to a concrete scenario the user can manually verify
against their own real Nightscout data.
## 8. Open questions for the user's edit pass
1. Confirm the clinical thresholds/targets in §3 match what your hospital
actually uses, or note any local adaptations. - Use the suggested clinical thresholds and targets from §3
2. FR6 (hyperglycemia analysis) — keep
3. FR8 — which candidate stats actually matter to you / your team? All that are in FR8 now
4. FR10 — is a print/export view needed, or is viewing the dashboard directly
enough? - pdf export needed
5. Default report period: 14-day (clinical standard) or 90-day (matches your
old Libre 2 habit) as the page's default view? Default 90 days for statistics, but 14 days for recent glucose charts.