mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
chore(release): v1.4.30
Server-side prep for the next iOS TestFlight build. Five surfaces land together so the iOS engineer can pick them up in one cut-over: a locked externalId shape for daily-aggregated cumulative HealthKit rows, the SyncMode foundation columns + handshake + bulk-backfill endpoints, a first-class MoodEntry.note column that replaces the legacy tag-based workaround, a cross-source workout dedup helper, and two new MeasurementType enums plus the shared category overlay that drives the iOS permission picker and the future Insights nav.
This commit is contained in:
1 parent
945b556664
commit
3ece95f192
49 files changed
+4744
-32
No files matched your search
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* scripts/backfill-mood-note-column.ts
|
||||
*
|
||||
* One-shot backfill that lifts existing `tags: ["note:<text>"]`
|
||||
* entries out of `mood_entries.tags` into the new `mood_entries.note`
|
||||
* column. Operator runs this once after the v1.4.30 schema migration
|
||||
* applies.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. SELECT every MoodEntry whose `tags` JSON contains an entry
|
||||
* that begins with `note:`.
|
||||
* 2. Decode the JSON array, pull the first `note:<text>` entry as
|
||||
* the new note value, drop the entry from the array.
|
||||
* 3. UPDATE the row in a transaction: `note = <decoded text>`,
|
||||
* `tags = <slimmed JSON or null if empty>`.
|
||||
*
|
||||
* Idempotent — a row whose `tags` no longer carries any `note:`
|
||||
* entries is left alone. Re-running on a clean dataset is a no-op.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm tsx scripts/backfill-mood-note-column.ts # dry-run
|
||||
* pnpm tsx scripts/backfill-mood-note-column.ts --confirm # commit
|
||||
*/
|
||||
import "dotenv/config";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import { extractNoteAndSlimTags } from "@/lib/mood/note-backfill";
|
||||
|
||||
interface ParsedArgs {
|
||||
confirm: boolean;
|
||||
}
|
||||
|
||||
function parseArgs(argv: readonly string[]): ParsedArgs {
|
||||
const out: ParsedArgs = { confirm: false };
|
||||
for (const arg of argv) {
|
||||
if (arg === "--confirm") out.confirm = true;
|
||||
else if (arg === "--help" || arg === "-h") {
|
||||
console.log(
|
||||
"Usage: pnpm tsx scripts/backfill-mood-note-column.ts [--confirm]",
|
||||
);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
interface RowSnapshot {
|
||||
id: string;
|
||||
tags: string | null;
|
||||
note: string | null;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const dryRun = !args.confirm;
|
||||
|
||||
if (dryRun) {
|
||||
console.log("[backfill-note] DRY RUN — no rows will be written. Pass --confirm to commit.");
|
||||
} else {
|
||||
console.log("[backfill-note] COMMIT MODE — updating rows in place.");
|
||||
}
|
||||
|
||||
// Naive scan: filter in-memory rather than via JSON predicates so
|
||||
// the query stays portable across Postgres minor versions. The
|
||||
// mood_entries table is small (per-user lifetime budget).
|
||||
const candidates: RowSnapshot[] = await prisma.moodEntry.findMany({
|
||||
where: {
|
||||
tags: { not: null },
|
||||
note: null,
|
||||
},
|
||||
select: { id: true, tags: true, note: true },
|
||||
});
|
||||
|
||||
console.log(`[backfill-note] scanned ${candidates.length} candidate rows`);
|
||||
|
||||
let updated = 0;
|
||||
for (const row of candidates) {
|
||||
if (!row.tags) continue;
|
||||
const { note, newTags } = extractNoteAndSlimTags(row.tags);
|
||||
if (note === null) continue;
|
||||
|
||||
updated += 1;
|
||||
if (!dryRun) {
|
||||
await prisma.moodEntry.update({
|
||||
where: { id: row.id },
|
||||
data: { note, tags: newTags },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[backfill-note] ${updated} row${updated === 1 ? "" : "s"} ${dryRun ? "would be" : "were"} updated`);
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
main().catch((err: unknown) => {
|
||||
console.error("[backfill-note] failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* scripts/drain-per-sample-cumulative.ts
|
||||
*
|
||||
* One-shot maintenance tool that collapses pre-Option-A per-sample
|
||||
* APPLE_HEALTH cumulative rows into one row per day per cumulative
|
||||
* type. See R-A §6 / v1.4.30 §"Cutover sequence".
|
||||
*
|
||||
* Scope per `CUMULATIVE_HK_TYPES`:
|
||||
* ACTIVITY_STEPS, ACTIVE_ENERGY_BURNED, FLIGHTS_CLIMBED,
|
||||
* WALKING_RUNNING_DISTANCE, TIME_IN_DAYLIGHT.
|
||||
*
|
||||
* Idempotent — re-running after a successful drain is a no-op.
|
||||
*
|
||||
* Usage:
|
||||
* # Dry-run for one user (default; no DB writes):
|
||||
* pnpm tsx scripts/drain-per-sample-cumulative.ts --user clx123
|
||||
*
|
||||
* # Commit the drain for one user:
|
||||
* pnpm tsx scripts/drain-per-sample-cumulative.ts --user clx123 --confirm
|
||||
*
|
||||
* # Dry-run for every user:
|
||||
* pnpm tsx scripts/drain-per-sample-cumulative.ts --all
|
||||
*
|
||||
* # Commit the drain for every user (requires --confirm):
|
||||
* pnpm tsx scripts/drain-per-sample-cumulative.ts --all --confirm
|
||||
*
|
||||
* Operator runs this once after the iOS TestFlight build adopts
|
||||
* `HealthKitStatisticsService.swift`. The drain runs idempotently; a
|
||||
* re-run reports zero buckets collapsed.
|
||||
*/
|
||||
import "dotenv/config";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import { drainPerSampleCumulative } from "@/lib/measurements/drain-per-sample-cumulative";
|
||||
|
||||
interface ParsedArgs {
|
||||
userId?: string;
|
||||
all: boolean;
|
||||
confirm: boolean;
|
||||
}
|
||||
|
||||
function parseArgs(argv: readonly string[]): ParsedArgs {
|
||||
const out: ParsedArgs = { all: false, confirm: false };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--user") {
|
||||
out.userId = argv[++i];
|
||||
} else if (arg === "--all") {
|
||||
out.all = true;
|
||||
} else if (arg === "--confirm") {
|
||||
out.confirm = true;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
console.log(
|
||||
"Usage: pnpm tsx scripts/drain-per-sample-cumulative.ts [--user <id> | --all] [--confirm]",
|
||||
);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.userId && !args.all) {
|
||||
console.error(
|
||||
"Pick a scope: --user <id> (single user) or --all (every user). Default mode is dry-run; pass --confirm to commit.",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
if (args.userId && args.all) {
|
||||
console.error("--user and --all are mutually exclusive.");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const dryRun = !args.confirm;
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
"[drain] DRY RUN — no rows will be written. Pass --confirm to commit.",
|
||||
);
|
||||
} else {
|
||||
console.log("[drain] COMMIT MODE — collapsing rows in place.");
|
||||
}
|
||||
|
||||
try {
|
||||
const summary = await drainPerSampleCumulative(prisma, {
|
||||
userId: args.userId,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
console.log("[drain] summary:");
|
||||
console.log(JSON.stringify(summary.totals, null, 2));
|
||||
if (summary.buckets.length > 0) {
|
||||
console.log(
|
||||
`[drain] first ${Math.min(5, summary.buckets.length)} buckets (of ${summary.buckets.length}):`,
|
||||
);
|
||||
for (const b of summary.buckets.slice(0, 5)) {
|
||||
console.log(
|
||||
` - user=${b.userId} type=${b.type} day=${b.dateKey} samples=${b.perSampleCount} sum=${b.sumValue} extId=${b.externalId}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log("[drain] no buckets needed collapsing — already at parity.");
|
||||
}
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err: unknown) => {
|
||||
console.error("[drain] failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in new issue
Block a user