Merge the final display polish into release/v1.42.0

This commit is contained in:
Marc-André Bombeck committed 2026-10-09 17:25:35 +02:00
commit dcfec1f35d
144 files changed
+4866 -2817

No files matched your search

+1 -1
View File
@@ -44,7 +44,7 @@ export const dynamic = "force-dynamic";
const LAST_UPDATED = "2026-08-02";
export const metadata: Metadata = {
title: "About — HealthLog",
title: "About | HealthLog",
description:
"Open-source credits and third-party data attributions for the HealthLog project.",
robots: { index: true, follow: true },
@@ -84,7 +84,7 @@ const handler = apiHandler(
details: { reason: "missing_confirmation", backupId: id },
});
return apiError(
"Confirmation token missing — replacing an account's data requires confirm: 'RESTORE'",
"Confirmation token missing. Replacing an account's data requires confirm: 'RESTORE'",
422,
);
}
@@ -45,7 +45,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
userCode = decoded.userCode;
} catch {
cookieStore.delete("central_codex_device");
return apiError("Invalid device-auth state — restart the flow", 400);
return apiError("Invalid device-auth state. Restart the flow", 400);
}
let result;
@@ -55,7 +55,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
const message = err instanceof Error ? err.message : "unknown";
annotate({ meta: { central_codex_device_poll_error: message } });
return apiError(
"Central Codex device-poll failed — please restart the flow",
"Central Codex device-poll failed. Please restart the flow",
502,
);
}
+3 -3
View File
@@ -276,13 +276,13 @@ function classifyTestFailure(
return {
reasonCode: "credentials",
reason:
"Provider rejected the credentials — re-authenticate in AI settings.",
"Provider rejected the credentials. Re-authenticate in AI settings.",
};
}
if (status === 429) {
return {
reasonCode: "rate_limited",
reason: "Provider rate-limited the request — try again shortly.",
reason: "Provider rate-limited the request. Try again shortly.",
};
}
if (status >= 500) {
@@ -298,7 +298,7 @@ function classifyTestFailure(
if (status >= 400 && status < 500) {
return {
reasonCode: "bad_request",
reason: `The provider rejected the request (HTTP ${status}) — the endpoint answered, so this is a request-shape or model-name problem, not connectivity.`,
reason: `The provider rejected the request (HTTP ${status}). The endpoint answered, so this is a request-shape or model-name problem, not connectivity.`,
};
}
return {
+2 -2
View File
@@ -47,7 +47,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
userCode = decoded.userCode;
} catch {
cookieStore.delete("codex_device");
return apiError("Invalid device-auth state — restart the flow", 400);
return apiError("Invalid device-auth state. Restart the flow", 400);
}
let result;
@@ -56,7 +56,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
} catch (err) {
const message = err instanceof Error ? err.message : "unknown";
annotate({ meta: { codex_device_poll_error: message } });
return apiError("Codex device-poll failed — please restart the flow", 502);
return apiError("Codex device-poll failed. Please restart the flow", 502);
}
if (result.status === "pending") {
@@ -88,7 +88,7 @@ const healthScoreConfigPatchSchema = z
*/
const BREADTH_REFUSAL = {
no_pillars_selected:
"A health score needs at least one area of health to score. Keep one selected — the score will say which areas it is built from.",
"A health score needs at least one area of health to score. Keep one selected. The score will say which areas it is built from.",
} as const;
export const GET = apiHandler(async () => {
+1 -1
View File
@@ -135,7 +135,7 @@ export const DELETE = apiHandler(
if (passkeyCount <= 1 && !hasPassword) {
return apiError(
"Cannot delete — at least one authentication method must remain",
"Cannot delete. At least one authentication method must remain",
400,
);
}
+2 -2
View File
@@ -96,14 +96,14 @@ export const POST = apiHandler(async (request: NextRequest) => {
// the token and re-authenticates.
if (result.reason === "already_used") {
return apiError(
"Refresh token reuse detected — please log in again.",
"Refresh token reuse detected. Please log in again.",
401,
{ errorCode: "auth.refresh.reuse" },
);
}
if (result.reason === "revoked") {
return apiError(
"Refresh token has been revoked — please log in again.",
"Refresh token has been revoked. Please log in again.",
401,
{ errorCode: "auth.refresh.revoked" },
);
+1 -1
View File
@@ -58,7 +58,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (existingSession) {
annotate({ action: { name: "auth.register.refused_authenticated" } });
return apiError(
"Already signed in — sign out first to use an invitation",
"Already signed in. Sign out first to use an invitation",
409,
{ errorCode: "auth.already_authenticated" },
);
+1 -1
View File
@@ -297,7 +297,7 @@ export const PATCH = apiHandler(
if (outcome === "contradictory-reminder") {
return apiError(
"A booked visit cannot also close a checkup — it has not happened yet",
"A booked visit cannot also close a checkup. It has not happened yet",
422,
{ errorCode: "encounter.reminder-conflict" },
);
+1 -1
View File
@@ -301,7 +301,7 @@ async function postEncounter(request: NextRequest): Promise<Response> {
}
if (created === "contradictory-reminder") {
return apiError(
"A planned visit cannot also close a checkup — it has not happened yet",
"A planned visit cannot also close a checkup. It has not happened yet",
422,
{ errorCode: "encounter.reminder-conflict" },
);
+1 -1
View File
@@ -149,7 +149,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
);
if (okRows.length > MAX_ROWS) {
return apiError(
`CSV has ${okRows.length} valid rows — the limit is ${MAX_ROWS} per import`,
`CSV has ${okRows.length} valid rows. The limit is ${MAX_ROWS} per import`,
422,
);
}
@@ -1240,7 +1240,7 @@ exports[`coach chat golden transcripts > 04 inbound refusal on a new conversatio
},
{
"call": "appendMessage",
"content": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data — please rephrase your question in those terms.",
"content": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data. Please rephrase your question in those terms.",
"metricSource": {
"metrics": [
"general",
@@ -1256,7 +1256,7 @@ exports[`coach chat golden transcripts > 04 inbound refusal on a new conversatio
"contentType": "text/event-stream; charset=utf-8",
"frames": [
{
"token": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data — please rephrase your question in those terms.",
"token": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data. Please rephrase your question in those terms.",
"type": "token",
},
{
@@ -1302,7 +1302,7 @@ exports[`coach chat golden transcripts > 05 replay injection in a stored user tu
},
{
"call": "appendMessage",
"content": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data — please rephrase your question in those terms.",
"content": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data. Please rephrase your question in those terms.",
"metricSource": {
"metrics": [
"general",
@@ -1318,7 +1318,7 @@ exports[`coach chat golden transcripts > 05 replay injection in a stored user tu
"contentType": "text/event-stream; charset=utf-8",
"frames": [
{
"token": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data — please rephrase your question in those terms.",
"token": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data. Please rephrase your question in those terms.",
"type": "token",
},
{
+6 -6
View File
@@ -810,7 +810,7 @@ export const POST = apiHandler((request: NextRequest) =>
// bounds cost. 429 with a distinct code so the client can say so.
if (e instanceof BriefingBudgetExceededError) {
return apiError(
"Daily AI token budget reached — insights will be available again tomorrow.",
"Daily AI token budget reached. Insights will be available again tomorrow.",
429,
{ errorCode: "insights.generate.budgetExceeded" },
);
@@ -859,7 +859,7 @@ export const POST = apiHandler((request: NextRequest) =>
);
if (allAuth) {
return apiError(
"AI provider rejected the request — check your API key in Settings > AI",
"AI provider rejected the request. Check your API key in Settings > AI",
422,
);
}
@@ -881,7 +881,7 @@ export const POST = apiHandler((request: NextRequest) =>
);
if (allTransport) {
return apiError(
"AI provider connection failed — check your AI settings",
"AI provider connection failed. Check your AI settings",
422,
);
}
@@ -907,7 +907,7 @@ export const POST = apiHandler((request: NextRequest) =>
const status = err.httpStatus ?? 0;
if (status === 401 || status === 403) {
return apiError(
"AI provider rejected the request — check your API key in Settings > AI",
"AI provider rejected the request. Check your API key in Settings > AI",
422,
);
}
@@ -921,7 +921,7 @@ export const POST = apiHandler((request: NextRequest) =>
);
}
return apiError(
"AI provider connection failed — check your AI settings",
"AI provider connection failed. Check your AI settings",
422,
);
}
@@ -945,7 +945,7 @@ export const POST = apiHandler((request: NextRequest) =>
if (result.finishReason === "length") {
annotate({ meta: { insights_response_truncated: true } });
return apiError(
"AI response was cut off before the JSON completed — raise the token limit (INSIGHTS_MAX_TOKENS)",
"AI response was cut off before the JSON completed. Raise the token limit (INSIGHTS_MAX_TOKENS)",
422,
{ errorCode: "ai_response_truncated" },
);
+2 -2
View File
@@ -172,14 +172,14 @@ export const PUT = apiHandler(
const rowIsQualitative = existing.valueText !== null;
if (rowIsQualitative && d.value !== undefined) {
return apiError(
"This is a qualitative reading — edit its result text, not a numeric value",
"This is a qualitative reading. Edit its result text, not a numeric value",
422,
{ errorCode: "labs.update.qualitativeExpected" },
);
}
if (!rowIsQualitative && d.valueText !== undefined) {
return apiError(
"This is a numeric reading — edit its value, not a qualitative result",
"This is a numeric reading. Edit its value, not a qualitative result",
422,
{ errorCode: "labs.update.numericExpected" },
);
+3 -3
View File
@@ -85,7 +85,7 @@ function badRequest(error: string, description: string): Response {
function html(body: string, status = 200): Response {
return new Response(
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>HealthLog — Authorize access</title></head><body>${body}</body></html>`,
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>HealthLog: Authorize access</title></head><body>${body}</body></html>`,
{
status,
headers: { "content-type": "text/html; charset=utf-8" },
@@ -419,13 +419,13 @@ async function authorizeGet(request: NextRequest): Promise<Response> {
const provenance =
v.clientSource === "cimd"
? `<p>Verified origin: <code>${htmlEscape(v.clientOrigin)}</code></p>`
: `<p><strong>Unverified application</strong> (dynamically registered — the name above is self-reported and not verified).</p>`;
: `<p><strong>Unverified application</strong> (dynamically registered, the name above is self-reported and not verified).</p>`;
// Plain-language access summary. When write is granted, say so explicitly —
// the app will be able to READ and LOG/WRITE health data, not just read.
const writeGranted = v.scope.split(/\s+/).includes(SCOPE_HEALTH_WRITE);
const accessSummary = writeGranted
? `<p><strong>This grants read AND write access:</strong> the application will be able to read your own health records <strong>and log new measurements and mood entries</strong> to your account on your behalf. It cannot delete or change existing entries, edit medications, or reach admin functions.</p>`
: `<p>Scope: <code>${htmlEscape(v.scope)}</code> — read-only access to your own health records.</p>`;
: `<p>Scope: <code>${htmlEscape(v.scope)}</code>. Read-only access to your own health records.</p>`;
// What a read reaches, in plain words, before the person clicks Allow.
// Document text is named on its own because it is the widest thing a
// connection can read (v1.39.3: search and fetch reach it).
@@ -176,7 +176,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (plan.entries.length > MAX_QUEUED_ROWS) {
return apiError(
`The file holds ${plan.entries.length} importable rows — the limit is ${MAX_QUEUED_ROWS} per import`,
`The file holds ${plan.entries.length} importable rows. The limit is ${MAX_QUEUED_ROWS} per import`,
422,
{ errorCode: "too_many_rows" },
);
+1 -1
View File
@@ -66,7 +66,7 @@ async function checkAndWarn(
}
if (auth.via === "query") {
getEvent()?.addWarning(
`withings webhook secret received via legacy URL query — migrate to ${MIGRATION_URL} (re-subscribe with the path-segment token form)`,
`withings webhook secret received via legacy URL query. Migrate to ${MIGRATION_URL} (re-subscribe with the path-segment token form)`,
);
}
return null;
+1 -1
View File
@@ -27,7 +27,7 @@ export default function InsightsActiveEnergyPage() {
yAxisUnit="kcal"
emptyStateIcon={<Flame className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any active energy yet — what's a reasonable daily target, and how does it compare to total calories?"
coachPrefill="I haven't logged any active energy yet. What's a reasonable daily target, and how does it compare to total calories?"
/>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ export default function InsightsLaermereignissePage() {
yAxisUnit=""
emptyStateIcon={<Volume2 className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any loud-environment events yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any loud-environment events yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -60,7 +60,7 @@ export default function InsightsBlutzuckerPage() {
captureType="BLOOD_GLUCOSE"
targetSummarySlug="blood-glucose"
afterChart={<GlucoseClinicalPanel />}
coachPrefill="I haven't logged any blood glucose yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any blood glucose yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -81,7 +81,7 @@ export default function InsightsBlutdruckPage() {
</Link>
</Button>
}
coachPrefill="I haven't recorded any blood pressure yet — why does it matter, and what should I know before I start?"
coachPrefill="I haven't recorded any blood pressure yet. Why does it matter, and what should I know before I start?"
/>
</SubPageShell>
);
+1 -1
View File
@@ -69,7 +69,7 @@ export default function InsightsBmiPage() {
</Link>
</Button>
}
coachPrefill="I haven't recorded any weight yet — why does BMI matter for me, and what should I know before I start tracking it?"
coachPrefill="I haven't recorded any weight yet. Why does BMI matter for me, and what should I know before I start tracking it?"
/>
</SubPageShell>
);
+1 -1
View File
@@ -24,7 +24,7 @@ export default function InsightsBodyTemperaturePage() {
color="var(--chart-3)"
emptyStateIcon={<Thermometer className="size-6" />}
emptyStateCtaType="BODY_TEMPERATURE"
coachPrefill="I haven't logged any body temperature yet — what's the healthy range, and what should I do if it drifts?"
coachPrefill="I haven't logged any body temperature yet. What's the healthy range, and what should I do if it drifts?"
/>
);
}
+1 -1
View File
@@ -25,7 +25,7 @@ export default function InsightsKoerperwasserPage() {
emptyStateIcon={<Droplet className="size-6" />}
emptyStateCtaType={null}
captureType="TOTAL_BODY_WATER"
coachPrefill="I haven't logged any total body water yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any total body water yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -25,7 +25,7 @@ export default function InsightsKnochenmassePage() {
emptyStateIcon={<Bone className="size-6" />}
emptyStateCtaType={null}
captureType="BONE_MASS"
coachPrefill="I haven't logged any bone mass yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any bone mass yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
@@ -24,7 +24,7 @@ export default function InsightsBreathingDisturbancesPage() {
unitKey="insights.units.breathingEvents"
emptyStateIcon={<Wind className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any sleep-breathing-disturbance data yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any sleep-breathing-disturbance data yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -27,7 +27,7 @@ export default function InsightsCardioFitnessPage() {
statIcon={Gauge}
emptyStateIcon={<Gauge className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any cardio-fitness (VO₂ max) data yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any cardio-fitness (VO₂ max) data yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ export default function InsightsCardioRecoveryPage() {
yAxisUnit="bpm"
emptyStateIcon={<HeartPulse className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any cardio-recovery data yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any cardio-recovery data yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ export default function InsightsTageslichtPage() {
yAxisUnit="min"
emptyStateIcon={<Sun className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any time spent in daylight yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any time spent in daylight yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
@@ -26,7 +26,7 @@ export default function InsightsDoppelstandphasePage() {
yAxisUnit="%"
emptyStateIcon={<Footprints className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any double support time yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any double support time yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
@@ -26,7 +26,7 @@ export default function InsightsLaermbelastungPage() {
yAxisUnit="dBA"
emptyStateIcon={<Volume2 className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any environmental sound exposure yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any environmental sound exposure yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -23,7 +23,7 @@ export default function InsightsFallsPage() {
unitKey="insights.units.falls"
emptyStateIcon={<PersonStanding className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any fall data yet — what does this metric tell me about my health, and how do I lower my fall risk?"
coachPrefill="I haven't logged any fall data yet. What does this metric tell me about my health, and how do I lower my fall risk?"
/>
);
}
+1 -1
View File
@@ -24,7 +24,7 @@ export default function InsightsFettfreieMassePage() {
color="var(--chart-1)"
emptyStateIcon={<Scale className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any fat-free mass yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any fat-free mass yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -24,7 +24,7 @@ export default function InsightsFettmassePage() {
color="var(--chart-5)"
emptyStateIcon={<Droplets className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any fat mass yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any fat mass yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -25,7 +25,7 @@ export default function InsightsStockwerkePage() {
unitKey="insights.units.flights"
emptyStateIcon={<TrendingUp className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any flights climbed yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any flights climbed yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ export default function InsightsKopfhoererpegelPage() {
yAxisUnit="dBA"
emptyStateIcon={<Headphones className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any headphone audio exposure yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any headphone audio exposure yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -37,7 +37,7 @@ export default function InsightsHrvPage() {
yAxisUnit="ms"
emptyStateIcon={<Activity className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any HRV data yet — what does heart-rate variability tell me, and how do I capture it?"
coachPrefill="I haven't logged any HRV data yet. What does heart-rate variability tell me, and how do I capture it?"
// S10 / H1 — device-attributed pointer into the ECG viewer, in the
// HRV / heart context. Self-gates to nothing without recordings.
afterAssessment={<EcgCrossLink />}
+1 -1
View File
@@ -24,7 +24,7 @@ export default function InsightsMagermassePage() {
color="var(--chart-1)"
emptyStateIcon={<Scale className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any lean body mass yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any lean body mass yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -132,7 +132,7 @@ export default function InsightsMoodPageClient() {
<Link href="/mood">{t("insights.emptyState.mood.cta")}</Link>
</Button>
}
coachPrefill="I haven't logged any mood entries yet — why does mood tracking matter, and how should I start?"
coachPrefill="I haven't logged any mood entries yet. Why does mood tracking matter, and how should I start?"
/>
</SubPageShell>
);
+1 -1
View File
@@ -24,7 +24,7 @@ export default function InsightsMuskelmassePage() {
color="var(--chart-1)"
emptyStateIcon={<Dumbbell className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any muscle mass yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any muscle mass yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -37,7 +37,7 @@ export default function InsightsOxygenSaturationPage() {
emptyStateIcon={<Wind className="size-6" />}
emptyStateCtaType={null}
captureType="OXYGEN_SATURATION"
coachPrefill="I haven't logged any oxygen-saturation data yet — what does SpO₂ tell me, and what range should I aim for?"
coachPrefill="I haven't logged any oxygen-saturation data yet. What does SpO₂ tell me, and what range should I aim for?"
/>
);
}
@@ -26,7 +26,7 @@ export default function InsightsPulswellengeschwindigkeitPage() {
yAxisUnit="m/s"
emptyStateIcon={<Activity className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any pulse wave velocity yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any pulse wave velocity yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -114,7 +114,7 @@ export default function InsightsPulsPage() {
</Link>
</Button>
}
coachPrefill="I haven't recorded any resting pulse yet — why does it matter, and what should I know before I start?"
coachPrefill="I haven't recorded any resting pulse yet. Why does it matter, and what should I know before I start?"
/>
</SubPageShell>
);
+1 -1
View File
@@ -25,7 +25,7 @@ export default function InsightsAtemfrequenzPage() {
unitKey="insights.units.respiratoryRate"
emptyStateIcon={<Wind className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any respiratory rate yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any respiratory rate yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -25,7 +25,7 @@ export default function InsightsSixMinuteWalkPage() {
yAxisUnit="m"
emptyStateIcon={<Footprints className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any six-minute-walk data yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any six-minute-walk data yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -32,7 +32,7 @@ export default function InsightsHauttemperaturPage() {
color="var(--chart-5)"
emptyStateIcon={<Thermometer className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any skin temperature yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any skin temperature yet. What does this metric tell me about my health, and how do I improve it?"
afterChart={
<ConnectedDeviceScoreTile
type="BODY_TEMPERATURE_DEVIATION"
+1 -1
View File
@@ -67,7 +67,7 @@ export default function InsightsSchlafPage() {
</Link>
</Button>
}
coachPrefill="I don't have any sleep data yet — why does sleep tracking matter, and what should I know before I connect a source?"
coachPrefill="I don't have any sleep data yet. Why does sleep tracking matter, and what should I know before I connect a source?"
/>
</SubPageShell>
);
+1 -1
View File
@@ -25,7 +25,7 @@ export default function InsightsStairAscentSpeedPage() {
yAxisUnit="m/s"
emptyStateIcon={<Gauge className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any stair-ascent-speed data yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any stair-ascent-speed data yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
@@ -25,7 +25,7 @@ export default function InsightsStairDescentSpeedPage() {
yAxisUnit="m/s"
emptyStateIcon={<Gauge className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any stair-descent-speed data yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any stair-descent-speed data yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ export default function InsightsSchrittlaengePage() {
yAxisUnit="m"
emptyStateIcon={<Footprints className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any walking step length yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any walking step length yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -31,7 +31,7 @@ export default function InsightsStepsPage() {
emptyStateIcon={<Footprints className="size-6" />}
emptyStateCtaType={null}
captureType="ACTIVITY_STEPS"
coachPrefill="I haven't logged any steps yet — what's a reasonable daily step target, and how does walking more help my health?"
coachPrefill="I haven't logged any steps yet. What's a reasonable daily step target, and how does walking more help my health?"
/>
);
}
+1 -1
View File
@@ -25,7 +25,7 @@ export default function InsightsGefaessalterPage() {
unitKey="insights.units.years"
emptyStateIcon={<HeartPulse className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any vascular age yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any vascular age yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ export default function InsightsViszeralfettPage() {
yAxisUnit=""
emptyStateIcon={<Droplets className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any visceral fat yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any visceral fat yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ export default function InsightsGangasymmetriePage() {
yAxisUnit="%"
emptyStateIcon={<Footprints className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any walking asymmetry yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any walking asymmetry yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -24,7 +24,7 @@ export default function InsightsGehstreckePage() {
color="var(--chart-2)"
emptyStateIcon={<Footprints className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any walking and running distance yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any walking and running distance yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ export default function InsightsGehpulsPage() {
yAxisUnit="bpm"
emptyStateIcon={<HeartPulse className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any walking heart rate average yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any walking heart rate average yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -24,7 +24,7 @@ export default function InsightsGehgeschwindigkeitPage() {
color="var(--chart-2)"
emptyStateIcon={<Gauge className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any walking speed yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any walking speed yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ export default function InsightsGangstabilitaetPage() {
yAxisUnit="%"
emptyStateIcon={<Gauge className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any walking steadiness yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any walking steadiness yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+1 -1
View File
@@ -129,7 +129,7 @@ export default function InsightsGewichtPage() {
</Link>
</Button>
}
coachPrefill="I haven't recorded any weight yet — why does it matter, and what should I know before I start tracking?"
coachPrefill="I haven't recorded any weight yet. Why does it matter, and what should I know before I start tracking?"
/>
</SubPageShell>
);
+1 -1
View File
@@ -132,7 +132,7 @@ export default function InsightsWorkoutsPageClient() {
</Button>
) : null
}
coachPrefill="I haven't logged any workouts yet — why does tracking them matter, and what should I focus on first?"
coachPrefill="I haven't logged any workouts yet. Why does tracking them matter, and what should I focus on first?"
/>
) : (
<>
+1 -1
View File
@@ -24,7 +24,7 @@ export default function InsightsWristTemperaturePage() {
color="var(--chart-5)"
emptyStateIcon={<Thermometer className="size-6" />}
emptyStateCtaType={null}
coachPrefill="I haven't logged any wrist-temperature data yet — what does this metric tell me about my health, and how do I improve it?"
coachPrefill="I haven't logged any wrist-temperature data yet. What does this metric tell me about my health, and how do I improve it?"
/>
);
}
+2 -2
View File
@@ -21,7 +21,7 @@ const inter = Inter({
export const metadata: Metadata = {
title: "HealthLog",
description:
"Self-hosted health tracker — weight, blood pressure, glucose, mood, medications. Withings + Apple Health sync, transparent derived wellness metrics, AI Insights you own.",
"Self-hosted health tracker: weight, blood pressure, glucose, mood, medications. Withings + Apple Health sync, transparent derived wellness metrics, AI Insights you own.",
manifest: "/manifest.json",
icons: {
icon: "/favicon.svg",
@@ -51,7 +51,7 @@ export const metadata: Metadata = {
url: "/logo-readme.png",
width: 1000,
height: 1000,
alt: "HealthLog — your health data, your server",
alt: "HealthLog: your health data, your server",
},
],
},
+16 -2
View File
@@ -1,5 +1,6 @@
"use client";
import { MEASUREMENT_TYPE_LABEL_KEYS } from "@/lib/measurements/type-label-keys";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import React, { Suspense, useCallback, useMemo, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
@@ -448,6 +449,19 @@ export default function DashboardPageClient({
const rhr = data?.summaries?.RESTING_HEART_RATE;
const hasRestingHr = (rhr?.count ?? 0) > 0;
const pulseTileSummary = hasRestingHr ? rhr : p;
// The pulse widget shows ONE of two statistics, and its name says which:
// with resting rows it is the resting heart rate (one figure a day, the
// same series the day view names "Resting heart rate"); without them it
// is the pulse, whose latest value is the latest day's pulse (the mean of
// its hours' means, `summaries-slice.ts`), the number the day view shows
// as "Pulse" for that day. Calling the resting figure "Pulse" put 62 on
// the dashboard beside a day view reading 77 for the same name.
const pulseLabel = hasRestingHr
? t(MEASUREMENT_TYPE_LABEL_KEYS.RESTING_HEART_RATE)
: t("dashboard.pulseShort");
const pulseTitle = hasRestingHr
? t(MEASUREMENT_TYPE_LABEL_KEYS.RESTING_HEART_RATE)
: t("dashboard.pulse");
const bf = data?.summaries?.BODY_FAT;
const sleepSummary = data?.summaries?.SLEEP_DURATION;
// v1.11.4 — `summaries.SLEEP_DURATION` now carries per-NIGHT time-asleep
@@ -1131,7 +1145,7 @@ export default function DashboardPageClient({
<TrendCard
key="pulse"
fractionDigits={0}
label={t("dashboard.pulseShort")}
label={pulseLabel}
latest={pulseTileSummary?.latest ?? null}
unit="bpm"
avg7={pulseTileSummary?.avg7 ?? null}
@@ -1809,7 +1823,7 @@ export default function DashboardPageClient({
// rate WITHOUT the resting-band overlay (it would mark
// expected-high workout HR as "outside target").
types={hasRestingHr ? ["RESTING_HEART_RATE"] : ["PULSE"]}
title={t("dashboard.pulse")}
title={pulseTitle}
colors={["var(--success)"]}
unit="bpm"
valueBands={hasRestingHr ? pulseBands : undefined}
+1 -1
View File
@@ -50,7 +50,7 @@ const LAST_UPDATED_EN = new Intl.DateTimeFormat("en-GB", {
}).format(LAST_UPDATED_DATE);
export const metadata: Metadata = {
title: "Datenschutzerklärung / Privacy Policy — HealthLog",
title: "Datenschutzerklärung / Privacy Policy | HealthLog",
description:
"Wie HealthLog personenbezogene Gesundheitsdaten verarbeitet, Auftragsverarbeiter, DSGVO-Rechte und die EU-MDR-Medizinprodukte-Grenze. Bilingual DE/EN.",
robots: { index: true, follow: true },