← Back to task

Commit dfc71f8b

commit dfc71f8b6a6b29a9db23a343b8568b2c1f25eefa
Author: Coder Agent <coder@agents.omni>
Date:   Fri Apr 10 17:03:25 2026

    health: fix HR zone index in easy filter, clean up unused bindings
    
    - Use the second HR-zone boundary as Z2 upper bound in easy-effort filtering.
    - Remove unused time args from training/labs page render functions.
    - Surface intervals backfill/fetch/decode failures with warning logs that include date ranges.
    
    Task-Id: t-770

diff --git a/Omni/Health/Analyze.hs b/Omni/Health/Analyze.hs
index 6c5cd724..70f0ff55 100644
--- a/Omni/Health/Analyze.hs
+++ b/Omni/Health/Analyze.hs
@@ -633,7 +633,7 @@ isEasyRunEffort a =
 isBelowEasyHrThreshold :: TrainingActivity -> Bool
 isBelowEasyHrThreshold a =
   case (taAvgHR a, taHRZones a) of
-    (Just hr, Just (z2Upper : _)) -> hr <= z2Upper
+    (Just hr, Just (_ : z2Upper : _)) -> hr <= z2Upper
     _ -> False
 
 runEfficiencyFactor :: TrainingActivity -> Maybe Double
diff --git a/Omni/Health/Intervals.hs b/Omni/Health/Intervals.hs
index d9afda73..02038401 100644
--- a/Omni/Health/Intervals.hs
+++ b/Omni/Health/Intervals.hs
@@ -137,7 +137,18 @@ monthEnd day =
 
 fetchActivitiesRange :: Text -> Text -> Time.Day -> Time.Day -> IO [Aeson.Value]
 fetchActivitiesRange apiKey athleteId oldest newest = do
-  let url =
+  result <- fetchActivitiesRangeDetailed apiKey athleteId oldest newest
+  case result of
+    Left err -> do
+      putText <| "Warning: " <> err
+      pure []
+    Right activities -> pure activities
+
+fetchActivitiesRangeDetailed :: Text -> Text -> Time.Day -> Time.Day -> IO (Either Text [Aeson.Value])
+fetchActivitiesRangeDetailed apiKey athleteId oldest newest = do
+  let oldestTxt = Text.pack (Time.showGregorian oldest)
+      newestTxt = Text.pack (Time.showGregorian newest)
+      url =
         "https://intervals.icu/api/v1/athlete/"
           <> Text.unpack athleteId
           <> "/activities?oldest="
@@ -153,12 +164,26 @@ fetchActivitiesRange apiKey athleteId oldest newest = do
           <| req
   result <- try (HTTP.httpLBS req')
   case result of
-    Left (_ :: SomeException) -> pure []
+    Left (err :: SomeException) ->
+      pure
+        <| Left
+        <| "intervals fetch failed for "
+        <> oldestTxt
+        <> ".."
+        <> newestTxt
+        <> ": "
+        <> tshow err
     Right resp ->
       let body = HTTP.getResponseBody resp
        in case Aeson.decode body of
-            Just activities -> pure activities
-            Nothing -> pure []
+            Just activities -> pure (Right activities)
+            Nothing ->
+              pure
+                <| Left
+                <| "intervals decode failed for "
+                <> oldestTxt
+                <> ".."
+                <> newestTxt
 
 mergeRawActivities :: [Aeson.Value] -> [Aeson.Value] -> [Aeson.Value]
 mergeRawActivities existing incoming =
diff --git a/Omni/Health/Web.hs b/Omni/Health/Web.hs
index 5cd0aa03..904524c4 100644
--- a/Omni/Health/Web.hs
+++ b/Omni/Health/Web.hs
@@ -121,22 +121,20 @@ handleNutrition dataDir respond = do
 
 handleTraining :: FilePath -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
 handleTraining dataDir respond = do
-  now <- Time.getCurrentTime
   activities <- Intervals.fetchAndCacheTraining dataDir
   respond
     <| Wai.responseLBS HTTP.status200 [("Content-Type", "text/html; charset=utf-8")]
     <| L.renderBS
-    <| trainingPage now activities
+    <| trainingPage activities
 
 handleLabs :: FilePath -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
 handleLabs dataDir respond = do
-  now <- Time.getCurrentTime
   bwData <- loadFile (dataDir <> "/bloodwork.csv")
   let bloodwork = A.parseBloodworkCsv bwData
   respond
     <| Wai.responseLBS HTTP.status200 [("Content-Type", "text/html; charset=utf-8")]
     <| L.renderBS
-    <| labsPage now bloodwork
+    <| labsPage bloodwork
 
 -- | Handle CGM CSV upload.
 handleCgmUpload :: FilePath -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
@@ -233,14 +231,14 @@ nutritionPage now latestReading latestMeal foodRankings mealSpikeRankings hypoCr
     renderCorrelations correlations
     renderPostWorkout postWorkout postWorkoutComparison postWorkoutMealSpikes
 
-trainingPage :: Time.UTCTime -> [A.TrainingActivity] -> L.Html ()
-trainingPage _now activities =
+trainingPage :: [A.TrainingActivity] -> L.Html ()
+trainingPage activities =
   renderHealthPage "Health Training" "training" True <| do
     renderTrainingCharts activities
     renderAllTimeBests (A.computeTrainingBests activities)
 
-labsPage :: Time.UTCTime -> [A.BloodworkResult] -> L.Html ()
-labsPage _now bloodwork =
+labsPage :: [A.BloodworkResult] -> L.Html ()
+labsPage bloodwork =
   renderHealthPage "Health Labs" "labs" False <| do
     renderLabResults bloodwork