summaryrefslogtreecommitdiff
path: root/Omni/Jr
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-11-27 09:59:42 -0500
committerBen Sima <ben@bensima.com>2025-11-27 09:59:42 -0500
commitdbe2b82d2f53761d9504fb4e5cf37f64a2000b7a (patch)
tree505a236b1de090a411b0ce5b6ced034d42530ec7 /Omni/Jr
parentf151fbf764670369c54d2b9c03a5d1c8c1bb82fb (diff)
Show activity timeline on task detail page in web UI
All builds pass. Here's a summary of the changes I made: **Omni/Task/Core.hs:** - Added `getActivitiesForTask` function to query task_activity table for **Omni/Jr/Web.hs:** - Added `Data.Time` import for timestamp formatting - Updated `TaskDetailPage` type to include `[TaskCore.TaskActivity]` - Updated `taskDetailHandler` to fetch activities for the task - Added activity timeline rendering in `toHtml` instance for InProgress - Added helper functions: `renderActivity`, `stageClass`, `stageIcon`, ` **Omni/Jr/Web/Style.hs:** - Added `activityTimelineStyles` function with vertical timeline CSS - Added stage-specific colors (claiming=blue, running=yellow, reviewing= - Added dark mode styles for the activity timeline Task-Id: t-148.3
Diffstat (limited to 'Omni/Jr')
-rw-r--r--Omni/Jr/Web.hs54
-rw-r--r--Omni/Jr/Web/Style.hs102
2 files changed, 153 insertions, 3 deletions
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index 28f42a2..d117169 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -21,6 +21,7 @@ import qualified Data.List as List
import qualified Data.Text as Text
import qualified Data.Text.Lazy as LazyText
import qualified Data.Text.Lazy.Encoding as LazyText
+import Data.Time (UTCTime, defaultTimeLocale, formatTime)
import qualified Lucid
import qualified Network.Wai.Handler.Warp as Warp
import qualified Omni.Jr.Web.Style as Style
@@ -74,7 +75,7 @@ newtype ReadyQueuePage = ReadyQueuePage [TaskCore.Task]
data TaskListPage = TaskListPage [TaskCore.Task] TaskFilters
data TaskDetailPage
- = TaskDetailFound TaskCore.Task [TaskCore.Task]
+ = TaskDetailFound TaskCore.Task [TaskCore.Task] [TaskCore.TaskActivity]
| TaskDetailNotFound Text
data TaskReviewPage
@@ -271,7 +272,7 @@ instance Lucid.ToHtml TaskDetailPage where
Lucid.code_ (Lucid.toHtml tid)
" could not be found."
Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
- toHtml (TaskDetailFound task allTasks) =
+ toHtml (TaskDetailFound task allTasks activities) =
Lucid.doctypehtml_ <| do
pageHead (TaskCore.taskId task <> " - Jr")
Lucid.body_ <| do
@@ -342,6 +343,12 @@ instance Lucid.ToHtml TaskDetailPage where
Lucid.ul_ [Lucid.class_ "child-list"] <| do
traverse_ renderChild children
+ when (TaskCore.taskStatus task == TaskCore.InProgress && not (null activities)) <| do
+ Lucid.div_ [Lucid.class_ "activity-section"] <| do
+ Lucid.h3_ "Activity Timeline"
+ Lucid.div_ [Lucid.class_ "activity-timeline"] <| do
+ traverse_ renderActivity activities
+
when (TaskCore.taskStatus task == TaskCore.Review) <| do
Lucid.div_ [Lucid.class_ "review-link-section"] <| do
Lucid.a_
@@ -387,6 +394,45 @@ instance Lucid.ToHtml TaskDetailPage where
Lucid.span_ [Lucid.class_ "child-title"] <| Lucid.toHtml (" - " <> TaskCore.taskTitle child)
Lucid.span_ [Lucid.class_ "child-status"] <| Lucid.toHtml (" [" <> tshow (TaskCore.taskStatus child) <> "]")
+ renderActivity :: (Monad m) => TaskCore.TaskActivity -> Lucid.HtmlT m ()
+ renderActivity act =
+ Lucid.div_ [Lucid.class_ ("activity-item " <> stageClass (TaskCore.activityStage act))] <| do
+ Lucid.div_ [Lucid.class_ "activity-icon"] (stageIcon (TaskCore.activityStage act))
+ Lucid.div_ [Lucid.class_ "activity-content"] <| do
+ Lucid.div_ [Lucid.class_ "activity-header"] <| do
+ Lucid.span_ [Lucid.class_ "activity-stage"] (Lucid.toHtml (tshow (TaskCore.activityStage act)))
+ Lucid.span_ [Lucid.class_ "activity-time"] (Lucid.toHtml (formatTimestamp (TaskCore.activityTimestamp act)))
+ case TaskCore.activityMessage act of
+ Nothing -> pure ()
+ Just msg -> Lucid.p_ [Lucid.class_ "activity-message"] (Lucid.toHtml msg)
+ case TaskCore.activityMetadata act of
+ Nothing -> pure ()
+ Just meta ->
+ Lucid.details_ [Lucid.class_ "activity-metadata"] <| do
+ Lucid.summary_ "Metadata"
+ Lucid.pre_ [Lucid.class_ "metadata-json"] (Lucid.toHtml meta)
+
+ stageClass :: TaskCore.ActivityStage -> Text
+ stageClass stage = case stage of
+ TaskCore.Claiming -> "stage-claiming"
+ TaskCore.Running -> "stage-running"
+ TaskCore.Reviewing -> "stage-reviewing"
+ TaskCore.Retrying -> "stage-retrying"
+ TaskCore.Completed -> "stage-completed"
+ TaskCore.Failed -> "stage-failed"
+
+ stageIcon :: (Monad m) => TaskCore.ActivityStage -> Lucid.HtmlT m ()
+ stageIcon stage = case stage of
+ TaskCore.Claiming -> "●"
+ TaskCore.Running -> "▶"
+ TaskCore.Reviewing -> "◎"
+ TaskCore.Retrying -> "↻"
+ TaskCore.Completed -> "✓"
+ TaskCore.Failed -> "✗"
+
+ formatTimestamp :: UTCTime -> Text
+ formatTimestamp = Text.pack <. formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S"
+
instance Lucid.ToHtml TaskReviewPage where
toHtmlRaw = Lucid.toHtml
toHtml (ReviewPageNotFound tid) =
@@ -645,7 +691,9 @@ server =
tasks <- liftIO TaskCore.loadTasks
case TaskCore.findTask tid tasks of
Nothing -> pure (TaskDetailNotFound tid)
- Just task -> pure (TaskDetailFound task tasks)
+ Just task -> do
+ activities <- liftIO (TaskCore.getActivitiesForTask tid)
+ pure (TaskDetailFound task tasks activities)
taskStatusHandler :: Text -> StatusForm -> Servant.Handler (Headers '[Header "Location" Text] NoContent)
taskStatusHandler tid (StatusForm newStatus) = do
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
index c1ad47e..8b6a8a7 100644
--- a/Omni/Jr/Web/Style.hs
+++ b/Omni/Jr/Web/Style.hs
@@ -29,6 +29,7 @@ stylesheet = do
statusBadges
buttonStyles
formStyles
+ activityTimelineStyles
responsiveStyles
darkModeStyles
@@ -434,6 +435,96 @@ formStyles = do
fontSize (px 14)
Stylesheet.key "resize" ("vertical" :: Text)
+activityTimelineStyles :: Css
+activityTimelineStyles = do
+ ".activity-section" ? do
+ marginTop (em 1.5)
+ backgroundColor white
+ borderRadius (px 8) (px 8) (px 8) (px 8)
+ padding (px 16) (px 16) (px 16) (px 16)
+ boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.1) (shadow (px 0) (px 1))))
+ ".activity-timeline" ? do
+ position relative
+ paddingLeft (px 24)
+ marginTop (px 12)
+ ".activity-timeline" # before ? do
+ Stylesheet.key "content" ("''" :: Text)
+ position absolute
+ left (px 8)
+ top (px 0)
+ bottom (px 0)
+ width (px 2)
+ backgroundColor "#e5e7eb"
+ ".activity-item" ? do
+ position relative
+ display flex
+ Stylesheet.key "gap" ("12px" :: Text)
+ paddingBottom (px 16)
+ marginBottom (px 0)
+ ".activity-item" # lastChild ? paddingBottom (px 0)
+ ".activity-icon" ? do
+ position absolute
+ left (px (-20))
+ width (px 18)
+ height (px 18)
+ borderRadius (pct 50) (pct 50) (pct 50) (pct 50)
+ display flex
+ alignItems center
+ justifyContent center
+ fontSize (px 10)
+ fontWeight bold
+ backgroundColor white
+ border (px 2) solid "#e5e7eb"
+ ".activity-content" ? do
+ Stylesheet.key "flex" ("1" :: Text)
+ ".activity-header" ? do
+ display flex
+ alignItems center
+ Stylesheet.key "gap" ("8px" :: Text)
+ marginBottom (px 4)
+ ".activity-stage" ? do
+ fontWeight (weight 600)
+ fontSize (px 14)
+ ".activity-time" ? do
+ fontSize (px 12)
+ color "#6b7280"
+ ".activity-message" ? do
+ margin (px 4) (px 0) (px 0) (px 0)
+ fontSize (px 14)
+ color "#374151"
+ ".activity-metadata" ? do
+ marginTop (px 8)
+ (".activity-metadata" |> "summary") ? do
+ fontSize (px 12)
+ color "#6b7280"
+ cursor pointer
+ ".metadata-json" ? do
+ fontSize (px 11)
+ backgroundColor "#f3f4f6"
+ padding (px 8) (px 8) (px 8) (px 8)
+ borderRadius (px 4) (px 4) (px 4) (px 4)
+ marginTop (px 4)
+ maxHeight (px 200)
+ overflow auto
+ ".stage-claiming" |> ".activity-icon" ? do
+ borderColor "#3b82f6"
+ color "#3b82f6"
+ ".stage-running" |> ".activity-icon" ? do
+ borderColor "#f59e0b"
+ color "#f59e0b"
+ ".stage-reviewing" |> ".activity-icon" ? do
+ borderColor "#8b5cf6"
+ color "#8b5cf6"
+ ".stage-retrying" |> ".activity-icon" ? do
+ borderColor "#f97316"
+ color "#f97316"
+ ".stage-completed" |> ".activity-icon" ? do
+ borderColor "#10b981"
+ color "#10b981"
+ ".stage-failed" |> ".activity-icon" ? do
+ borderColor "#ef4444"
+ color "#ef4444"
+
responsiveStyles :: Css
responsiveStyles = do
query Media.screen [Media.maxWidth (px 600)] <| do
@@ -529,6 +620,17 @@ darkModeStyles =
".stats-row" ? borderBottomColor "#374151"
".progress-bar" ? backgroundColor "#374151"
".progress-fill" ? backgroundColor "#60a5fa"
+ ".activity-section" ? do
+ backgroundColor "#1f2937"
+ boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.3) (shadow (px 0) (px 2))))
+ ".activity-timeline" # before ? backgroundColor "#374151"
+ ".activity-icon" ? do
+ backgroundColor "#1f2937"
+ borderColor "#374151"
+ ".activity-time" ? color "#9ca3af"
+ ".activity-message" ? color "#d1d5db"
+ (".activity-metadata" |> "summary") ? color "#9ca3af"
+ ".metadata-json" ? backgroundColor "#374151"
prefersDark :: Stylesheet.Feature
prefersDark =