mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
Merge the final display polish into release/v1.42.0
This commit is contained in:
commit
dcfec1f35d
144 files changed
+4866
-2817
No files matched your search
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
);
|
||||
|
||||
@@ -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" },
|
||||
);
|
||||
|
||||
@@ -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" },
|
||||
);
|
||||
|
||||
@@ -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" },
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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" },
|
||||
);
|
||||
|
||||
@@ -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" },
|
||||
);
|
||||
|
||||
@@ -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" },
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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?"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in new issue
Block a user