mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(backup): carry a custom tag's own words and its scale, and stop claiming a guard that does not exist
Four things an independent read found, three of them things this release said about itself that were not true. A custom mood tag travelled without its label, its icon, its sort order and its SCALE. The label is the smaller loss: the person who named a factor "Migräne" would have got a key back. The scale is the real one — the inverse flag marks a factor where a high value is bad, and dropping it does not lose a label, it silently reverses the meaning of every rating already recorded against that tag. Custom cycle symptoms lost their label the same way. Both now carry the ciphertext verbatim, like every other encrypted column in a disaster-recovery payload. The plan file claimed, in the present tense, that a guard requires both a payload reader and a restore branch for every carried model. No such guard exists. The commit that introduced the file said so honestly; the file itself said the opposite, and roughly 31 models have neither end. BACKED_UP means "must be", not "is", and both the plan and its test now say that plainly. The wrong sentence is recorded rather than quietly swapped, because it is the same defect the file was written to prevent. The export route still told operators that labs, allergies, illness episodes and workouts could be exported but never restored. The restore route re-creates all of them, and since this release nutrient day totals too. The scope now points at the plan instead of repeating it beside the code, which is how that sentence came to be wrong. And the export card promised "every record you own". It does not, so it no longer says so — six locales.
This commit is contained in:
1 parent
2449e381b9
commit
4ad498dbff
13 files changed
+71
-21
No files matched your search
+1
-1
@@ -4499,7 +4499,7 @@
|
||||
},
|
||||
"fullBackup": {
|
||||
"title": "Voll-Backup",
|
||||
"description": "Alle deine Daten in einer JSON-Datei — Messwerte, Medikamente, Stimmung, Zyklus, Laborwerte, Krankheitsepisoden, Allergien, Familienanamnese und Workouts. Gleiches Format wie das wöchentliche Auto-Backup.",
|
||||
"description": "Deine Messwerte, Medikamente, Stimmung, Zyklus, Laborwerte, Krankheitsepisoden, Allergien, Familienanamnese, Nährstoffe und Workouts in einer JSON-Datei. Gleiche Form wie die wöchentliche automatische Sicherung. Einige Satzarten sind noch nicht enthalten — das Manifest der Datei nennt, was drinsteht.",
|
||||
"scopeNote": "Dokumentmetadaten und die KI-Zusammenfassung, falls vorhanden, sind als Klartext enthalten; die Originaldateien der Dokumente sowie GPS-Routen und Einzelmesswerte von Workouts sind es nicht — lade diese separat aus dem Dokumententresor bzw. dem Workout-Export herunter.",
|
||||
"encryptToggle": "Mit einer Passphrase verschlüsseln",
|
||||
"passphrasePlaceholder": "Passphrase (mind. 12 Zeichen)",
|
||||
|
||||
+1
-1
@@ -4499,7 +4499,7 @@
|
||||
},
|
||||
"fullBackup": {
|
||||
"title": "Full backup",
|
||||
"description": "Every record you own in one JSON file — measurements, medications, mood, cycle, labs, illness episodes, allergies, family history, and workouts. Same shape as the weekly auto-backup.",
|
||||
"description": "Your measurements, medications, mood, cycle, labs, illness episodes, allergies, family history, nutrients and workouts in one JSON file. Same shape as the weekly auto-backup. Some record types are not carried yet — the file's manifest names what is in it.",
|
||||
"scopeNote": "Document metadata and the AI-generated summary, if one exists, are included as plain text; original document files and workout GPS/sample data are not — download those separately from the vault or the workout export.",
|
||||
"encryptToggle": "Encrypt with a passphrase",
|
||||
"passphrasePlaceholder": "Passphrase (min. 12 characters)",
|
||||
|
||||
+1
-1
@@ -4499,7 +4499,7 @@
|
||||
},
|
||||
"fullBackup": {
|
||||
"title": "Copia completa",
|
||||
"description": "Todos tus datos en un único archivo JSON: mediciones, medicamentos, estado de ánimo, ciclo, analíticas, episodios de enfermedad, alergias, antecedentes familiares y entrenamientos. Mismo formato que la copia automática semanal.",
|
||||
"description": "Tus mediciones, medicamentos, estado de ánimo, ciclo, analíticas, episodios de enfermedad, alergias, antecedentes familiares, nutrientes y entrenamientos en un archivo JSON. El mismo formato que la copia automática semanal. Algunos tipos de registro aún no se incluyen: el manifiesto del archivo indica qué contiene.",
|
||||
"scopeNote": "Los metadatos de los documentos y el resumen de IA, si existe, se incluyen en texto plano; los archivos originales de los documentos y los datos de rutas GPS/muestras de los entrenamientos no están incluidos — descárgalos por separado desde el archivo de documentos o la exportación de entrenamientos.",
|
||||
"encryptToggle": "Cifrar con una contraseña",
|
||||
"passphrasePlaceholder": "Contraseña (mín. 12 caracteres)",
|
||||
|
||||
+1
-1
@@ -4499,7 +4499,7 @@
|
||||
},
|
||||
"fullBackup": {
|
||||
"title": "Sauvegarde complète",
|
||||
"description": "Toutes vos données dans un seul fichier JSON : mesures, médicaments, humeur, cycle, analyses de laboratoire, épisodes de maladie, allergies, antécédents familiaux et entraînements. Même format que la sauvegarde automatique hebdomadaire.",
|
||||
"description": "Vos mesures, médicaments, humeur, cycle, analyses, épisodes de maladie, allergies, antécédents familiaux, nutriments et séances dans un seul fichier JSON. Même format que la sauvegarde automatique hebdomadaire. Certains types d'enregistrement ne sont pas encore inclus : le manifeste du fichier indique ce qu'il contient.",
|
||||
"scopeNote": "Les métadonnées des documents et le résumé IA, s'il existe, sont inclus en texte clair ; les fichiers originaux des documents ainsi que les tracés GPS/données d'échantillons des entraînements ne le sont pas — téléchargez-les séparément depuis le coffre de documents ou l'export des entraînements.",
|
||||
"encryptToggle": "Chiffrer avec une phrase secrète",
|
||||
"passphrasePlaceholder": "Phrase secrète (12 caractères min.)",
|
||||
|
||||
+1
-1
@@ -4499,7 +4499,7 @@
|
||||
},
|
||||
"fullBackup": {
|
||||
"title": "Backup completo",
|
||||
"description": "Tutti i tuoi dati in un unico file JSON: misurazioni, farmaci, umore, ciclo, esami di laboratorio, episodi di malattia, allergie, anamnesi familiare e allenamenti. Stesso formato del backup automatico settimanale.",
|
||||
"description": "Le tue misurazioni, i farmaci, l'umore, il ciclo, le analisi, gli episodi di malattia, le allergie, l'anamnesi familiare, i nutrienti e gli allenamenti in un unico file JSON. Stesso formato del backup automatico settimanale. Alcuni tipi di dato non sono ancora inclusi: il manifest del file indica cosa contiene.",
|
||||
"scopeNote": "I metadati dei documenti e il riepilogo IA, se presente, sono inclusi in chiaro; i file originali dei documenti e i dati di percorso GPS/campionamento degli allenamenti non lo sono — scaricali separatamente dall'archivio documenti o dall'esportazione degli allenamenti.",
|
||||
"encryptToggle": "Cifra con una passphrase",
|
||||
"passphrasePlaceholder": "Passphrase (min. 12 caratteri)",
|
||||
|
||||
+1
-1
@@ -4499,7 +4499,7 @@
|
||||
},
|
||||
"fullBackup": {
|
||||
"title": "Pełna kopia zapasowa",
|
||||
"description": "Wszystkie Twoje dane w jednym pliku JSON — pomiary, leki, nastrój, cykl, wyniki badań laboratoryjnych, epizody chorobowe, alergie, wywiad rodzinny i treningi. Taki sam format jak cotygodniowa automatyczna kopia.",
|
||||
"description": "Twoje pomiary, leki, nastrój, cykl, wyniki badań, epizody chorobowe, alergie, wywiad rodzinny, składniki odżywcze i treningi w jednym pliku JSON. Ten sam kształt co cotygodniowa kopia automatyczna. Niektóre rodzaje wpisów nie są jeszcze przenoszone — manifest pliku wskazuje, co zawiera.",
|
||||
"scopeNote": "Metadane dokumentów oraz podsumowanie AI, jeśli istnieje, są uwzględnione jako czysty tekst; oryginalne pliki dokumentów oraz dane tras GPS/próbek treningów nie są uwzględnione — pobierz je osobno z archiwum dokumentów lub eksportu treningów.",
|
||||
"encryptToggle": "Zaszyfruj hasłem-frazą",
|
||||
"passphrasePlaceholder": "Hasło-fraza (min. 12 znaków)",
|
||||
|
||||
@@ -16,8 +16,11 @@ import {
|
||||
*
|
||||
* This is the first half of the backup guard: classification. The second half
|
||||
* — that every `BACKED_UP` model actually has a payload reader AND a restore
|
||||
* branch — lives beside it, because a model that is classified as carried and
|
||||
* then never restored is the defect, not the fix.
|
||||
* branch — DOES NOT EXIST YET. It is the harder half and the one that matters,
|
||||
* because a model classified as carried and then never restored is the defect
|
||||
* rather than the fix, and this test would pass either way.
|
||||
*
|
||||
* Until it lands, `BACKED_UP` means "must be carried", not "is carried".
|
||||
*
|
||||
* The failure this prevents: a model is added to `schema.prisma`, it is
|
||||
* user-scoped from birth, and it is outside the backup by default. Nothing
|
||||
|
||||
@@ -620,6 +620,12 @@ const handler = apiHandler(
|
||||
categoryId: tag.categoryId,
|
||||
kind: tag.kind,
|
||||
isActive: tag.isActive,
|
||||
icon: tag.icon ?? null,
|
||||
sortOrder: tag.sortOrder,
|
||||
labelEncrypted: tag.labelEncrypted ?? null,
|
||||
scaleMin: tag.scaleMin,
|
||||
scaleMax: tag.scaleMax,
|
||||
inverse: tag.inverse,
|
||||
},
|
||||
// `key` is globally unique, so a seeded key would collide.
|
||||
// Leave the catalogue row alone; the links resolve either way.
|
||||
|
||||
@@ -11,15 +11,19 @@
|
||||
* `.json` so the browser writes a `.json` file even though the route
|
||||
* segment doesn't carry the extension.
|
||||
*
|
||||
* Backup-completeness — the payload also carries lab results, the
|
||||
* biomarker catalog, illness episodes (incl. flares/exacerbations) with
|
||||
* their day-logs, allergies, family history, and workout summaries. These
|
||||
* domains are EXPORT-ONLY today: `POST /api/admin/backups/[id]/restore`
|
||||
* only re-creates the original measurements/medications/mood/cycle
|
||||
* domains, so restoring a snapshot does not (yet) recreate these sections
|
||||
* — see `src/lib/export/records-backup.ts`. Document original files and
|
||||
* workout GPS/sample time series are never included; the payload's
|
||||
* `manifest` field discloses both, and the export UI states this too.
|
||||
* Backup completeness — what this file carries, and what it does not, is
|
||||
* declared in `src/lib/export/backup-plan.ts` rather than described here. A
|
||||
* sentence beside the code is how the previous version of this comment came
|
||||
* to be wrong: it said lab results, biomarkers, illness episodes, allergies,
|
||||
* family history and workouts were EXPORT-ONLY and could not be restored.
|
||||
* The restore route re-creates all of them, and since v1.33.1 nutrient day
|
||||
* totals as well.
|
||||
*
|
||||
* The gap that IS real: roughly 31 of the models the plan marks `BACKED_UP`
|
||||
* have neither a reader here nor a restore branch yet. The plan names each
|
||||
* one. Document original files and workout GPS/sample time series are never
|
||||
* included; the payload's `manifest` field discloses both, and the export UI
|
||||
* states this too.
|
||||
*
|
||||
* Auth: cookie session OR Bearer token (`requireAuth`).
|
||||
* Rate-limit: shared `export:<userId>` bucket (10/h).
|
||||
|
||||
@@ -230,6 +230,7 @@ export async function buildCycleBackupSection(
|
||||
icon: sym.icon,
|
||||
sortOrder: sym.sortOrder,
|
||||
isActive: sym.isActive,
|
||||
labelEncrypted: sym.labelEncrypted,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -404,6 +405,7 @@ export async function restoreCycleData(
|
||||
icon: sym.icon ?? null,
|
||||
sortOrder: sym.sortOrder,
|
||||
isActive: sym.isActive,
|
||||
labelEncrypted: sym.labelEncrypted ?? null,
|
||||
},
|
||||
// `key` is globally unique, so a seeded key would collide. Leave the
|
||||
// catalogue row alone: the account's links resolve against it either way.
|
||||
|
||||
@@ -20,10 +20,22 @@
|
||||
* ── The three verdicts ──────────────────────────────────────────────────────
|
||||
*
|
||||
* `BACKED_UP` — the account would lose something real if this were missing.
|
||||
* The guard requires BOTH a payload reader AND a restore
|
||||
* branch for every one of these, because carried-but-never-
|
||||
* restored is the exact defect this file exists to prevent and
|
||||
* it looks identical to working until someone needs it.
|
||||
*
|
||||
* **Read this as "must be", not "is".** Today the guard beside
|
||||
* this file checks only that every model HAS a verdict. The
|
||||
* two-ended check — that each `BACKED_UP` model has both a
|
||||
* payload reader and a restore branch — does not exist yet,
|
||||
* and roughly 31 of the models listed here have neither end.
|
||||
* Three were closed in v1.33.1 (custom cycle symptoms,
|
||||
* nutrient day totals, custom mood tags); the rest are named
|
||||
* here so the gap is written down instead of invisible, which
|
||||
* is the only honest thing this list can do before the guard
|
||||
* exists.
|
||||
*
|
||||
* An earlier draft of this comment claimed the two-ended guard
|
||||
* in the present tense. It did not exist then either. That is
|
||||
* the same defect this file was written to prevent, so it is
|
||||
* recorded rather than quietly corrected.
|
||||
* `DERIVED` — recomputable from `BACKED_UP` rows. Leaving it out keeps the
|
||||
* file smaller and cannot lose anything; the reason names what
|
||||
* recomputes it.
|
||||
|
||||
@@ -326,6 +326,14 @@ export async function buildFullBackupPayload(
|
||||
categoryId: tag.categoryId,
|
||||
kind: tag.kind,
|
||||
isActive: tag.isActive,
|
||||
icon: tag.icon,
|
||||
sortOrder: tag.sortOrder,
|
||||
// Ciphertext verbatim, like every other *Encrypted column in a DR
|
||||
// payload: the same instance's key reads it back unchanged.
|
||||
labelEncrypted: tag.labelEncrypted,
|
||||
scaleMin: tag.scaleMin,
|
||||
scaleMax: tag.scaleMax,
|
||||
inverse: tag.inverse,
|
||||
})),
|
||||
cycleProfile: cycle.cycleProfile,
|
||||
cycles: cycle.cycles,
|
||||
|
||||
@@ -331,6 +331,19 @@ const customMoodTagSchema = z
|
||||
// "BINARY" | "RATED", enforced in app code rather than a DB enum.
|
||||
kind: z.string().min(1),
|
||||
isActive: z.boolean().default(true),
|
||||
icon: z.string().nullable().optional(),
|
||||
sortOrder: z.number().int().default(0),
|
||||
// The user's own words for the tag, encrypted at rest and carried
|
||||
// verbatim. Without it the tag comes back as a bare key and the person
|
||||
// who named it "Migräne" gets `custom:cm3x9…` instead.
|
||||
labelEncrypted: z.string().nullable().optional(),
|
||||
// The scale a RATED factor was recorded on. `inverse` marks a factor
|
||||
// where a HIGH value is bad (stress, conflict). Dropping it does not lose
|
||||
// a label, it silently reverses the meaning of every rating already
|
||||
// stored against that tag.
|
||||
scaleMin: z.number().int().default(1),
|
||||
scaleMax: z.number().int().default(5),
|
||||
inverse: z.boolean().default(false),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -372,6 +385,8 @@ const customCycleSymptomSchema = z
|
||||
icon: z.string().nullable().optional(),
|
||||
sortOrder: z.number().int().default(0),
|
||||
isActive: z.boolean().default(true),
|
||||
// Same reasoning as the mood tag's: the user's own words live here.
|
||||
labelEncrypted: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
|
||||
Reference in new issue
Block a user