← Back to task

Commit 39d9072f

commit 39d9072fc857d944c545ed4ae8df2a5a046946b6
Author: Ben Sima <ben@bensima.com>
Date:   Thu Nov 27 16:50:00 2025

    Create /epics page listing all epics
    
    The build passes. The `/epics` page has been successfully created
    and al
    
    Task-Id: t-155.2

diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index f556e1b2..72de7dbb 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -61,6 +61,7 @@ type API =
       :> QueryParam "type" Text
       :> Get '[Lucid.HTML] TaskListPage
     :<|> "kb" :> Get '[Lucid.HTML] KBPage
+    :<|> "epics" :> Get '[Lucid.HTML] EpicsPage
     :<|> "tasks" :> Capture "id" Text :> Get '[Lucid.HTML] TaskDetailPage
     :<|> "tasks" :> Capture "id" Text :> "status" :> ReqBody '[FormUrlEncoded] StatusForm :> Post '[Lucid.HTML] StatusBadgePartial
     :<|> "tasks" :> Capture "id" Text :> "description" :> ReqBody '[FormUrlEncoded] DescriptionForm :> PostRedirect
@@ -130,6 +131,8 @@ data StatsPage = StatsPage TaskCore.TaskStats (Maybe Text)
 
 newtype KBPage = KBPage [TaskCore.Task]
 
+newtype EpicsPage = EpicsPage [TaskCore.Task]
+
 newtype RecentActivityPartial = RecentActivityPartial [TaskCore.Task]
 
 newtype ReadyCountPartial = ReadyCountPartial Int
@@ -214,7 +217,7 @@ navbar =
       Lucid.div_ [Lucid.class_ "navbar-dropdown"] <| do
         Lucid.button_ [Lucid.class_ "navbar-dropdown-btn"] "Plans ▾"
         Lucid.div_ [Lucid.class_ "navbar-dropdown-content"] <| do
-          Lucid.a_ [Lucid.href_ "/tasks?type=Epic", Lucid.class_ "navbar-dropdown-item"] "Epics"
+          Lucid.a_ [Lucid.href_ "/epics", Lucid.class_ "navbar-dropdown-item"] "Epics"
           Lucid.a_ [Lucid.href_ "/kb", Lucid.class_ "navbar-dropdown-item"] "KB"
       Lucid.a_ [Lucid.href_ "/stats", Lucid.class_ "navbar-link"] "Stats"
 
@@ -387,6 +390,37 @@ instance Lucid.ToHtml KBPage where
               Just desc ->
                 Lucid.p_ [Lucid.class_ "kb-preview"] (Lucid.toHtml (Text.take 200 desc <> "..."))
 
+instance Lucid.ToHtml EpicsPage where
+  toHtmlRaw = Lucid.toHtml
+  toHtml (EpicsPage tasks) =
+    Lucid.doctypehtml_ <| do
+      pageHead "Epics - Jr"
+      Lucid.body_ <| do
+        navbar
+        Lucid.div_ [Lucid.class_ "container"] <| do
+          Lucid.h1_ <| Lucid.toHtml ("Epics (" <> tshow (length tasks) <> ")")
+          Lucid.p_ [Lucid.class_ "info-msg"] "All epics (large, multi-task projects)."
+          if null tasks
+            then Lucid.p_ [Lucid.class_ "empty-msg"] "No epics found."
+            else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderEpicCard tasks
+    where
+      renderEpicCard :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
+      renderEpicCard t =
+        Lucid.a_
+          [ Lucid.class_ "task-card task-card-link",
+            Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
+          ]
+          <| do
+            Lucid.div_ [Lucid.class_ "task-header"] <| do
+              Lucid.span_ [Lucid.class_ "task-id"] (Lucid.toHtml (TaskCore.taskId t))
+              statusBadge (TaskCore.taskStatus t)
+              Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
+            Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
+            case TaskCore.taskDescription t of
+              Nothing -> pure ()
+              Just desc ->
+                Lucid.p_ [Lucid.class_ "kb-preview"] (Lucid.toHtml (Text.take 200 desc <> "..."))
+
 instance Lucid.ToHtml TaskListPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (TaskListPage tasks filters) =
@@ -1239,6 +1273,7 @@ server =
     :<|> statsHandler
     :<|> taskListHandler
     :<|> kbHandler
+    :<|> epicsHandler
     :<|> taskDetailHandler
     :<|> taskStatusHandler
     :<|> taskDescriptionHandler
@@ -1304,6 +1339,13 @@ server =
       let epicTasks = filter (\t -> TaskCore.taskType t == TaskCore.Epic) allTasks
       pure (KBPage epicTasks)
 
+    epicsHandler :: Servant.Handler EpicsPage
+    epicsHandler = do
+      allTasks <- liftIO TaskCore.loadTasks
+      let epicTasks = filter (\t -> TaskCore.taskType t == TaskCore.Epic) allTasks
+          sortedEpics = List.sortBy (compare `on` TaskCore.taskPriority) epicTasks
+      pure (EpicsPage sortedEpics)
+
     parseStatus :: Text -> Maybe TaskCore.Status
     parseStatus = readMaybe <. Text.unpack