diff options
Diffstat (limited to 'Omni')
| -rw-r--r-- | Omni/Jr/Web.hs | 1107 | ||||
| -rw-r--r-- | Omni/Jr/Web/Style.hs | 36 |
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" |
