Files
HealthLog/src/lib/validations/backup-summary.ts
T
Marc-André Bombeck fea1d1e0b5 chore: strip planning tags from comments and enable the rule
Output of `node scripts/strip-planning-tags.mjs --write`: 3,157 comment
lines in 1,308 files lose the work-item identifier at their head. Release
versions and issue references stay; nothing outside comment text
changes, which the script checks per file against a comment-free
printout. healthlog/no-planning-tags is enabled at error level with this
commit, so the tree it lands on is clean.
2026-10-20 09:08:40 +02:00

231 lines
9.7 KiB
TypeScript

/**
* What a backup is, without the schema that checks one.
*
* `backupPayloadSchema` (`backup.ts`) is the largest Zod tree in the server:
* built at import, it holds about 12 MB of heap. Everything here is what the
* backup paths need besides that check: the schema version, whether a file's
* version is one this server reads, and the per-section counts of a payload
* that has already been parsed. Importing this module never builds the
* schema, so a module on the boot path (the restore job, the stored-backup
* readers, the export writer) can import it freely and load `backup.ts` with
* a dynamic `import()` at the moment a file is actually validated.
*/
import type { BackupPayload } from "@/lib/validations/backup";
export const BACKUP_SCHEMA_VERSION = "2" as const;
export const LEGACY_BACKUP_SCHEMA_VERSION = "1" as const;
/**
* Numeric counts of each backed-up record kind. Returned in the
* upload + restore API responses so the admin sees what they
* uploaded/restored without having to download the file again.
*/
export interface BackupSummary {
schemaVersion: string;
userId: string;
exportedAt: string;
measurements: number;
medications: number;
intakeEvents: number;
/** Side effects recorded against a drug, across every medication. */
medicationSideEffects: number;
/** Pinned efficacy targets, across every medication. */
medicationEfficacyTargets: number;
/** Archived schedule eras, across every medication. */
medicationScheduleRevisions: number;
moodEntries: number;
/** v1.15.0 — observed cycle spans in the backup. */
cycles: number;
/** v1.15.0 — cycle day-logs in the backup. */
cycleDayLogs: number;
/** Lab results in the backup. */
labResults: number;
nutrientDays: number;
/** User-scoped biomarker catalog entries in the backup. */
biomarkers: number;
/** Illness episodes, including flares/exacerbations. */
illnessEpisodes: number;
/** Illness day-logs across every episode. */
illnessDayLogs: number;
/** Allergy/intolerance records in the backup. */
allergies: number;
/** Family-history entries in the backup. */
familyHistory: number;
/** Workout summary records in the backup. */
workouts: number;
/** Document records (ciphertext included in canonical DR payloads). */
documents: number;
/** Which documents were filed against which condition. */
documentConditionLinks: number;
/** Facts staged out of a document, with their review decision. */
extractedFacts: number;
/** 1 when the account's durable self-context rides the file, 0 otherwise. */
healthProfile: number;
/** Effective-dated structured health-profile revisions. */
healthProfileFactRevisions: number;
/** Metrics the account defined itself. */
customMetrics: number;
/** Readings across every user-defined metric. */
customMetricEntries: number;
/** Persisted accepted correlation identities and dismissal decisions. */
correlationPatterns: number;
/** Stored day curves for the cumulative metrics, across every metric. */
intradayProfiles: number;
/** Local days whose health score was written down as it was shown. */
healthScoreRecords: number;
/** 1 when the record's setup answers ride the file, 0 otherwise. */
onboardingRecords: number;
/** v1.37.19 — the visit address book. */
practitioners: number;
/** v1.37.19 — doctor visits, planned and past. */
encounters: number;
/** v1.37.19 — the three encounter link tables, summed. */
encounterLinks: number;
/** v1.37.19 — immunization log entries. */
vaccinations: number;
/** v1.37.19 — vaccination↔document links. */
vaccinationLinks: number;
/** v1.43 — the five relation link tables, summed. */
relationLinks: number;
/** v1.42 (#1005) — the person's own vaccine definitions. */
customVaccines: number;
/** v1.37.20 (#223 / iOS #68) — Vorsorge reminder cadences. */
measurementReminders: number;
/** v1.37.20 (#223 / iOS #68) — completion-ledger rows across every reminder. */
measurementReminderEvents: number;
/** Coach conversation threads. */
coachConversations: number;
/** Coach turns across every thread. */
coachMessages: number;
/** Durable facts, agreed plans and things to bring back up. */
coachFacts: number;
coachPlans: number;
coachReminders: number;
/** Completed screener administrations. Disaster-recovery payloads only. */
mentalHealthAssessments: number;
/** Consent records. Disaster-recovery payloads only. */
consentReceipts: number;
/** Personal bests across every metric and sport slot. */
personalRecords: number;
/** Badges the account has unlocked, each with the day it was earned. */
userAchievements: number;
/** Local days with an environmental reading. */
environmentContexts: number;
/** Declared stretches spent away from home. */
environmentTravelLocations: number;
/** Local days of Dawarich movement values. */
dawarichDays: number;
/** ECG strips, with their rhythm verdict and their trace. */
ecgRecordings: number;
}
export function summarizeBackup(payload: BackupPayload): BackupSummary {
return {
schemaVersion: payload.schemaVersion,
userId: payload.userId,
exportedAt: payload.exportedAt,
measurements: payload.measurements.length,
medications: payload.medications.length,
intakeEvents: payload.intakeEvents.length,
medicationSideEffects: payload.medications.reduce(
(sum, medication) => sum + medication.sideEffects.length,
0,
),
medicationEfficacyTargets: payload.medications.reduce(
(sum, medication) => sum + medication.efficacyTargets.length,
0,
),
medicationScheduleRevisions: payload.medications.reduce(
(sum, medication) => sum + medication.scheduleRevisions.length,
0,
),
moodEntries: payload.moodEntries.length,
cycles: payload.cycles.length,
cycleDayLogs: payload.cycleDayLogs.length,
nutrientDays: payload.nutrientDays.length,
labResults: payload.labResults.length,
biomarkers: payload.biomarkers.length,
illnessEpisodes: payload.illnessEpisodes.length,
illnessDayLogs: payload.illnessEpisodes.reduce(
(sum, e) => sum + e.dayLogs.length,
0,
),
allergies: payload.allergies.length,
familyHistory: payload.familyHistory.length,
workouts: payload.workouts.length,
documents: payload.documents.length,
// Counted from the release that carries them, so the admin's "what did I
// just restore" answer never under-counts a filed vault the way it once
// did for visits.
documentConditionLinks: payload.documentConditionLinks.length,
extractedFacts: payload.extractedFacts.length,
healthProfile: payload.healthProfile ? 1 : 0,
healthProfileFactRevisions: payload.healthProfileFacts.length,
customMetrics: payload.customMetrics.length,
customMetricEntries: payload.customMetrics.reduce(
(sum, metric) => sum + metric.entries.length,
0,
),
correlationPatterns: payload.correlationPatterns.length,
intradayProfiles: payload.intradayProfiles.length,
healthScoreRecords: payload.healthScoreRecords.length,
onboardingRecords: payload.onboardingRecord ? 1 : 0,
// v1.37.19 — the sections restored since 08-01 were written and
// restored but absent from this report, so the admin's "what did I just
// restore" answer silently under-counted a file that carried visits or
// an Impfpass.
practitioners: payload.practitioners.length,
encounters: payload.encounters.length,
encounterLinks:
payload.encounterDocumentLinks.length +
payload.encounterLabLinks.length +
payload.encounterConditionLinks.length,
vaccinations: payload.vaccinations.length,
vaccinationLinks: payload.vaccinationDocumentLinks.length,
relationLinks:
payload.labResultDocumentLinks.length +
payload.encounterMedicationLinks.length +
payload.encounterVaccinationLinks.length +
payload.medicationDocumentLinks.length +
payload.medicationConditionLinks.length,
customVaccines: payload.customVaccines.length,
// v1.37.20 (#223 / iOS #68) — counted from the release that carries them,
// so the admin's "what did I just restore" answer never under-counts a
// file with reminders the way it once did for visits.
measurementReminders: payload.measurementReminders.length,
measurementReminderEvents: payload.measurementReminderEvents.length,
coachConversations: payload.coachConversations.length,
coachMessages: payload.coachConversations.reduce(
(total, conversation) => total + conversation.messages.length,
0,
),
coachFacts: payload.coachFacts.length,
coachPlans: payload.coachPlans.length,
coachReminders: payload.coachReminders.length,
mentalHealthAssessments: payload.mentalHealthAssessments.length,
consentReceipts: payload.consentReceipts.length,
// Counted from the release that carries them, so the admin's "what did I
// just restore" answer never under-counts a file with bests, badges or an
// environmental history the way it once did for visits.
personalRecords: payload.personalRecords.length,
userAchievements: payload.userAchievements.length,
environmentContexts: payload.environmentContexts.length,
environmentTravelLocations: payload.environmentTravelLocations.length,
dawarichDays: payload.dawarichDays.length,
ecgRecordings: payload.ecgRecordings.length,
};
}
/**
* The schemaVersion the system understands today. Used by the upload
* route to reject inbound files written by a *future* HealthLog instance
* — restoring them under the current code might silently drop data the
* new shape carried.
*/
export function isCompatibleSchemaVersion(version: string): boolean {
return (
version === LEGACY_BACKUP_SCHEMA_VERSION ||
version === BACKUP_SCHEMA_VERSION
);
}