diff --git a/messages/de.json b/messages/de.json index 674c2f869..dc0e00086 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4292,8 +4292,8 @@ "select": "Auswählen", "saveError": "Änderung konnte nicht gespeichert werden.", "homeSaved": "Heimatort gespeichert.", - "travelAdded": "Reise-Ausnahme hinzugefügt.", - "travelRemoved": "Reise-Ausnahme entfernt.", + "travelAdded": "Ortszeitraum hinzugefügt.", + "travelRemoved": "Ortszeitraum entfernt.", "backfillQueued": "Nacherfassung eingeplant.", "backfillError": "Nacherfassung konnte nicht gestartet werden.", "startDate": "Startdatum", @@ -4301,20 +4301,21 @@ "storedDays": "{count} Tage Wetter gespeichert.", "home": { "title": "Heimatort", - "description": "Ein grober Ort auf Stadtebene – niemals ein genauer Standortverlauf. Wetter und Tageslicht werden für diesen Ort an allen Tagen ohne Reise-Ausnahme abgerufen.", + "description": "Ein grober Standort auf Stadtebene – niemals eine genaue Spur. Wetter und Tageslicht werden für diesen Ort an Tagen ohne Ortszeitraum abgerufen, ab dem Tag der Festlegung.", "current": "Aktuell: {label}", - "none": "Noch kein Heimatort festgelegt." + "none": "Noch kein Heimatort festgelegt.", + "since": "Wetter wird ab {date} erfasst. Frühere Tage bleiben leer, bis du einen Ortszeitraum hinzufügst." }, "travel": { - "title": "Reise-Ausnahmen", - "description": "Für einen Zeitraum, den du woanders verbracht hast, den Ort festlegen, damit das Wetter zu deinem tatsächlichen Aufenthalt passt.", - "none": "Keine Reise-Ausnahmen.", + "title": "Ortszeiträume", + "description": "Füge einen Ort und einen Zeitraum hinzu – eine Reise oder einen früheren Abschnitt deiner Verlaufsdaten. Der Zeitraum überschreibt für seine Tage deinen Wohnort, damit das Wetter zu deinem tatsächlichen Aufenthalt passt. So korrigierst du die Vergangenheit.", + "none": "Noch keine Ortszeiträume.", "add": "{label} hinzufügen", - "remove": "Reise-Ausnahme entfernen" + "remove": "Ortszeitraum entfernen" }, "backfill": { "title": "Vergangene Tage nacherfassen", - "description": "Wetter für einen vergangenen Zeitraum abrufen und speichern, damit ältere Tage in die Korrelationen einfließen.", + "description": "Rufe Wetter für einen vergangenen Zeitraum ab und speichere es, damit ältere Tage in die Korrelationen einfließen. Der Start steht standardmäßig auf dem Gültigkeitsdatum deines Wohnorts; um weiter zurückzureichen, füge zuerst einen Ortszeitraum für diesen Bereich hinzu.", "run": "Nacherfassen" } } diff --git a/messages/en.json b/messages/en.json index bcdee2c5e..e6e776db1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4292,8 +4292,8 @@ "select": "Select", "saveError": "Could not save your change.", "homeSaved": "Home location saved.", - "travelAdded": "Travel override added.", - "travelRemoved": "Travel override removed.", + "travelAdded": "Location period added.", + "travelRemoved": "Location period removed.", "backfillQueued": "Backfill queued.", "backfillError": "Could not start the backfill.", "startDate": "Start date", @@ -4301,20 +4301,21 @@ "storedDays": "{count} days of weather stored.", "home": { "title": "Home location", - "description": "A coarse, city-level location — never a precise track. Weather and daylight are fetched for this place on days no travel override applies.", + "description": "A coarse, city-level location — never a precise track. Weather and daylight are fetched for this place on days no location period applies, from the date you set it onward.", "current": "Current: {label}", - "none": "No home location set yet." + "none": "No home location set yet.", + "since": "Weather tracked from {date} onward. Earlier days stay empty until you add a location period." }, "travel": { - "title": "Travel overrides", - "description": "For a date range you spent elsewhere, set the location so the weather matches where you actually were.", - "none": "No travel overrides.", + "title": "Location periods", + "description": "Add a place and a date range — a trip, or a stretch of earlier history. The period overrides your home for the days it covers, so weather matches where you actually were. This is how you correct the past.", + "none": "No location periods yet.", "add": "Add {label}", - "remove": "Remove travel override" + "remove": "Remove location period" }, "backfill": { "title": "Backfill past days", - "description": "Fetch and store weather for a past date range so older days can join the correlations.", + "description": "Fetch and store weather for a past date range so older days join the correlations. The start defaults to your home's effective date; to reach further back, add a location period for that range first.", "run": "Backfill" } } diff --git a/messages/es.json b/messages/es.json index 663e4b38c..b9687c4c0 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4292,8 +4292,8 @@ "select": "Seleccionar", "saveError": "No se pudo guardar el cambio.", "homeSaved": "Ubicación principal guardada.", - "travelAdded": "Excepción de viaje añadida.", - "travelRemoved": "Excepción de viaje eliminada.", + "travelAdded": "Periodo de ubicación añadido.", + "travelRemoved": "Periodo de ubicación eliminado.", "backfillQueued": "Relleno programado.", "backfillError": "No se pudo iniciar el relleno.", "startDate": "Fecha de inicio", @@ -4301,20 +4301,21 @@ "storedDays": "{count} días de tiempo almacenados.", "home": { "title": "Ubicación principal", - "description": "Una ubicación aproximada a nivel de ciudad, nunca un rastro preciso. El tiempo y las horas de luz se obtienen para este lugar los días sin excepción de viaje.", + "description": "Una ubicación aproximada a nivel de ciudad, nunca un rastro preciso. El tiempo y la luz diurna se obtienen para este lugar en los días sin un periodo de ubicación, desde la fecha en que lo configuras.", "current": "Actual: {label}", - "none": "Aún no hay ubicación principal." + "none": "Aún no hay ubicación principal.", + "since": "Tiempo registrado desde {date}. Los días anteriores quedan vacíos hasta que añadas un periodo de ubicación." }, "travel": { - "title": "Excepciones de viaje", - "description": "Para un rango de fechas que pasaste en otro sitio, define la ubicación para que el tiempo coincida con dónde estuviste realmente.", - "none": "Sin excepciones de viaje.", + "title": "Periodos de ubicación", + "description": "Añade un lugar y un rango de fechas: un viaje o un tramo de tu historial anterior. El periodo anula tu hogar en los días que cubre, para que el tiempo coincida con donde estuviste realmente. Así corriges el pasado.", + "none": "Aún no hay periodos de ubicación.", "add": "Añadir {label}", - "remove": "Eliminar excepción de viaje" + "remove": "Eliminar periodo de ubicación" }, "backfill": { "title": "Rellenar días pasados", - "description": "Obtén y guarda el tiempo de un rango de fechas pasado para que los días anteriores entren en las correlaciones.", + "description": "Obtén y guarda el tiempo de un rango de fechas pasado para que los días antiguos entren en las correlaciones. El inicio toma por defecto la fecha de vigencia de tu hogar; para llegar más atrás, añade primero un periodo de ubicación para ese rango.", "run": "Rellenar" } } diff --git a/messages/fr.json b/messages/fr.json index dd098d408..7df786db9 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4292,8 +4292,8 @@ "select": "Sélectionner", "saveError": "Impossible d'enregistrer la modification.", "homeSaved": "Lieu de référence enregistré.", - "travelAdded": "Exception de voyage ajoutée.", - "travelRemoved": "Exception de voyage supprimée.", + "travelAdded": "Période de localisation ajoutée.", + "travelRemoved": "Période de localisation supprimée.", "backfillQueued": "Récupération planifiée.", "backfillError": "Impossible de lancer la récupération.", "startDate": "Date de début", @@ -4301,20 +4301,21 @@ "storedDays": "{count} jours de météo enregistrés.", "home": { "title": "Lieu de référence", - "description": "Un lieu approximatif à l'échelle de la ville, jamais un suivi précis. La météo et la durée du jour sont récupérées pour ce lieu les jours sans exception de voyage.", + "description": "Une localisation approximative à l'échelle de la ville, jamais un tracé précis. La météo et la lumière du jour sont récupérées pour ce lieu les jours sans période de localisation, à partir de la date où vous le définissez.", "current": "Actuel : {label}", - "none": "Aucun lieu de référence défini." + "none": "Aucun lieu de référence défini.", + "since": "Météo enregistrée à partir du {date}. Les jours antérieurs restent vides jusqu'à ce que vous ajoutiez une période de localisation." }, "travel": { - "title": "Exceptions de voyage", - "description": "Pour une période passée ailleurs, définissez le lieu afin que la météo corresponde à l'endroit où vous étiez réellement.", - "none": "Aucune exception de voyage.", + "title": "Périodes de localisation", + "description": "Ajoutez un lieu et une plage de dates : un voyage ou une portion de votre historique passé. La période remplace votre domicile pour les jours qu'elle couvre, afin que la météo corresponde à l'endroit où vous étiez réellement. C'est ainsi que vous corrigez le passé.", + "none": "Aucune période de localisation pour l'instant.", "add": "Ajouter {label}", - "remove": "Supprimer l'exception de voyage" + "remove": "Supprimer la période de localisation" }, "backfill": { "title": "Récupérer les jours passés", - "description": "Récupérez et enregistrez la météo d'une période passée pour que les jours plus anciens entrent dans les corrélations.", + "description": "Récupérez et enregistrez la météo d'une plage de dates passée pour que les jours anciens rejoignent les corrélations. Le début reprend par défaut la date d'effet de votre domicile ; pour remonter plus loin, ajoutez d'abord une période de localisation pour cette plage.", "run": "Récupérer" } } diff --git a/messages/it.json b/messages/it.json index f85dcb938..e4f800775 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4292,8 +4292,8 @@ "select": "Seleziona", "saveError": "Impossibile salvare la modifica.", "homeSaved": "Posizione principale salvata.", - "travelAdded": "Eccezione di viaggio aggiunta.", - "travelRemoved": "Eccezione di viaggio rimossa.", + "travelAdded": "Periodo di posizione aggiunto.", + "travelRemoved": "Periodo di posizione rimosso.", "backfillQueued": "Recupero pianificato.", "backfillError": "Impossibile avviare il recupero.", "startDate": "Data di inizio", @@ -4301,20 +4301,21 @@ "storedDays": "{count} giorni di meteo salvati.", "home": { "title": "Posizione principale", - "description": "Una posizione approssimativa a livello di città, mai un tracciamento preciso. Il meteo e le ore di luce vengono recuperati per questo luogo nei giorni senza eccezioni di viaggio.", + "description": "Una posizione approssimativa a livello di città, mai un tracciato preciso. Meteo e luce diurna vengono recuperati per questo luogo nei giorni senza un periodo di posizione, a partire dalla data in cui lo imposti.", "current": "Attuale: {label}", - "none": "Nessuna posizione principale impostata." + "none": "Nessuna posizione principale impostata.", + "since": "Meteo registrato dal {date}. I giorni precedenti restano vuoti finché non aggiungi un periodo di posizione." }, "travel": { - "title": "Eccezioni di viaggio", - "description": "Per un intervallo di date trascorso altrove, imposta la posizione in modo che il meteo corrisponda a dove eri davvero.", - "none": "Nessuna eccezione di viaggio.", + "title": "Periodi di posizione", + "description": "Aggiungi un luogo e un intervallo di date: un viaggio o un tratto della tua cronologia passata. Il periodo prevale sulla tua residenza nei giorni che copre, così il meteo corrisponde a dove ti trovavi davvero. È così che correggi il passato.", + "none": "Nessun periodo di posizione.", "add": "Aggiungi {label}", - "remove": "Rimuovi eccezione di viaggio" + "remove": "Rimuovi periodo di posizione" }, "backfill": { "title": "Recupera i giorni passati", - "description": "Recupera e salva il meteo di un intervallo di date passato così i giorni più vecchi entrano nelle correlazioni.", + "description": "Recupera e salva il meteo di un intervallo di date passato così i giorni più vecchi entrano nelle correlazioni. L'inizio è impostato in modo predefinito sulla data di validità della tua residenza; per andare più indietro, aggiungi prima un periodo di posizione per quell'intervallo.", "run": "Recupera" } } diff --git a/messages/pl.json b/messages/pl.json index c56dbf0df..52381c2ba 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4292,8 +4292,8 @@ "select": "Wybierz", "saveError": "Nie udało się zapisać zmiany.", "homeSaved": "Zapisano lokalizację domową.", - "travelAdded": "Dodano wyjątek podróży.", - "travelRemoved": "Usunięto wyjątek podróży.", + "travelAdded": "Dodano okres lokalizacji.", + "travelRemoved": "Usunięto okres lokalizacji.", "backfillQueued": "Zaplanowano uzupełnianie.", "backfillError": "Nie udało się rozpocząć uzupełniania.", "startDate": "Data początkowa", @@ -4301,20 +4301,21 @@ "storedDays": "Zapisano dni pogody: {count}.", "home": { "title": "Lokalizacja domowa", - "description": "Przybliżona lokalizacja na poziomie miasta — nigdy dokładny ślad. Pogoda i długość dnia są pobierane dla tego miejsca w dni bez wyjątku podróży.", + "description": "Przybliżona lokalizacja na poziomie miasta – nigdy dokładny ślad. Pogoda i światło dzienne są pobierane dla tego miejsca w dni bez okresu lokalizacji, od dnia jego ustawienia.", "current": "Obecnie: {label}", - "none": "Nie ustawiono jeszcze lokalizacji domowej." + "none": "Nie ustawiono jeszcze lokalizacji domowej.", + "since": "Pogoda zapisywana od {date}. Wcześniejsze dni pozostają puste, dopóki nie dodasz okresu lokalizacji." }, "travel": { - "title": "Wyjątki podróży", - "description": "Dla zakresu dat spędzonego gdzie indziej ustaw lokalizację, aby pogoda odpowiadała temu, gdzie naprawdę byłeś.", - "none": "Brak wyjątków podróży.", + "title": "Okresy lokalizacji", + "description": "Dodaj miejsce i zakres dat: podróż lub fragment wcześniejszej historii. Okres zastępuje Twój dom w dniach, które obejmuje, aby pogoda odpowiadała miejscu, w którym naprawdę byłeś. W ten sposób korygujesz przeszłość.", + "none": "Brak okresów lokalizacji.", "add": "Dodaj {label}", - "remove": "Usuń wyjątek podróży" + "remove": "Usuń okres lokalizacji" }, "backfill": { "title": "Uzupełnij minione dni", - "description": "Pobierz i zapisz pogodę dla minionego zakresu dat, aby starsze dni weszły do korelacji.", + "description": "Pobierz i zapisz pogodę dla minionego zakresu dat, aby starsze dni weszły do korelacji. Początek domyślnie przyjmuje datę obowiązywania Twojego domu; aby sięgnąć dalej wstecz, najpierw dodaj okres lokalizacji dla tego zakresu.", "run": "Uzupełnij" } } diff --git a/prisma/migrations/0219_v125_env_home_since/migration.sql b/prisma/migrations/0219_v125_env_home_since/migration.sql new file mode 100644 index 000000000..16c2453d2 --- /dev/null +++ b/prisma/migrations/0219_v125_env_home_since/migration.sql @@ -0,0 +1,36 @@ +-- v1.25 (W-ENV) — conservative past-location handling for the environmental +-- context module. +-- +-- Adds `users.home_since` (effective-from instant for the home location) and a +-- `DEVICE` value to the location-source enum. Day resolution attributes a past +-- day to the home location ONLY when the day is on/after `home_since`; earlier +-- days are skipped rather than fabricated from the current home. The deep past +-- is filled by explicit dated location periods instead. +-- +-- `DEVICE` is the reserved forward path for a future client-supplied coarse +-- per-day location (iOS v2). The value exists so the resolver and the wire +-- contract already accept it; nothing writes it server-side yet. +-- +-- Backfill of existing accounts: any row that already has a home location gets +-- `home_since` stamped to the migration instant. This is conservative — the +-- exact day a pre-upgrade account moved into its home is unknown, so history +-- before the upgrade is left to explicit location periods rather than guessed. +-- +-- Additive; no existing weather row touched. Idempotent guards so reruns are +-- safe on prod. +-- +-- Reversibility (down): +-- ALTER TABLE "users" DROP COLUMN IF EXISTS "home_since"; +-- -- (the `DEVICE` enum value cannot be dropped in place; it is inert until a +-- -- future client writes it, so a roll-back leaves it harmless.) + +-- AlterTable: effective-from instant for the home location (nullable). +ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "home_since" TIMESTAMP(3); + +-- Backfill: stamp existing home rows so they resolve from the upgrade forward. +UPDATE "users" + SET "home_since" = CURRENT_TIMESTAMP + WHERE "home_lat" IS NOT NULL AND "home_since" IS NULL; + +-- AlterEnum: reserve the DEVICE source for the iOS v2 forward path. +ALTER TYPE "environment_location_source" ADD VALUE IF NOT EXISTS 'DEVICE'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 073e17c88..3f82e6ac2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -63,6 +63,12 @@ model User { homeLon Float? @map("home_lon") homeLabel String? @map("home_label") homeTimezone String? @map("home_timezone") + // v1.25 — effective-from instant for the home location. Stamped the moment a + // home is set/updated. Day resolution attributes a past day to home ONLY when + // the day is on/after this instant; earlier days are skipped (never fabricated + // from the current home). To fill the deep past the user adds an explicit + // dated location period instead. Backfill defaults its start to this instant. + homeSince DateTime? @map("home_since") // Codex OAuth (ChatGPT subscription) codexAccessTokenEncrypted String? @map("codex_access_token_encrypted") @@ -811,11 +817,16 @@ model User { // ─── v1.25 (W-ENV) Environmental-context module ────────────────────────────── /// How the location for an environment observation was resolved. HOME = the -/// user's configured home location; TRAVEL = a manual date-range override. -/// (iOS coarse device location is a v2 follow-up and not represented here.) +/// user's configured home location; TRAVEL = an explicit dated location period +/// (a past trip OR a corrected stretch of history) that overrides home for the +/// days it covers; DEVICE = a future client-supplied coarse per-day location. +/// DEVICE is the priority forward path for the iOS v2 client: the value exists +/// so the resolver and the wire contract already accept it, but nothing writes +/// it server-side yet. enum EnvironmentLocationSource { HOME TRAVEL + DEVICE } /// A per-day environmental observation (daily weather / daylight) for the @@ -876,10 +887,13 @@ model EnvironmentContext { @@map("environment_contexts") } -/// A manual travel override: for any day in `[startDate, endDate]` the weather -/// fetch resolves against this location instead of the home location. Declared, -/// not inferred — a stated trip dominates the home fallback so a winter week in -/// a sunny locale is not mis-attributed to the home city. Coarse location only. +/// An explicit dated LOCATION PERIOD: for any day in `[startDate, endDate]` the +/// weather fetch resolves against this location instead of the home location. +/// Declared, not inferred — it dominates the home fallback so a stretch spent +/// elsewhere is not mis-attributed to the home city. Two uses share this one +/// model: a discrete trip away from home, AND the mechanism to correct history +/// (e.g. a whole-history home city for the years before the current home became +/// effective). Coarse location only. (Stored under `source = TRAVEL`.) model EnvironmentTravelLocation { id String @id @default(cuid()) userId String @map("user_id") diff --git a/src/app/api/environment/backfill/route.ts b/src/app/api/environment/backfill/route.ts index 61e6b117b..1a02e93aa 100644 --- a/src/app/api/environment/backfill/route.ts +++ b/src/app/api/environment/backfill/route.ts @@ -21,7 +21,11 @@ import { requireModuleEnabled } from "@/lib/modules/gate"; import { checkAnalyticsReadRateLimit } from "@/lib/rate-limit"; import { prisma } from "@/lib/db"; import { environmentBackfillSchema } from "@/lib/validations/environment"; -import { ENVIRONMENT_MAX_BACKFILL_DAYS } from "@/lib/environment/service"; +import { + ENVIRONMENT_MAX_BACKFILL_DAYS, + defaultBackfillRange, + utcDayKey, +} from "@/lib/environment/service"; import { enqueueEnvironmentFetch } from "@/lib/jobs/environment-fetch"; const MS_PER_DAY = 24 * 60 * 60 * 1000; @@ -49,7 +53,33 @@ export const POST = apiHandler(async (request: NextRequest) => { }); } - const { startDate, endDate } = parsed.data; + const profile = await prisma.user.findUnique({ + where: { id: user.id }, + select: { homeLat: true, homeSince: true }, + }); + if (profile?.homeLat == null) { + return apiError("Set a home location before backfilling.", 409, { + errorCode: "environment.no_home", + }); + } + + // Resolve the effective span. An omitted bound defaults to the conservative + // [homeSince .. today] range — never a fixed reach into the past onto the + // current home. Days before homeSince still resolve to SKIP in the worker, so + // even an explicit wider start cannot fabricate weather for the pre-home past. + const today = utcDayKey(new Date()); + const fallback = defaultBackfillRange(profile.homeSince) ?? { + startDate: today, + endDate: today, + }; + const startDate = parsed.data.startDate ?? fallback.startDate; + const endDate = parsed.data.endDate ?? fallback.endDate; + if (startDate > endDate) { + return apiError("startDate must be on or before endDate", 422, { + errorCode: "environment.invalid", + }); + } + const [sy, sm, sd] = startDate.split("-").map(Number); const [ey, em, ed] = endDate.split("-").map(Number); const spanDays = @@ -64,16 +94,6 @@ export const POST = apiHandler(async (request: NextRequest) => { ); } - const profile = await prisma.user.findUnique({ - where: { id: user.id }, - select: { homeLat: true }, - }); - if (profile?.homeLat == null) { - return apiError("Set a home location before backfilling.", 409, { - errorCode: "environment.no_home", - }); - } - const enqueued = await enqueueEnvironmentFetch({ userId: user.id, startDate, diff --git a/src/app/api/environment/home/route.ts b/src/app/api/environment/home/route.ts index 46bd6443e..ded1c12e5 100644 --- a/src/app/api/environment/home/route.ts +++ b/src/app/api/environment/home/route.ts @@ -41,11 +41,16 @@ export const PUT = apiHandler(async (request: NextRequest) => { } const { lat, lon, label, timezone } = parsed.data; + // Stamp the effective-from instant: from now on this home resolves days; days + // before it stay un-attributed (filled via explicit location periods). Every + // set/update re-stamps — the home is "effective from the moment it is set". + const homeSince = new Date(); const home = { homeLat: roundCoarse(lat), homeLon: roundCoarse(lon), homeLabel: label, homeTimezone: timezone, + homeSince, }; await prisma.user.update({ where: { id: user.id }, data: home }); @@ -62,6 +67,7 @@ export const PUT = apiHandler(async (request: NextRequest) => { lon: home.homeLon, label: home.homeLabel, timezone: home.homeTimezone, + since: homeSince.toISOString(), }, }); }); diff --git a/src/app/api/environment/route.ts b/src/app/api/environment/route.ts index 3d393384c..2d6e1ceb2 100644 --- a/src/app/api/environment/route.ts +++ b/src/app/api/environment/route.ts @@ -29,6 +29,7 @@ export const GET = apiHandler(async () => { homeLon: true, homeLabel: true, homeTimezone: true, + homeSince: true, timezone: true, }, }), @@ -59,6 +60,9 @@ export const GET = apiHandler(async () => { lon: profile.homeLon, label: profile.homeLabel, timezone: profile.homeTimezone ?? profile.timezone, + // Effective-from instant: the settings surface uses it to label the + // home and to prefill the backfill start (conservative default range). + since: profile.homeSince?.toISOString() ?? null, } : null; diff --git a/src/components/settings/environment-section.tsx b/src/components/settings/environment-section.tsx index 68cc87b37..5f66bc2fb 100644 --- a/src/components/settings/environment-section.tsx +++ b/src/components/settings/environment-section.tsx @@ -52,6 +52,9 @@ interface EnvironmentOverview { lon: number; label: string | null; timezone: string; + /** ISO instant the home became effective (drives the conservative backfill + * default + the "tracked since" label). */ + since: string | null; } | null; travel: TravelDTO[]; context: { @@ -162,9 +165,9 @@ export function EnvironmentSection() { const backfill = useMutation({ mutationKey: queryKeys.environment(), - mutationFn: () => + mutationFn: (startDate: string) => apiPost("/api/environment/backfill", { - startDate: backfillStart, + startDate, endDate: backfillEnd, }), onSuccess: () => { @@ -192,6 +195,11 @@ export function EnvironmentSection() { const data = overview.data; const home = data?.home ?? null; + // Conservative default: backfill starts at the home's effective-from date. The + // user can still type an earlier start, but pre-home days resolve to SKIP on + // the server — to fill the deep past they add an explicit location period. + const effectiveBackfillStart = + backfillStart || home?.since?.slice(0, 10) || ""; return (
- {t("settings.sections.environment.home.current", { - label: home.label ?? `${home.lat}, ${home.lon}`, - })} -
++ {t("settings.sections.environment.home.current", { + label: home.label ?? `${home.lat}, ${home.lon}`, + })} +
+ {home.since && ( ++ {t("settings.sections.environment.home.since", { + date: home.since.slice(0, 10), + })} +
+ )} +{t("settings.sections.environment.home.none")} @@ -394,7 +411,7 @@ export function EnvironmentSection() { setBackfillStart(e.target.value)} />