mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(environment): fill a day before the home only inside a location period
The past-days backfill placed every day without a row at the current home, also before the home was set. Weather and air of a place the person was not at would flow into correlations, the Coach and the day view as fact. A day before homeSince is now filled only when a location period covers it; with no homeSince stored the home still covers every day. Days without a place are not counted in the progress.
This commit is contained in:
1 parent
d81a6ddf25
commit
36dbfb8af7
4 files changed
+79
-28
No files matched your search
+7
-5
@@ -45,11 +45,13 @@ an earlier fold computed from part of a day.
|
||||
Location & weather (on by default), attributions beside the values, and a
|
||||
dashboard chip on a day with high pollen, a warm night or very poor air.
|
||||
- **Air quality for the past.** A background job fills air quality, pollen
|
||||
and UV for every earlier local day with an entry: after the update, when
|
||||
the home is set and when air quality is switched on. A trip day is fetched
|
||||
at the trip's place, other days at the home, also before it was set. The
|
||||
source reaches back to 2013 in Europe and to August 2022 elsewhere, pollen
|
||||
from 2021 in Europe only; older days stay empty. Nearby days share one
|
||||
and UV for earlier local days with an entry: after the update, when the
|
||||
home is set and when air quality is switched on. A day inside a location
|
||||
period is fetched at that place, a day from the home's effective date on
|
||||
at the home; a day before it with no period stays empty rather than
|
||||
borrowing a place the person was not at. The source reaches back to 2013
|
||||
in Europe and to August 2022 elsewhere, pollen from 2021 in Europe only;
|
||||
older days stay empty. Nearby days share one
|
||||
request of up to 90 days, a run holds itself to 300 of the account's 400
|
||||
daily calls and half of each instance window, and a long history spreads
|
||||
over several nights. Location & weather shows the progress (migration
|
||||
|
||||
@@ -135,13 +135,27 @@ describe("resolveHistoryLocation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("places a day before the home was set at the home", () => {
|
||||
expect(resolveHistoryLocation("2020-03-01", home, [trip])).toMatchObject({
|
||||
it("places a day from the home's effective date on at the home", () => {
|
||||
expect(resolveHistoryLocation("2026-09-01", home, [trip])).toMatchObject({
|
||||
lat: 51.5,
|
||||
source: "HOME",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves a day before the home was set without a place, unless a period covers it", () => {
|
||||
expect(resolveHistoryLocation("2026-08-31", home, [trip])).toBeNull();
|
||||
expect(resolveHistoryLocation("2020-03-01", home, [trip])).toBeNull();
|
||||
expect(resolveHistoryLocation("2024-07-14", home, [trip])).toMatchObject({
|
||||
source: "TRAVEL",
|
||||
});
|
||||
});
|
||||
|
||||
it("places every day at a home stored without an effective date", () => {
|
||||
expect(
|
||||
resolveHistoryLocation("2015-01-01", { ...home, since: null }, []),
|
||||
).toMatchObject({ lat: 51.5, source: "HOME" });
|
||||
});
|
||||
|
||||
it("places nothing without a home or a period", () => {
|
||||
expect(resolveHistoryLocation("2020-03-01", null, [trip])).toBeNull();
|
||||
});
|
||||
|
||||
@@ -15,13 +15,17 @@
|
||||
* (only the air-quality part is fetched then, as the nightly gap fill
|
||||
* does);
|
||||
* - otherwise in the dated location period that covers it, else at the
|
||||
* home. Unlike the nightly resolver this also places days before
|
||||
* `homeSince` at the home: those are exactly the days the history is for,
|
||||
* and a period entered for a trip still wins. Such a day gets its weather
|
||||
* row with the air quality, because the air quality is stored on it.
|
||||
* home, but the home only from `homeSince` on (or on every day when no
|
||||
* `homeSince` is stored). A day before the home was set is never placed
|
||||
* there: the weather and air of a place the person was not at would be
|
||||
* wrong data in the correlations, the Coach and the day view, and an
|
||||
* empty day is honest where a wrong one is not. Such a day is filled
|
||||
* only when a period covers it. A day without a row gets its weather row
|
||||
* with the air quality, because the air quality is stored on it.
|
||||
*
|
||||
* A day inside a period whose sealed location does not open is skipped (fail
|
||||
* closed, as in the service), and so is a day with no place at all. A day the
|
||||
* closed, as in the service), and so is a day with no place at all; neither
|
||||
* counts towards the progress, which counts only days that can be filled. A day the
|
||||
* source does not reach at its location (before 2013 in Europe, before August
|
||||
* 2022 elsewhere, `earliestAirQualityDay`) is neither fetched nor counted: it
|
||||
* stays without a value, which says nothing about the air, rather than
|
||||
@@ -189,7 +193,9 @@ export function historyRequestFits(
|
||||
|
||||
/**
|
||||
* Where a day without a row is placed: the period that covers it, else the
|
||||
* home, also before `homeSince` (see the header). Pure; exported for tests.
|
||||
* home from `homeSince` on, or on every day when no `homeSince` is stored
|
||||
* (see the header). Null for a day before the home with no period. Pure;
|
||||
* exported for tests.
|
||||
*/
|
||||
export function resolveHistoryLocation(
|
||||
day: string,
|
||||
@@ -201,7 +207,7 @@ export function resolveHistoryLocation(
|
||||
return { lat: t.lat, lon: t.lon, label: t.label, source: "TRAVEL" };
|
||||
}
|
||||
}
|
||||
if (home) {
|
||||
if (home && (home.since === null || day >= home.since)) {
|
||||
return { lat: home.lat, lon: home.lon, label: home.label, source: "HOME" };
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
*
|
||||
* - every past local day with an entry is filled: a day with a weather row
|
||||
* gets its air quality, a day without one gets both, a day of a trip is
|
||||
* fetched at the trip's place, a day before the home was set at the home;
|
||||
* fetched at the trip's place, a day from the home's effective date at
|
||||
* the home;
|
||||
* - a day before the home was set is filled only inside a location period;
|
||||
* without one it stays empty and is not counted;
|
||||
* - a day before the source reaches stays without a row and is not counted,
|
||||
* and the last week is left to the nightly fetch;
|
||||
* - nearby days share one request, the coordinates sent are the coarse
|
||||
@@ -32,6 +35,7 @@ const OWNER = "aq-history-owner";
|
||||
const NOW = new Date("2026-10-09T03:00:00Z");
|
||||
const BOCHUM = { lat: 51.5, lon: 7.2, label: "Bochum" };
|
||||
const LISBON = { lat: 38.7, lon: -9.1, label: "Lisbon" };
|
||||
const ESSEN = { lat: 51.5, lon: 7.0, label: "Essen" };
|
||||
|
||||
function daysBetween(start: string, end: string): string[] {
|
||||
const out: string[] = [];
|
||||
@@ -108,8 +112,8 @@ beforeEach(async () => {
|
||||
timezone: "Europe/Berlin",
|
||||
homeLocationEncrypted: sealLocation(BOCHUM),
|
||||
homeTimezone: "Europe/Berlin",
|
||||
// Set recently: every day below lies before it.
|
||||
homeSince: new Date("2026-09-01T00:00:00Z"),
|
||||
// Days before it are filled only inside a location period.
|
||||
homeSince: new Date("2025-01-01T00:00:00Z"),
|
||||
modulePreferencesJson: { environment: true },
|
||||
},
|
||||
});
|
||||
@@ -121,16 +125,27 @@ beforeEach(async () => {
|
||||
locationEncrypted: sealLocation(LISBON),
|
||||
},
|
||||
});
|
||||
// Entries: one before the source reaches, two nearby days in 2020, one on
|
||||
// a trip, one on a day with a weather row lacking air quality, one on a
|
||||
// day already done, a mood entry, and one inside the last week.
|
||||
await prisma.environmentTravelLocation.create({
|
||||
data: {
|
||||
userId: OWNER,
|
||||
startDate: "2020-03-01",
|
||||
endDate: "2020-03-10",
|
||||
locationEncrypted: sealLocation(ESSEN),
|
||||
},
|
||||
});
|
||||
// Entries: one before the source reaches, two nearby days on a 2020 trip,
|
||||
// one before the home with no trip, one on a 2024 trip, one on a day with a
|
||||
// weather row lacking air quality, one on a day already done, one at home
|
||||
// without a row, a mood entry, and one inside the last week.
|
||||
for (const day of [
|
||||
"2012-06-01",
|
||||
"2020-03-01",
|
||||
"2020-03-05",
|
||||
"2020-06-01",
|
||||
"2024-07-05",
|
||||
"2025-05-01",
|
||||
"2025-06-01",
|
||||
"2025-09-10",
|
||||
"2026-10-06",
|
||||
]) {
|
||||
await measurementOn(day);
|
||||
@@ -185,8 +200,8 @@ describe("air-quality history backfill", () => {
|
||||
const result = await runAirQualityHistory(OWNER, { now: NOW });
|
||||
expect(result).toMatchObject({
|
||||
status: "complete",
|
||||
total: 5,
|
||||
done: 5,
|
||||
total: 6,
|
||||
done: 6,
|
||||
remaining: 0,
|
||||
});
|
||||
|
||||
@@ -200,16 +215,29 @@ describe("air-quality history backfill", () => {
|
||||
"2024-07-05",
|
||||
"2025-05-01",
|
||||
"2025-06-01",
|
||||
"2025-09-10",
|
||||
]);
|
||||
// Before the home and outside every period: no row, not counted.
|
||||
expect(rows.find((r) => r.date === "2020-06-01")).toBeUndefined();
|
||||
for (const row of rows) expect(row.aqFetchedAt).not.toBeNull();
|
||||
const byDate = new Map(rows.map((r) => [r.date, r]));
|
||||
// A day before the home was set: placed at the home, weather and air.
|
||||
// A day before the home was set, inside a period: at the period's place.
|
||||
expect(byDate.get("2020-03-01")).toMatchObject({
|
||||
source: "HOME",
|
||||
source: "TRAVEL",
|
||||
pm25Mean: 12,
|
||||
tempMax: 5,
|
||||
lat: null,
|
||||
});
|
||||
expect(
|
||||
openLocation(byDate.get("2020-03-01")!.locationEncrypted!),
|
||||
).toMatchObject({ lat: 51.5, lon: 7.0 });
|
||||
// A day after the home was set, without a row: at the home.
|
||||
const home = byDate.get("2025-09-10")!;
|
||||
expect(home).toMatchObject({ source: "HOME", pm25Mean: 12, tempMax: 5 });
|
||||
expect(openLocation(home.locationEncrypted!)).toMatchObject({
|
||||
lat: 51.5,
|
||||
lon: 7.2,
|
||||
});
|
||||
// Pollen is not asked for before 2021, so it stays null, not zero.
|
||||
expect(byDate.get("2020-03-01")!.pollenBirchMax).toBeNull();
|
||||
// A trip day: at the trip's place.
|
||||
@@ -238,6 +266,7 @@ describe("air-quality history backfill", () => {
|
||||
u.searchParams.get("end_date"),
|
||||
]),
|
||||
).toEqual([
|
||||
["2025-09-10", "2025-09-10"],
|
||||
["2025-05-01", "2025-05-01"],
|
||||
["2024-07-05", "2024-07-05"],
|
||||
["2020-03-01", "2020-03-05"],
|
||||
@@ -254,12 +283,12 @@ describe("air-quality history backfill", () => {
|
||||
sent
|
||||
.filter((u) => u.pathname === "/v1/archive")
|
||||
.map((u) => u.searchParams.get("start_date")),
|
||||
).toEqual(["2024-07-05", "2020-03-01"]);
|
||||
).toEqual(["2025-09-10", "2024-07-05", "2020-03-01"]);
|
||||
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { id: OWNER } });
|
||||
expect(user.environmentAqHistoryJson).toMatchObject({
|
||||
total: 5,
|
||||
done: 5,
|
||||
total: 6,
|
||||
done: 6,
|
||||
complete: true,
|
||||
});
|
||||
|
||||
@@ -284,7 +313,7 @@ describe("air-quality history backfill", () => {
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { id: OWNER } });
|
||||
expect(user.environmentAqHistoryJson).toMatchObject({
|
||||
total: 5,
|
||||
total: 6,
|
||||
done: 1,
|
||||
complete: false,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user