diff --git a/messages/de.json b/messages/de.json
index 951aa164c..340969ccb 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -2865,7 +2865,8 @@
"flights": "Etagen",
"falls": "Stürze",
"years": "Jahre",
- "breathingEvents": "Ereignisse"
+ "breathingEvents": "Ereignisse",
+ "respiratoryRate": "Atemzüge/min"
},
"sleep": {
"title": "Schlaf",
diff --git a/messages/en.json b/messages/en.json
index ead59de68..df7fa6515 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -2865,7 +2865,8 @@
"flights": "flights",
"falls": "falls",
"years": "years",
- "breathingEvents": "events"
+ "breathingEvents": "events",
+ "respiratoryRate": "breaths/min"
},
"sleep": {
"title": "Sleep",
diff --git a/messages/es.json b/messages/es.json
index 088243721..c4f599020 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -2865,7 +2865,8 @@
"flights": "pisos",
"falls": "caídas",
"years": "años",
- "breathingEvents": "eventos"
+ "breathingEvents": "eventos",
+ "respiratoryRate": "respiraciones/min"
},
"sleep": {
"title": "Sueño",
diff --git a/messages/fr.json b/messages/fr.json
index ac626a5e9..5120603c8 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -2865,7 +2865,8 @@
"flights": "étages",
"falls": "chutes",
"years": "ans",
- "breathingEvents": "événements"
+ "breathingEvents": "événements",
+ "respiratoryRate": "respirations/min"
},
"sleep": {
"title": "Sommeil",
diff --git a/messages/it.json b/messages/it.json
index 882ca79f3..e4f2e2566 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -2865,7 +2865,8 @@
"flights": "piani",
"falls": "cadute",
"years": "anni",
- "breathingEvents": "eventi"
+ "breathingEvents": "eventi",
+ "respiratoryRate": "respiri/min"
},
"sleep": {
"title": "Sonno",
diff --git a/messages/pl.json b/messages/pl.json
index 1e1f83bcc..c696e7113 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -2865,7 +2865,8 @@
"flights": "piętra",
"falls": "upadki",
"years": "lata",
- "breathingEvents": "zdarzenia"
+ "breathingEvents": "zdarzenia",
+ "respiratoryRate": "oddechów/min"
},
"sleep": {
"title": "Sen",
diff --git a/src/__tests__/backup-plan-classification.test.ts b/src/__tests__/backup-plan-classification.test.ts
index 676a3feb2..29433f3b6 100644
--- a/src/__tests__/backup-plan-classification.test.ts
+++ b/src/__tests__/backup-plan-classification.test.ts
@@ -187,12 +187,15 @@ function relationFieldNames(model: string): string[] {
const READ_OPS = /(findMany|findUnique|findFirst|findUniqueOrThrow|groupBy)/;
const WRITE_OPS = /(create|createMany|upsert|update|updateMany)/;
+const READ_RELATION_OPS = /[{[t]/;
+const WRITE_RELATION_OPS = /\{\s*create\b/;
-/** Does any of `files` touch `model` with an operation matching `ops`? */
+/** Does any of `files` touch `model` through matching delegate and relation operations? */
function touches(
files: readonly string[],
model: string,
- ops: RegExp,
+ delegateOps: RegExp,
+ relationOps: RegExp,
): boolean {
const delegate = delegateName(model);
const relations = relationFieldNames(model);
@@ -202,16 +205,28 @@ function touches(
for (const match of source.matchAll(
new RegExp(`\\.${delegate}\\s*\\.\\s*(\\w+)`, "g"),
)) {
- if (ops.test(match[1])) return true;
+ if (delegateOps.test(match[1])) return true;
}
- // A nested relation key: `schedules: {`, `dayLogs: [`, `entries: true`.
return relations.some((relation) =>
- new RegExp(`\\b${relation}\\s*:\\s*[{[t]`).test(source),
+ new RegExp(`\\b${relation}\\s*:\\s*(?:${relationOps.source})`).test(
+ source,
+ ),
);
});
}
describe("every backed-up model travels both ways, or is named as debt", () => {
+ it("does not count a nested relation read as a restore write", () => {
+ expect(
+ touches(
+ ["src/app/api/admin/backups/[id]/restore/route.ts"],
+ "User",
+ WRITE_OPS,
+ WRITE_RELATION_OPS,
+ ),
+ ).toBe(false);
+ });
+
it("declares writer and restore files that exist", () => {
for (const file of [...BACKUP_WRITER_FILES, ...BACKUP_RESTORE_FILES]) {
expect(() => read(file), `${file} is declared but missing`).not.toThrow();
@@ -250,7 +265,8 @@ describe("every backed-up model travels both ways, or is named as debt", () => {
it("finds a payload reader for every model claimed two-ended", () => {
const missing = TWO_ENDED_MODELS.filter(
- (model) => !touches(BACKUP_WRITER_FILES, model, READ_OPS),
+ (model) =>
+ !touches(BACKUP_WRITER_FILES, model, READ_OPS, READ_RELATION_OPS),
);
expect(
missing,
@@ -261,7 +277,8 @@ describe("every backed-up model travels both ways, or is named as debt", () => {
it("finds a restore branch for every model claimed two-ended", () => {
const missing = TWO_ENDED_MODELS.filter(
- (model) => !touches(BACKUP_RESTORE_FILES, model, WRITE_OPS),
+ (model) =>
+ !touches(BACKUP_RESTORE_FILES, model, WRITE_OPS, WRITE_RELATION_OPS),
);
expect(
missing,
diff --git a/src/__tests__/insights-unit-literal-guard.test.ts b/src/__tests__/insights-unit-literal-guard.test.ts
index d65de1b44..907dbd673 100644
--- a/src/__tests__/insights-unit-literal-guard.test.ts
+++ b/src/__tests__/insights-unit-literal-guard.test.ts
@@ -34,7 +34,6 @@ const LANGUAGE_NEUTRAL_UNITS = new Set([
"%",
"/10",
"bpm",
- "breaths/min",
"dBA",
"kcal",
"kg/m²",
diff --git a/src/app/insights/respiratory-rate/page.tsx b/src/app/insights/respiratory-rate/page.tsx
index 726f6a216..90b47e650 100644
--- a/src/app/insights/respiratory-rate/page.tsx
+++ b/src/app/insights/respiratory-rate/page.tsx
@@ -22,8 +22,7 @@ export default function InsightsAtemfrequenzPage() {
i18nPrefix="insights.respiratoryRate"
explainerMetric="respiratoryRate"
color="var(--info)"
- unit="breaths/min"
- yAxisUnit="breaths/min"
+ unitKey="insights.units.respiratoryRate"
emptyStateIcon={}
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?"
diff --git a/src/lib/export/__tests__/intraday-profile-backup.test.ts b/src/lib/export/__tests__/intraday-profile-backup.test.ts
index 946666766..b41a1915c 100644
--- a/src/lib/export/__tests__/intraday-profile-backup.test.ts
+++ b/src/lib/export/__tests__/intraday-profile-backup.test.ts
@@ -7,6 +7,8 @@
* which is the failure mode the whole backup guard exists to make loud.
*/
import { describe, expect, it, vi } from "vitest";
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
// The slot-count module reaches for the global Prisma client at load; this
// test only wants the constant off it.
@@ -73,6 +75,13 @@ function tx(created: Array>) {
}
describe("intraday profile backup", () => {
+ it("keeps the backup source reviewable as text", () => {
+ const source = readFileSync(
+ resolve(process.cwd(), "src/lib/export/intraday-profile-backup.ts"),
+ );
+ expect(source.includes(0)).toBe(false);
+ });
+
it("carries the same slot count the reader indexes by", () => {
// Re-declared rather than imported, so it is pinned here instead: a change
// to one without the other would put rows in the table that every read
diff --git a/src/lib/export/intraday-profile-backup.ts b/src/lib/export/intraday-profile-backup.ts
index 233ea2630..3ce7caf8f 100644
Binary files a/src/lib/export/intraday-profile-backup.ts and b/src/lib/export/intraday-profile-backup.ts differ