Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## [1.10.3] — 2026-06-03 — Personalised strain, a daily signal card, deeper derived metrics

### Added

- **A "Today's signal" card.** The coincident-deviation read — which notices when several of your vitals sit off their personal baseline at once — now leads the Insights overview as a calm daily card instead of a flag buried in the grid. It shows an all-clear on a normal day, names the vitals to keep an eye on when a few drift, and always frames them as possible factors, never a cause or a diagnosis.
- **Derived bands for more of what your watch measures.** Overnight wrist temperature and stair-climbing and stair-descent pace each get a personal typical-range band, and your device's estimated six-minute-walk distance is placed against a published reference for your age, height, weight and sex. Each is shown with its method and its cited standard, and only once there is enough history. They appear under a new "Mobility & body" group.
- **Trailing trend sparklines** on the derived tiles, drawn from the readings the tile already uses.

### Changed

- **The Strain score is now anchored to your own training load.** It reads how hard a day was relative to your own recent training-day effort rather than a fixed population figure, so a genuinely hard day reads high even while you are building back up; with too little history it falls back to a general reference and says so.
- Age-banded reference norms now interpolate across bracket boundaries instead of stepping, and the sleep midpoint is computed in your own timezone.

## [1.10.2] — 2026-06-03 — Honest AI connection test, consistent insights, retention re-enabled

### Fixed
Expand Down
15 changes: 12 additions & 3 deletions docs/api/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.10.2
version: 1.10.3
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.

Expand Down Expand Up @@ -2466,6 +2466,10 @@ paths:
- RECOVERY_SCORE
- STRESS_SCORE
- STRAIN_SCORE
- WRIST_TEMPERATURE_BASELINE
- STAIR_ASCENT_SPEED_BASELINE
- STAIR_DESCENT_SPEED_BASELINE
- SIX_MINUTE_WALK_BAND
description: "Derived-metric id to compute (e.g. VITALS_BASELINE, FITNESS_AGE, VASCULAR_AGE_DELTA, HRV_BALANCE, BMI,
READINESS). Closed enum: an unknown id 422s. Metrics whose compute has not yet landed return an
`insufficient` value with reason `not_implemented`."
Expand Down Expand Up @@ -7769,6 +7773,10 @@ components:
- RECOVERY_SCORE
- STRESS_SCORE
- STRAIN_SCORE
- WRIST_TEMPERATURE_BASELINE
- STAIR_ASCENT_SPEED_BASELINE
- STAIR_DESCENT_SPEED_BASELINE
- SIX_MINUTE_WALK_BAND
description: Echoes the requested derived-metric id (tags the union).
status:
type: string
Expand All @@ -7784,8 +7792,9 @@ components:
type: string
additionalProperties: {}
- type: "null"
description: Metric-specific value object when status is 'ok' (e.g. { type, center, low, high, spread, sampleDays, k }
for VITALS_BASELINE); null when 'insufficient'.
description: Metric-specific value object when status is 'ok' (e.g. { type, center, low, high, spread, sampleDays, k,
series } for VITALS_BASELINE, where `series` is the trailing per-day mean values for the inline sparkline);
null when 'insufficient'.
coverage:
$ref: "#/components/schemas/DerivedCoverage"
confidence:
Expand Down
39 changes: 35 additions & 4 deletions messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2550,7 +2550,14 @@
"normal": "Normalbereich (WHO)",
"overweight": "Übergewicht (WHO)",
"obese": "Adipositas (WHO)"
}
},
"sixMinuteBand": {
"green": "{percent}% des für dein Profil vorhergesagten Werts",
"yellow": "{percent}% des für dein Profil vorhergesagten Werts",
"red": "{percent}% des für dein Profil vorhergesagten Werts"
},
"sixMinuteNoBand": "Ergänze Alter, Größe, Gewicht und Geschlecht, um dies einzuordnen",
"mobilitySectionTitle": "Mobilität & Körper"
},
"anatomy": {
"outOf": "/100",
Expand Down Expand Up @@ -2618,9 +2625,25 @@
"description": "Ein nächtlicher Stress-Proxy aus dem Tagesverlauf deiner HRV."
},
"STRAIN_SCORE": {
"method": "Ein nächtlicher 0–100-Belastungs-Proxy aus deiner herzfrequenzgewichteten Aktivitätslast (ein TRIMP-ähnliches Trainingsimpuls-Modell). Höher heißt ein anstrengenderer Tag.",
"method": "Ein nächtlicher 0–100-Belastungs-Proxy aus deiner herzfrequenzgewichteten Aktivitätslast (ein TRIMP-ähnliches Trainingsimpuls-Modell). Höher heißt ein anstrengenderer Tag. Die Skala ist an deine eigene jüngste Trainingstagslast angelehnt — relativ zu deiner typischen Anstrengung —, sodass ein für dich wirklich harter Tag hoch ausfällt. Solange du noch Verlauf aufbaust, wird stattdessen ein allgemeiner Referenzwert verwendet.",
"caveat": "Ein beschreibender Aktivitätslast-Proxy, kein klinisches oder trainingsgerechtes Lastmaß.",
"description": "Ein nächtlicher Aktivitätslast-Proxy aus deiner herzfrequenzgewichteten Anstrengung."
"description": "Ein nächtlicher Aktivitätslast-Proxy aus deiner herzfrequenzgewichteten Anstrengung, relativ zu deiner typischen Anstrengung."
},
"WRIST_TEMPERATURE_BASELINE": {
"method": "Deine typische nächtliche Handgelenkstemperatur ist der Median deiner nächtlichen Messwerte im Zeitraum, ± einer robusten Streuung (skalierte mittlere absolute Abweichung). Sie folgt DEINEN Werten — letzte Nacht wird als Abweichung von deiner eigenen Baseline gezeigt, nicht von einer Bevölkerungsnorm.",
"caveat": "Nur ein persönliches Abweichungsband — beschreibend, kein Anzeichen für Krankheit, Fieber oder Zyklusphase. Du interpretierst es selbst."
},
"STAIR_ASCENT_SPEED_BASELINE": {
"method": "Dein typisches Treppensteig-Tempo ist der Median deiner Tageswerte im Zeitraum, ± einer robusten Streuung (skalierte mittlere absolute Abweichung). Das Band wird aus deinen Daten gebildet — es gibt keinen Bevölkerungs-Grenzwert für Treppentempo, da es von Treppengeometrie und Beinlänge abhängt.",
"caveat": "Ein persönliches Mobilitäts-Trendband — beschreibend, keine Gebrechlichkeits- oder Sturzrisiko-Bewertung."
},
"STAIR_DESCENT_SPEED_BASELINE": {
"method": "Dein typisches Treppen-Abstiegstempo ist der Median deiner Tageswerte im Zeitraum, ± einer robusten Streuung (skalierte mittlere absolute Abweichung). Das Band wird aus deinen Daten gebildet — es gibt keinen Bevölkerungs-Grenzwert für Treppentempo, da es von Treppengeometrie und Beinlänge abhängt.",
"caveat": "Ein persönliches Mobilitäts-Trendband — beschreibend, keine Gebrechlichkeits- oder Sturzrisiko-Bewertung."
},
"SIX_MINUTE_WALK_BAND": {
"method": "Die von deinem Gerät GESCHÄTZTE 6-Minuten-Gehstrecke, eingeordnet gegen die Referenz von Enright & Sherrill für Alter, Größe, Gewicht und Geschlecht, als Prozent des Vorhersagewerts. Die Strecke ist die Schätzung des Geräts — hier nie neu berechnet. Ohne Alter, Größe, Gewicht und Geschlecht wird der Prozentwert ausgeblendet und nur Strecke und Trend gezeigt.",
"caveat": "Eine gegen eine veröffentlichte Referenz neu eingeordnete Schätzung — ein Hinweis zur funktionellen Kapazität, kein klinischer 6-Minuten-Gehtest und keine Diagnose."
}
},
"scores": {
Expand All @@ -2632,7 +2655,15 @@
"coincident": {
"label": "Mehrere Vitalwerte außerhalb der Baseline",
"summary": "{count} deiner Vitalwerte lagen heute außerhalb ihres persönlichen Bereichs.",
"vitals": "Außerhalb des Bereichs: {list}"
"vitals": "Außerhalb des Bereichs: {list}",
"cardTitle": "Signal des Tages",
"allClear": "Alle deine Vitalwerte liegen heute in ihrem persönlichen Bereich.",
"allClearMeta": "{count} deiner Vitalwerte gegen deinen eigenen typischen Bereich geprüft.",
"watch": "Ein Vitalwert liegt heute außerhalb seines üblichen Bereichs.",
"watchVital": "Außerhalb des Bereichs: {vital}. Oft nur einmalig — behalte es in den nächsten Tagen im Blick.",
"firedHeadline": "{count} deiner Vitalwerte liegen heute außerhalb ihres persönlichen Bereichs.",
"factors": "Mögliche Faktoren — nie eine Ursache: ein hartes Training, schlechter Schlaf, Alkohol, Höhe, Stress oder Krankheit. Das ist ein Hinweis aus deinen eigenen Baselines, keine Diagnose.",
"building": "Deine persönlichen Baselines entstehen — erfasse ein paar Tage mehr, dann erscheint dies."
}
},
"cardioFitness": {
Expand Down
39 changes: 35 additions & 4 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2550,7 +2550,14 @@
"normal": "Normal range (WHO)",
"overweight": "Overweight (WHO)",
"obese": "Obese (WHO)"
}
},
"sixMinuteBand": {
"green": "{percent}% of predicted for your profile",
"yellow": "{percent}% of predicted for your profile",
"red": "{percent}% of predicted for your profile"
},
"sixMinuteNoBand": "Add your age, height, weight and sex to frame this",
"mobilitySectionTitle": "Mobility & body"
},
"anatomy": {
"outOf": "/100",
Expand Down Expand Up @@ -2618,9 +2625,25 @@
"description": "A nightly stress proxy from the shape of your intra-day HRV."
},
"STRAIN_SCORE": {
"method": "A nightly 0–100 strain proxy from your heart-rate-weighted activity load (a TRIMP-style training-impulse model). Higher means a harder day.",
"method": "A nightly 0–100 strain proxy from your heart-rate-weighted activity load (a TRIMP-style training-impulse model). Higher means a harder day. The scale is anchored to your own recent training-day load — relative to your typical effort — so a genuinely hard day for you reads high. While you are still building history it uses a general reference instead.",
"caveat": "A descriptive activity-load proxy, not a clinical or training-grade load measure.",
"description": "A nightly activity-load proxy from your heart-rate-weighted effort."
"description": "A nightly activity-load proxy from your heart-rate-weighted effort, relative to your typical effort."
},
"WRIST_TEMPERATURE_BASELINE": {
"method": "Your typical overnight wrist temperature is the median of your nightly readings over the window, ± a robust spread (median absolute deviation, scaled). It tracks YOUR readings — last night is shown as a deviation from your own baseline, not a population norm.",
"caveat": "A personal-deviation band only — descriptive, not an illness, fever or cycle-phase signal. Interpret it yourself."
},
"STAIR_ASCENT_SPEED_BASELINE": {
"method": "Your typical stair-climbing pace is the median of your daily values over the window, ± a robust spread (median absolute deviation, scaled). The band is built from your data — there is no population stair-speed cut-off, because pace depends on stair geometry and leg length.",
"caveat": "A personal mobility-trend band — descriptive, not a frailty or fall-risk assessment."
},
"STAIR_DESCENT_SPEED_BASELINE": {
"method": "Your typical stair-descent pace is the median of your daily values over the window, ± a robust spread (median absolute deviation, scaled). The band is built from your data — there is no population stair-speed cut-off, because pace depends on stair geometry and leg length.",
"caveat": "A personal mobility-trend band — descriptive, not a frailty or fall-risk assessment."
},
"SIX_MINUTE_WALK_BAND": {
"method": "Your device's ESTIMATED six-minute-walk distance placed against the Enright & Sherrill reference for your age, height, weight and sex, as a percent of predicted. The distance is the device's estimate — never recomputed here. Without your age, height, weight and sex the percent is hidden and only the distance and trend are shown.",
"caveat": "An estimate re-framed against a published reference — a functional-capacity awareness signal, not a clinical 6-minute-walk test or a diagnosis."
}
},
"scores": {
Expand All @@ -2632,7 +2655,15 @@
"coincident": {
"label": "Several vitals off baseline",
"summary": "{count} of your vitals sat outside their personal range today.",
"vitals": "Outside range: {list}"
"vitals": "Outside range: {list}",
"cardTitle": "Today's signal",
"allClear": "All your vitals are within their personal range today.",
"allClearMeta": "Checked {count} of your vitals against your own typical range.",
"watch": "One vital is outside its usual range today.",
"watchVital": "Outside range: {vital}. Often just a one-off — worth a glance over the next few days.",
"firedHeadline": "{count} of your vitals are outside their personal range today.",
"factors": "Possible factors — never a cause: a hard workout, poor sleep, alcohol, altitude, stress, or illness. This is awareness from your own baselines, not a diagnosis.",
"building": "Building your personal baselines — track a few more days and this will appear."
}
},
"cardioFitness": {
Expand Down
39 changes: 35 additions & 4 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2550,7 +2550,14 @@
"normal": "Rango normal (OMS)",
"overweight": "Sobrepeso (OMS)",
"obese": "Obesidad (OMS)"
}
},
"sixMinuteBand": {
"green": "{percent}% de lo previsto para tu perfil",
"yellow": "{percent}% de lo previsto para tu perfil",
"red": "{percent}% de lo previsto para tu perfil"
},
"sixMinuteNoBand": "Añade tu edad, altura, peso y sexo para encuadrar esto",
"mobilitySectionTitle": "Movilidad y cuerpo"
},
"anatomy": {
"outOf": "/100",
Expand Down Expand Up @@ -2618,9 +2625,25 @@
"description": "Un indicador nocturno de estrés a partir de la forma de tu VFC durante el día."
},
"STRAIN_SCORE": {
"method": "Un indicador nocturno de esfuerzo de 0 a 100 a partir de tu carga de actividad ponderada por frecuencia cardíaca (un modelo de impulso de entrenamiento tipo TRIMP). Más alto significa un día más exigente.",
"method": "Un indicador nocturno de esfuerzo de 0 a 100 a partir de tu carga de actividad ponderada por frecuencia cardíaca (un modelo de impulso de entrenamiento tipo TRIMP). Más alto significa un día más exigente. La escala se ancla a tu propia carga reciente de días de entrenamiento —relativa a tu esfuerzo habitual—, de modo que un día realmente duro para ti aparece alto. Mientras aún estás acumulando historial usa una referencia general en su lugar.",
"caveat": "Un indicador descriptivo de carga de actividad, no una medida de carga clínica ni de nivel de entrenamiento.",
"description": "Un indicador nocturno de carga de actividad a partir de tu esfuerzo ponderado por frecuencia cardíaca."
"description": "Un indicador nocturno de carga de actividad a partir de tu esfuerzo ponderado por frecuencia cardíaca, relativo a tu esfuerzo habitual."
},
"WRIST_TEMPERATURE_BASELINE": {
"method": "Tu temperatura típica de muñeca durante la noche es la mediana de tus lecturas nocturnas en el periodo, ± una dispersión robusta (desviación absoluta de la mediana, escalada). Sigue TUS lecturas: la noche pasada se muestra como una desviación respecto a tu propia base, no a una norma poblacional.",
"caveat": "Solo una banda de desviación personal: descriptiva, no una señal de enfermedad, fiebre ni fase del ciclo. Tú la interpretas."
},
"STAIR_ASCENT_SPEED_BASELINE": {
"method": "Tu ritmo típico al subir escaleras es la mediana de tus valores diarios en el periodo, ± una dispersión robusta (desviación absoluta de la mediana, escalada). La banda se construye con tus datos: no hay un umbral poblacional de velocidad en escaleras, porque el ritmo depende de la geometría de la escalera y la longitud de las piernas.",
"caveat": "Una banda personal de tendencia de movilidad: descriptiva, no una evaluación de fragilidad ni de riesgo de caídas."
},
"STAIR_DESCENT_SPEED_BASELINE": {
"method": "Tu ritmo típico al bajar escaleras es la mediana de tus valores diarios en el periodo, ± una dispersión robusta (desviación absoluta de la mediana, escalada). La banda se construye con tus datos: no hay un umbral poblacional de velocidad en escaleras, porque el ritmo depende de la geometría de la escalera y la longitud de las piernas.",
"caveat": "Una banda personal de tendencia de movilidad: descriptiva, no una evaluación de fragilidad ni de riesgo de caídas."
},
"SIX_MINUTE_WALK_BAND": {
"method": "La distancia ESTIMADA de marcha de seis minutos de tu dispositivo situada frente a la referencia de Enright y Sherrill para tu edad, estatura, peso y sexo, como porcentaje de lo previsto. La distancia es la estimación del dispositivo: nunca se recalcula aquí. Sin tu edad, estatura, peso y sexo se oculta el porcentaje y solo se muestran la distancia y la tendencia.",
"caveat": "Una estimación reformulada frente a una referencia publicada: una señal de capacidad funcional, no una prueba clínica de marcha de seis minutos ni un diagnóstico."
}
},
"scores": {
Expand All @@ -2632,7 +2655,15 @@
"coincident": {
"label": "Varias constantes fuera de la base",
"summary": "Hoy, {count} de tus constantes quedaron fuera de su rango personal.",
"vitals": "Fuera de rango: {list}"
"vitals": "Fuera de rango: {list}",
"cardTitle": "Señal de hoy",
"allClear": "Hoy todas tus constantes están dentro de su rango personal.",
"allClearMeta": "Se revisaron {count} de tus constantes frente a tu propio rango habitual.",
"watch": "Hoy una constante está fuera de su rango habitual.",
"watchVital": "Fuera de rango: {vital}. A menudo es algo puntual; conviene vigilarlo los próximos días.",
"firedHeadline": "Hoy, {count} de tus constantes están fuera de su rango personal.",
"factors": "Posibles factores, nunca una causa: un entrenamiento intenso, dormir mal, alcohol, altitud, estrés o una enfermedad. Es un aviso a partir de tus propias bases, no un diagnóstico.",
"building": "Construyendo tus bases personales: registra unos días más y esto aparecerá."
}
},
"cardioFitness": {
Expand Down
Loading
Loading