summaryrefslogtreecommitdiff
path: root/Omni/Jr
diff options
context:
space:
mode:
Diffstat (limited to 'Omni/Jr')
-rw-r--r--Omni/Jr/Web.hs1107
-rw-r--r--Omni/Jr/Web/Style.hs36
2 files changed, 623 insertions, 520 deletions
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index a479bf9..2f8e693 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -338,6 +338,54 @@ pageBody content =
navbar
content
+data Breadcrumb = Breadcrumb
+ { _crumbLabel :: Text,
+ _crumbHref :: Maybe Text
+ }
+
+type Breadcrumbs = [Breadcrumb]
+
+pageBodyWithCrumbs :: (Monad m) => Breadcrumbs -> Lucid.HtmlT m () -> Lucid.HtmlT m ()
+pageBodyWithCrumbs crumbs content =
+ Lucid.body_ [Lucid.makeAttribute "hx-boost" "true"] <| do
+ navbar
+ unless (null crumbs) <| do
+ Lucid.div_ [Lucid.class_ "breadcrumb-container"] <| do
+ Lucid.div_ [Lucid.class_ "container"] <| renderBreadcrumbs crumbs
+ content
+
+renderBreadcrumbs :: (Monad m) => Breadcrumbs -> Lucid.HtmlT m ()
+renderBreadcrumbs [] = pure ()
+renderBreadcrumbs crumbs =
+ Lucid.nav_ [Lucid.class_ "breadcrumbs", Lucid.makeAttribute "aria-label" "Breadcrumb"] <| do
+ Lucid.ol_ [Lucid.class_ "breadcrumb-list"] <| do
+ traverse_ renderCrumb (zip [0 ..] crumbs)
+ where
+ renderCrumb :: (Monad m') => (Int, Breadcrumb) -> Lucid.HtmlT m' ()
+ renderCrumb (idx, Breadcrumb label mHref) = do
+ Lucid.li_ [Lucid.class_ "breadcrumb-item"] <| do
+ when (idx > 0) <| Lucid.span_ [Lucid.class_ "breadcrumb-sep"] ">"
+ case mHref of
+ Just href -> Lucid.a_ [Lucid.href_ href] (Lucid.toHtml label)
+ Nothing -> Lucid.span_ [Lucid.class_ "breadcrumb-current"] (Lucid.toHtml label)
+
+getAncestors :: [TaskCore.Task] -> TaskCore.Task -> [TaskCore.Task]
+getAncestors allTasks task =
+ case TaskCore.taskParent task of
+ Nothing -> [task]
+ Just pid -> case TaskCore.findTask pid allTasks of
+ Nothing -> [task]
+ Just parent -> getAncestors allTasks parent ++ [task]
+
+taskBreadcrumbs :: [TaskCore.Task] -> TaskCore.Task -> Breadcrumbs
+taskBreadcrumbs allTasks task =
+ let ancestors = getAncestors allTasks task
+ taskCrumbs = [Breadcrumb (TaskCore.taskId t) (Just ("/tasks/" <> TaskCore.taskId t)) | t <- List.init ancestors]
+ currentCrumb = Breadcrumb (TaskCore.taskId task) Nothing
+ in [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks")]
+ ++ taskCrumbs
+ ++ [currentCrumb]
+
navbar :: (Monad m) => Lucid.HtmlT m ()
navbar =
Lucid.nav_ [Lucid.class_ "navbar"] <| do
@@ -592,107 +640,111 @@ instance Lucid.ToHtml HomePage where
instance Lucid.ToHtml ReadyQueuePage where
toHtmlRaw = Lucid.toHtml
toHtml (ReadyQueuePage tasks _now) =
- Lucid.doctypehtml_ <| do
- pageHead "Ready Queue - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ <| Lucid.toHtml ("Ready Queue (" <> tshow (length tasks) <> " tasks)")
- if null tasks
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks are ready for work."
- else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Ready Queue" Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Ready Queue - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ <| Lucid.toHtml ("Ready Queue (" <> tshow (length tasks) <> " tasks)")
+ if null tasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks are ready for work."
+ else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
instance Lucid.ToHtml BlockedPage where
toHtmlRaw = Lucid.toHtml
toHtml (BlockedPage tasks _now) =
- Lucid.doctypehtml_ <| do
- pageHead "Blocked Tasks - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ <| Lucid.toHtml ("Blocked Tasks (" <> tshow (length tasks) <> " tasks)")
- Lucid.p_ [Lucid.class_ "info-msg"] "Tasks with unmet blocking dependencies."
- if null tasks
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No blocked tasks."
- else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Blocked" Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Blocked Tasks - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ <| Lucid.toHtml ("Blocked Tasks (" <> tshow (length tasks) <> " tasks)")
+ Lucid.p_ [Lucid.class_ "info-msg"] "Tasks with unmet blocking dependencies."
+ if null tasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No blocked tasks."
+ else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
instance Lucid.ToHtml InterventionPage where
toHtmlRaw = Lucid.toHtml
toHtml (InterventionPage tasks _now) =
- Lucid.doctypehtml_ <| do
- pageHead "Needs Intervention - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ <| Lucid.toHtml ("Needs Intervention (" <> tshow (length tasks) <> " tasks)")
- Lucid.p_ [Lucid.class_ "info-msg"] "Tasks that have failed 3+ times and need human help."
- if null tasks
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks need intervention."
- else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Needs Intervention" Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Needs Intervention - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ <| Lucid.toHtml ("Needs Intervention (" <> tshow (length tasks) <> " tasks)")
+ Lucid.p_ [Lucid.class_ "info-msg"] "Tasks that have failed 3+ times and need human help."
+ if null tasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks need intervention."
+ else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
instance Lucid.ToHtml KBPage where
toHtmlRaw = Lucid.toHtml
toHtml (KBPage facts) =
- Lucid.doctypehtml_ <| do
- pageHead "Knowledge Base - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ "Knowledge Base"
- Lucid.p_ [Lucid.class_ "info-msg"] "Facts learned during task execution."
-
- Lucid.details_ [Lucid.class_ "create-fact-section"] <| do
- Lucid.summary_ [Lucid.class_ "btn btn-primary create-fact-toggle"] "Create New Fact"
- Lucid.form_
- [ Lucid.method_ "POST",
- Lucid.action_ "/kb/create",
- Lucid.class_ "fact-create-form"
- ]
- <| do
- Lucid.div_ [Lucid.class_ "form-group"] <| do
- Lucid.label_ [Lucid.for_ "project"] "Project:"
- Lucid.input_
- [ Lucid.type_ "text",
- Lucid.name_ "project",
- Lucid.id_ "project",
- Lucid.class_ "form-input",
- Lucid.required_ "required",
- Lucid.placeholder_ "e.g., Omni/Jr"
- ]
- Lucid.div_ [Lucid.class_ "form-group"] <| do
- Lucid.label_ [Lucid.for_ "content"] "Fact Content:"
- Lucid.textarea_
- [ Lucid.name_ "content",
- Lucid.id_ "content",
- Lucid.class_ "form-textarea",
- Lucid.rows_ "4",
- Lucid.required_ "required",
- Lucid.placeholder_ "Describe the fact or knowledge..."
- ]
- ""
- Lucid.div_ [Lucid.class_ "form-group"] <| do
- Lucid.label_ [Lucid.for_ "files"] "Related Files (comma-separated):"
- Lucid.input_
- [ Lucid.type_ "text",
- Lucid.name_ "files",
- Lucid.id_ "files",
- Lucid.class_ "form-input",
- Lucid.placeholder_ "path/to/file1.hs, path/to/file2.hs"
- ]
- Lucid.div_ [Lucid.class_ "form-group"] <| do
- Lucid.label_ [Lucid.for_ "confidence"] "Confidence (0.0 - 1.0):"
- Lucid.input_
- [ Lucid.type_ "number",
- Lucid.name_ "confidence",
- Lucid.id_ "confidence",
- Lucid.class_ "form-input",
- Lucid.step_ "0.1",
- Lucid.min_ "0",
- Lucid.max_ "1",
- Lucid.value_ "0.8"
- ]
- Lucid.div_ [Lucid.class_ "form-actions"] <| do
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-primary"] "Create Fact"
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Knowledge Base" Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Knowledge Base - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ "Knowledge Base"
+ Lucid.p_ [Lucid.class_ "info-msg"] "Facts learned during task execution."
+
+ Lucid.details_ [Lucid.class_ "create-fact-section"] <| do
+ Lucid.summary_ [Lucid.class_ "btn btn-primary create-fact-toggle"] "Create New Fact"
+ Lucid.form_
+ [ Lucid.method_ "POST",
+ Lucid.action_ "/kb/create",
+ Lucid.class_ "fact-create-form"
+ ]
+ <| do
+ Lucid.div_ [Lucid.class_ "form-group"] <| do
+ Lucid.label_ [Lucid.for_ "project"] "Project:"
+ Lucid.input_
+ [ Lucid.type_ "text",
+ Lucid.name_ "project",
+ Lucid.id_ "project",
+ Lucid.class_ "form-input",
+ Lucid.required_ "required",
+ Lucid.placeholder_ "e.g., Omni/Jr"
+ ]
+ Lucid.div_ [Lucid.class_ "form-group"] <| do
+ Lucid.label_ [Lucid.for_ "content"] "Fact Content:"
+ Lucid.textarea_
+ [ Lucid.name_ "content",
+ Lucid.id_ "content",
+ Lucid.class_ "form-textarea",
+ Lucid.rows_ "4",
+ Lucid.required_ "required",
+ Lucid.placeholder_ "Describe the fact or knowledge..."
+ ]
+ ""
+ Lucid.div_ [Lucid.class_ "form-group"] <| do
+ Lucid.label_ [Lucid.for_ "files"] "Related Files (comma-separated):"
+ Lucid.input_
+ [ Lucid.type_ "text",
+ Lucid.name_ "files",
+ Lucid.id_ "files",
+ Lucid.class_ "form-input",
+ Lucid.placeholder_ "path/to/file1.hs, path/to/file2.hs"
+ ]
+ Lucid.div_ [Lucid.class_ "form-group"] <| do
+ Lucid.label_ [Lucid.for_ "confidence"] "Confidence (0.0 - 1.0):"
+ Lucid.input_
+ [ Lucid.type_ "number",
+ Lucid.name_ "confidence",
+ Lucid.id_ "confidence",
+ Lucid.class_ "form-input",
+ Lucid.step_ "0.1",
+ Lucid.min_ "0",
+ Lucid.max_ "1",
+ Lucid.value_ "0.8"
+ ]
+ Lucid.div_ [Lucid.class_ "form-actions"] <| do
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-primary"] "Create Fact"
- if null facts
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No facts recorded yet."
- else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderFactCard facts
+ if null facts
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No facts recorded yet."
+ else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderFactCard facts
where
renderFactCard :: (Monad m) => TaskCore.Fact -> Lucid.HtmlT m ()
renderFactCard f =
@@ -726,93 +778,96 @@ instance Lucid.ToHtml KBPage where
instance Lucid.ToHtml FactDetailPage where
toHtmlRaw = Lucid.toHtml
toHtml (FactDetailNotFound fid) =
- Lucid.doctypehtml_ <| do
- pageHead "Fact Not Found - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ "Fact Not Found"
- Lucid.p_ [Lucid.class_ "error-msg"] (Lucid.toHtml ("Fact with ID " <> tshow fid <> " not found."))
- Lucid.a_ [Lucid.href_ "/kb", Lucid.class_ "btn btn-secondary"] "Back to Knowledge Base"
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> tshow fid) Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Fact Not Found - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ "Fact Not Found"
+ Lucid.p_ [Lucid.class_ "error-msg"] (Lucid.toHtml ("Fact with ID " <> tshow fid <> " not found."))
+ Lucid.a_ [Lucid.href_ "/kb", Lucid.class_ "btn btn-secondary"] "Back to Knowledge Base"
toHtml (FactDetailFound fact now) =
- Lucid.doctypehtml_ <| do
- pageHead "Fact Detail - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.div_ [Lucid.class_ "task-detail-header"] <| do
- Lucid.h1_ <| do
- Lucid.span_ [Lucid.class_ "detail-id"] (Lucid.toHtml ("Fact #" <> maybe "-" tshow (TaskCore.factId fact)))
- Lucid.div_ [Lucid.class_ "task-meta-row"] <| do
- Lucid.span_ [Lucid.class_ "meta-label"] "Project:"
- Lucid.span_ [Lucid.class_ "meta-value"] (Lucid.toHtml (TaskCore.factProject fact))
- Lucid.span_ [Lucid.class_ "meta-label"] "Confidence:"
- confidenceBadgeDetail (TaskCore.factConfidence fact)
- Lucid.span_ [Lucid.class_ "meta-label"] "Created:"
- Lucid.span_ [Lucid.class_ "meta-value"] (renderRelativeTimestamp now (TaskCore.factCreatedAt fact))
-
- Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h2_ "Content"
- Lucid.form_
- [ Lucid.method_ "POST",
- Lucid.action_ ("/kb/" <> maybe "-" tshow (TaskCore.factId fact) <> "/edit"),
- Lucid.class_ "fact-edit-form"
- ]
- <| do
- Lucid.div_ [Lucid.class_ "form-group"] <| do
- Lucid.label_ [Lucid.for_ "content"] "Fact Content:"
- Lucid.textarea_
- [ Lucid.name_ "content",
- Lucid.id_ "content",
- Lucid.class_ "form-textarea",
- Lucid.rows_ "6"
- ]
- (Lucid.toHtml (TaskCore.factContent fact))
-
- Lucid.div_ [Lucid.class_ "form-group"] <| do
- Lucid.label_ [Lucid.for_ "files"] "Related Files (comma-separated):"
- Lucid.input_
- [ Lucid.type_ "text",
- Lucid.name_ "files",
- Lucid.id_ "files",
- Lucid.class_ "form-input",
- Lucid.value_ (Text.intercalate ", " (TaskCore.factRelatedFiles fact))
- ]
+ let fid' = maybe "-" tshow (TaskCore.factId fact)
+ crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> fid') Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Fact Detail - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.div_ [Lucid.class_ "task-detail-header"] <| do
+ Lucid.h1_ <| do
+ Lucid.span_ [Lucid.class_ "detail-id"] (Lucid.toHtml ("Fact #" <> maybe "-" tshow (TaskCore.factId fact)))
+ Lucid.div_ [Lucid.class_ "task-meta-row"] <| do
+ Lucid.span_ [Lucid.class_ "meta-label"] "Project:"
+ Lucid.span_ [Lucid.class_ "meta-value"] (Lucid.toHtml (TaskCore.factProject fact))
+ Lucid.span_ [Lucid.class_ "meta-label"] "Confidence:"
+ confidenceBadgeDetail (TaskCore.factConfidence fact)
+ Lucid.span_ [Lucid.class_ "meta-label"] "Created:"
+ Lucid.span_ [Lucid.class_ "meta-value"] (renderRelativeTimestamp now (TaskCore.factCreatedAt fact))
- Lucid.div_ [Lucid.class_ "form-group"] <| do
- Lucid.label_ [Lucid.for_ "confidence"] "Confidence (0.0 - 1.0):"
- Lucid.input_
- [ Lucid.type_ "number",
- Lucid.name_ "confidence",
- Lucid.id_ "confidence",
- Lucid.class_ "form-input",
- Lucid.step_ "0.1",
- Lucid.min_ "0",
- Lucid.max_ "1",
- Lucid.value_ (tshow (TaskCore.factConfidence fact))
- ]
+ Lucid.div_ [Lucid.class_ "detail-section"] <| do
+ Lucid.h2_ "Content"
+ Lucid.form_
+ [ Lucid.method_ "POST",
+ Lucid.action_ ("/kb/" <> maybe "-" tshow (TaskCore.factId fact) <> "/edit"),
+ Lucid.class_ "fact-edit-form"
+ ]
+ <| do
+ Lucid.div_ [Lucid.class_ "form-group"] <| do
+ Lucid.label_ [Lucid.for_ "content"] "Fact Content:"
+ Lucid.textarea_
+ [ Lucid.name_ "content",
+ Lucid.id_ "content",
+ Lucid.class_ "form-textarea",
+ Lucid.rows_ "6"
+ ]
+ (Lucid.toHtml (TaskCore.factContent fact))
+
+ Lucid.div_ [Lucid.class_ "form-group"] <| do
+ Lucid.label_ [Lucid.for_ "files"] "Related Files (comma-separated):"
+ Lucid.input_
+ [ Lucid.type_ "text",
+ Lucid.name_ "files",
+ Lucid.id_ "files",
+ Lucid.class_ "form-input",
+ Lucid.value_ (Text.intercalate ", " (TaskCore.factRelatedFiles fact))
+ ]
- Lucid.div_ [Lucid.class_ "form-actions"] <| do
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-primary"] "Save Changes"
+ Lucid.div_ [Lucid.class_ "form-group"] <| do
+ Lucid.label_ [Lucid.for_ "confidence"] "Confidence (0.0 - 1.0):"
+ Lucid.input_
+ [ Lucid.type_ "number",
+ Lucid.name_ "confidence",
+ Lucid.id_ "confidence",
+ Lucid.class_ "form-input",
+ Lucid.step_ "0.1",
+ Lucid.min_ "0",
+ Lucid.max_ "1",
+ Lucid.value_ (tshow (TaskCore.factConfidence fact))
+ ]
- case TaskCore.factSourceTask fact of
- Nothing -> pure ()
- Just tid -> do
- Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h2_ "Source Task"
- Lucid.a_ [Lucid.href_ ("/tasks/" <> tid), Lucid.class_ "task-link"] (Lucid.toHtml tid)
-
- Lucid.div_ [Lucid.class_ "detail-section danger-zone"] <| do
- Lucid.h2_ "Danger Zone"
- Lucid.form_
- [ Lucid.method_ "POST",
- Lucid.action_ ("/kb/" <> maybe "-" tshow (TaskCore.factId fact) <> "/delete"),
- Lucid.class_ "delete-form",
- Lucid.makeAttribute "onsubmit" "return confirm('Are you sure you want to delete this fact?');"
- ]
- <| do
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-danger"] "Delete Fact"
+ Lucid.div_ [Lucid.class_ "form-actions"] <| do
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-primary"] "Save Changes"
+
+ case TaskCore.factSourceTask fact of
+ Nothing -> pure ()
+ Just tid -> do
+ Lucid.div_ [Lucid.class_ "detail-section"] <| do
+ Lucid.h2_ "Source Task"
+ Lucid.a_ [Lucid.href_ ("/tasks/" <> tid), Lucid.class_ "task-link"] (Lucid.toHtml tid)
+
+ Lucid.div_ [Lucid.class_ "detail-section danger-zone"] <| do
+ Lucid.h2_ "Danger Zone"
+ Lucid.form_
+ [ Lucid.method_ "POST",
+ Lucid.action_ ("/kb/" <> maybe "-" tshow (TaskCore.factId fact) <> "/delete"),
+ Lucid.class_ "delete-form",
+ Lucid.makeAttribute "onsubmit" "return confirm('Are you sure you want to delete this fact?');"
+ ]
+ <| do
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-danger"] "Delete Fact"
- Lucid.div_ [Lucid.class_ "back-link"] <| do
- Lucid.a_ [Lucid.href_ "/kb"] "← Back to Knowledge Base"
+ Lucid.div_ [Lucid.class_ "back-link"] <| do
+ Lucid.a_ [Lucid.href_ "/kb"] "← Back to Knowledge Base"
where
confidenceBadgeDetail :: (Monad m) => Double -> Lucid.HtmlT m ()
confidenceBadgeDetail conf =
@@ -826,15 +881,16 @@ instance Lucid.ToHtml FactDetailPage where
instance Lucid.ToHtml EpicsPage where
toHtmlRaw = Lucid.toHtml
toHtml (EpicsPage epics allTasks) =
- Lucid.doctypehtml_ <| do
- pageHead "Epics - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ <| Lucid.toHtml ("Epics (" <> tshow (length epics) <> ")")
- Lucid.p_ [Lucid.class_ "info-msg"] "All epics (large, multi-task projects)."
- if null epics
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No epics found."
- else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ (renderEpicCardWithStats allTasks) epics
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Epics" Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Epics - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ <| Lucid.toHtml ("Epics (" <> tshow (length epics) <> ")")
+ Lucid.p_ [Lucid.class_ "info-msg"] "All epics (large, multi-task projects)."
+ if null epics
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No epics found."
+ else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ (renderEpicCardWithStats allTasks) epics
epicProgressBar :: (Monad m) => Int -> Int -> Int -> Int -> Lucid.HtmlT m ()
epicProgressBar doneCount inProgressCount openCount totalCount =
@@ -906,68 +962,69 @@ getDescendants allTasks parentId =
instance Lucid.ToHtml TaskListPage where
toHtmlRaw = Lucid.toHtml
toHtml (TaskListPage tasks filters _now) =
- Lucid.doctypehtml_ <| do
- pageHead "Tasks - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ <| Lucid.toHtml ("Tasks (" <> tshow (length tasks) <> ")")
-
- Lucid.div_ [Lucid.class_ "filter-form"] <| do
- Lucid.form_
- [ Lucid.method_ "GET",
- Lucid.action_ "/tasks",
- Lucid.makeAttribute "hx-get" "/partials/task-list",
- Lucid.makeAttribute "hx-target" "#task-list",
- Lucid.makeAttribute "hx-push-url" "/tasks",
- Lucid.makeAttribute "hx-trigger" "submit, change from:select"
- ]
- <| do
- Lucid.div_ [Lucid.class_ "filter-row"] <| do
- Lucid.div_ [Lucid.class_ "filter-group"] <| do
- Lucid.label_ [Lucid.for_ "status"] "Status:"
- Lucid.select_ [Lucid.name_ "status", Lucid.id_ "status", Lucid.class_ "filter-select"] <| do
- Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterStatus filters)) "All"
- statusFilterOption TaskCore.Open (filterStatus filters)
- statusFilterOption TaskCore.InProgress (filterStatus filters)
- statusFilterOption TaskCore.Review (filterStatus filters)
- statusFilterOption TaskCore.Approved (filterStatus filters)
- statusFilterOption TaskCore.Done (filterStatus filters)
-
- Lucid.div_ [Lucid.class_ "filter-group"] <| do
- Lucid.label_ [Lucid.for_ "priority"] "Priority:"
- Lucid.select_ [Lucid.name_ "priority", Lucid.id_ "priority", Lucid.class_ "filter-select"] <| do
- Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterPriority filters)) "All"
- priorityFilterOption TaskCore.P0 (filterPriority filters)
- priorityFilterOption TaskCore.P1 (filterPriority filters)
- priorityFilterOption TaskCore.P2 (filterPriority filters)
- priorityFilterOption TaskCore.P3 (filterPriority filters)
- priorityFilterOption TaskCore.P4 (filterPriority filters)
-
- Lucid.div_ [Lucid.class_ "filter-group"] <| do
- Lucid.label_ [Lucid.for_ "namespace"] "Namespace:"
- Lucid.input_
- [ Lucid.type_ "text",
- Lucid.name_ "namespace",
- Lucid.id_ "namespace",
- Lucid.class_ "filter-input",
- Lucid.placeholder_ "e.g. Omni/Jr",
- Lucid.value_ (fromMaybe "" (filterNamespace filters))
- ]
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Tasks - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ <| Lucid.toHtml ("Tasks (" <> tshow (length tasks) <> ")")
+
+ Lucid.div_ [Lucid.class_ "filter-form"] <| do
+ Lucid.form_
+ [ Lucid.method_ "GET",
+ Lucid.action_ "/tasks",
+ Lucid.makeAttribute "hx-get" "/partials/task-list",
+ Lucid.makeAttribute "hx-target" "#task-list",
+ Lucid.makeAttribute "hx-push-url" "/tasks",
+ Lucid.makeAttribute "hx-trigger" "submit, change from:select"
+ ]
+ <| do
+ Lucid.div_ [Lucid.class_ "filter-row"] <| do
+ Lucid.div_ [Lucid.class_ "filter-group"] <| do
+ Lucid.label_ [Lucid.for_ "status"] "Status:"
+ Lucid.select_ [Lucid.name_ "status", Lucid.id_ "status", Lucid.class_ "filter-select"] <| do
+ Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterStatus filters)) "All"
+ statusFilterOption TaskCore.Open (filterStatus filters)
+ statusFilterOption TaskCore.InProgress (filterStatus filters)
+ statusFilterOption TaskCore.Review (filterStatus filters)
+ statusFilterOption TaskCore.Approved (filterStatus filters)
+ statusFilterOption TaskCore.Done (filterStatus filters)
+
+ Lucid.div_ [Lucid.class_ "filter-group"] <| do
+ Lucid.label_ [Lucid.for_ "priority"] "Priority:"
+ Lucid.select_ [Lucid.name_ "priority", Lucid.id_ "priority", Lucid.class_ "filter-select"] <| do
+ Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterPriority filters)) "All"
+ priorityFilterOption TaskCore.P0 (filterPriority filters)
+ priorityFilterOption TaskCore.P1 (filterPriority filters)
+ priorityFilterOption TaskCore.P2 (filterPriority filters)
+ priorityFilterOption TaskCore.P3 (filterPriority filters)
+ priorityFilterOption TaskCore.P4 (filterPriority filters)
+
+ Lucid.div_ [Lucid.class_ "filter-group"] <| do
+ Lucid.label_ [Lucid.for_ "namespace"] "Namespace:"
+ Lucid.input_
+ [ Lucid.type_ "text",
+ Lucid.name_ "namespace",
+ Lucid.id_ "namespace",
+ Lucid.class_ "filter-input",
+ Lucid.placeholder_ "e.g. Omni/Jr",
+ Lucid.value_ (fromMaybe "" (filterNamespace filters))
+ ]
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "filter-btn"] "Filter"
- Lucid.a_
- [ Lucid.href_ "/tasks",
- Lucid.class_ "clear-btn",
- Lucid.makeAttribute "hx-get" "/partials/task-list",
- Lucid.makeAttribute "hx-target" "#task-list",
- Lucid.makeAttribute "hx-push-url" "/tasks"
- ]
- "Clear"
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "filter-btn"] "Filter"
+ Lucid.a_
+ [ Lucid.href_ "/tasks",
+ Lucid.class_ "clear-btn",
+ Lucid.makeAttribute "hx-get" "/partials/task-list",
+ Lucid.makeAttribute "hx-target" "#task-list",
+ Lucid.makeAttribute "hx-push-url" "/tasks"
+ ]
+ "Clear"
- Lucid.div_ [Lucid.id_ "task-list"] <| do
- if null tasks
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks match the current filters."
- else Lucid.div_ [Lucid.class_ "list-group"] <| traverse_ renderListGroupItem tasks
+ Lucid.div_ [Lucid.id_ "task-list"] <| do
+ if null tasks
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks match the current filters."
+ else Lucid.div_ [Lucid.class_ "list-group"] <| traverse_ renderListGroupItem tasks
where
maybeSelected :: (Eq a) => Maybe a -> Maybe a -> [Lucid.Attribute]
maybeSelected opt current = [Lucid.selected_ "selected" | opt == current]
@@ -985,148 +1042,150 @@ instance Lucid.ToHtml TaskListPage where
instance Lucid.ToHtml TaskDetailPage where
toHtmlRaw = Lucid.toHtml
toHtml (TaskDetailNotFound tid) =
- Lucid.doctypehtml_ <| do
- pageHead "Task Not Found - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ "Task Not Found"
- Lucid.p_ <| do
- "The task "
- Lucid.code_ (Lucid.toHtml tid)
- " could not be found."
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Task Not Found - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ "Task Not Found"
+ Lucid.p_ <| do
+ "The task "
+ Lucid.code_ (Lucid.toHtml tid)
+ " could not be found."
toHtml (TaskDetailFound task allTasks activities maybeRetry commits maybeAggMetrics now) =
- Lucid.doctypehtml_ <| do
- pageHead (TaskCore.taskId task <> " - Jr")
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ <| Lucid.toHtml (TaskCore.taskTitle task)
-
- renderRetryContextBanner (TaskCore.taskId task) maybeRetry
-
- Lucid.div_ [Lucid.class_ "task-detail"] <| do
- Lucid.div_ [Lucid.class_ "task-meta"] <| do
- Lucid.div_ [Lucid.class_ "task-meta-primary"] <| do
- Lucid.code_ [Lucid.class_ "task-meta-id"] (Lucid.toHtml (TaskCore.taskId task))
- metaSep
- Lucid.span_ [Lucid.class_ "task-meta-type"] (Lucid.toHtml (tshow (TaskCore.taskType task)))
- metaSep
- statusBadgeWithForm (TaskCore.taskStatus task) (TaskCore.taskId task)
- metaSep
- Lucid.span_ [Lucid.class_ "task-meta-priority"] <| do
- Lucid.toHtml (tshow (TaskCore.taskPriority task))
- Lucid.span_ [Lucid.class_ "priority-desc"] (Lucid.toHtml (priorityDesc (TaskCore.taskPriority task)))
- case TaskCore.taskNamespace task of
- Nothing -> pure ()
- Just ns -> do
+ let crumbs = taskBreadcrumbs allTasks task
+ in Lucid.doctypehtml_ <| do
+ pageHead (TaskCore.taskId task <> " - Jr")
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ <| Lucid.toHtml (TaskCore.taskTitle task)
+
+ renderRetryContextBanner (TaskCore.taskId task) maybeRetry
+
+ Lucid.div_ [Lucid.class_ "task-detail"] <| do
+ Lucid.div_ [Lucid.class_ "task-meta"] <| do
+ Lucid.div_ [Lucid.class_ "task-meta-primary"] <| do
+ Lucid.code_ [Lucid.class_ "task-meta-id"] (Lucid.toHtml (TaskCore.taskId task))
metaSep
- Lucid.span_ [Lucid.class_ "task-meta-ns"] (Lucid.toHtml ns)
-
- Lucid.div_ [Lucid.class_ "task-meta-secondary"] <| do
- case TaskCore.taskParent task of
- Nothing -> pure ()
- Just pid -> do
- Lucid.span_ [Lucid.class_ "task-meta-label"] "Parent:"
- Lucid.a_ [Lucid.href_ ("/tasks/" <> pid), Lucid.class_ "task-link"] (Lucid.toHtml pid)
+ Lucid.span_ [Lucid.class_ "task-meta-type"] (Lucid.toHtml (tshow (TaskCore.taskType task)))
metaSep
- Lucid.span_ [Lucid.class_ "task-meta-label"] "Created"
- renderRelativeTimestamp now (TaskCore.taskCreatedAt task)
- metaSep
- Lucid.span_ [Lucid.class_ "task-meta-label"] "Updated"
- renderRelativeTimestamp now (TaskCore.taskUpdatedAt task)
-
- let deps = TaskCore.taskDependencies task
- unless (null deps) <| do
- Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h3_ "Dependencies"
- Lucid.ul_ [Lucid.class_ "dep-list"] <| do
- traverse_ renderDependency deps
-
- case TaskCore.taskType task of
- TaskCore.Epic -> do
- for_ maybeAggMetrics (renderAggregatedMetrics allTasks task)
- Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h3_ "Design"
- if Text.null (TaskCore.taskDescription task)
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No design document yet."
- else Lucid.div_ [Lucid.class_ "markdown-content"] (renderMarkdown (TaskCore.taskDescription task))
- Lucid.details_ [Lucid.class_ "edit-description"] <| do
- Lucid.summary_ "Edit Design"
- Lucid.form_ [Lucid.method_ "POST", Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/description")] <| do
- Lucid.textarea_
- [ Lucid.name_ "description",
- Lucid.class_ "description-textarea",
- Lucid.rows_ "15",
- Lucid.placeholder_ "Enter design in Markdown format..."
- ]
- (Lucid.toHtml (TaskCore.taskDescription task))
- Lucid.div_ [Lucid.class_ "form-actions"] <| do
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Save Design"
- _ ->
- Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h3_ "Description"
- if Text.null (TaskCore.taskDescription task)
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No description yet."
- else Lucid.pre_ [Lucid.class_ "description"] (Lucid.toHtml (TaskCore.taskDescription task))
- Lucid.details_ [Lucid.class_ "edit-description"] <| do
- Lucid.summary_ "Edit Description"
- Lucid.form_
- [ Lucid.method_ "POST",
- Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/description"),
- Lucid.makeAttribute "hx-post" ("/tasks/" <> TaskCore.taskId task <> "/description"),
- Lucid.makeAttribute "hx-swap" "none"
- ]
- <| do
- Lucid.textarea_
- [ Lucid.name_ "description",
- Lucid.class_ "description-textarea",
- Lucid.rows_ "10",
- Lucid.placeholder_ "Enter description..."
+ statusBadgeWithForm (TaskCore.taskStatus task) (TaskCore.taskId task)
+ metaSep
+ Lucid.span_ [Lucid.class_ "task-meta-priority"] <| do
+ Lucid.toHtml (tshow (TaskCore.taskPriority task))
+ Lucid.span_ [Lucid.class_ "priority-desc"] (Lucid.toHtml (priorityDesc (TaskCore.taskPriority task)))
+ case TaskCore.taskNamespace task of
+ Nothing -> pure ()
+ Just ns -> do
+ metaSep
+ Lucid.span_ [Lucid.class_ "task-meta-ns"] (Lucid.toHtml ns)
+
+ Lucid.div_ [Lucid.class_ "task-meta-secondary"] <| do
+ case TaskCore.taskParent task of
+ Nothing -> pure ()
+ Just pid -> do
+ Lucid.span_ [Lucid.class_ "task-meta-label"] "Parent:"
+ Lucid.a_ [Lucid.href_ ("/tasks/" <> pid), Lucid.class_ "task-link"] (Lucid.toHtml pid)
+ metaSep
+ Lucid.span_ [Lucid.class_ "task-meta-label"] "Created"
+ renderRelativeTimestamp now (TaskCore.taskCreatedAt task)
+ metaSep
+ Lucid.span_ [Lucid.class_ "task-meta-label"] "Updated"
+ renderRelativeTimestamp now (TaskCore.taskUpdatedAt task)
+
+ let deps = TaskCore.taskDependencies task
+ unless (null deps) <| do
+ Lucid.div_ [Lucid.class_ "detail-section"] <| do
+ Lucid.h3_ "Dependencies"
+ Lucid.ul_ [Lucid.class_ "dep-list"] <| do
+ traverse_ renderDependency deps
+
+ case TaskCore.taskType task of
+ TaskCore.Epic -> do
+ for_ maybeAggMetrics (renderAggregatedMetrics allTasks task)
+ Lucid.div_ [Lucid.class_ "detail-section"] <| do
+ Lucid.h3_ "Design"
+ if Text.null (TaskCore.taskDescription task)
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No design document yet."
+ else Lucid.div_ [Lucid.class_ "markdown-content"] (renderMarkdown (TaskCore.taskDescription task))
+ Lucid.details_ [Lucid.class_ "edit-description"] <| do
+ Lucid.summary_ "Edit Design"
+ Lucid.form_ [Lucid.method_ "POST", Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/description")] <| do
+ Lucid.textarea_
+ [ Lucid.name_ "description",
+ Lucid.class_ "description-textarea",
+ Lucid.rows_ "15",
+ Lucid.placeholder_ "Enter design in Markdown format..."
+ ]
+ (Lucid.toHtml (TaskCore.taskDescription task))
+ Lucid.div_ [Lucid.class_ "form-actions"] <| do
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Save Design"
+ _ ->
+ Lucid.div_ [Lucid.class_ "detail-section"] <| do
+ Lucid.h3_ "Description"
+ if Text.null (TaskCore.taskDescription task)
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No description yet."
+ else Lucid.pre_ [Lucid.class_ "description"] (Lucid.toHtml (TaskCore.taskDescription task))
+ Lucid.details_ [Lucid.class_ "edit-description"] <| do
+ Lucid.summary_ "Edit Description"
+ Lucid.form_
+ [ Lucid.method_ "POST",
+ Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/description"),
+ Lucid.makeAttribute "hx-post" ("/tasks/" <> TaskCore.taskId task <> "/description"),
+ Lucid.makeAttribute "hx-swap" "none"
]
- (Lucid.toHtml (TaskCore.taskDescription task))
- Lucid.div_ [Lucid.class_ "form-actions"] <| do
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Save Description"
-
- let children = filter (maybe False (TaskCore.matchesId (TaskCore.taskId task)) <. TaskCore.taskParent) allTasks
- unless (null children) <| do
- Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h3_ "Child Tasks"
- Lucid.ul_ [Lucid.class_ "child-list"] <| do
- traverse_ renderChild children
-
- unless (null commits) <| do
- Lucid.div_ [Lucid.class_ "detail-section"] <| do
- Lucid.h3_ "Git Commits"
- Lucid.div_ [Lucid.class_ "commit-list"] <| do
- traverse_ (renderCommit (TaskCore.taskId task)) commits
-
- 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"
- 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_
- [ Lucid.href_ ("/tasks/" <> TaskCore.taskId task <> "/review"),
- Lucid.class_ "review-link-btn"
- ]
- "Review This Task"
+ <| do
+ Lucid.textarea_
+ [ Lucid.name_ "description",
+ Lucid.class_ "description-textarea",
+ Lucid.rows_ "10",
+ Lucid.placeholder_ "Enter description..."
+ ]
+ (Lucid.toHtml (TaskCore.taskDescription task))
+ Lucid.div_ [Lucid.class_ "form-actions"] <| do
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Save Description"
+
+ let children = filter (maybe False (TaskCore.matchesId (TaskCore.taskId task)) <. TaskCore.taskParent) allTasks
+ unless (null children) <| do
+ Lucid.div_ [Lucid.class_ "detail-section"] <| do
+ Lucid.h3_ "Child Tasks"
+ Lucid.ul_ [Lucid.class_ "child-list"] <| do
+ traverse_ renderChild children
+
+ unless (null commits) <| do
+ Lucid.div_ [Lucid.class_ "detail-section"] <| do
+ Lucid.h3_ "Git Commits"
+ Lucid.div_ [Lucid.class_ "commit-list"] <| do
+ traverse_ (renderCommit (TaskCore.taskId task)) commits
+
+ 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"
+ 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_
+ [ Lucid.href_ ("/tasks/" <> TaskCore.taskId task <> "/review"),
+ Lucid.class_ "review-link-btn"
+ ]
+ "Review This Task"
where
renderDependency :: (Monad m) => TaskCore.Dependency -> Lucid.HtmlT m ()
renderDependency dep =
@@ -1409,161 +1468,169 @@ renderRetryContextBanner tid (Just ctx) =
instance Lucid.ToHtml TaskReviewPage where
toHtmlRaw = Lucid.toHtml
toHtml (ReviewPageNotFound tid) =
- Lucid.doctypehtml_ <| do
- pageHead "Task Not Found - Jr Review"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ "Task Not Found"
- Lucid.p_ <| do
- "The task "
- Lucid.code_ (Lucid.toHtml tid)
- " could not be found."
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Task Not Found - Jr Review"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ "Task Not Found"
+ Lucid.p_ <| do
+ "The task "
+ Lucid.code_ (Lucid.toHtml tid)
+ " could not be found."
toHtml (ReviewPageFound task reviewInfo) =
- Lucid.doctypehtml_ <| do
- pageHead ("Review: " <> TaskCore.taskId task <> " - Jr")
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ "Review Task"
-
- Lucid.div_ [Lucid.class_ "task-summary"] <| do
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
- Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Title:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskTitle task))
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
- Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
-
- case reviewInfo of
- ReviewNoCommit ->
- Lucid.div_ [Lucid.class_ "no-commit-msg"] <| do
- Lucid.h3_ "No Commit Found"
- Lucid.p_ "No commit with this task ID was found in the git history."
- Lucid.p_ "The worker may not have completed yet, or the commit message doesn't include the task ID."
- ReviewMergeConflict commitSha conflictFiles ->
- Lucid.div_ [Lucid.class_ "conflict-warning"] <| do
- Lucid.h3_ "Merge Conflict Detected"
- Lucid.p_ <| do
- "Commit "
- Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
- " cannot be cleanly merged."
- Lucid.p_ "Conflicting files:"
- Lucid.ul_ <| traverse_ (Lucid.li_ <. Lucid.toHtml) conflictFiles
- ReviewReady commitSha diffText -> do
- Lucid.div_ [Lucid.class_ "diff-section"] <| do
- Lucid.h3_ <| do
- "Commit: "
- Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
- Lucid.pre_ [Lucid.class_ "diff-block"] (Lucid.toHtml diffText)
-
- Lucid.div_ [Lucid.class_ "review-actions"] <| do
- Lucid.form_
- [ Lucid.method_ "POST",
- Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/accept"),
- Lucid.class_ "inline-form"
- ]
- <| do
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "accept-btn"] "Accept"
+ let tid = TaskCore.taskId task
+ crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead ("Review: " <> TaskCore.taskId task <> " - Jr")
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ "Review Task"
+
+ Lucid.div_ [Lucid.class_ "task-summary"] <| do
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
+ Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Title:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskTitle task))
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
+ Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
+
+ case reviewInfo of
+ ReviewNoCommit ->
+ Lucid.div_ [Lucid.class_ "no-commit-msg"] <| do
+ Lucid.h3_ "No Commit Found"
+ Lucid.p_ "No commit with this task ID was found in the git history."
+ Lucid.p_ "The worker may not have completed yet, or the commit message doesn't include the task ID."
+ ReviewMergeConflict commitSha conflictFiles ->
+ Lucid.div_ [Lucid.class_ "conflict-warning"] <| do
+ Lucid.h3_ "Merge Conflict Detected"
+ Lucid.p_ <| do
+ "Commit "
+ Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
+ " cannot be cleanly merged."
+ Lucid.p_ "Conflicting files:"
+ Lucid.ul_ <| traverse_ (Lucid.li_ <. Lucid.toHtml) conflictFiles
+ ReviewReady commitSha diffText -> do
+ Lucid.div_ [Lucid.class_ "diff-section"] <| do
+ Lucid.h3_ <| do
+ "Commit: "
+ Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
+ Lucid.pre_ [Lucid.class_ "diff-block"] (Lucid.toHtml diffText)
+
+ Lucid.div_ [Lucid.class_ "review-actions"] <| do
+ Lucid.form_
+ [ Lucid.method_ "POST",
+ Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/accept"),
+ Lucid.class_ "inline-form"
+ ]
+ <| do
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "accept-btn"] "Accept"
- Lucid.form_
- [ Lucid.method_ "POST",
- Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/reject"),
- Lucid.class_ "reject-form"
- ]
- <| do
- Lucid.textarea_
- [ Lucid.name_ "notes",
- Lucid.class_ "reject-notes",
- Lucid.placeholder_ "Rejection notes (optional)"
+ Lucid.form_
+ [ Lucid.method_ "POST",
+ Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/reject"),
+ Lucid.class_ "reject-form"
]
- ""
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "reject-btn"] "Reject"
+ <| do
+ Lucid.textarea_
+ [ Lucid.name_ "notes",
+ Lucid.class_ "reject-notes",
+ Lucid.placeholder_ "Rejection notes (optional)"
+ ]
+ ""
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "reject-btn"] "Reject"
instance Lucid.ToHtml TaskDiffPage where
toHtmlRaw = Lucid.toHtml
toHtml (DiffPageNotFound tid commitHash') =
- Lucid.doctypehtml_ <| do
- pageHead "Commit Not Found - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ "Commit Not Found"
- Lucid.p_ <| do
- "Could not find commit "
- Lucid.code_ (Lucid.toHtml commitHash')
- Lucid.a_ [Lucid.href_ ("/tasks/" <> tid), Lucid.class_ "back-link"] "← Back to task"
+ let shortHash = Text.take 8 commitHash'
+ crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Commit Not Found - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ "Commit Not Found"
+ Lucid.p_ <| do
+ "Could not find commit "
+ Lucid.code_ (Lucid.toHtml commitHash')
+ Lucid.a_ [Lucid.href_ ("/tasks/" <> tid), Lucid.class_ "back-link"] "← Back to task"
toHtml (DiffPageFound tid commitHash' diffOutput) =
- Lucid.doctypehtml_ <| do
- pageHead ("Diff " <> Text.take 8 commitHash' <> " - Jr")
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.div_ [Lucid.class_ "diff-header"] <| do
- Lucid.a_ [Lucid.href_ ("/tasks/" <> tid), Lucid.class_ "back-link"] "← Back to task"
- Lucid.h1_ <| do
- "Commit "
- Lucid.code_ (Lucid.toHtml (Text.take 8 commitHash'))
- Lucid.pre_ [Lucid.class_ "diff-block"] (Lucid.toHtml diffOutput)
+ let shortHash = Text.take 8 commitHash'
+ crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead ("Diff " <> shortHash <> " - Jr")
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.div_ [Lucid.class_ "diff-header"] <| do
+ Lucid.a_ [Lucid.href_ ("/tasks/" <> tid), Lucid.class_ "back-link"] "← Back to task"
+ Lucid.h1_ <| do
+ "Commit "
+ Lucid.code_ (Lucid.toHtml shortHash)
+ Lucid.pre_ [Lucid.class_ "diff-block"] (Lucid.toHtml diffOutput)
instance Lucid.ToHtml StatsPage where
toHtmlRaw = Lucid.toHtml
toHtml (StatsPage stats maybeEpic) =
- Lucid.doctypehtml_ <| do
- pageHead "Task Statistics - Jr"
- pageBody <| do
- Lucid.div_ [Lucid.class_ "container"] <| do
- Lucid.h1_ <| case maybeEpic of
- Nothing -> "Task Statistics"
- Just epicId -> Lucid.toHtml ("Statistics for Epic: " <> epicId)
-
- Lucid.form_ [Lucid.method_ "GET", Lucid.action_ "/stats", Lucid.class_ "filter-form"] <| do
- Lucid.div_ [Lucid.class_ "filter-row"] <| do
- Lucid.div_ [Lucid.class_ "filter-group"] <| do
- Lucid.label_ [Lucid.for_ "epic"] "Epic:"
- Lucid.input_
- [ Lucid.type_ "text",
- Lucid.name_ "epic",
- Lucid.id_ "epic",
- Lucid.class_ "filter-input",
- Lucid.placeholder_ "Epic ID (optional)",
- Lucid.value_ (fromMaybe "" maybeEpic)
- ]
- Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "filter-btn"] "Filter"
- Lucid.a_ [Lucid.href_ "/stats", Lucid.class_ "clear-btn"] "Clear"
-
- Lucid.h2_ "By Status"
- multiColorProgressBar stats
- Lucid.div_ [Lucid.class_ "stats-grid"] <| do
- statCard "Open" (TaskCore.openTasks stats) (TaskCore.totalTasks stats)
- statCard "In Progress" (TaskCore.inProgressTasks stats) (TaskCore.totalTasks stats)
- statCard "Review" (TaskCore.reviewTasks stats) (TaskCore.totalTasks stats)
- statCard "Approved" (TaskCore.approvedTasks stats) (TaskCore.totalTasks stats)
- statCard "Done" (TaskCore.doneTasks stats) (TaskCore.totalTasks stats)
-
- Lucid.h2_ "By Priority"
- Lucid.div_ [Lucid.class_ "stats-section"] <| do
- traverse_ (uncurry renderPriorityRow) (TaskCore.tasksByPriority stats)
-
- Lucid.h2_ "By Namespace"
- Lucid.div_ [Lucid.class_ "stats-section"] <| do
- if null (TaskCore.tasksByNamespace stats)
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No namespaces found."
- else traverse_ (uncurry (renderNamespaceRow (TaskCore.totalTasks stats))) (TaskCore.tasksByNamespace stats)
-
- Lucid.h2_ "Summary"
- Lucid.div_ [Lucid.class_ "summary-section"] <| do
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Total Tasks:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.totalTasks stats)))
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Epics:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.totalEpics stats)))
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Ready:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.readyTasks stats)))
- Lucid.div_ [Lucid.class_ "detail-row"] <| do
- Lucid.span_ [Lucid.class_ "detail-label"] "Blocked:"
- Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.blockedTasks stats)))
+ let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Stats" Nothing]
+ in Lucid.doctypehtml_ <| do
+ pageHead "Task Statistics - Jr"
+ pageBodyWithCrumbs crumbs <| do
+ Lucid.div_ [Lucid.class_ "container"] <| do
+ Lucid.h1_ <| case maybeEpic of
+ Nothing -> "Task Statistics"
+ Just epicId -> Lucid.toHtml ("Statistics for Epic: " <> epicId)
+
+ Lucid.form_ [Lucid.method_ "GET", Lucid.action_ "/stats", Lucid.class_ "filter-form"] <| do
+ Lucid.div_ [Lucid.class_ "filter-row"] <| do
+ Lucid.div_ [Lucid.class_ "filter-group"] <| do
+ Lucid.label_ [Lucid.for_ "epic"] "Epic:"
+ Lucid.input_
+ [ Lucid.type_ "text",
+ Lucid.name_ "epic",
+ Lucid.id_ "epic",
+ Lucid.class_ "filter-input",
+ Lucid.placeholder_ "Epic ID (optional)",
+ Lucid.value_ (fromMaybe "" maybeEpic)
+ ]
+ Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "filter-btn"] "Filter"
+ Lucid.a_ [Lucid.href_ "/stats", Lucid.class_ "clear-btn"] "Clear"
+
+ Lucid.h2_ "By Status"
+ multiColorProgressBar stats
+ Lucid.div_ [Lucid.class_ "stats-grid"] <| do
+ statCard "Open" (TaskCore.openTasks stats) (TaskCore.totalTasks stats)
+ statCard "In Progress" (TaskCore.inProgressTasks stats) (TaskCore.totalTasks stats)
+ statCard "Review" (TaskCore.reviewTasks stats) (TaskCore.totalTasks stats)
+ statCard "Approved" (TaskCore.approvedTasks stats) (TaskCore.totalTasks stats)
+ statCard "Done" (TaskCore.doneTasks stats) (TaskCore.totalTasks stats)
+
+ Lucid.h2_ "By Priority"
+ Lucid.div_ [Lucid.class_ "stats-section"] <| do
+ traverse_ (uncurry renderPriorityRow) (TaskCore.tasksByPriority stats)
+
+ Lucid.h2_ "By Namespace"
+ Lucid.div_ [Lucid.class_ "stats-section"] <| do
+ if null (TaskCore.tasksByNamespace stats)
+ then Lucid.p_ [Lucid.class_ "empty-msg"] "No namespaces found."
+ else traverse_ (uncurry (renderNamespaceRow (TaskCore.totalTasks stats))) (TaskCore.tasksByNamespace stats)
+
+ Lucid.h2_ "Summary"
+ Lucid.div_ [Lucid.class_ "summary-section"] <| do
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Total Tasks:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.totalTasks stats)))
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Epics:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.totalEpics stats)))
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Ready:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.readyTasks stats)))
+ Lucid.div_ [Lucid.class_ "detail-row"] <| do
+ Lucid.span_ [Lucid.class_ "detail-label"] "Blocked:"
+ Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.blockedTasks stats)))
where
statCard :: (Monad m) => Text -> Int -> Int -> Lucid.HtmlT m ()
statCard label count total =
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
index 84e01ed..49f7976 100644
--- a/Omni/Jr/Web/Style.hs
+++ b/Omni/Jr/Web/Style.hs
@@ -24,6 +24,7 @@ stylesheet = do
baseStyles
layoutStyles
navigationStyles
+ breadcrumbStyles
cardStyles
listGroupStyles
statusBadges
@@ -264,6 +265,36 @@ navigationStyles = do
Stylesheet.key "gap" ("6px" :: Text)
marginBottom (px 8)
+breadcrumbStyles :: Css
+breadcrumbStyles = do
+ ".breadcrumb-container" ? do
+ backgroundColor "#f9fafb"
+ borderBottom (px 1) solid "#e5e7eb"
+ padding (px 6) (px 0) (px 6) (px 0)
+ ".breadcrumb-list" ? do
+ display flex
+ alignItems center
+ flexWrap Flexbox.wrap
+ Stylesheet.key "gap" ("4px" :: Text)
+ margin (px 0) (px 0) (px 0) (px 0)
+ padding (px 0) (px 0) (px 0) (px 0)
+ listStyleType none
+ fontSize (px 12)
+ ".breadcrumb-item" ? do
+ display flex
+ alignItems center
+ Stylesheet.key "gap" ("4px" :: Text)
+ ".breadcrumb-sep" ? do
+ color "#9ca3af"
+ Stylesheet.key "user-select" ("none" :: Text)
+ ".breadcrumb-current" ? do
+ color "#6b7280"
+ fontWeight (weight 500)
+ (".breadcrumb-list" ** a) ? do
+ color "#0066cc"
+ textDecoration none
+ (".breadcrumb-list" ** a) # hover ? textDecoration underline
+
cardStyles :: Css
cardStyles = do
".card"
@@ -1273,6 +1304,11 @@ darkModeStyles =
".nav-brand" ? color "#f3f4f6"
"h2" <> "h3" ? color "#d1d5db"
a ? color "#60a5fa"
+ ".breadcrumb-container" ? do
+ backgroundColor "#1f2937"
+ borderBottomColor "#374151"
+ ".breadcrumb-sep" ? color "#6b7280"
+ ".breadcrumb-current" ? color "#9ca3af"
".detail-label"
<> ".priority"