chore(release): v1.28.55 — the Today view comes to life

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.
This commit is contained in:
Marc-André Bombeck committed 2026-07-16 23:33:33 +02:00
1 parent 04cb80da6a
commit e272453327
5 files changed
+48 -35

No files matched your search

+19 -1
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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/*`
@@ -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<GenerateOutcome> => ({
status: "generated",
providerType: "mock",
}),
);
const generate = vi.fn(async (): Promise<GenerateOutcome> => ({
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<GenerateOutcome> => ({ status: "unchanged" }),
);
const generate = vi.fn(async (): Promise<GenerateOutcome> => ({
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<GenerateOutcome> => ({
status: "skipped",
reason: "no-provider",
}),
);
const generate = vi.fn(async (): Promise<GenerateOutcome> => ({
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<GenerateOutcome> => ({
status: "generated",
providerType: "mock",
}),
);
const generate = vi.fn(async (): Promise<GenerateOutcome> => ({
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<GenerateOutcome> => ({
status: "failed",
reason: "provider-timeout",
}),
);
const generate = vi.fn(async (): Promise<GenerateOutcome> => ({
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<GenerateOutcome> => ({ status: "unchanged" }),
);
const generate = vi.fn(async (): Promise<GenerateOutcome> => ({
status: "unchanged",
}));
const result = await runMorningDigestRefresh(
prisma as never,