[feature] Project & milestone progress: auto-calculate from task completion #30

Closed
opened 2026-05-05 08:06:56 +00:00 by casper-stevens · 4 comments
Member

Context

Identified in home#211: the progress bar on projects and milestones is unresponsive / always shows 0.

Current state

Both Project.progress (u8, 0-100) and Milestone.progress (u8, 0-100) are stored fields. Nothing in the handlers or services auto-calculates these values from task completion. If the OSIS backend doesn't set them, they stay at 0.

Work required

Option A — Calculate in handler (no persistence)

In projects_detail and milestones_detail handlers, after loading tasks:

let done_count = tasks.iter().filter(|t| t.status == TaskStatus::Done || t.status == TaskStatus::Closed).count();
let progress = if tasks.is_empty() { 0 } else { (done_count * 100 / tasks.len()) as u8 };

Pass as a separate computed_progress field to the template (do not overwrite the stored value).

Option B — Persist on task status change

When a task is updated, recalculate and save the parent project/milestone progress.

Option A is simpler and avoids stale data; recommend starting there.

Notes

  • Milestone progress should count tasks linked via task.milestone_sid
  • Project progress should count all tasks linked via task.project_sid
  • Display the computed value in the progress bar even if project.progress == 0
## Context Identified in home#211: the progress bar on projects and milestones is unresponsive / always shows 0. ## Current state Both `Project.progress` (u8, 0-100) and `Milestone.progress` (u8, 0-100) are stored fields. Nothing in the handlers or services auto-calculates these values from task completion. If the OSIS backend doesn't set them, they stay at 0. ## Work required ### Option A — Calculate in handler (no persistence) In `projects_detail` and `milestones_detail` handlers, after loading tasks: ```rust let done_count = tasks.iter().filter(|t| t.status == TaskStatus::Done || t.status == TaskStatus::Closed).count(); let progress = if tasks.is_empty() { 0 } else { (done_count * 100 / tasks.len()) as u8 }; ``` Pass as a separate `computed_progress` field to the template (do not overwrite the stored value). ### Option B — Persist on task status change When a task is updated, recalculate and save the parent project/milestone progress. Option A is simpler and avoids stale data; recommend starting there. ## Notes - Milestone progress should count tasks linked via `task.milestone_sid` - Project progress should count all tasks linked via `task.project_sid` - Display the computed value in the progress bar even if `project.progress == 0`
Author
Member

Implementation Spec for Issue #30

Objective

The progress bar on project detail and milestone detail pages always displays 0% because Project.progress and Milestone.progress are stored fields that nothing ever writes. This spec adds a computed progress value derived from task completion counts, calculated in each handler after tasks are fetched and passed to the template as a new computed_progress field. The stored values are never mutated.

Approach

Option A (recommended): Calculate in handler, pass as computed_progress to template (no persistence).

Requirements

  • Compute computed_progress: u8 in projects_detail as (done_count * 100 / tasks.len()) as u8, where done tasks have TaskStatus::Done or TaskStatus::Closed. If tasks is empty, result is 0.
  • Compute computed_progress: u8 in milestones_detail with the same formula over the tasks fetched via get_tasks_for_milestone.
  • Do not overwrite project.progress or milestone.progress (no persistence).
  • Add computed_progress: u8 as a new field on ProjectDetailTemplate and MilestoneDetailTemplate.
  • In ProjectDetailTemplate::render, replace self.project.progress used in the progress bar with self.computed_progress.
  • In MilestoneDetailTemplate::render, replace self.milestone.progress used in the progress bar with self.computed_progress.
  • TaskStatus::Cancelled is NOT counted as done.

Files to Modify

File What changes
crates/hero_biz_ui/src/web/templates/mod.rs Add computed_progress: u8 field to both template structs; use it in both render methods
crates/hero_biz_ui/src/web/handlers/mod.rs Compute computed_progress in projects_detail and milestones_detail; pass to template struct

Implementation Plan

Step 1 — Add computed_progress to ProjectDetailTemplate struct

File: crates/hero_biz_ui/src/web/templates/mod.rs

  • Add pub computed_progress: u8, to the struct definition

Step 2 — Add computed_progress to MilestoneDetailTemplate struct

File: crates/hero_biz_ui/src/web/templates/mod.rs

  • Add pub computed_progress: u8, to the struct definition

Step 3 — Update ProjectDetailTemplate::render to use computed_progress

File: crates/hero_biz_ui/src/web/templates/mod.rs

  • Change the progress = self.project.progress binding to progress = self.computed_progress

Step 4 — Update MilestoneDetailTemplate::render to use computed_progress

File: crates/hero_biz_ui/src/web/templates/mod.rs

  • Change the progress = self.milestone.progress binding to progress = self.computed_progress

Step 5 — Compute progress in projects_detail handler

File: crates/hero_biz_ui/src/web/handlers/mod.rs

  • After fetching tasks, compute done_count and computed_progress
  • Add computed_progress to the ProjectDetailTemplate { ... } struct literal

Step 6 — Compute progress in milestones_detail handler

File: crates/hero_biz_ui/src/web/handlers/mod.rs

  • After fetching tasks, compute done_count and computed_progress
  • Add computed_progress to the MilestoneDetailTemplate { ... } struct literal

Step 7 — Verify TaskStatus import in handlers

File: crates/hero_biz_ui/src/web/handlers/mod.rs

  • Confirm TaskStatus is imported (it likely already is from the Kanban handler); add import if missing

Acceptance Criteria

  • ProjectDetailTemplate struct has a pub computed_progress: u8 field
  • MilestoneDetailTemplate struct has a pub computed_progress: u8 field
  • ProjectDetailTemplate::render uses self.computed_progress for the progress bar
  • MilestoneDetailTemplate::render uses self.computed_progress for the progress bar
  • projects_detail handler computes and passes computed_progress
  • milestones_detail handler computes and passes computed_progress
  • A project with 0 tasks shows 0%
  • A project with 2 of 4 tasks Done/Closed shows 50%
  • TaskStatus::Cancelled tasks are not counted as done
  • cargo build passes with no errors

Notes

  • The format string in both render methods uses a single named argument progress = ... that appears twice in the HTML (the % text and the width: style). Changing only the binding on the Rust side is sufficient.
  • The stored project.progress and milestone.progress fields are not removed and may still appear in other parts of the template (e.g., the milestone list row in project detail), which are intentionally left unchanged.
  • Integer truncation in (done_count * 100 / tasks.len()) as u8 is intentional and matches the issue formula.
## Implementation Spec for Issue #30 ### Objective The progress bar on project detail and milestone detail pages always displays 0% because `Project.progress` and `Milestone.progress` are stored fields that nothing ever writes. This spec adds a computed progress value derived from task completion counts, calculated in each handler after tasks are fetched and passed to the template as a new `computed_progress` field. The stored values are never mutated. ### Approach Option A (recommended): Calculate in handler, pass as `computed_progress` to template (no persistence). ### Requirements - Compute `computed_progress: u8` in `projects_detail` as `(done_count * 100 / tasks.len()) as u8`, where done tasks have `TaskStatus::Done` or `TaskStatus::Closed`. If `tasks` is empty, result is `0`. - Compute `computed_progress: u8` in `milestones_detail` with the same formula over the tasks fetched via `get_tasks_for_milestone`. - Do not overwrite `project.progress` or `milestone.progress` (no persistence). - Add `computed_progress: u8` as a new field on `ProjectDetailTemplate` and `MilestoneDetailTemplate`. - In `ProjectDetailTemplate::render`, replace `self.project.progress` used in the progress bar with `self.computed_progress`. - In `MilestoneDetailTemplate::render`, replace `self.milestone.progress` used in the progress bar with `self.computed_progress`. - `TaskStatus::Cancelled` is NOT counted as done. ### Files to Modify | File | What changes | |---|---| | `crates/hero_biz_ui/src/web/templates/mod.rs` | Add `computed_progress: u8` field to both template structs; use it in both `render` methods | | `crates/hero_biz_ui/src/web/handlers/mod.rs` | Compute `computed_progress` in `projects_detail` and `milestones_detail`; pass to template struct | ### Implementation Plan #### Step 1 — Add `computed_progress` to `ProjectDetailTemplate` struct File: `crates/hero_biz_ui/src/web/templates/mod.rs` - Add `pub computed_progress: u8,` to the struct definition #### Step 2 — Add `computed_progress` to `MilestoneDetailTemplate` struct File: `crates/hero_biz_ui/src/web/templates/mod.rs` - Add `pub computed_progress: u8,` to the struct definition #### Step 3 — Update `ProjectDetailTemplate::render` to use `computed_progress` File: `crates/hero_biz_ui/src/web/templates/mod.rs` - Change the `progress = self.project.progress` binding to `progress = self.computed_progress` #### Step 4 — Update `MilestoneDetailTemplate::render` to use `computed_progress` File: `crates/hero_biz_ui/src/web/templates/mod.rs` - Change the `progress = self.milestone.progress` binding to `progress = self.computed_progress` #### Step 5 — Compute progress in `projects_detail` handler File: `crates/hero_biz_ui/src/web/handlers/mod.rs` - After fetching tasks, compute `done_count` and `computed_progress` - Add `computed_progress` to the `ProjectDetailTemplate { ... }` struct literal #### Step 6 — Compute progress in `milestones_detail` handler File: `crates/hero_biz_ui/src/web/handlers/mod.rs` - After fetching tasks, compute `done_count` and `computed_progress` - Add `computed_progress` to the `MilestoneDetailTemplate { ... }` struct literal #### Step 7 — Verify `TaskStatus` import in handlers File: `crates/hero_biz_ui/src/web/handlers/mod.rs` - Confirm `TaskStatus` is imported (it likely already is from the Kanban handler); add import if missing ### Acceptance Criteria - [ ] `ProjectDetailTemplate` struct has a `pub computed_progress: u8` field - [ ] `MilestoneDetailTemplate` struct has a `pub computed_progress: u8` field - [ ] `ProjectDetailTemplate::render` uses `self.computed_progress` for the progress bar - [ ] `MilestoneDetailTemplate::render` uses `self.computed_progress` for the progress bar - [ ] `projects_detail` handler computes and passes `computed_progress` - [ ] `milestones_detail` handler computes and passes `computed_progress` - [ ] A project with 0 tasks shows 0% - [ ] A project with 2 of 4 tasks Done/Closed shows 50% - [ ] `TaskStatus::Cancelled` tasks are not counted as done - [ ] `cargo build` passes with no errors ### Notes - The format string in both `render` methods uses a single named argument `progress = ...` that appears twice in the HTML (the `%` text and the `width:` style). Changing only the binding on the Rust side is sufficient. - The stored `project.progress` and `milestone.progress` fields are not removed and may still appear in other parts of the template (e.g., the milestone list row in project detail), which are intentionally left unchanged. - Integer truncation in `(done_count * 100 / tasks.len()) as u8` is intentional and matches the issue formula.
Author
Member

Test Results

  • Total: 1
  • Passed: 1
  • Failed: 0

All tests passed. The single test parser::tests::test_name_fix in the hero_biz_ui crate passed successfully. The remaining crates (hero_biz, hero_biz_app, hero_biz_sdk) have no tests defined yet.

## Test Results - Total: 1 - Passed: 1 - Failed: 0 All tests passed. The single test `parser::tests::test_name_fix` in the `hero_biz_ui` crate passed successfully. The remaining crates (`hero_biz`, `hero_biz_app`, `hero_biz_sdk`) have no tests defined yet.
Author
Member

Implementation Summary

Implemented Option A: auto-calculate progress in handlers without persisting the value.

Changes Made

crates/hero_biz_ui/src/web/templates/mod.rs

  • Added computed_progress: u8 field to ProjectDetailTemplate
  • Added computed_progress: u8 field to MilestoneDetailTemplate
  • ProjectDetailTemplate::render now uses self.computed_progress for the progress bar instead of self.project.progress
  • MilestoneDetailTemplate::render now uses self.computed_progress for the progress bar instead of self.milestone.progress

crates/hero_biz_ui/src/web/handlers/mod.rs

  • In projects_detail: after loading tasks, computes done_count (tasks with TaskStatus::Done or TaskStatus::Closed) and derives computed_progress as (done_count * 100 / tasks.len()) as u8; passes it into the template struct
  • In milestones_detail: same computation over the milestone's tasks; passes computed_progress into the template struct
  • Empty task list correctly yields 0% (no division by zero)
  • TaskStatus::Cancelled is excluded from the done count

Test Results

  • 1 test, 1 passed, 0 failed
  • cargo build clean

Acceptance Criteria

  • ProjectDetailTemplate has computed_progress: u8
  • MilestoneDetailTemplate has computed_progress: u8
  • Progress bar uses computed value, not stored value
  • Empty task list shows 0%
  • Cancelled tasks are not counted as done
  • cargo build passes with no errors
## Implementation Summary Implemented Option A: auto-calculate progress in handlers without persisting the value. ### Changes Made **`crates/hero_biz_ui/src/web/templates/mod.rs`** - Added `computed_progress: u8` field to `ProjectDetailTemplate` - Added `computed_progress: u8` field to `MilestoneDetailTemplate` - `ProjectDetailTemplate::render` now uses `self.computed_progress` for the progress bar instead of `self.project.progress` - `MilestoneDetailTemplate::render` now uses `self.computed_progress` for the progress bar instead of `self.milestone.progress` **`crates/hero_biz_ui/src/web/handlers/mod.rs`** - In `projects_detail`: after loading tasks, computes `done_count` (tasks with `TaskStatus::Done` or `TaskStatus::Closed`) and derives `computed_progress` as `(done_count * 100 / tasks.len()) as u8`; passes it into the template struct - In `milestones_detail`: same computation over the milestone's tasks; passes `computed_progress` into the template struct - Empty task list correctly yields 0% (no division by zero) - `TaskStatus::Cancelled` is excluded from the done count ### Test Results - 1 test, 1 passed, 0 failed - `cargo build` clean ### Acceptance Criteria - [x] `ProjectDetailTemplate` has `computed_progress: u8` - [x] `MilestoneDetailTemplate` has `computed_progress: u8` - [x] Progress bar uses computed value, not stored value - [x] Empty task list shows 0% - [x] Cancelled tasks are not counted as done - [x] `cargo build` passes with no errors
Author
Member

Implemented in e5261a5923. Progress is now computed from task completion in all views: project list, milestone list, projects dashboard, project detail milestone rows, and person detail milestone rows.

Implemented in e5261a592361e29c300b2b31d22e558227d5f0ed. Progress is now computed from task completion in all views: project list, milestone list, projects dashboard, project detail milestone rows, and person detail milestone rows.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_biz#30
No description provided.