@@ -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,