Sort project cards on all projects page by activity tier

Amolith created

Change summary

src/cmd/webui/handlers.rs | 41 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)

Detailed changes

src/cmd/webui/handlers.rs 🔗

@@ -20,6 +20,25 @@ use super::AppState;
 
 const PAGE_SIZE: usize = 25;
 
+/// Activity tier for sorting project cards on the index page.
+/// Lower values sort first (most active projects at the top).
+fn project_card_activity_tier(card: &ProjectCard) -> u32 {
+    match card {
+        ProjectCard::Ok {
+            in_progress, open, ..
+        } => {
+            if *in_progress > 0 {
+                0 // Has in-progress tasks
+            } else if *open > 0 {
+                1 // Has open tasks only
+            } else {
+                2 // Only closed or empty
+            }
+        }
+        ProjectCard::Err { .. } => 3, // Error state
+    }
+}
+
 pub(super) async fn index_handler(State(state): State<AppState>) -> Response {
     let root = state.data_root.clone();
     let result = tokio::task::spawn_blocking(move || -> Result<IndexTemplate> {
@@ -53,6 +72,28 @@ pub(super) async fn index_handler(State(state): State<AppState>) -> Response {
             }
         }
 
+        // Sort cards by activity tier, then by name alphabetically
+        // Tier 0: in-progress tasks (most active)
+        // Tier 1: open tasks only
+        // Tier 2: closed/empty only
+        // Tier 3: errors
+        cards.sort_by(|a, b| {
+            let tier_a = project_card_activity_tier(a);
+            let tier_b = project_card_activity_tier(b);
+            match tier_a.cmp(&tier_b) {
+                std::cmp::Ordering::Equal => {
+                    let name_a = match a {
+                        ProjectCard::Ok { name, .. } | ProjectCard::Err { name, .. } => name,
+                    };
+                    let name_b = match b {
+                        ProjectCard::Ok { name, .. } | ProjectCard::Err { name, .. } => name,
+                    };
+                    name_a.cmp(name_b)
+                }
+                other => other,
+            }
+        });
+
         Ok(IndexTemplate {
             all_projects: projects,
             active_project: None,