mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
feat(environment): air quality, pollen and UV per day, locations sealed at rest
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.
This commit is contained in:
1 parent
aca497d9ac
commit
3c98d95a40
73 files changed
+6061
-385
No files matched your search
@@ -30,7 +30,6 @@ import {
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
moduleDisabledResponse,
|
||||
notImplementedResponse,
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
@@ -136,6 +135,90 @@ const environmentOverview = z
|
||||
.describe(
|
||||
"Upstream attribution string. Display it wherever the observations are shown.",
|
||||
),
|
||||
airQuality: z
|
||||
.object({
|
||||
enabled: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"The account's switch (`PATCH /api/environment/preferences`).",
|
||||
),
|
||||
operatorDisabled: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"True when the operator turned air quality off for the instance; the account switch then has no effect.",
|
||||
),
|
||||
days: z
|
||||
.number()
|
||||
.int()
|
||||
.describe(
|
||||
"Stored days the air-quality feed returned hourly values for.",
|
||||
),
|
||||
latestDate: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe("Newest such day (YYYY-MM-DD)."),
|
||||
domain: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe(
|
||||
"The model domain of that day: `cams_europe` (about 11 km, with pollen) or `cams_global` (about 45 km, no pollen).",
|
||||
),
|
||||
})
|
||||
.describe("v1.42 — the air-quality part of the module."),
|
||||
latestDay: z
|
||||
.object({
|
||||
date: z.string().describe("YYYY-MM-DD of the newest stored day."),
|
||||
tempMin: z.number().nullable(),
|
||||
tempMax: z.number().nullable(),
|
||||
apparentMax: z
|
||||
.number()
|
||||
.nullable()
|
||||
.describe("Daily feels-like maximum, °C."),
|
||||
airQuality: z
|
||||
.object({
|
||||
pm25Mean: z
|
||||
.number()
|
||||
.nullable()
|
||||
.describe("PM2.5 daily mean, µg/m³."),
|
||||
pm10Mean: z.number().nullable().describe("PM10 daily mean, µg/m³."),
|
||||
no2Mean: z.number().nullable().describe("NO2 daily mean, µg/m³."),
|
||||
o3Max8h: z
|
||||
.number()
|
||||
.nullable()
|
||||
.describe("Ozone, highest 8-hour running mean, µg/m³."),
|
||||
eaqiMax: z
|
||||
.number()
|
||||
.nullable()
|
||||
.describe("European air-quality index, daily maximum."),
|
||||
uvIndexMax: z.number().nullable(),
|
||||
dustMax: z.number().nullable().describe("Dust, µg/m³."),
|
||||
pollen: z
|
||||
.object({
|
||||
alder: z.number().nullable(),
|
||||
birch: z.number().nullable(),
|
||||
grass: z.number().nullable(),
|
||||
mugwort: z.number().nullable(),
|
||||
olive: z.number().nullable(),
|
||||
ragweed: z.number().nullable(),
|
||||
})
|
||||
.describe(
|
||||
"Daily maxima, grains/m³; null where not covered (outside Europe, before mid 2022).",
|
||||
),
|
||||
})
|
||||
.nullable()
|
||||
.describe(
|
||||
"Null while air quality is off for the account or the instance, and for a day whose air-quality part was not fetched yet. Null values inside mean not covered, never zero.",
|
||||
),
|
||||
})
|
||||
.nullable()
|
||||
.describe(
|
||||
"v1.42 — the newest stored day, usually yesterday or the day before (the archive settles late). No forecast. Null when nothing is stored.",
|
||||
),
|
||||
attributions: z
|
||||
.array(z.string())
|
||||
.describe(
|
||||
"v1.42 — every attribution line, weather first; the air-quality lines (Open-Meteo, Copernicus) while air quality is on. Display them wherever the values are shown.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "EnvironmentOverview",
|
||||
@@ -187,7 +270,7 @@ export const environmentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Environment"],
|
||||
summary: "Change the environment switches",
|
||||
description:
|
||||
"Turns the air-quality part of the module on or off for this account (on by default). Off means the nightly fetch skips the air-quality feed and the overview leaves the fields out; days already stored are kept. Module-gated. Strict body.",
|
||||
"Turns the air-quality part of the module on or off for this account (on by default). Off means the nightly fetch skips the air-quality feed, and the overview, the Coach, MCP and the correlations leave the air-quality values out; days already stored keep theirs. Turning it on queues a refresh, and the nightly gap fill catches up on older days. Module-gated. Strict body: an unknown key is a 422.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
@@ -208,7 +291,6 @@ export const environmentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
...moduleDisabledResponse,
|
||||
...stdResponses,
|
||||
...notImplementedResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user