ID:t-770
Title:Health Dashboard: Sub-pages, Training Page, Full Activity Cache, All-Time Bests
Status:Review
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