style: apply prettier across the tree

One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
This commit is contained in:
Marc-André Bombeck committed 2026-06-19 22:20:31 +02:00
1 parent b3fea2ef59
commit 1f4ba5939d
1000 files changed
+12130 -8385

No files matched your search

+27 -15
View File
@@ -295,34 +295,40 @@ describe("fetchDataPoints", () => {
// steps / distance / calories / floors, sleep and exercise are INTERVAL data
// types — Google anchors them on `interval.start_time`. A sample_time filter
// 400s/empties for these and stalls incremental sync.
const stepsMock = installFetchMock([{ status: 200, body: { dataPoints: [] } }]);
const stepsMock = installFetchMock([
{ status: 200, body: { dataPoints: [] } },
]);
await fetchDataPoints(FITBIT_DATA_TYPES.steps, "at", "fetchSteps", {
start: new Date("2026-03-04T10:00:00.000Z"),
});
expect(
new URL((stepsMock.mock.calls[0] as unknown as [string])[0]).searchParams.get(
"filter",
),
new URL(
(stepsMock.mock.calls[0] as unknown as [string])[0],
).searchParams.get("filter"),
).toBe('steps.interval.start_time >= "2026-03-04T10:00:00.000Z"');
const sleepMock = installFetchMock([{ status: 200, body: { dataPoints: [] } }]);
const sleepMock = installFetchMock([
{ status: 200, body: { dataPoints: [] } },
]);
await fetchDataPoints(FITBIT_DATA_TYPES.sleep, "at", "fetchSleep", {
start: new Date("2026-03-04T10:00:00.000Z"),
});
expect(
new URL((sleepMock.mock.calls[0] as unknown as [string])[0]).searchParams.get(
"filter",
),
new URL(
(sleepMock.mock.calls[0] as unknown as [string])[0],
).searchParams.get("filter"),
).toBe('sleep.interval.start_time >= "2026-03-04T10:00:00.000Z"');
const exMock = installFetchMock([{ status: 200, body: { dataPoints: [] } }]);
const exMock = installFetchMock([
{ status: 200, body: { dataPoints: [] } },
]);
await fetchDataPoints(FITBIT_DATA_TYPES.exercise, "at", "fetchExercise", {
start: new Date("2026-03-04T10:00:00.000Z"),
});
expect(
new URL((exMock.mock.calls[0] as unknown as [string])[0]).searchParams.get(
"filter",
),
new URL(
(exMock.mock.calls[0] as unknown as [string])[0],
).searchParams.get("filter"),
).toBe('exercise.interval.start_time >= "2026-03-04T10:00:00.000Z"');
});
@@ -466,7 +472,9 @@ describe("activity mappers (cumulative daily)", () => {
});
it("maps distance in metres, converting km → m when reported in km", () => {
expect(mapDistance({ distance: { meters: 6200, interval } })[0]).toMatchObject({
expect(
mapDistance({ distance: { meters: 6200, interval } })[0],
).toMatchObject({
type: "WALKING_RUNNING_DISTANCE",
value: 6200,
unit: "m",
@@ -506,7 +514,9 @@ describe("activity mappers (cumulative daily)", () => {
expect(out[0]).toMatchObject({ type: "VO2_MAX", value: 47.3 });
// VO2 max of 0 is garbage — dropped (unlike the running totals).
expect(
mapVo2Max({ vo2_max: { milliliters_per_kilogram_per_minute: 0, date: day } }),
mapVo2Max({
vo2_max: { milliliters_per_kilogram_per_minute: 0, date: day },
}),
).toHaveLength(0);
});
});
@@ -667,7 +677,9 @@ describe("workout mapping", () => {
});
it("returns null for a session with no usable time span", () => {
expect(mapWorkout({ exercise: { startTime: "2026-05-10T07:00:00.000Z" } })).toBeNull();
expect(
mapWorkout({ exercise: { startTime: "2026-05-10T07:00:00.000Z" } }),
).toBeNull();
expect(
mapWorkout({
exercise: {
@@ -72,9 +72,9 @@ describe("classifyFitbitError", () => {
});
it("parses the status out of a legacy message shape", () => {
expect(classifyFitbitError(new Error("Fitbit fetchProfile error: 401"))).toBe(
"reauth_required",
);
expect(
classifyFitbitError(new Error("Fitbit fetchProfile error: 401")),
).toBe("reauth_required");
expect(classifyFitbitError(new Error("Fitbit sync error: 500"))).toBe(
"transient",
);
@@ -202,9 +202,7 @@ describe("syncUserFitbit — single cycle-wide watermark", () => {
// Every resource receives the identical resolved `start` (lastSyncedAt - 24h
// overlap). A per-resource read+stamp would have given later resources a
// start of ~now, dropping the gap after an outage longer than the overlap.
const expectedStart = new Date(
LAST_SYNCED.getTime() - 24 * 60 * 60 * 1000,
);
const expectedStart = new Date(LAST_SYNCED.getTime() - 24 * 60 * 60 * 1000);
for (const fn of [
syncUserMetrics,
syncUserActivity,
+24 -13
View File
@@ -15,10 +15,9 @@ const { prismaMock, refreshAccessTokenMock, recordSyncFailure } = vi.hoisted(
update: vi.fn(),
},
measurement: {
findMany:
vi.fn<(arg: Record<string, unknown>) => Promise<unknown[]>>(
async () => [],
),
findMany: vi.fn<(arg: Record<string, unknown>) => Promise<unknown[]>>(
async () => [],
),
createMany: vi.fn<
(arg: Record<string, unknown>) => Promise<{ count: number }>
>(async (arg) => ({
@@ -30,16 +29,17 @@ const { prismaMock, refreshAccessTokenMock, recordSyncFailure } = vi.hoisted(
},
},
refreshAccessTokenMock: vi.fn(),
recordSyncFailure: vi.fn<(...a: unknown[]) => Promise<void>>(async () => {}),
recordSyncFailure: vi.fn<(...a: unknown[]) => Promise<void>>(
async () => {},
),
}),
);
vi.mock("@/lib/db", () => ({ prisma: prismaMock }));
vi.mock("@/lib/rollups/measurement-rollups", () => ({
collapseToTypeDayKeys: (
rows: Array<{ type: string; measuredAt: Date }>,
) => rows,
collapseToTypeDayKeys: (rows: Array<{ type: string; measuredAt: Date }>) =>
rows,
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => ({ rowsUpserted: 0, durationMs: 0 })),
}));
@@ -311,8 +311,20 @@ describe("upsertFitbitMeasurements — batched, tombstone-safe write", () => {
]);
const { imported } = await upsertFitbitMeasurements("user1", [
{ type: "WEIGHT", value: 1, unit: "kg", measuredAt, externalId: "live-key" },
{ type: "WEIGHT", value: 2, unit: "kg", measuredAt, externalId: "fresh-key" },
{
type: "WEIGHT",
value: 1,
unit: "kg",
measuredAt,
externalId: "live-key",
},
{
type: "WEIGHT",
value: 2,
unit: "kg",
measuredAt,
externalId: "fresh-key",
},
]);
expect(imported).toBe(2);
@@ -345,9 +357,8 @@ describe("upsertFitbitMeasurements — batched, tombstone-safe write", () => {
});
it("defers the rollup hook and returns touched keys when deferRollup is set", async () => {
const { recomputeBucketsForMeasurement } = await import(
"@/lib/rollups/measurement-rollups"
);
const { recomputeBucketsForMeasurement } =
await import("@/lib/rollups/measurement-rollups");
const measuredAt = new Date("2026-05-10T07:00:00.000Z");
prismaMock.measurement.findMany.mockResolvedValue([]);
+61 -23
View File
@@ -54,7 +54,8 @@ export interface FitbitCredentials {
function getRedirectUri(): string {
const explicit = process.env.FITBIT_REDIRECT_URI;
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
const raw = explicit ?? (appUrl ? `${appUrl}/api/fitbit/callback` : undefined);
const raw =
explicit ?? (appUrl ? `${appUrl}/api/fitbit/callback` : undefined);
if (!raw) {
throw new Error(
@@ -74,7 +75,10 @@ function getRedirectUri(): string {
parsed.hostname === "127.0.0.1" ||
parsed.hostname === "[::1]" ||
parsed.hostname === "::1";
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLoopback)) {
if (
parsed.protocol !== "https:" &&
!(parsed.protocol === "http:" && isLoopback)
) {
throw new Error(
`Fitbit redirect_uri must be https (or http on localhost): ${parsed.origin}`,
);
@@ -510,7 +514,9 @@ export async function fetchDataPoints(
},
);
const json = (await res.json().catch(() => null)) as FitbitDataPointPage | null;
const json = (await res
.json()
.catch(() => null)) as FitbitDataPointPage | null;
const verdict = classifyFitbitResponse(res.status);
getEvent()?.addExternalCall({
service: "fitbit",
@@ -591,10 +597,7 @@ export type FitbitSleepStage =
* the small set of shapes the field is likely to take (a bare `value`, a typed
* sub-object, a `{ value: { fpVal } }` wrapper, …); the first finite hit wins.
*/
function firstNumber(
point: FitbitDataPoint,
paths: string[],
): number | null {
function firstNumber(point: FitbitDataPoint, paths: string[]): number | null {
for (const path of paths) {
const v = readPath(point, path);
if (positive(v)) return v;
@@ -660,7 +663,10 @@ function resolveMeasuredAt(
const d = new Date(t);
if (!Number.isNaN(d.getTime())) return d;
}
const civil = readPath(point, `${dataType.filter}.interval.civil_start_time`);
const civil = readPath(
point,
`${dataType.filter}.interval.civil_start_time`,
);
if (typeof civil === "string") {
const d = new Date(civil);
if (!Number.isNaN(d.getTime())) return d;
@@ -685,7 +691,10 @@ function resolveMeasuredAt(
* field-tag this makes the upsert key idempotent across re-fetches (a re-sync of
* the same window overwrites in place rather than minting a duplicate).
*/
function externalAnchor(point: FitbitDataPoint, dataType: FitbitDataType): string {
function externalAnchor(
point: FitbitDataPoint,
dataType: FitbitDataType,
): string {
const at = resolveMeasuredAt(point, dataType, new Date(0));
// The mapper-driven interval types (steps/distance/calories/floors) are daily
// totals, so they share the civil-day externalId grain with `date` summaries —
@@ -705,7 +714,13 @@ function externalAnchor(point: FitbitDataPoint, dataType: FitbitDataType): strin
function mapSimple(
point: FitbitDataPoint,
dataType: FitbitDataType,
spec: { type: string; unit: string; fieldTag: string; valuePaths: string[]; factor?: number },
spec: {
type: string;
unit: string;
fieldTag: string;
valuePaths: string[];
factor?: number;
},
): FitbitMappedMeasurement[] {
let value = firstNumber(point, spec.valuePaths);
if (value === null) return [];
@@ -813,7 +828,9 @@ export function mapRespiratoryRate(
});
}
export function mapHeartRate(point: FitbitDataPoint): FitbitMappedMeasurement[] {
export function mapHeartRate(
point: FitbitDataPoint,
): FitbitMappedMeasurement[] {
const dt = FITBIT_DATA_TYPES.heartRate;
return mapSimple(point, dt, {
type: "PULSE",
@@ -875,7 +892,12 @@ export const FITBIT_FIELD_MAP: Record<
filter: "weight",
note: "picker ranks a real Withings scale above Fitbit",
},
bodyFat: { type: "BODY_FAT", unit: "%", path: "body-fat", filter: "body_fat" },
bodyFat: {
type: "BODY_FAT",
unit: "%",
path: "body-fat",
filter: "body_fat",
},
oxygenSaturation: {
type: "OXYGEN_SATURATION",
unit: "%",
@@ -1047,7 +1069,10 @@ export function mapSteps(point: FitbitDataPoint): FitbitMappedMeasurement[] {
type: "ACTIVITY_STEPS",
unit: "steps",
fieldTag: "steps",
valuePaths: [...valuePaths(dt.filter, "count"), ...valuePaths(dt.filter, "steps")],
valuePaths: [
...valuePaths(dt.filter, "count"),
...valuePaths(dt.filter, "steps"),
],
});
}
@@ -1055,10 +1080,7 @@ export function mapDistance(point: FitbitDataPoint): FitbitMappedMeasurement[] {
const dt = FITBIT_DATA_TYPES.distance;
// Google may report metres or kilometres; prefer the explicit-metres field,
// else convert km → m. Both pass the non-negative guard.
const meters = firstNonNegativeNumber(
point,
valuePaths(dt.filter, "meters"),
);
const meters = firstNonNegativeNumber(point, valuePaths(dt.filter, "meters"));
if (meters !== null) {
return [
{
@@ -1104,7 +1126,10 @@ export function mapFloors(point: FitbitDataPoint): FitbitMappedMeasurement[] {
type: "FLIGHTS_CLIMBED",
unit: "flights",
fieldTag: "floors",
valuePaths: [...valuePaths(dt.filter, "count"), ...valuePaths(dt.filter, "floors")],
valuePaths: [
...valuePaths(dt.filter, "count"),
...valuePaths(dt.filter, "floors"),
],
});
}
@@ -1152,8 +1177,15 @@ const FITBIT_SLEEP_STAGE_MAP: Record<string, FitbitSleepStage> = {
*/
export function mapFitbitSleepStage(raw: unknown): FitbitSleepStage | null {
if (typeof raw !== "string") return null;
const key = raw.trim().toLowerCase().replace(/[\s-]+/g, "_");
return FITBIT_SLEEP_STAGE_MAP[key] ?? FITBIT_SLEEP_STAGE_MAP[key.replace(/_/g, "")] ?? null;
const key = raw
.trim()
.toLowerCase()
.replace(/[\s-]+/g, "_");
return (
FITBIT_SLEEP_STAGE_MAP[key] ??
FITBIT_SLEEP_STAGE_MAP[key.replace(/_/g, "")] ??
null
);
}
/** One sleep-stage segment pulled defensively off a Google sleep session. */
@@ -1331,7 +1363,10 @@ const FITBIT_EXERCISE_TYPE_MAP: Record<string, string> = {
/** Resolve a Google exercise activity type to a canonical sport label. */
export function mapFitbitSportType(raw: unknown): string {
if (typeof raw !== "string" || raw.trim() === "") return "other";
const key = raw.trim().toLowerCase().replace(/[\s-]+/g, "_");
const key = raw
.trim()
.toLowerCase()
.replace(/[\s-]+/g, "_");
return (
FITBIT_EXERCISE_TYPE_MAP[key] ??
FITBIT_EXERCISE_TYPE_MAP[key.replace(/_/g, "")] ??
@@ -1402,7 +1437,9 @@ export function mapWorkout(point: FitbitDataPoint): FitbitMappedWorkout | null {
]);
if (!startedAt || !endedAt || endedAt <= startedAt) return null;
const durationSec = Math.round((endedAt.getTime() - startedAt.getTime()) / 1000);
const durationSec = Math.round(
(endedAt.getTime() - startedAt.getTime()) / 1000,
);
const sessionId =
(typeof readPath(point, `${f}.session_id`) === "string" &&
@@ -1453,7 +1490,8 @@ export function mapWorkout(point: FitbitDataPoint): FitbitMappedWorkout | null {
endedAt,
durationSec,
totalEnergyKcal: energyKcal !== null ? Math.round(energyKcal) : null,
totalDistanceM: distanceM !== null && distanceM >= 0 ? round2(distanceM) : null,
totalDistanceM:
distanceM !== null && distanceM >= 0 ? round2(distanceM) : null,
avgHeartRate: avgHr !== null && avgHr > 0 ? Math.round(avgHr) : null,
maxHeartRate: maxHr !== null && maxHr > 0 ? Math.round(maxHr) : null,
minHeartRate: minHr !== null && minHr > 0 ? Math.round(minHr) : null,
+18 -18
View File
@@ -37,17 +37,17 @@ both forms per type so a fetcher can never encode the wrong one.
Scope: `googlehealth.health_metrics_and_measurements.readonly`.
| Data type (path / filter) | MeasurementType | Unit | fieldTag | Grain | Note |
|---|---|---|---|---|---|
| `weight` / `weight` | `WEIGHT` | kg | `weight` | sample | Picker ranks a real Withings scale above Fitbit. |
| `body-fat` / `body_fat` | `BODY_FAT` | % | `body_fat` | sample | |
| `daily-oxygen-saturation` / `daily_oxygen_saturation` | `OXYGEN_SATURATION` | % | `spo2` | daily | Already 0–100. |
| `daily-heart-rate-variability` / `daily_heart_rate_variability` | `HEART_RATE_VARIABILITY` | ms | `hrv` | daily | **Decision:** SDNN slot (Apple-comparable), NOT `HRV_RMSSD`. Fitbit reports an RMSSD-style nightly HRV — confirm the estimator at build and reconsider `HRV_RMSSD` if warranted. |
| `daily-resting-heart-rate` / `daily_resting_heart_rate` | `RESTING_HEART_RATE` | bpm | `rhr` | daily | |
| `daily-respiratory-rate` / `daily_respiratory_rate` | `RESPIRATORY_RATE` | breaths/min | `resp_rate` | daily | |
| `heart-rate` / `heart_rate` | `PULSE` | bpm | `hr` | sample | Intraday spot HR. |
| `daily-sleep-temperature-derivations` / `daily_sleep_temperature_derivations` | `WRIST_TEMPERATURE` | celsius | `wrist_temp` | daily | Closest semantic slot to Apple sleeping-wrist-temp. Confirm absolute-vs-baseline at build; the positive guard rejects a baseline-delta reading. |
| `height` / `height` | `User.heightCm` | cm | — | sample | Profile seed — written to `User.heightCm` ONLY when null; never minted as a Measurement (WHOOP `mapBody` pattern). m→cm when reported in metres. |
| Data type (path / filter) | MeasurementType | Unit | fieldTag | Grain | Note |
| ----------------------------------------------------------------------------- | ------------------------ | ----------- | ------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `weight` / `weight` | `WEIGHT` | kg | `weight` | sample | Picker ranks a real Withings scale above Fitbit. |
| `body-fat` / `body_fat` | `BODY_FAT` | % | `body_fat` | sample | |
| `daily-oxygen-saturation` / `daily_oxygen_saturation` | `OXYGEN_SATURATION` | % | `spo2` | daily | Already 0–100. |
| `daily-heart-rate-variability` / `daily_heart_rate_variability` | `HEART_RATE_VARIABILITY` | ms | `hrv` | daily | **Decision:** SDNN slot (Apple-comparable), NOT `HRV_RMSSD`. Fitbit reports an RMSSD-style nightly HRV — confirm the estimator at build and reconsider `HRV_RMSSD` if warranted. |
| `daily-resting-heart-rate` / `daily_resting_heart_rate` | `RESTING_HEART_RATE` | bpm | `rhr` | daily | |
| `daily-respiratory-rate` / `daily_respiratory_rate` | `RESPIRATORY_RATE` | breaths/min | `resp_rate` | daily | |
| `heart-rate` / `heart_rate` | `PULSE` | bpm | `hr` | sample | Intraday spot HR. |
| `daily-sleep-temperature-derivations` / `daily_sleep_temperature_derivations` | `WRIST_TEMPERATURE` | celsius | `wrist_temp` | daily | Closest semantic slot to Apple sleeping-wrist-temp. Confirm absolute-vs-baseline at build; the positive guard rejects a baseline-delta reading. |
| `height` / `height` | `User.heightCm` | cm | — | sample | Profile seed — written to `User.heightCm` ONLY when null; never minted as a Measurement (WHOOP `mapBody` pattern). m→cm when reported in metres. |
Every value passes a finite + strictly-positive guard (the launch metrics are
all positive — a zero/NaN is a garbage/empty reading and is dropped).
@@ -68,13 +68,13 @@ place rather than minting a duplicate, matching the Apple-Health
(steps/distance/floors/active-energy) **preserve a 0** (a rest day is real data,
not a gap); VO2 max stays strictly positive (daily latest-wins).
| Data type (path / filter) | MeasurementType | Unit | fieldTag | Note |
|---|---|---|---|---|
| `steps` / `steps` | `ACTIVITY_STEPS` | steps | `steps` | daily total; 0 valid |
| `distance` / `distance` | `WALKING_RUNNING_DISTANCE` | m | `distance` | metres (km → m when reported in km) |
| `active-calories` / `active_calories` | `ACTIVE_ENERGY_BURNED` | kcal | `active_calories` | **ACTIVE portion only** — NOT total caloriesOut (which folds in BMR) |
| `floors` / `floors` | `FLIGHTS_CLIMBED` | flights | `floors` | daily total; 0 valid |
| `vo2-max` / `vo2_max` | `VO2_MAX` | mL/(kg·min) | `vo2_max` | daily latest-wins; strictly positive |
| Data type (path / filter) | MeasurementType | Unit | fieldTag | Note |
| ------------------------------------- | -------------------------- | ----------- | ----------------- | -------------------------------------------------------------------- |
| `steps` / `steps` | `ACTIVITY_STEPS` | steps | `steps` | daily total; 0 valid |
| `distance` / `distance` | `WALKING_RUNNING_DISTANCE` | m | `distance` | metres (km → m when reported in km) |
| `active-calories` / `active_calories` | `ACTIVE_ENERGY_BURNED` | kcal | `active_calories` | **ACTIVE portion only** — NOT total caloriesOut (which folds in BMR) |
| `floors` / `floors` | `FLIGHTS_CLIMBED` | flights | `floors` | daily total; 0 valid |
| `vo2-max` / `vo2_max` | `VO2_MAX` | mL/(kg·min) | `vo2_max` | daily latest-wins; strictly positive |
## Sleep bundle (W5)
+5 -1
View File
@@ -60,7 +60,11 @@ interface MetricResource {
/** The launch metric resources (Measurement-producing). Height is handled separately. */
const METRIC_RESOURCES: MetricResource[] = [
{ dataType: FITBIT_DATA_TYPES.weight, map: mapWeight, verb: "fetchWeight" },
{ dataType: FITBIT_DATA_TYPES.bodyFat, map: mapBodyFat, verb: "fetchBodyFat" },
{
dataType: FITBIT_DATA_TYPES.bodyFat,
map: mapBodyFat,
verb: "fetchBodyFat",
},
{
dataType: FITBIT_DATA_TYPES.oxygenSaturation,
map: mapOxygenSaturation,
+16 -11
View File
@@ -360,9 +360,7 @@ export async function upsertFitbitMeasurements(
} else {
// A duplicate fresh key inside the same batch — overwrite the planned
// create's payload so last-write-wins, matching the prior upsert loop.
const idx = toCreate.findIndex(
(c) => `${c.type}${c.externalId}` === key,
);
const idx = toCreate.findIndex((c) => `${c.type}${c.externalId}` === key);
if (idx >= 0) {
toCreate[idx] = {
type,
@@ -421,7 +419,10 @@ export async function upsertFitbitMeasurements(
syncVersion: { increment: 1 },
},
});
touched.push({ type: r.type as MeasurementType, measuredAt: r.measuredAt });
touched.push({
type: r.type as MeasurementType,
measuredAt: r.measuredAt,
});
imported++;
} catch (err) {
getEvent()?.addWarning(`Fitbit: failed to update measurement: ${err}`);
@@ -526,13 +527,17 @@ export async function syncUserFitbit(
deferRollup: opts.fullSync === true,
};
const [{ syncUserMetrics }, { syncUserActivity }, { syncUserSleep }, { syncUserWorkout }] =
await Promise.all([
import("./sync-metrics"),
import("./sync-activity"),
import("./sync-sleep"),
import("./sync-workout"),
]);
const [
{ syncUserMetrics },
{ syncUserActivity },
{ syncUserSleep },
{ syncUserWorkout },
] = await Promise.all([
import("./sync-metrics"),
import("./sync-activity"),
import("./sync-sleep"),
import("./sync-workout"),
]);
const resources = [
syncUserMetrics,