← Back to task

Commit f7b875b6

commit f7b875b650e791f604fcf35e04f06310352cb352
Author: Ben Sima <ben@bensima.com>
Date:   Sat Nov 29 20:40:25 2025

    Add cost and duration metrics to homepage Task Status section
    
    The implementation is complete. The build passed with `bild --test
    Omni/
    
    **Summary of changes:**
    
    1. **Omni/Task/Core.hs**: Added `getGlobalAggregatedMetrics ::
    IO Aggreg
    
    2. **Omni/Jr/Web.hs**:
       - Updated `HomePage` data type to include `AggregatedMetrics`
       paramet - Updated `homeHandler` to fetch global metrics via
       `getGlobalAggrega - Updated `ToHtml HomePage` instance to add
       two new cards: "Cost" (fo - Added helper functions `metricCard`,
       `formatCost`, and `formatDurat
    
    3. **Omni/Jr/Web/Style.hs**: Added `badge-neutral` CSS styling
    (gray bor
    
    Task-Id: t-179

diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index bed26a62..533d7612 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -139,7 +139,7 @@ instance Accept CSS where
 instance MimeRender CSS LazyText.Text where
   mimeRender _ = LazyText.encodeUtf8
 
-data HomePage = HomePage TaskCore.TaskStats [TaskCore.Task] [TaskCore.Task] Bool UTCTime
+data HomePage = HomePage TaskCore.TaskStats [TaskCore.Task] [TaskCore.Task] Bool TaskCore.AggregatedMetrics UTCTime
 
 data ReadyQueuePage = ReadyQueuePage [TaskCore.Task] UTCTime
 
@@ -611,7 +611,7 @@ renderListGroupItem t =
 
 instance Lucid.ToHtml HomePage where
   toHtmlRaw = Lucid.toHtml
-  toHtml (HomePage stats readyTasks recentTasks hasMoreRecent _now) =
+  toHtml (HomePage stats readyTasks recentTasks hasMoreRecent globalMetrics _now) =
     Lucid.doctypehtml_ <| do
       pageHead "Jr Dashboard"
       pageBody <| do
@@ -623,6 +623,8 @@ instance Lucid.ToHtml HomePage where
             statCard "Review" (TaskCore.reviewTasks stats) "badge-review" "/tasks?status=Review"
             statCard "Approved" (TaskCore.approvedTasks stats) "badge-approved" "/tasks?status=Approved"
             statCard "Done" (TaskCore.doneTasks stats) "badge-done" "/tasks?status=Done"
+            metricCard "Cost" (formatCost (TaskCore.aggTotalCostCents globalMetrics))
+            metricCard "Duration" (formatDuration (TaskCore.aggTotalDurationSeconds globalMetrics))
 
           Lucid.h2_ <| do
             "Ready Queue "
@@ -671,6 +673,28 @@ instance Lucid.ToHtml HomePage where
           Lucid.div_ [Lucid.class_ "stat-count"] (Lucid.toHtml (tshow count))
           Lucid.div_ [Lucid.class_ "stat-label"] (Lucid.toHtml label)
 
+      metricCard :: (Monad m) => Text -> Text -> Lucid.HtmlT m ()
+      metricCard label value =
+        Lucid.div_ [Lucid.class_ "stat-card badge-neutral"] <| do
+          Lucid.div_ [Lucid.class_ "stat-count"] (Lucid.toHtml value)
+          Lucid.div_ [Lucid.class_ "stat-label"] (Lucid.toHtml label)
+
+      formatCost :: Int -> Text
+      formatCost cents =
+        let dollars = fromIntegral cents / 100.0 :: Double
+         in Text.pack ("$" <> showFFloat (Just 2) dollars "")
+
+      formatDuration :: Int -> Text
+      formatDuration totalSeconds
+        | totalSeconds < 60 = tshow totalSeconds <> "s"
+        | totalSeconds < 3600 =
+            let mins = totalSeconds `div` 60
+             in tshow mins <> "m"
+        | otherwise =
+            let hours = totalSeconds `div` 3600
+                mins = (totalSeconds `mod` 3600) `div` 60
+             in tshow hours <> "h " <> tshow mins <> "m"
+
 instance Lucid.ToHtml ReadyQueuePage where
   toHtmlRaw = Lucid.toHtml
   toHtml (ReadyQueuePage tasks _now) =
@@ -2018,10 +2042,11 @@ server =
       stats <- liftIO <| TaskCore.getTaskStats Nothing
       readyTasks <- liftIO TaskCore.getReadyTasks
       allTasks <- liftIO TaskCore.loadTasks
+      globalMetrics <- liftIO TaskCore.getGlobalAggregatedMetrics
       let sortedTasks = List.sortBy (flip compare `on` TaskCore.taskUpdatedAt) allTasks
           recentTasks = take 5 sortedTasks
           hasMoreRecent = length allTasks > 5
-      pure (HomePage stats readyTasks recentTasks hasMoreRecent now)
+      pure (HomePage stats readyTasks recentTasks hasMoreRecent globalMetrics now)
 
     readyQueueHandler :: Servant.Handler ReadyQueuePage
     readyQueueHandler = do
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
index 0628c536..ad1ff027 100644
--- a/Omni/Jr/Web/Style.hs
+++ b/Omni/Jr/Web/Style.hs
@@ -336,6 +336,8 @@ cardStyles = do
   (".stat-card.badge-approved" |> ".stat-count") ? color "#0e7490"
   ".stat-card.badge-done" ? borderLeft (px 4) solid "#10b981"
   (".stat-card.badge-done" |> ".stat-count") ? color "#065f46"
+  ".stat-card.badge-neutral" ? borderLeft (px 4) solid "#6b7280"
+  (".stat-card.badge-neutral" |> ".stat-count") ? color "#374151"
   ".task-card" ? do
     transition "border-color" (ms 150) ease (sec 0)
   ".task-card" # hover ? do
@@ -1382,6 +1384,7 @@ darkModeStyles =
     (".stat-card.badge-review" |> ".stat-count") ? color "#a78bfa"
     (".stat-card.badge-approved" |> ".stat-count") ? color "#22d3ee"
     (".stat-card.badge-done" |> ".stat-count") ? color "#34d399"
+    (".stat-card.badge-neutral" |> ".stat-count") ? color "#9ca3af"
 
     ".progress-bar" ? backgroundColor "#374151"
     ".progress-fill" ? backgroundColor "#60a5fa"
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index 773a01f6..722e696b 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -1320,6 +1320,28 @@ getAggregatedMetrics epicId = do
       (Just start, Just end) -> floor (diffUTCTime end start)
       _ -> 0
 
+-- | Get aggregated metrics for all tasks globally (not scoped to an epic)
+getGlobalAggregatedMetrics :: IO AggregatedMetrics
+getGlobalAggregatedMetrics = do
+  allTasks <- loadTasks
+  let completedCount = length [t | t <- allTasks, taskStatus t == Done]
+      taskIds = map taskId allTasks
+  activities <- concat </ traverse getActivitiesForTask taskIds
+  let totalCost = sum [c | act <- activities, Just c <- [activityCostCents act]]
+      totalTokens = sum [t | act <- activities, Just t <- [activityTokensUsed act]]
+      totalDuration = sum [calcDuration act | act <- activities]
+  pure
+    AggregatedMetrics
+      { aggTotalCostCents = totalCost,
+        aggTotalDurationSeconds = totalDuration,
+        aggCompletedTasks = completedCount,
+        aggTotalTokens = totalTokens
+      }
+  where
+    calcDuration act = case (activityStartedAt act, activityCompletedAt act) of
+      (Just start, Just end) -> floor (diffUTCTime end start)
+      _ -> 0
+
 -- | Get tasks with unmet blocking dependencies (not ready, not done)
 getBlockedTasks :: IO [Task]
 getBlockedTasks = do