From 3b8d40fea3d10414597d252952914fee11021006 Mon Sep 17 00:00:00 2001
From: Bryan Van Deusen
Date: Fri, 10 Apr 2026 08:38:02 -0400
Subject: [PATCH] feat: overall completion bar on project cards; auto-collapse
done milestones
ProjectListView: add an overall task completion bar above the per-milestone
bars showing the percentage of all project tasks that are done.
ProjectView: milestones where every task is complete now start collapsed
by default, keeping the view clean for projects with many finished stages.
Co-Authored-By: Claude Sonnet 4.6
---
frontend/src/views/ProjectListView.vue | 48 ++++++++++++++++++++++++++
frontend/src/views/ProjectView.vue | 10 ++++++
2 files changed, 58 insertions(+)
diff --git a/frontend/src/views/ProjectListView.vue b/frontend/src/views/ProjectListView.vue
index f0e7079..e45e68a 100644
--- a/frontend/src/views/ProjectListView.vue
+++ b/frontend/src/views/ProjectListView.vue
@@ -113,6 +113,14 @@ function truncate(text: string | null, max = 120): string {
if (!text) return "";
return text.length > max ? text.slice(0, max) + "..." : text;
}
+
+function overallPct(project: Project): { total: number; pct: number } {
+ const counts = project.summary?.task_counts;
+ if (!counts) return { total: 0, pct: 0 };
+ const total = (counts.todo ?? 0) + (counts.in_progress ?? 0) + (counts.done ?? 0);
+ const pct = total > 0 ? Math.round((counts.done ?? 0) / total * 100) : 0;
+ return { total, pct };
+}
@@ -164,6 +172,18 @@ function truncate(text: string | null, max = 120): string {
{{ truncate(project.description) }}
+
+
+
+
{{ overallPct(project).pct }}%
+
+
0 && ms.completed === ms.total) {
+ collapsedMilestones.value.add(ms.id);
+ }
+ }
+}
+
async function loadProject() {
loading.value = true;
error.value = null;
@@ -130,6 +138,7 @@ async function loadProject() {
editStatus.value = data.status;
editDirty.value = false;
milestones.value = data.summary?.milestone_summary ?? [];
+ autoCollapseCompleted(milestones.value);
} catch {
error.value = "Failed to load project.";
} finally {
@@ -143,6 +152,7 @@ async function loadMilestones() {
`/api/projects/${projectId.value}/milestones`
);
milestones.value = data.milestones;
+ autoCollapseCompleted(milestones.value);
} catch {
// silent
}