Author SHA1 Message Date
willemjan 6004f76d76 Merge pull request 'Add pump profile fetching and Insulineprofiel page' (#29) from feature/insulin-profile into main
CI/CD / test (push) Successful in 51s
CI/CD / sonarqube (push) Successful in 24s
CI/CD / build-and-scan (push) Successful in 2m9s
CI/CD / deploy (push) Successful in 22s
willemjan/glucose-dashboard/pipeline/head This commit looks good
Reviewed-on: #29
2026-09-05 19:43:58 +00:00
willemjan 79249958c2 Add pump profile fetching and Insulineprofiel page
willemjan/glucose-dashboard/pipeline/head This commit looks good
Fetch and parse the latest-used Nightscout profile (basal schedule, ISF,
carb ratio, DIA, targets) and show it as a new dashboard page: a list of
basal/ISF/carb-ratio segments plus a bar chart of basal rate by hour of
day. This is the data foundation for treatment overlays on the glucose
charts and the redesigned PDF report in follow-up PRs.
2026-09-05 21:37:17 +02:00
willemjan 8641022219 Merge pull request 'test jenkins' (#28) from dev into main
CI/CD / test (push) Successful in 9s
CI/CD / sonarqube (push) Successful in 22s
CI/CD / build-and-scan (push) Successful in 1m56s
willemjan/glucose-dashboard/pipeline/head Something is wrong with the build of this commit
CI/CD / deploy (push) Successful in 16s
Reviewed-on: #28
2026-08-10 21:20:28 +00:00
willemjan db3204ab58 Merge pull request 'Show estimated HbA1c in mmol/mol (IFCC) alongside % (#25)' (#27) from dev into main
CI/CD / test (push) Successful in 9s
CI/CD / sonarqube (push) Successful in 20s
CI/CD / build-and-scan (push) Successful in 49s
CI/CD / deploy (push) Successful in 3s
2026-08-01 21:13:00 +00:00
willemjan 25c3fe6730 Merge pull request 'GMI trend: daily scatter + rolling average + linear trend/CI (#19)' (#26) from dev into main
CI/CD / test (push) Successful in 10s
CI/CD / sonarqube (push) Successful in 20s
CI/CD / build-and-scan (push) Successful in 43s
CI/CD / deploy (push) Successful in 2s
2026-08-01 20:58:12 +00:00
willemjan d637ad39e4 Merge pull request 'Fix bolus/basal insulin categorization: split SMB from manual corrections (#20)' (#24) from dev into main
CI/CD / test (push) Successful in 9s
CI/CD / sonarqube (push) Successful in 20s
CI/CD / build-and-scan (push) Successful in 40s
CI/CD / deploy (push) Successful in 3s
2026-08-01 20:53:10 +00:00
willemjan 5addd6a49b Merge pull request 'Add TIR bar/threshold lines, Dagoverzicht + hypo/hyper Level-1 charts to PDF (#14, #15, #16, #17, #18)' (#23) from dev into main
CI/CD / test (push) Successful in 9s
CI/CD / sonarqube (push) Successful in 23s
CI/CD / build-and-scan (push) Successful in 42s
CI/CD / deploy (push) Successful in 3s
2026-08-01 20:48:51 +00:00
willemjan 95f006078f Merge pull request 'Add clinical threshold lines + TIR stacked bar chart (#14, #15)' (#22) from dev into main
CI/CD / test (push) Successful in 7s
CI/CD / sonarqube (push) Successful in 21s
CI/CD / build-and-scan (push) Successful in 41s
CI/CD / deploy (push) Successful in 2s
2026-08-01 20:28:23 +00:00
willemjan 4bb9251d7d Merge pull request 'Fix treatments API pagination cursor field (created_at, not date)' (#21) from dev into main
CI/CD / test (push) Successful in 8s
CI/CD / sonarqube (push) Successful in 19s
CI/CD / build-and-scan (push) Successful in 41s
CI/CD / deploy (push) Successful in 3s
2026-08-01 19:43:40 +00:00
willemjan 6c6b175a28 Merge pull request 'Fix multi-day Nightscout fetch truncation + data-completeness warnings' (#12) from dev into main
CI/CD / test (push) Successful in 7s
CI/CD / sonarqube (push) Successful in 20s
CI/CD / build-and-scan (push) Successful in 41s
CI/CD / deploy (push) Successful in 3s
2026-07-31 12:22:10 +00:00
willemjan c39646ceaa Merge pull request 'PDF export (FR10) - completes reporting requirements' (#10) from dev into main
CI/CD / test (push) Successful in 11s
CI/CD / sonarqube (push) Successful in 20s
CI/CD / build-and-scan (push) Successful in 1m58s
CI/CD / deploy (push) Successful in 14s
2026-07-26 19:11:06 +00:00
willemjan 52c9284f85 Merge pull request 'Trends page: GMI trend, weekday pattern, bolus, site-age (FR7, FR8)' (#9) from dev into main
CI/CD / test (push) Successful in 6s
CI/CD / sonarqube (push) Successful in 18s
CI/CD / build-and-scan (push) Successful in 40s
CI/CD / deploy (push) Successful in 4s
2026-07-26 19:06:24 +00:00
willemjan 419508dd5e Merge pull request 'Dagoverzicht + Hypo/Hyper Events pages (FR4, FR5, FR6)' (#8) from dev into main
CI/CD / test (push) Successful in 5s
CI/CD / sonarqube (push) Successful in 17s
CI/CD / build-and-scan (push) Successful in 44s
CI/CD / deploy (push) Successful in 3s
2026-07-26 18:13:56 +00:00
willemjan 689452cf4c Merge pull request 'Samenvatting + AGP Profiel pages (FR2, FR3, FR9)' (#7) from dev into main
CI/CD / test (push) Successful in 6s
CI/CD / sonarqube (push) Successful in 22s
CI/CD / build-and-scan (push) Successful in 47s
CI/CD / deploy (push) Successful in 3s
2026-07-26 18:09:41 +00:00
willemjan e600d6cf7d Merge pull request 'Reporting foundation: multipage nav, auth gate, date-range fetch' (#6) from dev into main
CI/CD / test (push) Successful in 6s
CI/CD / sonarqube (push) Successful in 17s
CI/CD / build-and-scan (push) Successful in 38s
CI/CD / deploy (push) Successful in 3s
2026-07-26 18:01:15 +00:00
willemjan 10aa315e40 Merge pull request 'Pin trivy-action to last docker-based release' (#5) from dev into main
CI/CD / test (push) Successful in 6s
CI/CD / sonarqube (push) Successful in 19s
CI/CD / build-and-scan (push) Successful in 57s
CI/CD / deploy (push) Successful in 19s
2026-07-26 12:54:59 +00:00
willemjan e3443e9f9f Merge pull request 'Fix registry hostname (duckdns) and Trivy github auth' (#4) from dev into main
CI/CD / test (push) Successful in 6s
CI/CD / sonarqube (push) Successful in 20s
CI/CD / build-and-scan (push) Failing after 10s
CI/CD / deploy (push) Skipped
2026-07-25 20:05:33 +00:00
willemjan 274d27c5bc Merge pull request 'Fix pytest module discovery in CI' (#3) from dev into main
CI/CD / test (push) Successful in 5s
CI/CD / sonarqube (push) Successful in 1m17s
CI/CD / build-and-scan (push) Failing after 4m37s
CI/CD / deploy (push) Skipped
2026-07-25 19:12:20 +00:00
willemjan f17e4f6c39 Merge pull request 'Nightscout-koppeling + CI/CD-fixes' (#2) from dev into main
CI/CD / test (push) Failing after 49s
CI/CD / sonarqube (push) Skipped
CI/CD / build-and-scan (push) Skipped
CI/CD / deploy (push) Skipped
2026-07-25 07:30:10 +00:00
6 changed files with 423 additions and 1 deletions
+18
View File
@@ -252,3 +252,21 @@ def fetch_treatments_range(start_date: date, end_date: date) -> pd.DataFrame:
since = datetime.combine(start_date, time.min, tzinfo=timezone.utc) since = datetime.combine(start_date, time.min, tzinfo=timezone.utc)
until = datetime.combine(end_date, time.max, tzinfo=timezone.utc) until = datetime.combine(end_date, time.max, tzinfo=timezone.utc)
return _treatments_since(since, until) return _treatments_since(since, until)
def fetch_profile() -> dict:
"""Haal het meest recent gebruikte pompprofiel op.
Nightscout's /api/v1/profile.json geeft profieldocumenten terug, nieuwste
eerst; met count=1 krijgen we alleen het actief gebruikte document. Een
profiel is geen tijdreeks (in tegenstelling tot entries/treatments) dus
geen paginering nodig — één klein verzoek volstaat.
Geeft {} terug als er geen profiel bestaat (bijv. lege Nightscout-instantie).
Het resultaat is het rauwe Nightscout-documument; zie profile_data.parse_profile()
voor de genormaliseerde vorm (basaalschema, ISF, koolhydraatratio, DIA, streefwaarden).
"""
data = _get("/api/v1/profile.json", {"count": 1})
if not data:
return {}
return data[0]
+98
View File
@@ -0,0 +1,98 @@
import pandas as pd
import plotly.graph_objects as go
import requests
import streamlit as st
from auth import require_auth
from nightscout_client import NightscoutConfigError
from profile_data import segments_table, segments_to_hourly_bars, total_daily_basal
from ui_common import get_colors, load_profile
st.set_page_config(page_title="Glucose dashboard — Insulineprofiel", page_icon="📊", layout="wide")
require_auth()
st.title("Insulineprofiel")
st.caption("Het meest recent gebruikte pompprofiel: basaalschema, ISF, koolhydraatratio en DIA.")
try:
profile = load_profile()
except NightscoutConfigError as exc:
st.error(str(exc))
st.stop()
except (requests.RequestException, ValueError) as exc:
st.error(f"Kon geen profiel ophalen van Nightscout: {exc}")
st.stop()
if not profile:
st.info("Geen pompprofiel gevonden op deze Nightscout-instantie.")
st.stop()
colors = get_colors()
col1, col2 = st.columns(2)
col1.metric("DIA (werkingsduur insuline)", f"{profile['dia']:.1f} uur")
col2.caption(f"Profielnaam: {profile['name'] or '—'}")
st.subheader("Basaalschema")
bars = segments_to_hourly_bars(profile["basal"])
fig = go.Figure(
go.Scatter(
x=[b["hour"] for b in bars],
y=[b["value"] for b in bars],
mode="lines",
line={"shape": "hv", "color": colors["basal"], "width": 2},
fill="tozeroy",
fillcolor=colors["basal"],
opacity=0.7,
name="Basaal",
hovertemplate="%{x:.0f}:00 · %{y:.2f} E/uur<extra></extra>",
)
)
fig.update_layout(
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color=colors["text"],
height=320,
margin={"l": 10, "r": 10, "t": 10, "b": 10},
xaxis={"title": "Uur van de dag", "range": [0, 24], "dtick": 2, "gridcolor": colors["grid"]},
yaxis={"title": "E/uur", "gridcolor": colors["grid"], "rangemode": "tozero"},
showlegend=False,
)
st.plotly_chart(fig, width="stretch")
basal_rows = segments_table(profile["basal"])
st.dataframe(
pd.DataFrame(basal_rows).rename(
columns={"start": "Van", "end": "Tot", "value": "Basaalsnelheid (E/uur)"}
),
width="stretch",
hide_index=True,
)
col_isf, col_ratio = st.columns(2)
with col_isf:
st.subheader("Insulinegevoeligheid (ISF)")
isf_rows = segments_table(profile["sens"])
st.dataframe(
pd.DataFrame(isf_rows).rename(
columns={"start": "Van", "end": "Tot", "value": "ISF (mmol/L per E)"}
),
width="stretch",
hide_index=True,
)
with col_ratio:
st.subheader("Koolhydraatratio")
ratio_rows = segments_table(profile["carbratio"])
st.dataframe(
pd.DataFrame(ratio_rows).rename(
columns={"start": "Van", "end": "Tot", "value": "Ratio (g koolhydraten per E)"}
),
width="stretch",
hide_index=True,
)
st.caption(
f"Totale dagelijkse basaal: {total_daily_basal(profile['basal']):.1f} E "
"(volgens profielschema, exclusief tijdelijke aanpassingen)."
)
+146
View File
@@ -0,0 +1,146 @@
"""
Parsing van het Nightscout-pompprofiel (/api/v1/profile): basaalschema,
insulinegevoeligheid (ISF), koolhydraatratio, DIA en streefwaarden.
Framework-vrije module (geen streamlit-import), zelfde opzet als reporting.py:
puur functies op dicts, zodat dit los van de UI te unit-testen is. Zie
nightscout_client.fetch_profile() voor de ruwe fetch; deze module normaliseert
alleen (uren -> minuten sinds middernacht, mg/dl -> mmol/L waar nodig).
"""
from __future__ import annotations
from nightscout_client import mgdl_to_mmol
SEGMENT_FIELDS = ("basal", "sens", "carbratio", "target_low", "target_high")
def _time_to_minutes(time_str: str) -> int:
"""Zet 'HH:MM' (Nightscout's segmenttijd-notatie) om naar minuten sinds middernacht."""
hours, minutes = time_str.split(":")
return int(hours) * 60 + int(minutes)
def _parse_segments(raw_segments: list[dict], *, to_mmol: bool = False) -> list[dict]:
"""Zet Nightscout-segmenten ([{'time': 'HH:MM', 'value': ...}, ...]) om naar
[{'minutes': int, 'value': float}, ...], oplopend gesorteerd op tijd."""
segments = []
for seg in raw_segments:
value = float(seg["value"])
if to_mmol:
value = mgdl_to_mmol(value)
segments.append({"minutes": _time_to_minutes(seg["time"]), "value": value})
return sorted(segments, key=lambda s: s["minutes"])
def parse_profile(raw: dict) -> dict:
"""Parse één Nightscout-profieldocument (zoals teruggegeven door
nightscout_client.fetch_profile()) naar een genormaliseerde dict:
{
"name": str,
"units": "mmol", # altijd genormaliseerd, ongeacht bronformaat
"dia": float, # uur
"basal": [{"minutes": int, "value": float}, ...], # E/uur
"sens": [{"minutes": int, "value": float}, ...], # mmol/L per E (ISF)
"carbratio": [{"minutes": int, "value": float}, ...], # g koolhydraten per E
"target_low": [{"minutes": int, "value": float}, ...], # mmol/L
"target_high": [{"minutes": int, "value": float}, ...], # mmol/L
}
Geeft {} terug als er geen (bruikbaar) profiel is.
"""
if not raw:
return {}
default_name = raw.get("defaultProfile")
store = raw.get("store", {})
profile = store.get(default_name) if default_name else None
if profile is None and store:
# defaultProfile ontbreekt of matcht niet met een sleutel in store:
# val terug op het enige/eerste beschikbare profiel.
profile = next(iter(store.values()))
if profile is None:
return {}
units = str(profile.get("units", "mg/dl")).strip().lower()
to_mmol = units != "mmol"
return {
"name": default_name or "",
"units": "mmol",
"dia": float(profile.get("dia", 0) or 0),
"basal": _parse_segments(profile.get("basal", [])),
"sens": _parse_segments(profile.get("sens", []), to_mmol=to_mmol),
"carbratio": _parse_segments(profile.get("carbratio", [])),
"target_low": _parse_segments(profile.get("target_low", []), to_mmol=to_mmol),
"target_high": _parse_segments(profile.get("target_high", []), to_mmol=to_mmol),
}
def value_at(segments: list[dict], minutes: int) -> float | None:
"""Geef de waarde terug die actief is op het gegeven tijdstip (minuten
sinds middernacht) — het laatste segment met `minutes <= t`. Ligt `t` vóór
het eerste segment van de dag, dan geldt het laatste segment (dat loopt
door vanaf de vorige dag, want een profiel dekt altijd de volle 24 uur).
Geeft None terug als `segments` leeg is.
"""
if not segments:
return None
active = None
for seg in segments:
if seg["minutes"] <= minutes:
active = seg
else:
break
if active is None:
active = segments[-1]
return active["value"]
def _format_time(minutes: int) -> str:
"""Zet minuten sinds middernacht om naar 'HH:MM'."""
return f"{minutes // 60:02d}:{minutes % 60:02d}"
def segments_table(segments: list[dict]) -> list[dict]:
"""Zet segmenten om naar rijen met een start- en eindtijd voor tabelweergave:
[{'start': 'HH:MM', 'end': 'HH:MM', 'value': float}, ...]. Het laatste
segment loopt door tot 24:00 (een profiel dekt altijd de volle dag)."""
rows = []
for i, seg in enumerate(segments):
end_minutes = segments[i + 1]["minutes"] if i + 1 < len(segments) else 24 * 60
rows.append(
{
"start": _format_time(seg["minutes"]),
"end": "24:00" if end_minutes == 24 * 60 else _format_time(end_minutes),
"value": seg["value"],
}
)
return rows
def total_daily_basal(segments: list[dict]) -> float:
"""Totale basaaldosis per dag in E: som van elk segment se duur (uur) x snelheid (E/uur)."""
if not segments:
return 0.0
total = 0.0
for i, seg in enumerate(segments):
end_minutes = segments[i + 1]["minutes"] if i + 1 < len(segments) else 24 * 60
total += seg["value"] * (end_minutes - seg["minutes"]) / 60
return total
def segments_to_hourly_bars(segments: list[dict]) -> list[dict]:
"""Zet segmenten om naar een lijst geschikt voor een staafgrafiek per uur van
de dag: [{'hour': float, 'value': float}, ...], met een extra punt op uur 24
(gelijk aan het eerste segment) zodat een staaf/stap-grafiek de volle dag dekt.
Gebruikt door de basaalgrafiek op de Insulineprofiel-pagina.
"""
if not segments:
return []
bars = [{"hour": seg["minutes"] / 60, "value": seg["value"]} for seg in segments]
bars.append({"hour": 24.0, "value": segments[0]["value"]})
return bars
+23
View File
@@ -357,6 +357,29 @@ def test_fetch_treatments_range_retrieves_full_history_even_with_a_hidden_server
assert result["timestamp"].max().date() == end_date assert result["timestamp"].max().date() == end_date
def test_fetch_profile_returns_first_document(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
payload = [{"defaultProfile": "Default", "store": {"Default": {"dia": "5"}}}]
seen_params = []
monkeypatch.setattr(
ns.requests,
"get",
lambda *a, **k: (seen_params.append(k["params"]), FakeResponse(payload))[1],
)
result = ns.fetch_profile()
assert result == payload[0]
assert seen_params[0]["count"] == 1
def test_fetch_profile_handles_empty_response(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse([]))
assert ns.fetch_profile() == {}
def test_paginated_get_uses_created_at_field_for_treatments_cursor(monkeypatch): def test_paginated_get_uses_created_at_field_for_treatments_cursor(monkeypatch):
"""Losstaande, gerichte assertie op het daadwerkelijke queryparameter-veld """Losstaande, gerichte assertie op het daadwerkelijke queryparameter-veld
— moet `find[created_at][$lte]` zijn, nooit `find[date][$lte]`, voor het — moet `find[created_at][$lte]` zijn, nooit `find[date][$lte]`, voor het
+117
View File
@@ -0,0 +1,117 @@
import pytest
import profile_data as pd_
def _raw_profile(units="mg/dl", default_name="Default"):
return {
"defaultProfile": default_name,
"store": {
"Default": {
"units": units,
"dia": "5",
"basal": [
{"time": "00:00", "value": "0.8"},
{"time": "06:00", "value": "1.2"},
{"time": "22:00", "value": "0.7"},
],
"sens": [{"time": "00:00", "value": "50"}],
"carbratio": [{"time": "00:00", "value": "10"}],
"target_low": [{"time": "00:00", "value": "90"}],
"target_high": [{"time": "00:00", "value": "120"}],
}
},
}
def test_parse_profile_returns_empty_dict_for_empty_input():
assert pd_.parse_profile({}) == {}
def test_parse_profile_converts_mgdl_to_mmol():
profile = pd_.parse_profile(_raw_profile(units="mg/dl"))
assert profile["units"] == "mmol"
# 90 mg/dl -> ~5.0 mmol/L, 120 mg/dl -> ~6.7 mmol/L, 50 mg/dl (ISF) -> ~2.8 mmol/L
assert profile["target_low"][0]["value"] == pytest.approx(5.0, abs=0.05)
assert profile["target_high"][0]["value"] == pytest.approx(6.7, abs=0.05)
assert profile["sens"][0]["value"] == pytest.approx(2.8, abs=0.05)
def test_parse_profile_leaves_mmol_profiles_unconverted():
profile = pd_.parse_profile(_raw_profile(units="mmol"))
assert profile["target_low"][0]["value"] == pytest.approx(90.0)
def test_parse_profile_does_not_convert_basal_or_carbratio():
profile = pd_.parse_profile(_raw_profile(units="mg/dl"))
assert profile["basal"][0]["value"] == pytest.approx(0.8)
assert profile["carbratio"][0]["value"] == pytest.approx(10.0)
def test_parse_profile_sorts_basal_segments_by_time():
raw = _raw_profile()
raw["store"]["Default"]["basal"] = [
{"time": "22:00", "value": "0.7"},
{"time": "00:00", "value": "0.8"},
{"time": "06:00", "value": "1.2"},
]
profile = pd_.parse_profile(raw)
assert [seg["minutes"] for seg in profile["basal"]] == [0, 360, 1320]
def test_parse_profile_falls_back_to_first_profile_when_default_name_missing():
raw = _raw_profile(default_name="DoesNotExist")
profile = pd_.parse_profile(raw)
assert profile["dia"] == pytest.approx(5.0)
def test_parse_profile_dia_and_name():
profile = pd_.parse_profile(_raw_profile())
assert profile["dia"] == pytest.approx(5.0)
assert profile["name"] == "Default"
def test_value_at_returns_active_segment():
segments = [{"minutes": 0, "value": 0.8}, {"minutes": 360, "value": 1.2}, {"minutes": 1320, "value": 0.7}]
assert pd_.value_at(segments, 100) == pytest.approx(0.8)
assert pd_.value_at(segments, 360) == pytest.approx(1.2)
assert pd_.value_at(segments, 1000) == pytest.approx(1.2)
assert pd_.value_at(segments, 1350) == pytest.approx(0.7)
def test_value_at_wraps_to_last_segment_before_first():
segments = [{"minutes": 360, "value": 1.2}, {"minutes": 1320, "value": 0.7}]
assert pd_.value_at(segments, 100) == pytest.approx(0.7)
def test_value_at_empty_segments_returns_none():
assert pd_.value_at([], 100) is None
def test_segments_table_computes_end_times_from_next_segment():
segments = [{"minutes": 0, "value": 0.8}, {"minutes": 360, "value": 1.2}]
rows = pd_.segments_table(segments)
assert rows == [
{"start": "00:00", "end": "06:00", "value": 0.8},
{"start": "06:00", "end": "24:00", "value": 1.2},
]
def test_segments_to_hourly_bars_closes_the_24h_loop():
segments = [{"minutes": 0, "value": 0.8}, {"minutes": 360, "value": 1.2}]
bars = pd_.segments_to_hourly_bars(segments)
assert bars[-1] == {"hour": 24.0, "value": 0.8}
def test_segments_to_hourly_bars_empty_input():
assert pd_.segments_to_hourly_bars([]) == []
def test_total_daily_basal_weights_by_segment_duration():
# 6h @ 0.8 + 18h @ 1.2 = 4.8 + 21.6 = 26.4
segments = [{"minutes": 0, "value": 0.8}, {"minutes": 360, "value": 1.2}]
assert pd_.total_daily_basal(segments) == pytest.approx(26.4)
def test_total_daily_basal_empty_input():
assert pd_.total_daily_basal([]) == 0.0
+21 -1
View File
@@ -14,7 +14,8 @@ import pandas as pd
import plotly.graph_objects as go import plotly.graph_objects as go
import streamlit as st import streamlit as st
from nightscout_client import fetch_entries_range, fetch_treatments_range from nightscout_client import fetch_entries_range, fetch_profile, fetch_treatments_range
from profile_data import parse_profile
from reporting import ( from reporting import (
HIGH, HIGH,
LOW, LOW,
@@ -57,6 +58,11 @@ PALETTE = {
"tir_in_range": "#0ca30c", "tir_in_range": "#0ca30c",
"tir_high": "#fab219", "tir_high": "#fab219",
"tir_very_high": "#c76f0a", "tir_very_high": "#c76f0a",
"basal": "#7b5ea3",
"bolus_food": "#1baf7a",
"bolus_correction": "#2a78d6",
"bolus_smb": "#898781",
"target": "#52514e",
}, },
"dark": { "dark": {
"glucose": "#3987e5", "glucose": "#3987e5",
@@ -76,6 +82,11 @@ PALETTE = {
"tir_in_range": "#0ca30c", "tir_in_range": "#0ca30c",
"tir_high": "#fab219", "tir_high": "#fab219",
"tir_very_high": "#d9852e", "tir_very_high": "#d9852e",
"basal": "#9b7ec2",
"bolus_food": "#199e70",
"bolus_correction": "#3987e5",
"bolus_smb": "#898781",
"target": "#c3c2b7",
}, },
} }
@@ -129,6 +140,15 @@ def load_range_data(start: date, end: date):
return glucose, treatments return glucose, treatments
@st.cache_data(ttl=21600, show_spinner="Profiel ophalen van Nightscout...")
def load_profile() -> dict:
"""Gecachete profiel-fetch. Langere TTL dan load_range_data (6 uur i.p.v.
1 uur): een pompprofiel wijzigt zelden, in tegenstelling tot glucose-/
behandeldata van een lopende periode."""
raw = fetch_profile()
return parse_profile(raw)
def add_threshold_lines(fig: go.Figure, colors: dict, row: int | None = None) -> None: def add_threshold_lines(fig: go.Figure, colors: dict, row: int | None = None) -> None:
"""Teken de 4 klinische drempellijnen (3.0/3.9/10.0/13.9 mmol/L) op een grafiek. """Teken de 4 klinische drempellijnen (3.0/3.9/10.0/13.9 mmol/L) op een grafiek.