Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [1.37.11] — 2026-08-10

### Changed

- The lifestyle-context card in Anamnese saves in one action. Smoking, alcohol and shift-work each had their own save button; now you set what you want across all three and one Save writes them together, with a single confirmation. If one of them cannot be saved, that entry stays unsaved and says so while the others go through, rather than a single message hiding which one failed.

## [1.37.10] — 2026-08-10

### Added
Expand Down
2 changes: 1 addition & 1 deletion 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.37.10
version: 1.37.11
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.

Expand Down
36 changes: 33 additions & 3 deletions e2e/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,32 @@ async function installA11yMocks(page: Page) {
}),
);

// Matches the current `MoodInsightsResponse` (mood-insights-shared.tsx): the
// sections short-circuit on `summary.totalEntries === 0` and then read
// `heatmap.cells`, so `totalEntries` and a populated `heatmap` are both
// required or the page throws into its error boundary. Breakdown arrays are
// present-but-empty; the correlations block carries every metric key.
await page.route("**/api/mood/insights", (route) =>
fulfilJson(route, {
summary: { count: 1 },
tagBreakdown: [],
trends: [],
summary: { totalEntries: 1, inTargetPct: null },
heatmap: {
windowDays: 30,
cells: [{ date: "2026-07-20", score: 4, samples: 1 }],
},
distribution: [],
weekday: [],
timeOfDay: { buckets: [], reliable: false, best: null, worst: null },
stability: null,
tags: [],
structuredTags: [],
narratives: [],
correlations: {
sleep: { result: null, points: [], n: 0 },
steps: { result: null, points: [], n: 0 },
pulse: { result: null, points: [], n: 0 },
weight: { result: null, points: [], n: 0 },
bloodPressureSystolic: { result: null, points: [], n: 0 },
},
}),
);

Expand Down Expand Up @@ -689,6 +710,15 @@ const INSIGHTS_ROUTES: readonly RouteCase[] = [
path: "/insights/weight",
painted: (page) => page.locator(".recharts-wrapper").first(),
},
{
// Mood is event-driven, so its sub-page renders the heatmap and the mood
// line chart from `/api/mood/insights` rather than a MeasurementType series.
// Gate on the line chart's recharts wrapper — the same painted signal the
// weight sub-page uses — so the scan waits for the heavy content to land.
name: "/insights/mood metric subpage",
path: "/insights/mood",
painted: (page) => page.locator(".recharts-wrapper").first(),
},
{
// The two states this page's OWN render produces: the list, or the
// no-workouts empty state. Never `[role="status"]`, because the busy
Expand Down
3 changes: 2 additions & 1 deletion messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -9210,7 +9210,8 @@
"unreadable": "Gespeicherter Wert konnte nicht gelesen werden",
"save": "Speichern",
"savedToast": "Lebensstil-Kontext gespeichert",
"saveError": "Lebensstil-Kontext konnte nicht gespeichert werden",
"savePartialError": "{kinds} konnte nicht gespeichert werden. Bitte erneut versuchen.",
"saveKindError": "Nicht gespeichert",
"loadError": "Lebensstil-Kontext konnte nicht geladen werden",
"remove": "{kind} entfernen",
"removeShort": "Entfernen",
Expand Down
3 changes: 2 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9210,7 +9210,8 @@
"unreadable": "Stored value could not be read",
"save": "Save",
"savedToast": "Lifestyle context saved",
"saveError": "Could not save lifestyle context",
"savePartialError": "Couldn't save {kinds}. Please try again.",
"saveKindError": "Not saved",
"loadError": "Could not load lifestyle context",
"remove": "Remove {kind}",
"removeShort": "Remove",
Expand Down
3 changes: 2 additions & 1 deletion messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -9210,7 +9210,8 @@
"unreadable": "No se pudo leer el valor guardado",
"save": "Guardar",
"savedToast": "Contexto del estilo de vida guardado",
"saveError": "No se pudo guardar el contexto del estilo de vida",
"savePartialError": "No se pudo guardar {kinds}. Inténtalo de nuevo.",
"saveKindError": "No guardado",
"loadError": "No se pudo cargar el contexto del estilo de vida",
"remove": "Eliminar {kind}",
"removeShort": "Quitar",
Expand Down
3 changes: 2 additions & 1 deletion messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -9210,7 +9210,8 @@
"unreadable": "La valeur enregistrée n'a pas pu être lue",
"save": "Enregistrer",
"savedToast": "Contexte du mode de vie enregistré",
"saveError": "Impossible d'enregistrer le contexte du mode de vie",
"savePartialError": "Impossible d'enregistrer {kinds}. Veuillez réessayer.",
"saveKindError": "Non enregistré",
"loadError": "Impossible de charger le contexte du mode de vie",
"remove": "Supprimer : {kind}",
"removeShort": "Supprimer",
Expand Down
3 changes: 2 additions & 1 deletion messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -9210,7 +9210,8 @@
"unreadable": "Impossibile leggere il valore salvato",
"save": "Salva",
"savedToast": "Contesto dello stile di vita salvato",
"saveError": "Impossibile salvare il contesto dello stile di vita",
"savePartialError": "Impossibile salvare {kinds}. Riprova.",
"saveKindError": "Non salvato",
"loadError": "Impossibile caricare il contesto dello stile di vita",
"remove": "Rimuovi {kind}",
"removeShort": "Rimuovi",
Expand Down
3 changes: 2 additions & 1 deletion messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -9210,7 +9210,8 @@
"unreadable": "Nie udało się odczytać zapisanej wartości",
"save": "Zapisz",
"savedToast": "Zapisano kontekst stylu życia",
"saveError": "Nie udało się zapisać kontekstu stylu życia",
"savePartialError": "Nie udało się zapisać {kinds}. Spróbuj ponownie.",
"saveKindError": "Nie zapisano",
"loadError": "Nie udało się wczytać kontekstu stylu życia",
"remove": "Usuń: {kind}",
"removeShort": "Usuń",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.37.10",
"version": "1.37.11",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
Expand Down
5 changes: 3 additions & 2 deletions src/components/charts/mood-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import {
import { useEffect, useState, useMemo, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Card, CardHeader, CardContent } from "@/components/ui/card";
import { TagChip } from "@/components/ui/tag-chip";
import { TileHeader } from "@/components/insights/tile-header";
import { useTranslations } from "@/lib/i18n/context";
import { makeBucketLabelFormatters } from "@/lib/charts/bucket-label";
import { readStoredTimezone } from "@/lib/timezone-mirror";
Expand Down Expand Up @@ -1034,7 +1035,7 @@ export function MoodChart({
back to side-by-side. */}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-base">{displayTitle}</CardTitle>
<TileHeader title={displayTitle} />
{activeBucket !== "day" && (
<TagChip className="hidden tracking-wide uppercase sm:inline-flex">
{t(
Expand Down
Loading
Loading