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

@@ -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;