3 Commits
Author SHA1 Message Date
willemjan f7a0b94a54 lint fix
CI/CD / test (push) Successful in 24s
CI/CD / sonarqube (push) Successful in 21s
CI/CD / build-and-scan (push) Successful in 52s
CI/CD / deploy (push) Successful in 3s
willemjan/glucose-dashboard/pipeline/head This commit looks good
2026-09-06 14:44:50 +02:00
willemjan 95b3aff310 Updated charts for glucose data and profile chart
CI/CD / test (push) Failing after 6s
CI/CD / sonarqube (push) Skipped
CI/CD / build-and-scan (push) Skipped
CI/CD / deploy (push) Skipped
2026-09-06 14:43:19 +02:00
willemjan bd66d3dacd Fix CI: downgrade artifact actions to v3 for Gitea Actions compatibility
CI/CD / test (push) Successful in 41s
CI/CD / sonarqube (push) Successful in 34s
CI/CD / build-and-scan (push) Successful in 1m56s
CI/CD / deploy (push) Successful in 17s
upload-artifact@v4/download-artifact@v4 use the newer Artifact API v2,
which Gitea's act_runner doesn't support yet (GHESNotSupportedError).
v3 uses the older, Gitea-compatible API.
2026-09-05 23:03:16 +02:00
7 changed files with 270 additions and 122 deletions
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
# zijn zonder een draaiende Streamlit-sessie.
run: pytest tests/ -v --cov=. --cov-report=term-missing --cov-report=xml
- name: Bewaar coverage-rapport
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: coverage.xml
@@ -43,7 +43,7 @@ jobs:
with:
fetch-depth: 0 # SonarQube wil de volledige git-historie zien
- name: Download coverage-rapport
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: coverage-report
- name: SonarQube scan
+27
View File
@@ -254,6 +254,33 @@ def fetch_treatments_range(start_date: date, end_date: date) -> pd.DataFrame:
return _treatments_since(since, until)
def fetch_entries_for_day(day: date) -> pd.DataFrame:
"""Haal glucosemetingen op voor precies één kalenderdag.
Dunne wrapper om fetch_entries_range() met start=end=day, voor de
dag-navigatie op de Dagoverzicht-pagina: laadt alleen de geselecteerde
dag in plaats van een hele periode, zodat het wisselen tussen dagen snel
blijft.
"""
return fetch_entries_range(day, day)
def fetch_treatments_for_day(day: date, lookback_hours: int = 6) -> pd.DataFrame:
"""Haal behandelingen op voor één kalenderdag, met een lookback-marge vóór middernacht.
reporting.effective_basal_series() heeft de starttijd van een Temp Basal
nodig om de basaalsnelheid vroeg in de dag correct te berekenen — ook als
die temp basal al vóór middernacht is gestart en doorloopt. Zonder deze
marge zou de eerste uren van elke los opgevraagde dag ten onrechte het
profieltempo tonen in plaats van een nog actieve temp basal. 6 uur ligt
ruim boven de gebruikelijke temp-basal-duur in gesloten-lus-modus
(doorgaans 30-60 minuten).
"""
since = datetime.combine(day, time.min, tzinfo=timezone.utc) - timedelta(hours=lookback_hours)
until = datetime.combine(day, time.max, tzinfo=timezone.utc)
return _treatments_since(since, until)
def fetch_profile() -> dict:
"""Haal het meest recent gebruikte pompprofiel op.
+132 -100
View File
@@ -1,3 +1,5 @@
from datetime import timedelta
import pandas as pd
import plotly.graph_objects as go
import requests
@@ -7,16 +9,23 @@ from plotly.subplots import make_subplots
from auth import require_auth
from data_processing import TARGET_HIGH, TARGET_LOW, classify_treatments
from nightscout_client import NightscoutConfigError
from reporting import classify_bolus, effective_basal_series, target_glucose_series
from reporting import (
BOLUS_CATEGORY_SMB,
classify_bolus,
daily_stats,
effective_basal_series,
target_glucose_series,
)
from ui_common import (
MMOL_HOVERTEMPLATE,
add_basal_area_trace,
add_bolus_marker_traces,
add_carb_bars_trace,
add_target_line_trace,
add_threshold_lines,
get_colors,
load_day_data,
load_profile,
load_range_data,
render_completeness_warning,
select_period_days,
)
@@ -25,17 +34,48 @@ st.set_page_config(page_title="Glucose dashboard — Dagoverzicht", page_icon="
require_auth()
st.title("Dagoverzicht")
st.caption("Eén glucosegrafiek per kalenderdag, met basaal/bolus/koolhydraten-overlay (FR4).")
st.caption(
"Eén dag per keer: boven glucose met koolhydraten/bolus-overlay, onder basaal "
"en SMB — apart gehouden omdat één gecombineerde grafiek te veel informatie bevatte."
)
start, end, days = select_period_days(default_label="14 dagen")
if days > 31:
st.info(
"Bij langere periodes wordt de pagina lang en kan laden traag zijn — "
"voor het bekijken van individuele dagen is 14 of 30 dagen prettiger."
# De periode-knop bepaalt alleen het navigeerbare venster (hoe ver terug je kunt
# bladeren), niet welke data wordt opgehaald — dat gebeurt hieronder voor precies
# de geselecteerde dag (load_day_data), zodat het wisselen tussen dagen snel blijft.
window_start, window_end, days = select_period_days(default_label="14 dagen")
state_key = "dagoverzicht_selected_day"
if state_key not in st.session_state:
st.session_state[state_key] = window_end
if not (window_start <= st.session_state[state_key] <= window_end):
st.session_state[state_key] = window_end
selected_day = st.session_state[state_key]
col_prev, col_date, col_next = st.columns([1, 3, 1])
with col_prev:
if st.button("← Vorige dag", disabled=selected_day <= window_start, width="stretch"):
selected_day = selected_day - timedelta(days=1)
st.session_state[state_key] = selected_day
with col_next:
if st.button("Volgende dag →", disabled=selected_day >= window_end, width="stretch"):
selected_day = selected_day + timedelta(days=1)
st.session_state[state_key] = selected_day
with col_date:
date_options = [window_end - timedelta(days=i) for i in range(days)]
chosen = st.selectbox(
"Datum",
options=date_options,
index=date_options.index(selected_day),
format_func=lambda d: d.strftime("%A %d-%m-%Y"),
label_visibility="collapsed",
)
if chosen != selected_day:
selected_day = chosen
st.session_state[state_key] = selected_day
try:
glucose, treatments = load_range_data(start, end)
glucose, treatments = load_day_data(selected_day)
profile = load_profile()
except NightscoutConfigError as exc:
st.error(str(exc))
@@ -45,132 +85,124 @@ except (requests.RequestException, ValueError) as exc:
st.stop()
if glucose.empty:
st.info("Geen glucosemetingen gevonden in deze periode.")
st.info("Geen glucosemetingen gevonden op deze dag.")
st.stop()
render_completeness_warning(glucose, start, end)
render_completeness_warning(glucose, selected_day, selected_day)
colors = get_colors()
glucose = glucose.copy()
glucose["date"] = glucose["timestamp"].dt.date
glucose["hour"] = glucose["timestamp"].dt.hour + glucose["timestamp"].dt.minute / 60
day_start = pd.Timestamp(selected_day)
day_end = day_start + pd.Timedelta(1, unit="D")
carbs_events = classify_treatments(treatments)["carbs"].copy()
if not carbs_events.empty:
carbs_events["date"] = carbs_events["timestamp"].dt.date
carbs_events["hour"] = carbs_events["timestamp"].dt.hour + carbs_events["timestamp"].dt.minute / 60
# treatments bevat een lookback-marge vóór middernacht (zie
# nightscout_client.fetch_treatments_for_day) zodat een temp basal die al
# vóór deze dag begon toch meetelt — effective_basal_series() clipt dat
# zelf aan [day_start, day_end). Voor de koolhydraten/bolus-overlays willen
# we die marge juist NIET, anders duiken gisteravond se stippen op vandaag op.
day_treatments = treatments[(treatments["timestamp"] >= day_start) & (treatments["timestamp"] < day_end)]
bolus_events = classify_bolus(treatments)
if not bolus_events.empty:
bolus_events["date"] = bolus_events["timestamp"].dt.date
bolus_events["hour"] = bolus_events["timestamp"].dt.hour + bolus_events["timestamp"].dt.minute / 60
carbs_day = classify_treatments(day_treatments)["carbs"]
bolus_day = classify_bolus(day_treatments)
basal_day = effective_basal_series(profile, treatments, day_start, day_end)
target_day = target_glucose_series(profile, day_start, day_end)
def _close_step_to_day_end(df: pd.DataFrame, value_col: str) -> pd.DataFrame:
"""Voeg een laatste punt op hour=24 toe (zelfde waarde als het laatste
"""Voeg een laatste punt op day_end toe (zelfde waarde als het laatste
segment) zodat een stapvlak/-lijn de volle dag doortekent i.p.v. te
stoppen bij het laatste breekpunt."""
if df.empty:
return df
last_value = df.iloc[-1][value_col]
closing_row = pd.DataFrame([{"hour": 24.0, value_col: last_value}])
closing_row = pd.DataFrame([{"timestamp": day_end, value_col: last_value}])
return pd.concat([df, closing_row], ignore_index=True)
dates = sorted(glucose["date"].unique())
row_height = 190
fig = make_subplots(
rows=len(dates),
cols=1,
shared_xaxes=True,
vertical_spacing=min(0.4 / max(len(dates) - 1, 1), 0.05),
specs=[[{"secondary_y": True}]] * len(dates),
)
basal_day = _close_step_to_day_end(basal_day, "rate")
target_day = _close_step_to_day_end(target_day, "target")
for i, day in enumerate(dates, start=1):
day_df = glucose[glucose["date"] == day]
day_start = pd.Timestamp(day)
day_end = day_start + pd.Timedelta(1, unit="D")
def build_day_figure(glucose_day, carbs_day, bolus_day, basal_day, target_day, colors) -> go.Figure:
fig = make_subplots(
rows=2,
cols=1,
shared_xaxes=True,
row_heights=[0.68, 0.32],
vertical_spacing=0.08,
specs=[[{"secondary_y": True}], [{"secondary_y": False}]],
)
# Rij 1: glucose + streefwaarde + koolhydraten + voeding-/correctiebolus.
fig.add_hrect(
y0=TARGET_LOW, y1=TARGET_HIGH, fillcolor=colors["band"], line_width=0, layer="below", row=i, col=1
)
# Handmatige titel-annotatie t.o.v. het domein van déze rij i.p.v.
# make_subplots' subplot_titles (die t.o.v. de hele figuur positioneert en
# daardoor bij veel dagen de gedeelde legenda bovenaan kan overlappen).
fig.add_annotation(
text=day.strftime("%A %d-%m-%Y"),
xref="x domain",
yref="y domain",
x=0.0,
y=1.15,
xanchor="left",
yanchor="bottom",
showarrow=False,
font={"size": 13, "color": colors["text"]},
row=i,
col=1,
y0=TARGET_LOW, y1=TARGET_HIGH, fillcolor=colors["band"], line_width=0, layer="below", row=1, col=1
)
add_target_line_trace(fig, target_day, colors, row=1)
add_carb_bars_trace(fig, carbs_day, colors, row=1, secondary_y=True)
basal_day = effective_basal_series(profile, treatments, day_start, day_end)
if not basal_day.empty:
basal_day["hour"] = (basal_day["timestamp"] - day_start).dt.total_seconds() / 3600
basal_day = _close_step_to_day_end(basal_day, "rate")
add_basal_area_trace(fig, basal_day, colors, row=i, secondary_y=True, x_col="hour")
target_day = target_glucose_series(profile, day_start, day_end)
if not target_day.empty:
target_day["hour"] = (target_day["timestamp"] - day_start).dt.total_seconds() / 3600
target_day = _close_step_to_day_end(target_day, "target")
add_target_line_trace(fig, target_day, colors, row=i, x_col="hour")
add_carb_bars_trace(fig, carbs_events[carbs_events["date"] == day], colors, row=i, secondary_y=True, x_col="hour")
lane_y = day_df["mmol"].min() - 1.0 if not day_df.empty else TARGET_LOW - 1.0
add_bolus_marker_traces(fig, bolus_events[bolus_events["date"] == day], colors, lane_y, row=i, x_col="hour")
top_bolus = bolus_day[bolus_day["category"] != BOLUS_CATEGORY_SMB] if not bolus_day.empty else bolus_day
lane_y = glucose_day["mmol"].min() - 1.0 if not glucose_day.empty else TARGET_LOW - 1.0
add_bolus_marker_traces(fig, top_bolus, colors, lane_y, row=1)
fig.add_trace(
go.Scatter(
x=day_df["hour"],
y=day_df["mmol"],
x=glucose_day["timestamp"],
y=glucose_day["mmol"],
mode="lines",
line={"color": colors["glucose"], "width": 2},
name="Glucose",
hovertemplate="%{y:.1f} mmol/L<extra></extra>",
hovertemplate=MMOL_HOVERTEMPLATE,
),
row=i,
row=1,
col=1,
secondary_y=False,
)
fig.update_yaxes(title_text="mmol/L", gridcolor=colors["grid"], row=i, col=1, secondary_y=False)
add_threshold_lines(fig, colors, row=1)
fig.update_yaxes(title_text="mmol/L", gridcolor=colors["grid"], row=1, col=1, secondary_y=False)
fig.update_yaxes(title_text="Koolhydraten (g)", showgrid=False, row=1, col=1, secondary_y=True)
# Rij 2: basaal + SMB — apart van rij 1 zodat de glucosegrafiek niet
# overladen raakt met insulinedetails.
add_basal_area_trace(fig, basal_day, colors, row=2)
smb_bolus = bolus_day[bolus_day["category"] == BOLUS_CATEGORY_SMB] if not bolus_day.empty else bolus_day
smb_lane_y = (basal_day["rate"].max() * 1.15) if not basal_day.empty else 1.0
add_bolus_marker_traces(fig, smb_bolus, colors, smb_lane_y, row=2)
fig.update_yaxes(
title_text="Basaal (E/uur) · KH (g)", showgrid=False, row=i, col=1, secondary_y=True
title_text="Basaal (E/uur)", gridcolor=colors["grid"], rangemode="tozero", row=2, col=1
)
add_threshold_lines(fig, colors, row=i)
# Elke dag herhaalt dezelfde spoornamen (Glucose/Basaal/Koolhydraten/...) — zonder
# deduplicatie zou de legenda 7x "Glucose" etc. tonen. Alleen de eerste keer per
# naam blijft in de legenda staan.
seen_names = set()
for trace in fig.data:
if trace.name in seen_names:
trace.showlegend = False
else:
seen_names.add(trace.name)
fig.update_xaxes(range=[day_start, day_end], dtick=4 * 3600 * 1000, gridcolor=colors["grid"])
fig.update_layout(
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color=colors["text"],
height=560,
showlegend=True,
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "left", "x": 0},
margin={"l": 10, "r": 10, "t": 40, "b": 10},
)
return fig
fig.update_xaxes(title_text="Uur van de dag", range=[0, 24], dtick=4, gridcolor=colors["grid"])
fig.update_layout(
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color=colors["text"],
height=row_height * len(dates) + 40,
showlegend=True,
legend={"orientation": "h", "yanchor": "bottom", "y": 1.0, "xanchor": "left", "x": 0},
margin={"l": 10, "r": 10, "t": 40, "b": 10},
)
fig = build_day_figure(glucose, carbs_day, bolus_day, basal_day, target_day, colors)
st.plotly_chart(fig, width="stretch")
stats = daily_stats(glucose, day_treatments, profile, day_start, day_end)
col1, col2, col3, col4 = st.columns(4)
col1.metric(
"Glucose (gem./min/max)",
f"{stats['mean_mmol']:.1f} mmol/L",
delta=f"{stats['min_mmol']:.1f} - {stats['max_mmol']:.1f}",
delta_color="off",
)
col2.metric("Totale insuline", f"{stats['total_insulin']:.1f} E")
col3.metric("Basaal / bolus / SMB", f"{stats['basal_insulin']:.1f} / {stats['bolus_insulin']:.1f} / {stats['smb_insulin']:.1f} E")
col4.metric(
"Hypo's / hyper's",
f"{stats['hypo_count']} / {stats['hyper_count']}",
delta=f"{stats['hypo_duration_minutes']:.0f} / {stats['hyper_duration_minutes']:.0f} min",
delta_color="off",
)
st.caption(
f"{len(dates)} dagen · {len(glucose)} metingen · stippen = bolusinsuline "
"(voeding/correctie/SMB, grootte naar dosis) · vlak = basaal · staafjes = koolhydraten · "
"streepstippellijn = streefwaarde uit pompprofiel"
"Boven: stippen = bolusinsuline (voeding/correctie, grootte naar dosis) · staafjes = koolhydraten · "
"streepstippellijn = streefwaarde uit pompprofiel. Onder: vlak = basaal · stippen = SMB."
)
+3 -3
View File
@@ -6,7 +6,7 @@ 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
from ui_common import get_colors, hex_to_rgba, load_profile
st.set_page_config(page_title="Glucose dashboard — Insulineprofiel", page_icon="📊", layout="wide")
require_auth()
@@ -42,8 +42,8 @@ fig = go.Figure(
mode="lines",
line={"shape": "hv", "color": colors["basal"], "width": 2},
fill="tozeroy",
fillcolor=colors["basal"],
opacity=0.7,
fillcolor=hex_to_rgba(colors["basal"], 0.3),
opacity=0.8,
name="Basaal",
hovertemplate="%{x:.0f}:00 · %{y:.2f} E/uur<extra></extra>",
)
+36 -16
View File
@@ -230,7 +230,7 @@ def _insulin_profile_figure_png(profile: dict) -> bytes:
if bars:
hours = [b["hour"] for b in bars]
values = [b["value"] for b in bars]
ax.step(hours, values, where="post", color=_BASAL_COLOR, linewidth=1.5)
ax.step(hours, values, where="post", color=_BASAL_COLOR, linewidth=1.5, alpha=0.8)
ax.fill_between(hours, values, step="post", color=_BASAL_COLOR, alpha=0.3)
ax.set_xlim(0, 24)
ax.set_xticks(range(0, 25, 2))
@@ -261,9 +261,8 @@ def _add_carbs_overlay(ax2, day_treatments: pd.DataFrame, day_start) -> None:
ax2.bar(hours, carbs["carbs"], width=0.25, color=_CARBS_COLOR, alpha=0.55, label="Koolhydraten")
def _add_bolus_overlay(ax, day_treatments: pd.DataFrame, day_start, lane_y: float) -> None:
bolus = classify_bolus(day_treatments)
for category in (BOLUS_CATEGORY_SMB, BOLUS_CATEGORY_CORRECTION, BOLUS_CATEGORY_FOOD):
def _add_bolus_overlay(ax, bolus: pd.DataFrame, day_start, lane_y: float, categories: tuple[str, ...]) -> None:
for category in categories:
subset = bolus[bolus["category"] == category] if not bolus.empty else bolus
if subset.empty:
continue
@@ -291,35 +290,56 @@ def _add_target_overlay(ax, profile: dict | None, day_start, day_end) -> None:
def _daily_figure_png(
day_df: pd.DataFrame, day_treatments: pd.DataFrame, profile: dict | None, day_label: str, day_start, day_end
) -> bytes:
"""Eén dag als grafiek: glucoselijn + basaal/bolus/koolhydraten/streefwaarde-overlay."""
"""Eén dag als twee gestapelde grafieken (gedeelde tijdas, één gedeelde legenda):
boven glucose + streefwaarde + koolhydraten + voeding/correctiebolus, onder
basaal + SMB — gescheiden omdat één gecombineerde grafiek te veel informatie
bevatte om nog leesbaar te zijn."""
hours = day_df["timestamp"].dt.hour + day_df["timestamp"].dt.minute / 60
fig, ax = plt.subplots(figsize=(11, 3.8))
fig, (ax, ax_basal) = plt.subplots(2, 1, figsize=(11, 5.2), sharex=True, height_ratios=[2.2, 1])
ax2 = ax.twinx()
basal = effective_basal_series(profile, day_treatments, day_start, day_end)
_add_basal_overlay(ax2, basal, day_start)
bolus = classify_bolus(day_treatments)
_add_carbs_overlay(ax2, day_treatments, day_start)
lane_y = (day_df["mmol"].min() - 1.0) if not day_df.empty else LOW - 1.0
_add_bolus_overlay(ax, day_treatments, day_start, lane_y)
_add_bolus_overlay(ax, bolus, day_start, lane_y, categories=(BOLUS_CATEGORY_CORRECTION, BOLUS_CATEGORY_FOOD))
_add_target_overlay(ax, profile, day_start, day_end)
ax.plot(hours, day_df["mmol"], color="#2a78d6", linewidth=1.4, label="Glucose", zorder=6)
_add_threshold_lines(ax)
ax.set_xlim(0, 24)
ax.set_xticks(range(0, 25, 2))
ax.set_ylabel("mmol/L", fontsize=9)
ax2.set_ylabel("Basaal (E/uur) · KH (g)", fontsize=9)
ax2.set_ylabel("Koolhydraten (g)", fontsize=9)
ax.set_title(day_label, fontsize=11, loc="left")
ax.tick_params(labelsize=8)
ax2.tick_params(labelsize=8)
basal = effective_basal_series(profile, day_treatments, day_start, day_end)
_add_basal_overlay(ax_basal, basal, day_start)
smb_lane_y = (basal["rate"].max() * 1.15) if not basal.empty else 1.0
_add_bolus_overlay(ax_basal, bolus, day_start, smb_lane_y, categories=(BOLUS_CATEGORY_SMB,))
ax_basal.set_xlim(0, 24)
ax_basal.set_xticks(range(0, 25, 2))
ax_basal.set_xlabel(_HOUR_OF_DAY_LABEL)
ax_basal.set_ylabel("Basaal (E/uur)", fontsize=9)
ax_basal.tick_params(labelsize=8)
lines1, labels1 = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax.legend(
lines1 + lines2, labels1 + labels2, loc="upper center", bbox_to_anchor=(0.5, -0.2), ncol=6, fontsize=7, frameon=False
lines3, labels3 = ax_basal.get_legend_handles_labels()
ax_basal.legend(
lines1 + lines2 + lines3,
labels1 + labels2 + labels3,
loc="upper center",
bbox_to_anchor=(0.5, -0.35),
ncol=6,
fontsize=7,
frameon=False,
)
fig.tight_layout()
# fig.tight_layout() introspecteert elke as se bounding box om marges te
# berekenen, en matplotlib ondersteunt dat niet betrouwbaar zodra er twee
# hoofd-assen (ax/ax_basal) plus een twinx (ax2) tegelijk in de figuur
# zitten (waarschuwing "Axes ... not compatible with tight_layout").
# subplots_adjust() met vaste marges omzeilt die introspectie.
fig.subplots_adjust(left=0.06, right=0.94, top=0.93, bottom=0.24, hspace=0.35)
return _fig_to_png(fig)
+44
View File
@@ -168,6 +168,50 @@ def test_fetch_treatments_range_filters_to_date_boundaries(monkeypatch):
assert result.iloc[0]["insulin"] == pytest.approx(4.0)
def test_fetch_entries_for_day_scopes_to_a_single_calendar_day(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
monkeypatch.delenv("NIGHTSCOUT_TOKEN", raising=False)
monkeypatch.delenv("NIGHTSCOUT_API_SECRET", raising=False)
payload = [
{"date": int(datetime(2026, 1, 15, 12, 0, tzinfo=timezone.utc).timestamp() * 1000), "sgv": 126},
{"date": int(datetime(2026, 1, 16, 12, 0, tzinfo=timezone.utc).timestamp() * 1000), "sgv": 100},
]
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse(payload))
result = ns.fetch_entries_for_day(date(2026, 1, 15))
assert len(result) == 1
assert result.iloc[0]["timestamp"].date() == date(2026, 1, 15)
def test_fetch_treatments_for_day_includes_lookback_before_midnight(monkeypatch):
"""Een Temp Basal die net vóór middernacht is gestart moet toch meekomen
(zie de docstring bij fetch_treatments_for_day) — zonder lookback-marge
zou effective_basal_series() de vroege ochtenduren fout berekenen."""
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
payload = [
{"created_at": "2026-01-14T22:00:00Z", "eventType": "Temp Basal", "absolute": 1.2, "duration": 480},
{"created_at": "2026-01-15T12:00:00Z", "eventType": "Meal Bolus", "insulin": 4.0, "carbs": 40},
{"created_at": "2026-01-10T08:00:00Z", "eventType": "Meal Bolus", "insulin": 3.0, "carbs": 30},
]
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse(payload))
result = ns.fetch_treatments_for_day(date(2026, 1, 15), lookback_hours=6)
assert len(result) == 2 # de 10 januari-bolus valt buiten de lookback-marge
assert set(result["eventType"]) == {"Temp Basal", "Meal Bolus"}
def test_fetch_treatments_for_day_excludes_treatments_before_the_lookback_window(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
payload = [
{"created_at": "2026-01-14T12:00:00Z", "eventType": "Meal Bolus", "insulin": 4.0, "carbs": 40},
]
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse(payload))
result = ns.fetch_treatments_for_day(date(2026, 1, 15), lookback_hours=6)
assert result.empty
def _entry(dt, sgv=100):
return {"date": int(dt.timestamp() * 1000), "sgv": sgv, "dateString": dt.isoformat()}
+26 -1
View File
@@ -14,7 +14,13 @@ import pandas as pd
import plotly.graph_objects as go
import streamlit as st
from nightscout_client import fetch_entries_range, fetch_profile, fetch_treatments_range
from nightscout_client import (
fetch_entries_for_day,
fetch_entries_range,
fetch_profile,
fetch_treatments_for_day,
fetch_treatments_range,
)
from profile_data import parse_profile
from reporting import (
HIGH,
@@ -148,6 +154,25 @@ def load_range_data(start: date, end: date): # pragma: no cover
return glucose, treatments
@st.cache_data(ttl=300, show_spinner="Data ophalen van Nightscout...")
def load_day_data(day: date): # pragma: no cover
"""Gecachete Nightscout-fetch voor precies één kalenderdag (Dagoverzicht-navigatie).
Kortere TTL dan load_range_data (5 min i.p.v. 1 uur): in tegenstelling tot
een afgeronde periode kan de geselecteerde dag "vandaag" zijn, waar
nieuwe metingen blijven binnenkomen. fetch_treatments_for_day() haalt
zelf een lookback-marge vóór middernacht op zodat effective_basal_series()
de vroege ochtenduren correct kan berekenen.
Dunne st.cache_data-wrapper rond al-geteste fetch_entries_for_day()/
fetch_treatments_for_day() — buiten scope van de unit-testdekking net als
pages/*.py, zie pyproject.toml se coverage-omit-lijst.
"""
glucose = fetch_entries_for_day(day)
treatments = fetch_treatments_for_day(day)
return glucose, treatments
@st.cache_data(ttl=21600, show_spinner="Profiel ophalen van Nightscout...")
def load_profile() -> dict: # pragma: no cover
"""Gecachete profiel-fetch. Langere TTL dan load_range_data (6 uur i.p.v.