diff options
Diffstat (limited to 'Omni/Jr')
| -rw-r--r-- | Omni/Jr/Web.hs | 162 | ||||
| -rw-r--r-- | Omni/Jr/Web/Style.hs | 41 |
2 files changed, 199 insertions, 4 deletions
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs index 3c24d71..5fc8126 100644 --- a/Omni/Jr/Web.hs +++ b/Omni/Jr/Web.hs @@ -21,10 +21,11 @@ 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 Data.Time (UTCTime, defaultTimeLocale, diffUTCTime, formatTime) import qualified Lucid import qualified Lucid.Base as Lucid import qualified Network.Wai.Handler.Warp as Warp +import Numeric (showFFloat) import qualified Omni.Jr.Web.Style as Style import qualified Omni.Task.Core as TaskCore import Servant @@ -71,6 +72,7 @@ type API = :> QueryParam "priority" Text :> QueryParam "namespace" Text :> Get '[Lucid.HTML] TaskListPartial + :<|> "partials" :> "task" :> Capture "id" Text :> "metrics" :> Get '[Lucid.HTML] TaskMetricsPartial data CSS @@ -91,7 +93,7 @@ newtype InterventionPage = InterventionPage [TaskCore.Task] data TaskListPage = TaskListPage [TaskCore.Task] TaskFilters data TaskDetailPage - = TaskDetailFound TaskCore.Task [TaskCore.Task] [TaskCore.TaskActivity] + = TaskDetailFound TaskCore.Task [TaskCore.Task] [TaskCore.TaskActivity] (Maybe TaskCore.RetryContext) | TaskDetailNotFound Text data TaskReviewPage @@ -113,6 +115,8 @@ data StatusBadgePartial = StatusBadgePartial TaskCore.Status Text newtype TaskListPartial = TaskListPartial [TaskCore.Task] +data TaskMetricsPartial = TaskMetricsPartial Text [TaskCore.TaskActivity] (Maybe TaskCore.RetryContext) + newtype RejectForm = RejectForm (Maybe Text) instance FromForm RejectForm where @@ -402,7 +406,7 @@ instance Lucid.ToHtml TaskDetailPage where "The task " Lucid.code_ (Lucid.toHtml tid) " could not be found." - toHtml (TaskDetailFound task allTasks activities) = + toHtml (TaskDetailFound task allTasks activities maybeRetry) = Lucid.doctypehtml_ <| do pageHead (TaskCore.taskId task <> " - Jr") Lucid.body_ <| do @@ -492,6 +496,21 @@ instance Lucid.ToHtml TaskDetailPage where Lucid.ul_ [Lucid.class_ "child-list"] <| do traverse_ renderChild children + let hasRunningActivity = any (\a -> TaskCore.activityStage a == TaskCore.Running) activities + when hasRunningActivity <| do + let isInProgress = TaskCore.taskStatus task == TaskCore.InProgress + htmxAttrs = + [ Lucid.makeAttribute "hx-get" ("/partials/task/" <> TaskCore.taskId task <> "/metrics"), + Lucid.makeAttribute "hx-trigger" "every 5s", + Lucid.makeAttribute "hx-swap" "innerHTML" + ] + sectionAttrs = + [Lucid.class_ "execution-section", Lucid.id_ "execution-details"] + <> [attr | isInProgress, attr <- htmxAttrs] + Lucid.div_ sectionAttrs <| do + Lucid.h3_ "Execution Details" + renderExecutionDetails (TaskCore.taskId task) activities maybeRetry + when (TaskCore.taskStatus task == TaskCore.InProgress && not (null activities)) <| do Lucid.div_ [Lucid.class_ "activity-section"] <| do Lucid.h3_ "Activity Timeline" @@ -566,6 +585,64 @@ instance Lucid.ToHtml TaskDetailPage where formatTimestamp :: UTCTime -> Text formatTimestamp = Text.pack <. formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" + renderExecutionDetails :: (Monad m) => Text -> [TaskCore.TaskActivity] -> Maybe TaskCore.RetryContext -> Lucid.HtmlT m () + renderExecutionDetails _ acts retryCtx = + case findRunningAct acts of + Nothing -> Lucid.p_ [Lucid.class_ "empty-msg"] "No worker execution data available." + Just act -> + Lucid.div_ [Lucid.class_ "execution-details"] <| do + case TaskCore.activityAmpThreadUrl act of + Nothing -> pure () + Just url -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Amp Thread:" + Lucid.a_ [Lucid.href_ url, Lucid.target_ "_blank", Lucid.class_ "metric-value amp-link"] (Lucid.toHtml url) + + case (TaskCore.activityStartedAt act, TaskCore.activityCompletedAt act) of + (Just start, Just end) -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Duration:" + Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatDur start end)) + (Just start, Nothing) -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Started:" + Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatTimestamp start)) + _ -> pure () + + case TaskCore.activityCostCents act of + Nothing -> pure () + Just cents -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Cost:" + Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatCostVal cents)) + + case retryCtx of + Nothing -> pure () + Just ctx -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Retry Attempt:" + Lucid.span_ [Lucid.class_ "metric-value retry-count"] (Lucid.toHtml (tshow (TaskCore.retryAttempt ctx) <> "/3")) + + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Last Activity:" + Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatTimestamp (TaskCore.activityTimestamp act))) + where + findRunningAct = List.find (\a -> TaskCore.activityStage a == TaskCore.Running) + + formatDur :: UTCTime -> UTCTime -> Text + formatDur start end = + let diffSecs = floor (diffUTCTime end start) :: Int + mins = diffSecs `div` 60 + secs = diffSecs `mod` 60 + in if mins > 0 + then tshow mins <> "m " <> tshow secs <> "s" + else tshow secs <> "s" + + formatCostVal :: Int -> Text + formatCostVal cents = + let dollars = fromIntegral cents / 100.0 :: Double + in "$" <> Text.pack (showFFloat (Just 2) dollars "") + instance Lucid.ToHtml TaskReviewPage where toHtmlRaw = Lucid.toHtml toHtml (ReviewPageNotFound tid) = @@ -772,6 +849,75 @@ instance Lucid.ToHtml TaskListPartial where then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks match the current filters." else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks +instance Lucid.ToHtml TaskMetricsPartial where + toHtmlRaw = Lucid.toHtml + toHtml (TaskMetricsPartial _tid activities maybeRetry) = + Lucid.div_ [Lucid.class_ "execution-details"] <| do + case findRunningActivity activities of + Nothing -> Lucid.p_ [Lucid.class_ "empty-msg"] "No worker execution data available." + Just act -> do + case TaskCore.activityAmpThreadUrl act of + Nothing -> pure () + Just url -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Amp Thread:" + Lucid.a_ [Lucid.href_ url, Lucid.target_ "_blank", Lucid.class_ "metric-value amp-link"] (Lucid.toHtml url) + + case (TaskCore.activityStartedAt act, TaskCore.activityCompletedAt act) of + (Just start, Just end) -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Duration:" + Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatDuration start end)) + (Just start, Nothing) -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Started:" + Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatTimestamp start)) + _ -> pure () + + case TaskCore.activityCostCents act of + Nothing -> pure () + Just cents -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Cost:" + Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatCost cents)) + + case TaskCore.activityTokensUsed act of + Nothing -> pure () + Just tokens -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Tokens:" + Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (tshow tokens)) + + case maybeRetry of + Nothing -> pure () + Just ctx -> + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Retry Attempt:" + Lucid.span_ [Lucid.class_ "metric-value retry-count"] (Lucid.toHtml (tshow (TaskCore.retryAttempt ctx) <> "/3")) + + Lucid.div_ [Lucid.class_ "metric-row"] <| do + Lucid.span_ [Lucid.class_ "metric-label"] "Last Activity:" + Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatTimestamp (TaskCore.activityTimestamp act))) + where + findRunningActivity = List.find (\a -> TaskCore.activityStage a == TaskCore.Running) + + formatTimestamp :: UTCTime -> Text + formatTimestamp = Text.pack <. formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" + + formatDuration :: UTCTime -> UTCTime -> Text + formatDuration start end = + let diffSecs = floor (diffUTCTime end start) :: Int + mins = diffSecs `div` 60 + secs = diffSecs `mod` 60 + in if mins > 0 + then tshow mins <> "m " <> tshow secs <> "s" + else tshow secs <> "s" + + formatCost :: Int -> Text + formatCost cents = + let dollars = fromIntegral cents / 100.0 :: Double + in "$" <> Text.pack (showFFloat (Just 2) dollars "") + -- | Simple markdown renderer for epic descriptions -- Supports: headers (#, ##, ###), lists (- or *), code blocks (```), inline code (`) renderMarkdown :: (Monad m) => Text -> Lucid.HtmlT m () @@ -904,6 +1050,7 @@ server = :<|> recentActivityHandler :<|> readyCountHandler :<|> taskListPartialHandler + :<|> taskMetricsPartialHandler where styleHandler :: Servant.Handler LazyText.Text styleHandler = pure Style.css @@ -988,7 +1135,8 @@ server = Nothing -> pure (TaskDetailNotFound tid) Just task -> do activities <- liftIO (TaskCore.getActivitiesForTask tid) - pure (TaskDetailFound task tasks activities) + retryCtx <- liftIO (TaskCore.getRetryContext tid) + pure (TaskDetailFound task tasks activities retryCtx) taskStatusHandler :: Text -> StatusForm -> Servant.Handler StatusBadgePartial taskStatusHandler tid (StatusForm newStatus) = do @@ -1056,6 +1204,12 @@ server = filteredTasks = applyFilters filters allTasks pure (TaskListPartial filteredTasks) + taskMetricsPartialHandler :: Text -> Servant.Handler TaskMetricsPartial + taskMetricsPartialHandler tid = do + activities <- liftIO (TaskCore.getActivitiesForTask tid) + maybeRetry <- liftIO (TaskCore.getRetryContext tid) + pure (TaskMetricsPartial tid activities maybeRetry) + getReviewInfo :: Text -> IO ReviewInfo getReviewInfo tid = do maybeCommit <- findCommitForTask tid diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs index dbe1daa..b4f4c76 100644 --- a/Omni/Jr/Web/Style.hs +++ b/Omni/Jr/Web/Style.hs @@ -28,6 +28,7 @@ stylesheet = do statusBadges buttonStyles formStyles + executionDetailsStyles activityTimelineStyles markdownStyles responsiveStyles @@ -497,6 +498,39 @@ formStyles = do ".form-actions" ? do marginTop (px 8) +executionDetailsStyles :: Css +executionDetailsStyles = do + ".execution-section" ? do + marginTop (em 1) + backgroundColor white + borderRadius (px 2) (px 2) (px 2) (px 2) + padding (px 8) (px 10) (px 8) (px 10) + border (px 1) solid "#d0d0d0" + ".execution-details" ? do + marginTop (px 8) + ".metric-row" ? do + display flex + flexWrap Flexbox.wrap + padding (px 4) (px 0) (px 4) (px 0) + borderBottom (px 1) solid "#e5e7eb" + ".metric-row" # lastChild ? borderBottom (px 0) none transparent + ".metric-label" ? do + fontWeight (weight 600) + width (px 120) + color "#6b7280" + fontSize (px 13) + ".metric-value" ? do + Stylesheet.key "flex" ("1" :: Text) + fontSize (px 13) + ".amp-link" ? do + color "#0066cc" + textDecoration none + wordBreak breakAll + ".amp-link" # hover ? textDecoration underline + ".retry-count" ? do + color "#f97316" + fontWeight (weight 600) + activityTimelineStyles :: Css activityTimelineStyles = do ".activity-section" ? do @@ -751,6 +785,13 @@ darkModeStyles = ".activity-message" ? color "#d1d5db" (".activity-metadata" |> "summary") ? color "#9ca3af" ".metadata-json" ? backgroundColor "#374151" + ".execution-section" ? do + backgroundColor "#1f2937" + borderColor "#374151" + ".metric-row" ? borderBottomColor "#374151" + ".metric-label" ? color "#9ca3af" + ".metric-value" ? color "#d1d5db" + ".amp-link" ? color "#60a5fa" ".markdown-content" ? color "#d1d5db" ".md-h1" ? borderBottomColor "#374151" ".md-inline-code" ? do |
