From e2724533273caf15f4530652541835ed11bd5974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 16 Jul 2026 23:33:33 +0200 Subject: [PATCH] =?UTF-8?q?chore(release):=20v1.28.55=20=E2=80=94=20the=20?= =?UTF-8?q?Today=20view=20comes=20to=20life?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles three daily-loop slices onto the Today view: - Coach check-in: a due plan review surfaces as a calm Today card with keep/adjust/let-go, wired to the existing plan lifecycle; silence never changes a plan. - Morning freshness: the sleep-dependent parts of the digest and score refresh the moment last night's sleep arrives (a debounced, timezone-correct per-user job across the Withings, WHOOP and Apple transports), with an honest provisional state until then. Adds migration 0244 (a per-user refresh marker). - Daily push: an opt-in, once-a-day morning notification carrying the day's read, off by default, riding the existing dispatch cascade — a calm note, never an alert. All read the overnight digest; nothing generates on open. --- CHANGELOG.md | 20 ++++++- docs/api/openapi.yaml | 2 +- package.json | 2 +- public/sw.js | 2 +- .../__tests__/morning-digest-refresh.test.ts | 57 +++++++++---------- 5 files changed, 48 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95cc80d69..f7afa492a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,25 @@ ## [Unreleased] -## [1.28.54] — 2026-07-16 — One hero on the dashboard +## [1.28.55] — 2026-07-16 — The Today view comes to life + +Three pieces turn the Today view into a daily loop: + +- **A coach check-in.** When a coach plan is due for a look-back, it surfaces as + a calm card in Today with keep, adjust, or let-go — one tap. Letting go retires + the plan quietly; silence never changes it. +- **A morning that reflects last night.** The score and read are prepared + overnight, but last night's sleep usually hasn't synced by then. Now, the + moment it arrives, the sleep-dependent parts refresh on their own — so by the + time you look, the day is up to date. Until sleep is in, Today says so plainly + instead of showing a stale score. +- **An optional daily nudge.** A once-a-day morning notification with the day's + read, off by default and opt-in per channel under Notifications. It carries the + refreshed digest when sleep is in, never repeats within a day, and is a calm + note — never an alert. + +All three read the digest prepared overnight; nothing is generated when you open +the app. With the Today view now leading the dashboard, the older opt-in hero card it replaced is retired, so the two never stack. The greeting stays, the tiles and diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 2142dcadb..baab5f7d1 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.28.54 + version: 1.28.55 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/package.json b/package.json index a7ff7fda3..500802a1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.28.54", + "version": "1.28.55", "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", diff --git a/public/sw.js b/public/sw.js index 93dd81d29..e89f2e39d 100644 --- a/public/sw.js +++ b/public/sw.js @@ -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.28.54"; + /* @sw-version-fallback */ "v1.28.55"; 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/*` diff --git a/src/lib/jobs/__tests__/morning-digest-refresh.test.ts b/src/lib/jobs/__tests__/morning-digest-refresh.test.ts index 844d2fbda..050b97d6c 100644 --- a/src/lib/jobs/__tests__/morning-digest-refresh.test.ts +++ b/src/lib/jobs/__tests__/morning-digest-refresh.test.ts @@ -6,7 +6,10 @@ import type { GenerateOutcome } from "@/lib/insights/comprehensive-generate"; const LOCAL_DATE = "2026-07-16"; function makePrisma( - user: { locale: string | null; morningDigestRefreshedOn: string | null } | null, + user: { + locale: string | null; + morningDigestRefreshedOn: string | null; + } | null, ) { const updateMock = vi.fn(async () => ({})); const prisma = { @@ -30,12 +33,10 @@ describe("runMorningDigestRefresh", () => { locale: "en", morningDigestRefreshedOn: null, }); - const generate = vi.fn( - async (): Promise => ({ - status: "generated", - providerType: "mock", - }), - ); + const generate = vi.fn(async (): Promise => ({ + status: "generated", + providerType: "mock", + })); const result = await runMorningDigestRefresh( prisma as never, @@ -57,9 +58,9 @@ describe("runMorningDigestRefresh", () => { locale: "de", morningDigestRefreshedOn: null, }); - const generate = vi.fn( - async (): Promise => ({ status: "unchanged" }), - ); + const generate = vi.fn(async (): Promise => ({ + status: "unchanged", + })); const result = await runMorningDigestRefresh( prisma as never, @@ -77,12 +78,10 @@ describe("runMorningDigestRefresh", () => { locale: "en", morningDigestRefreshedOn: null, }); - const generate = vi.fn( - async (): Promise => ({ - status: "skipped", - reason: "no-provider", - }), - ); + const generate = vi.fn(async (): Promise => ({ + status: "skipped", + reason: "no-provider", + })); const result = await runMorningDigestRefresh( prisma as never, @@ -99,12 +98,10 @@ describe("runMorningDigestRefresh", () => { locale: "en", morningDigestRefreshedOn: LOCAL_DATE, }); - const generate = vi.fn( - async (): Promise => ({ - status: "generated", - providerType: "mock", - }), - ); + const generate = vi.fn(async (): Promise => ({ + status: "generated", + providerType: "mock", + })); const result = await runMorningDigestRefresh( prisma as never, @@ -122,12 +119,10 @@ describe("runMorningDigestRefresh", () => { locale: "en", morningDigestRefreshedOn: null, }); - const generate = vi.fn( - async (): Promise => ({ - status: "failed", - reason: "provider-timeout", - }), - ); + const generate = vi.fn(async (): Promise => ({ + status: "failed", + reason: "provider-timeout", + })); const result = await runMorningDigestRefresh( prisma as never, @@ -142,9 +137,9 @@ describe("runMorningDigestRefresh", () => { it("reports missing-user when the row is gone", async () => { const { prisma, updateMock } = makePrisma(null); - const generate = vi.fn( - async (): Promise => ({ status: "unchanged" }), - ); + const generate = vi.fn(async (): Promise => ({ + status: "unchanged", + })); const result = await runMorningDigestRefresh( prisma as never,