Progress Is Not a Field
The 80% project that was not 80% done
Every project tracker has a progress bar. Usually somebody has to decide what number goes in it.
That sounds harmless until you have used one for more than a week.
I have had projects that were "80% done" for a month. Nothing was technically broken. The bar said 80%, I said 80%, and nobody had enough information to disagree with either of us. But the project still had an unfinished authentication flow, three missing screens, and a deployment that only worked on my laptop. It was 80% done in the same way a student is "almost done" with an assignment they have not opened yet.
That is the problem I wanted to solve with ProgresTracker. It is a shared workspace for planning work as projects, milestones, features, and tasks. The slightly opinionated part is that progress is not a number you type in. It is calculated from the work below it.
If a task is done, it counts. If it is not done, it does not. Everything else is just maths.
The number needed a source
The first version of the idea was much more normal. A project had a progress field. You could create a project, write a name, and set it to 0, 25, 60, whatever felt right.
That is easy to build, which should have made me suspicious.
The field immediately became detached from the work. Finishing a task did not change the project percentage unless somebody remembered to update both things. Adding a new feature meant asking a vague question: does the project suddenly become less complete because we discovered more work? Deleting a task could make a project look more complete without anyone actually doing anything.
The number had no source of truth. It was just a second, less useful version of the task list.
So I removed it.
Instead, the structure looks like this:
Project
└── Milestone
└── Feature
└── TaskThe task is the only place where completion is entered manually. A feature is complete when its tasks are complete. A milestone gets its percentage from its features. A project gets its percentage from its milestones.
The progress bar is not a status update anymore. It is a query.
The small calculation that changes the whole thing
At the bottom of the tree, the calculation is intentionally boring:
type Task = {
completed: boolean;
};
function progress(tasks: Task[]) {
if (tasks.length === 0) return 0;
const completed = tasks.filter((task) => task.completed).length;
return Math.round((completed / tasks.length) * 100);
}Three completed tasks out of four means 75%. No one needs to have a meeting about whether it is 70% or 80%. It is 75% because the work says it is 75%.
The same idea rolls upward. A feature can expose its own task percentage, and its parent milestone can combine the work inside all of its features. At the top, a project is only as complete as the tasks that actually belong to it.
function projectProgress(milestones: { tasks: Task[] }[]) {
const allTasks = milestones.flatMap((milestone) => milestone.tasks);
return progress(allTasks);
}The real data is nested more deeply than this, but I like how blunt the rule stays: flatten the tasks, count the completed ones, return the percentage.
Why I did not average percentages
There is one trap here that looks correct at first.
Say one feature has one task and it is done. Another feature has nine tasks and none are done. If I average the two feature percentages, I get this:
feature A: 100%
feature B: 0%
project: 50%The project is not 50% complete. One out of ten tasks is done. It is 10% complete.
That sounds like a fussy detail, but it changes what a progress bar means. Averaging percentages gives every feature equal importance, even if one is a five-minute copy change and the other is an entire payments flow. Counting tasks gives each unit of planned work one vote.
It is not perfect either. Tasks can be badly sized. One task can hide two days of work while another takes two minutes. But that is useful information. If a project looks stuck at 40%, maybe the calculation is not wrong. Maybe the task called "build dashboard" should have been six tasks from the beginning.
The tracker cannot fix bad planning for you. It can at least make bad planning visible.
Shared work makes fake numbers worse
This mattered even more once I thought about using the tracker with other people.
When you work alone, an optimistic percentage is mostly you lying to yourself. When a team shares that number, it becomes a promise other people plan around. Someone sees 90% and assumes the project is ready for testing. Someone else sees 60% and decides there is still time to add a feature. Both decisions can be wrong if the percentage is just a feeling from last Tuesday.
ProgresTracker keeps the work visible at the level where it can be discussed. A project tells you the high-level state. A milestone tells you what part of the plan is moving. A feature tells you what is being built. A task tells you what is still waiting for someone.
That is more useful than a prettier bar, because it gives the bar an explanation.
The empty-state question
One thing I had to decide was what zero tasks means.
Mathematically, an empty list does not have a completion percentage. Product-wise, showing NaN% on a new feature is not a great user experience. Calling it 100% is worse, because an empty feature has not achieved anything yet.
So an item with no tasks is 0% complete.
if (tasks.length === 0) return 0;It is a tiny line, but it carries an opinion: planning a container for work is not the same as doing the work. Create the milestone, feature, or project first. Add tasks when you know what needs doing. The percentage starts moving only when there is something real to move through.
What the app is actually for
I do not think everyone needs another project-management tool. There are already enough tabs asking us to drag cards from left to right.
I built ProgresTracker for a narrower problem: I wanted to open a project and know where it really stood without manually maintaining another dashboard. Not a motivational progress bar. Not a guessed number. A number connected to the tasks that are still open.
The stack is Next.js and Supabase, but the useful part is not the stack. It is the constraint. Once the percentage is derived, a few nice things happen automatically:
- Completing a task updates the parent work without extra bookkeeping.
- Adding unfinished work lowers the percentage honestly.
- Opening a project gives you a route from the summary down to the exact task holding it back.
I am still building it, which is the fun part and also the reason the tracker exists. The project has tasks. Those tasks have checkboxes. And yes, its own percentage comes from them.
That is probably the only progress bar I trust.