mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
13 files changed
+68
-5
No files matched your search
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## [1.39.9] — 2026-10-03
|
||||
|
||||
Workouts can be logged by hand, the body fat tile opens its own page, and
|
||||
every AI call honours the response-timeout setting.
|
||||
|
||||
### Added
|
||||
|
||||
- **Manual workouts (#1085).** A short form (sport, start, duration, optional
|
||||
distance and energy) from the dashboard "+ Add" menu, the capture picker and
|
||||
the workouts page, saved through `POST /api/workouts/batch` with
|
||||
`source: "MANUAL"` and a per-form `externalId`. New
|
||||
`DELETE /api/workouts/{id}` for manual workouts only (409
|
||||
`workout.delete.synced_source` for synced ones).
|
||||
- **Body fat page (#1084).** `/insights/body-fat`; insights layout id
|
||||
`body-fat`. Tile destinations live in one table gated on the module surface
|
||||
map.
|
||||
- **Wide lab scan review (#1059).** The review stage uses a 72 rem dialog on
|
||||
large screens, with a container-query row layout.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **AI response timeout (#1090).** Document reading, summaries, extraction,
|
||||
lab OCR, filing suggestions, medication extraction, the About-me questions,
|
||||
the document chat and the provider test run under the user's setting
|
||||
instead of a fixed 60 s; browser aborts derive from
|
||||
`ai.provider.responseTimeoutMs` on `/api/auth/me`. The lab OCR text mode no
|
||||
longer hits the 15 s client default. The settings placeholder shows the
|
||||
real defaults.
|
||||
- **Dashboard "+ Add" menu.** Mood and medication intake follow their
|
||||
modules, like the capture picker.
|
||||
|
||||
### Checks
|
||||
|
||||
- Tile destination guard, AI call timeout guard, menu and picker parity.
|
||||
- node-forge advisory GHSA-86w9-cpqp-85rv ignored until a patched release
|
||||
exists; only the unused APNs certificate path depends on it.
|
||||
|
||||
## [1.39.8] — 2026-10-02
|
||||
|
||||
New ECGs from the iPhone get through, a resting heart rate can be logged by
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: HealthLog API
|
||||
version: 1.39.8
|
||||
version: 1.39.9
|
||||
description: >-
|
||||
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
||||
|
||||
|
||||
@@ -3465,6 +3465,7 @@
|
||||
"durationRequired": "Gib an, wie lange das Workout gedauert hat.",
|
||||
"durationInvalid": "Gib ganze Stunden und Minuten von 0 bis 59 ein.",
|
||||
"durationTooLong": "Ein Workout dauert höchstens 24 Stunden.",
|
||||
"endInFuture": "Das Workout würde in der Zukunft enden. Stell den Beginn auf die Uhrzeit, zu der es angefangen hat.",
|
||||
"distanceInvalid": "Gib die Distanz als Zahl in einem realistischen Bereich ein.",
|
||||
"energyInvalid": "Gib die aktive Energie als Zahl bis 20.000 kcal ein."
|
||||
}
|
||||
|
||||
@@ -3465,6 +3465,7 @@
|
||||
"durationRequired": "Enter how long the workout lasted.",
|
||||
"durationInvalid": "Enter whole hours, and minutes from 0 to 59.",
|
||||
"durationTooLong": "A workout can last at most 24 hours.",
|
||||
"endInFuture": "It would end in the future. Set the start to when the workout began.",
|
||||
"distanceInvalid": "Enter the distance as a number within a realistic range.",
|
||||
"energyInvalid": "Enter active energy as a number up to 20,000 kcal."
|
||||
}
|
||||
|
||||
@@ -3465,6 +3465,7 @@
|
||||
"durationRequired": "Indica cuánto duró el entrenamiento.",
|
||||
"durationInvalid": "Introduce horas enteras y minutos de 0 a 59.",
|
||||
"durationTooLong": "Un entrenamiento puede durar como máximo 24 horas.",
|
||||
"endInFuture": "Terminaría en el futuro. Pon como inicio la hora en que empezó el entrenamiento.",
|
||||
"distanceInvalid": "Introduce la distancia como un número dentro de un rango realista.",
|
||||
"energyInvalid": "Introduce la energía activa como un número de hasta 20.000 kcal."
|
||||
}
|
||||
|
||||
@@ -3465,6 +3465,7 @@
|
||||
"durationRequired": "Indiquez la durée de l’entraînement.",
|
||||
"durationInvalid": "Saisissez des heures entières et des minutes de 0 à 59.",
|
||||
"durationTooLong": "Un entraînement dure au maximum 24 heures.",
|
||||
"endInFuture": "Il se terminerait dans le futur. Indiquez comme début l’heure à laquelle l’entraînement a commencé.",
|
||||
"distanceInvalid": "Saisissez la distance sous forme de nombre, dans une plage réaliste.",
|
||||
"energyInvalid": "Saisissez l’énergie active sous forme de nombre, jusqu’à 20 000 kcal."
|
||||
}
|
||||
|
||||
@@ -3465,6 +3465,7 @@
|
||||
"durationRequired": "Indica quanto è durato l’allenamento.",
|
||||
"durationInvalid": "Inserisci ore intere e minuti da 0 a 59.",
|
||||
"durationTooLong": "Un allenamento può durare al massimo 24 ore.",
|
||||
"endInFuture": "Finirebbe nel futuro. Imposta come inizio l’ora in cui è cominciato l’allenamento.",
|
||||
"distanceInvalid": "Inserisci la distanza come numero in un intervallo realistico.",
|
||||
"energyInvalid": "Inserisci l’energia attiva come numero fino a 20.000 kcal."
|
||||
}
|
||||
|
||||
@@ -3465,6 +3465,7 @@
|
||||
"durationRequired": "운동한 시간을 입력하세요.",
|
||||
"durationInvalid": "시간은 정수로, 분은 0에서 59 사이로 입력하세요.",
|
||||
"durationTooLong": "운동은 최대 24시간까지 입력할 수 있습니다.",
|
||||
"endInFuture": "종료 시간이 미래가 됩니다. 시작 시간을 운동을 시작한 시각으로 설정하세요.",
|
||||
"distanceInvalid": "거리를 현실적인 범위의 숫자로 입력하세요.",
|
||||
"energyInvalid": "활동 에너지는 20,000kcal 이하의 숫자로 입력하세요."
|
||||
}
|
||||
|
||||
@@ -3465,6 +3465,7 @@
|
||||
"durationRequired": "Podaj, jak długo trwał trening.",
|
||||
"durationInvalid": "Podaj pełne godziny i minuty od 0 do 59.",
|
||||
"durationTooLong": "Trening może trwać najwyżej 24 godziny.",
|
||||
"endInFuture": "Trening kończyłby się w przyszłości. Ustaw początek na godzinę, o której się zaczął.",
|
||||
"distanceInvalid": "Podaj dystans jako liczbę w realistycznym zakresie.",
|
||||
"energyInvalid": "Podaj energię czynną jako liczbę do 20 000 kcal."
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.39.8",
|
||||
"version": "1.39.9",
|
||||
"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",
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ try {
|
||||
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
|
||||
const CACHE_VERSION =
|
||||
(typeof self !== "undefined" && self.__APP_VERSION__) ||
|
||||
/* @sw-version-fallback */ "v1.39.8";
|
||||
/* @sw-version-fallback */ "v1.39.9";
|
||||
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
|
||||
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
|
||||
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
|
||||
|
||||
@@ -136,9 +136,9 @@ describe("buildManualWorkoutEntry — validation", () => {
|
||||
});
|
||||
|
||||
it("tolerates the minute the form was opened in", () => {
|
||||
// 14:00:30 Berlin is 30 s past `now`: inside the slack.
|
||||
// 13:15 Berlin + 45 min ends at 12:00Z, 30 s past `now`: inside the slack.
|
||||
const result = buildManualWorkoutEntry(
|
||||
draft({ start: "2026-09-15T14:00" }),
|
||||
draft({ start: "2026-09-15T13:15" }),
|
||||
{
|
||||
...CTX,
|
||||
now: new Date("2026-09-15T11:59:30Z"),
|
||||
@@ -147,6 +147,13 @@ describe("buildManualWorkoutEntry — validation", () => {
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("refuses a workout that would end in the future", () => {
|
||||
// Logged at 14:00 Berlin with the start left at 13:30: 45 min ends 14:15.
|
||||
expect(errorsOf({ start: "2026-09-15T13:30" }).duration).toBe(
|
||||
"insights.workouts.manual.errors.endInFuture",
|
||||
);
|
||||
});
|
||||
|
||||
it("asks for a start", () => {
|
||||
expect(errorsOf({ start: "" }).start).toBe(
|
||||
"insights.workouts.manual.errors.startRequired",
|
||||
|
||||
@@ -235,6 +235,18 @@ function manualWorkoutSchema(ctx: ManualWorkoutContext) {
|
||||
path: ["duration"],
|
||||
message: `${ERR}.durationTooLong`,
|
||||
});
|
||||
} else if (
|
||||
// The default start is "now", so a workout logged right after it
|
||||
// finished would otherwise be stored as ending in the future.
|
||||
startedAt !== null &&
|
||||
startedAt.getTime() + sec * 1000 >
|
||||
ctx.now.getTime() + FUTURE_TOLERANCE_MS
|
||||
) {
|
||||
issue.addIssue({
|
||||
code: "custom",
|
||||
path: ["duration"],
|
||||
message: `${ERR}.endInFuture`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user