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".
Split into two endpoints with proper targeting.
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))).
data RecentActivityMorePartial = RecentActivityMorePartial [TaskCore.Task] Int Bool
data RecentActivityNewPartial = RecentActivityNewPartial [TaskCore.Task] (Maybe Int)
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
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"
]
""
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"] ""
taskToUnixTs :: TaskCore.Task -> Int
taskToUnixTs t = round (utcTimeToPOSIXSeconds (TaskCore.taskUpdatedAt t))
Add import: Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds, posixSecondsToUTCTime)
Omni/Jr/Web.hs:
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
No activity yet.