Fix Recent Activity pagination and refresh bugs

t-169·WorkTask·
·
·
·Omni/Jr.hs
Created3 months ago·Updated3 months ago

Description

Edit

Problem

Two bugs in Recent Activity section on dashboard:

1. Load More button duplicates: When clicking "Load More", the partial returns both items AND a new button, and hx-swap="beforeend" appends both, resulting in multiple buttons.

2. Auto-refresh loses loaded content: The 10-second refresh replaces the entire container with first 5 items, losing any items loaded via "Load More".

Solution

Split into two endpoints with proper targeting.

HTML Structure (HomePage render)

Lucid.div_
  [ Lucid.class_ "recent-activity",
    Lucid.id_ "recent-activity",
    Lucid.makeAttribute "data-newest-ts" (tshow newestTimestamp),
    Lucid.makeAttribute "hx-get" "/partials/recent-activity-new",
    Lucid.makeAttribute "hx-trigger" "every 10s",
    Lucid.makeAttribute "hx-vals" "js:{since: this.dataset.newestTs}",
    Lucid.makeAttribute "hx-target" "#activity-list",
    Lucid.makeAttribute "hx-swap" "afterbegin"
  ]
  <| do
    Lucid.div_ [Lucid.id_ "activity-list", Lucid.class_ "list-group"]
      <| traverse_ renderListGroupItem recentTasks
    when hasMoreRecent
      <| Lucid.button_
        [ Lucid.id_ "activity-load-more",
          Lucid.class_ "btn btn-secondary load-more-btn",
          Lucid.makeAttribute "hx-get" "/partials/recent-activity-more?offset=5",
          Lucid.makeAttribute "hx-target" "#activity-list",
          Lucid.makeAttribute "hx-swap" "beforeend"
        ]
        "Load More"

Where newestTimestamp is round (utcTimeToPOSIXSeconds (taskUpdatedAt (head recentTasks))).

New Types

data RecentActivityMorePartial = RecentActivityMorePartial [TaskCore.Task] Int Bool
data RecentActivityNewPartial = RecentActivityNewPartial [TaskCore.Task] (Maybe Int)

API Routes

Replace existing recent-activity route with:

    :<|> "partials" :> "recent-activity-new" :> QueryParam "since" Int :> Get '[Lucid.HTML] RecentActivityNewPartial
    :<|> "partials" :> "recent-activity-more" :> QueryParam "offset" Int :> Get '[Lucid.HTML] RecentActivityMorePartial

Endpoint 1: GET /partials/recent-activity-new?since=<unix-ts>

Purpose: Prepend new items on auto-refresh (every 10s)

recentActivityNewHandler :: Maybe Int -> Servant.Handler RecentActivityNewPartial
recentActivityNewHandler maybeSince = do
  allTasks <- liftIO TaskCore.loadTasks
  let sinceTime = maybe (posixSecondsToUTCTime 0) (posixSecondsToUTCTime . fromIntegral) maybeSince
      sortedTasks = List.sortBy (flip compare `on` TaskCore.taskUpdatedAt) allTasks
      newTasks = filter (\t -> TaskCore.taskUpdatedAt t > sinceTime) sortedTasks
      newestTs = if null newTasks then maybeSince else Just (taskToUnixTs (head newTasks))
  pure (RecentActivityNewPartial newTasks newestTs)

Render returns raw list items + OOB attribute update:

instance Lucid.ToHtml RecentActivityNewPartial where
  toHtmlRaw = Lucid.toHtml
  toHtml (RecentActivityNewPartial tasks maybeNewestTs) = do
    traverse_ renderListGroupItem tasks
    case maybeNewestTs of
      Nothing -> pure ()
      Just ts ->
        Lucid.div_
          [ Lucid.id_ "recent-activity",
            Lucid.makeAttribute "data-newest-ts" (tshow ts),
            Lucid.makeAttribute "hx-swap-oob" "attributes:#recent-activity data-newest-ts"
          ]
          ""

Endpoint 2: GET /partials/recent-activity-more?offset=<n>

Purpose: Load older items on "Load More" click

recentActivityMoreHandler :: Maybe Int -> Servant.Handler RecentActivityMorePartial
recentActivityMoreHandler maybeOffset = do
  allTasks <- liftIO TaskCore.loadTasks
  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 (RecentActivityMorePartial pageTasks nextOffset hasMore)

Render returns items + OOB button replacement:

instance Lucid.ToHtml RecentActivityMorePartial where
  toHtmlRaw = Lucid.toHtml
  toHtml (RecentActivityMorePartial tasks nextOffset hasMore) = do
    traverse_ renderListGroupItem tasks
    if hasMore
      then
        Lucid.button_
          [ Lucid.id_ "activity-load-more",
            Lucid.class_ "btn btn-secondary load-more-btn",
            Lucid.makeAttribute "hx-get" ("/partials/recent-activity-more?offset=" <> tshow nextOffset),
            Lucid.makeAttribute "hx-target" "#activity-list",
            Lucid.makeAttribute "hx-swap" "beforeend",
            Lucid.makeAttribute "hx-swap-oob" "true"
          ]
          "Load More"
      else
        Lucid.span_ [Lucid.id_ "activity-load-more", Lucid.makeAttribute "hx-swap-oob" "true"] ""

Helper Function

taskToUnixTs :: TaskCore.Task -> Int
taskToUnixTs t = round (utcTimeToPOSIXSeconds (TaskCore.taskUpdatedAt t))

Add import: Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds, posixSecondsToUTCTime)

Files to Modify

Omni/Jr/Web.hs:

  • Add new partial types (RecentActivityNewPartial, RecentActivityMorePartial)
  • Remove old RecentActivityPartial type
  • Update API type (replace old route with two new ones)
  • Add handlers (recentActivityNewHandler, recentActivityMoreHandler)
  • Update HomePage render (add IDs, data-newest-ts, update HTMX attrs)
  • Add taskToUnixTs helper
  • Add import for Data.Time.Clock.POSIX

Testing

1. Load dashboard, verify initial 5 items shown 2. Click "Load More", verify items 6-10 appear, single button with offset=10 3. Click "Load More" again, verify items 11-15 appear, button updates 4. Wait 10 seconds, verify no items lost, any new activity prepends 5. Create a new task in another terminal, verify it appears at top within 10s 6. Click "Load More" until no more items, verify button disappears

Timeline (0)

No activity yet.