Files
HealthLog/src/lib/validations/environment.ts
T
Marc-André Bombeck c254135406 feat(environment): resolve past days conservatively, never guessing from home
Rework the per-day location resolution so it can no longer fabricate weather for
days before the home was set. Precedence, first hit wins:

  1. an explicit dated location period (a trip, or a corrected stretch of
     history) covering the day;
  2. DEVICE — a reserved slot for a future client-supplied coarse per-day
     location (ranks between the period and home; not produced server-side yet);
  3. the home location, but only for days on/after its effective-from date;
  4. otherwise the day is skipped.

The home route stamps `homeSince` whenever the home is set/updated and returns
it. The overview exposes it. Backfill bounds are now optional and default to the
conservative `[homeSince .. today]` span instead of a fixed reach onto the
current home; days before `homeSince` still resolve to a skip in the worker, so
even a wider explicit start cannot back-fill weather for the pre-home past. To
reach further back a user adds an explicit location period for that range.

Add unit coverage for the precedence (including the effective-from boundary and
the skip-not-guess rule) and the default backfill range.
2026-06-28 17:36:14 +02:00

76 lines
2.3 KiB
TypeScript

/**
* v1.25 (W-ENV) — environmental-context request validation.
*
* The home location, the travel overrides, and the backfill span. Coordinates
* are bounded to valid lat/lon; the labels are length-capped plaintext (a city
* name, never a quasi-identifier). Dates are strict YYYY-MM-DD. The shared
* shapes are reused by the OpenAPI registry so the wire contract stays
* single-source.
*/
import { z } from "zod/v4";
/** Strict YYYY-MM-DD calendar day. */
export const dayStringSchema = z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "Expected a YYYY-MM-DD date");
const latSchema = z.number().gte(-90).lte(90);
const lonSchema = z.number().gte(-180).lte(180);
const labelSchema = z.string().trim().min(1).max(200);
/** A coarse home location set in settings (city-level granularity). */
export const homeLocationSchema = z
.object({
lat: latSchema,
lon: lonSchema,
label: labelSchema,
/** IANA timezone (anchors the stored day-key). */
timezone: z.string().trim().min(1).max(64),
})
.strict();
export type HomeLocationInput = z.infer<typeof homeLocationSchema>;
/** A manual travel override covering an inclusive date range. */
export const travelLocationSchema = z
.object({
startDate: dayStringSchema,
endDate: dayStringSchema,
lat: latSchema,
lon: lonSchema,
label: labelSchema,
})
.strict()
.refine((v) => v.startDate <= v.endDate, {
message: "startDate must be on or before endDate",
path: ["endDate"],
});
export type TravelLocationInput = z.infer<typeof travelLocationSchema>;
/**
* A backfill request: fetch + store the environment rows over a date range.
* Both bounds are optional — an omitted span defaults to the conservative
* `[homeSince .. today]` range on the server (never a fixed reach into the past
* onto the current home).
*/
export const environmentBackfillSchema = z
.object({
startDate: dayStringSchema.optional(),
endDate: dayStringSchema.optional(),
})
.strict()
.refine((v) => !v.startDate || !v.endDate || v.startDate <= v.endDate, {
message: "startDate must be on or before endDate",
path: ["endDate"],
});
export type EnvironmentBackfillInput = z.infer<
typeof environmentBackfillSchema
>;
/** Geocoding search query (`?q=`). */
export const geocodeQuerySchema = z
.object({ q: z.string().trim().min(1).max(120) })
.strict();