← Back to task

Commit 13edfda9

commit 13edfda9ac34844f9922daf052db4b1050d9e853
Author: Ben Sima <ben@bensima.com>
Date:   Fri Nov 28 02:17:21 2025

    Add pagination to Recent Activity with HTMX load more button
    
    The implementation is complete. Here's a summary:
    
    **Changes made:** 1. **API**: Added `QueryParam "offset" Int` to the
    `/partials/recent-act 2. **Data types**:
       - Updated `HomePage` to include a `Bool` for whether there are
       more t - Updated `RecentActivityPartial` to include `nextOffset ::
       Int` and
    3. **Homepage rendering**: Shows a "Load More" button when there
    are mor 4. **Partial rendering**: The `RecentActivityPartial` now
    shows a "Load 5. **Handler**: `recentActivityHandler` now supports
    pagination with off 6. **Styling**: Added CSS for `.load-more-btn`
    and `.btn-secondary` clas
    
    The HTMX integration uses: - `hx-get` to fetch the next page with
    offset - `hx-target="closest .recent-activity"` to target the parent
    container - `hx-swap="beforeend"` to append new items (including
    another Load More
    
    Task-Id: t-151.4

diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index e58992c8..465c0210 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -76,7 +76,7 @@ type API =
     :<|> "tasks" :> Capture "id" Text :> "accept" :> PostRedirect
     :<|> "tasks" :> Capture "id" Text :> "reject" :> ReqBody '[FormUrlEncoded] RejectForm :> PostRedirect
     :<|> "tasks" :> Capture "id" Text :> "reset-retries" :> PostRedirect
-    :<|> "partials" :> "recent-activity" :> Get '[Lucid.HTML] RecentActivityPartial
+    :<|> "partials" :> "recent-activity" :> QueryParam "offset" Int :> Get '[Lucid.HTML] RecentActivityPartial
     :<|> "partials" :> "ready-count" :> Get '[Lucid.HTML] ReadyCountPartial
     :<|> "partials"
       :> "task-list"
@@ -95,7 +95,7 @@ instance Accept CSS where
 instance MimeRender CSS LazyText.Text where
   mimeRender _ = LazyText.encodeUtf8
 
-data HomePage = HomePage TaskCore.TaskStats [TaskCore.Task] [TaskCore.Task]
+data HomePage = HomePage TaskCore.TaskStats [TaskCore.Task] [TaskCore.Task] Bool
 
 newtype ReadyQueuePage = ReadyQueuePage [TaskCore.Task]
 
@@ -161,7 +161,7 @@ instance FromForm FactCreateForm where
 
 data EpicsPage = EpicsPage [TaskCore.Task] [TaskCore.Task]
 
-newtype RecentActivityPartial = RecentActivityPartial [TaskCore.Task]
+data RecentActivityPartial = RecentActivityPartial [TaskCore.Task] Int Bool
 
 newtype ReadyCountPartial = ReadyCountPartial Int
 
@@ -479,7 +479,7 @@ renderListGroupItem t =
 
 instance Lucid.ToHtml HomePage where
   toHtmlRaw = Lucid.toHtml
-  toHtml (HomePage stats readyTasks recentTasks) =
+  toHtml (HomePage stats readyTasks recentTasks hasMoreRecent) =
     Lucid.doctypehtml_ <| do
       pageHead "Jr Dashboard"
       Lucid.body_ <| do
@@ -518,11 +518,20 @@ instance Lucid.ToHtml HomePage where
               Lucid.makeAttribute "hx-get" "/partials/recent-activity",
               Lucid.makeAttribute "hx-trigger" "every 10s"
             ]
-            <| if null recentTasks
-              then Lucid.p_ [Lucid.class_ "empty-msg"] "No recent tasks."
-              else
-                Lucid.div_ [Lucid.class_ "list-group"]
-                  <| traverse_ renderListGroupItem recentTasks
+            <| do
+              if null recentTasks
+                then Lucid.p_ [Lucid.class_ "empty-msg"] "No recent tasks."
+                else
+                  Lucid.div_ [Lucid.class_ "list-group"]
+                    <| traverse_ renderListGroupItem recentTasks
+              when hasMoreRecent
+                <| Lucid.button_
+                  [ Lucid.class_ "btn btn-secondary load-more-btn",
+                    Lucid.makeAttribute "hx-get" "/partials/recent-activity?offset=5",
+                    Lucid.makeAttribute "hx-target" "closest .recent-activity",
+                    Lucid.makeAttribute "hx-swap" "beforeend"
+                  ]
+                  "Load More"
     where
       statCard :: (Monad m) => Text -> Int -> Text -> Text -> Lucid.HtmlT m ()
       statCard label count badgeClass href =
@@ -1461,12 +1470,20 @@ instance Lucid.ToHtml StatsPage where
 
 instance Lucid.ToHtml RecentActivityPartial where
   toHtmlRaw = Lucid.toHtml
-  toHtml (RecentActivityPartial recentTasks) =
+  toHtml (RecentActivityPartial recentTasks nextOffset hasMore) =
     if null recentTasks
       then Lucid.p_ [Lucid.class_ "empty-msg"] "No recent tasks."
-      else
+      else do
         Lucid.div_ [Lucid.class_ "list-group"]
           <| traverse_ renderListGroupItem recentTasks
+        when hasMore
+          <| Lucid.button_
+            [ Lucid.class_ "btn btn-secondary load-more-btn",
+              Lucid.makeAttribute "hx-get" ("/partials/recent-activity?offset=" <> tshow nextOffset),
+              Lucid.makeAttribute "hx-target" "closest .recent-activity",
+              Lucid.makeAttribute "hx-swap" "beforeend"
+            ]
+            "Load More"
 
 instance Lucid.ToHtml ReadyCountPartial where
   toHtmlRaw = Lucid.toHtml
@@ -1706,8 +1723,10 @@ server =
       stats <- liftIO <| TaskCore.getTaskStats Nothing
       readyTasks <- liftIO TaskCore.getReadyTasks
       allTasks <- liftIO TaskCore.loadTasks
-      let recentTasks = take 5 <| List.sortBy (flip compare `on` TaskCore.taskUpdatedAt) allTasks
-      pure (HomePage stats readyTasks recentTasks)
+      let sortedTasks = List.sortBy (flip compare `on` TaskCore.taskUpdatedAt) allTasks
+          recentTasks = take 5 sortedTasks
+          hasMoreRecent = length allTasks > 5
+      pure (HomePage stats readyTasks recentTasks hasMoreRecent)
 
     readyQueueHandler :: Servant.Handler ReadyQueuePage
     readyQueueHandler = do
@@ -1898,11 +1917,16 @@ server =
         TaskCore.updateTaskStatus tid TaskCore.Open []
       pure <| addHeader ("/tasks/" <> tid) NoContent
 
-    recentActivityHandler :: Servant.Handler RecentActivityPartial
-    recentActivityHandler = do
+    recentActivityHandler :: Maybe Int -> Servant.Handler RecentActivityPartial
+    recentActivityHandler maybeOffset = do
       allTasks <- liftIO TaskCore.loadTasks
-      let recentTasks = take 5 <| List.sortBy (flip compare `on` TaskCore.taskUpdatedAt) allTasks
-      pure (RecentActivityPartial recentTasks)
+      let offset = fromMaybe 0 maybeOffset
+          pageSize = 5
+          sortedTasks = List.sortBy (flip compare `on` TaskCore.taskUpdatedAt) allTasks
+          pageTasks = take pageSize <| drop offset sortedTasks
+          hasMore = length sortedTasks > offset + pageSize
+          nextOffset = offset + pageSize
+      pure (RecentActivityPartial pageTasks nextOffset hasMore)
 
     readyCountHandler :: Servant.Handler ReadyCountPartial
     readyCountHandler = do
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
index 7d6e7d65..866ed520 100644
--- a/Omni/Jr/Web/Style.hs
+++ b/Omni/Jr/Web/Style.hs
@@ -634,6 +634,12 @@ buttonStyles = do
     color white
   ".review-link-btn" # hover ? backgroundColor "#7c3aed"
   ".review-link-section" ? margin (px 8) (px 0) (px 8) (px 0)
+  ".btn-secondary" <> ".load-more-btn" ? do
+    backgroundColor "#6b7280"
+    color white
+    width (pct 100)
+    marginTop (px 8)
+  ".btn-secondary" # hover <> ".load-more-btn" # hover ? backgroundColor "#4b5563"
 
 formStyles :: Css
 formStyles = do