mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Fetch the Open-Meteo air-quality feed (CAMS, domains=auto) with the weather: 17 hourly variables folded into local days by fixed rules (means and maxima only with 18 of 24 hours, ozone as the highest 8-hour running mean, UV only with the midday hours), a value the feed did not cover stored as null, never as zero. Requests are cut into 90-day chunks, newest first, and chunks before mid 2022 ask for the eight pollutant and index variables only. An air-quality failure never costs the weather row; the day stays unfetched and the nightly gap fill (up to 90 days per account, newest first) asks for it again. Every Open-Meteo request asks an instance-wide budget kept in Postgres first (500 a minute, 4000 an hour, 8000 a day, weighted by the hosted pricing rule). A refused request is not sent and the run ends as done with budget_blocked. OPENMETEO_AIR_QUALITY_URL and ENVIRONMENT_AIR_QUALITY_DISABLED are honoured; the account switch lives on PATCH /api/environment/preferences. The home, each dated location period and each stored day keep their coarse location sealed as one value under healthlog/environment-location/v1. The writers write the sealed value only, the readers open it and fall back to the readable columns for rows the free-text encryption backfill has not reached, and the backfill now seals all three and clears the readable columns. The backup carries the location readable in a portable file and sealed in a disaster-recovery file; a restore seals either under the receiving key. Three correlation channels join the environment family (PM2.5, the ozone 8-hour high, the day's highest pollen count), read through the averaged same-day window and left empty while air quality is off. The Coach and MCP gain get_environment: module-gated, clamped to the lookback limit, no coordinate or place name anywhere, with provenance and the attribution lines. The settings section gets the switch, the coverage and the newest day's values; the dashboard shows a quiet chip on a day with high pollen, a warm night or very poor air. Copernicus and Open-Meteo are credited next to the values.
101 lines
3.0 KiB
TypeScript
101 lines
3.0 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";
|
|
|
|
import { isValidTimezone } from "@/lib/tz/format";
|
|
|
|
/** 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). Validated against the
|
|
* runtime IANA set, not accepted as a free string — a bogus zone would
|
|
* silently mis-key every stored environment day for the account.
|
|
*/
|
|
timezone: z
|
|
.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(64)
|
|
.refine(isValidTimezone, "Invalid IANA timezone"),
|
|
})
|
|
.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();
|
|
|
|
/**
|
|
* v1.42 (#615) — the account's environment switches. Today one: whether the
|
|
* air-quality, pollen and UV part is fetched and shown. Strict.
|
|
*/
|
|
export const environmentPreferencesSchema = z
|
|
.object({
|
|
airQualityEnabled: z.boolean(),
|
|
})
|
|
.strict();
|
|
|
|
export type EnvironmentPreferencesInput = z.infer<
|
|
typeof environmentPreferencesSchema
|
|
>;
|