diff --git a/app.py b/app.py index d61614e..d7dd6a6 100644 --- a/app.py +++ b/app.py @@ -164,14 +164,28 @@ def _json_response(body: bytes, etag: str | None = None) -> Response: # /cell bundle, and the offline migrate script. HTTP semantics (audit runs, cache # lookups, etags) stay in the endpoints; `run` is the audit run or None. -def _build_grade(cell, target, days, history, recent, cache_meta, place, run=None) -> dict: - """/grade payload: the last `days` observed days graded against their own - ±7-day windows, plus the target day's climatology summary.""" +def _build_grade(cell, target, days, history, recent, cache_meta, place, run=None, after=14) -> dict: + """/grade payload: a window of days centered on the target — `days` of history + before it, the target itself, then up to `after` days after it — plus the + target day's climatology summary. Days after the target are observed when they + are already in the past and forecast when they run into the future, so the + weekly view can frame two weeks of history around the orange target marker and + trail off into up to 14 days of observations / forecast after it. The forecast + only reaches ~7 days out, so a recent target naturally yields some observed days + plus 1-7 forecast days, while an older target fills the whole 14 with real obs.""" run = run or _NullRun() with run.phase("grading"): - # Only grade observed days up to and including the target date (last `days`). - recent = recent[recent["date"] <= target].sort_values("date").tail(days) - graded = _grade_rows(history, recent) + lo = target - pd.Timedelta(days=days) + hi = target + pd.Timedelta(days=after) + # Build the window from both sources. The recent+forecast bundle covers the + # last few weeks plus the forward forecast (the only source for future days); + # the archive reaches decades back for targets older than that bundle. Prefer + # the bundle row for any given date, filling the rest from the archive. + rwin = recent[(recent["date"] >= lo) & (recent["date"] <= hi)] + hwin = history[(history["date"] >= lo) & (history["date"] <= hi)] + hwin = hwin[~hwin["date"].isin(set(rwin["date"]))] + window = pd.concat([rwin, hwin]).sort_values("date") + graded = _grade_rows(history, window) _attach_dry_streaks(graded, history, recent) graded.reverse() # newest first for display climo = grading.climatology(history, int(target.dayofyear)) @@ -331,7 +345,10 @@ def api_grade( lat: float = Query(..., ge=-90, le=90), lon: float = Query(..., ge=-180, le=180), date: str | None = Query(None, description="target date YYYY-MM-DD (default today)"), - days: int = Query(14, ge=1, le=60, description="how many recent days to grade"), + days: int = Query(14, ge=1, le=60, description="days of history to grade before the target"), + after: int = Query(14, ge=0, le=14, + description="days to grade after the target (observed, or forecast when future; " + "the forecast reaches ~7 days out so future days cap there)"), ): if not grid.in_north_america(lat, lon): raise HTTPException( @@ -365,7 +382,7 @@ def api_grade( if history.empty: raise HTTPException(status_code=404, detail="No historical data for this cell.") - key = f"{target.date().isoformat()}:{days}" + key = f"{target.date().isoformat()}:{days}:{after}" token = f"{PAYLOAD_VER}:{_hist_end(history)}:{climate.recent_stamp(cell['id'])}" etag = _etag_for("grade", cell["id"], key, token) if _not_modified(request, etag): @@ -378,7 +395,7 @@ def api_grade( with run.phase("reverse_geocode"): place = climate.reverse_geocode(cell["center_lat"], cell["center_lon"]) - payload = _build_grade(cell, target, days, history, recent, cache_meta, place, run) + payload = _build_grade(cell, target, days, history, recent, cache_meta, place, run, after=after) return _json_response(store.put_payload("grade", cell["id"], key, token, payload), etag) @@ -647,8 +664,8 @@ def api_cell( else: rf_token = f"{PAYLOAD_VER}:{hist_end}:{climate.recent_stamp(cid)}" slices["grade"] = slice_for( - "grade", f"{today.date().isoformat()}:14", rf_token, - lambda: _build_grade(cell, today, 14, history, recent, cache_meta, place, run)) + "grade", f"{today.date().isoformat()}:14:14", rf_token, + lambda: _build_grade(cell, today, 14, history, recent, cache_meta, place, run, after=14)) slices["forecast"] = slice_for( "forecast", f"{today.date().isoformat()}:7", rf_token, lambda: _build_forecast(cell, 7, history, recent, today, place, run))