- Install Python 3.13 or later
- Install requirements vor virtual environments
sudo apt install python3.13-venv - Create the virtual environment for the project
python3 -m venv .venv - Activate the environment (do this every time in the console window where you need to run the application)
source .venv/bin/activateTip: To leave the venv, type the commanddeactivate(but why would you?) - Install project requirements
pip install -r requirements.txt - Run the server
uvicorn planner:app --reload - Use the application by browsing to
http://localhost:8000
You can override where the server loads the database.yaml file by adding
one of the following keys to data/config/server_config.yml:
database_path: path to the YAML file (absolute or relative todata/config)
Examples:
Absolute path:
database_path: /etc/plannertool/teamdb/database.yaml
Relative to data/config:
database_path: ../shared-configs/database.yaml
If neither key is present the server will fall back to data/config/database.yaml.
The server will run a setup first time. If you need to run the setup again, either delete the data/config/server_config.yml file or run python3 planner.py --setup.
- Install code coverage tool
npm install --save-dev c8 - Run the tests with coverage
npx c8 node scripts/run_js_tests.mjs - Run tests without coverage
node ./scripts/run_js_tests.mjs
export SESSION_ID=$(curl -s -X POST -H "Content-Type: application/json" -d '{"email":"[email protected]"}' localhost:8000/api/session | jq -r .sessionId)
echo "$SESSION_ID"
Create a configuration
curl -s -X POST -H "Content-Type: application/json"
-d '{"email":"[email protected]", "pat":"YOUR PAT"}'
localhost:8000/api/session
Run browser based tests: source .venv/bin/activate && npx playwright test modal-interactions.spec.js --config=playwright.smoke.config.js --project=chromium --reporter=list
Create a session export SESSION_ID=$(curl -s -X POST -H "Content-Type: application/json" -d '{"email":"[email protected]"}' http://localhost:8000/api/session | jq -r .sessionId)
curl -X GET -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/health curl -X POST -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/config curl -X GET -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/projects curl -X GET -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/tasks curl -X POST -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/tasks curl -X GET -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/teams curl -X GET -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/scenario curl -X GET -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/scenario?id= curl -X POST -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/scenario curl -X POST -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/cost # Return the cost JSON scheme curl -X GET -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/cost curl -X POST -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/admin/reload-config
curl -s -X POST -H "X-Session-Id: $SESSION_ID" -H "Content-Type: application/json"
-d '{"scenarioId":"scen123"}'
http://localhost:8000/api/cost | jq .
export SESSION_ID=$(curl -s -X POST -H "Content-Type: application/json" -d '{"email":"[email protected]"}' http://localhost:8000/api/session | jq -r .sessionId)
curl -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/cost | jq .
curl -s -X GET http://localhost:8000/api/cost | jq .
Practical client-side rules (what you should send)
To calculate a server-stored scenario: POST { "scenarioId": "" } This lets the server load the scenario and apply overrides, and response meta will show scenario_id and applied_overrides. To calculate a local/unsaved scenario (temporary overrides applied on the client): POST { "features": [ ...effective features with overrides...] } Send the full features list where each item has keys: id, project, start, end, capacity, plus optional title, type, state.
IMPORTANT: capacity must be a list of team allocations: [{"team": "team-name", "capacity": 80}, ...]
- Empty list
[]is valid (feature has no capacity allocated) - Float values like
1.0are NOT valid and will cause'float' object is not iterableerror - The backend
list_tasks()always returns capacity as a list
Response meta.scenario_id will be null (unless you also pass a scenarioId). GET /api/cost is fine for baseline cached result when session is authenticated.
{"op":"save","data":{"id":"scen_1766146121427_4976","name":"12-19 Scenario 1","overrides":{"516154":{"start":"2025-10-24","end":"2025-11-23"},"516364":{"start":"2025-10-24","end":"2025-11-23"},"516412":{"start":"2025-10-24","end":"2025-11-23"},"516413":{"start":"2025-10-24","end":"2025-11-23"},"516419":{"start":"2025-10-24","end":"2025-11-23"},"534751":{"start":"2025-10-24","end":"2025-11-23"},"535825":{"start":"2025-10-24","end":"2025-11-23"},"682664":{"start":"2025-12-17","end":"2026-06-22"},"688048":{"start":"2026-04-19","end":"2026-05-19"},"688049":{"start":"2026-02-20","end":"2026-04-18"},"688050":{"start":"2025-12-26","end":"2026-02-19"},"688051":{"start":"2026-05-23","end":"2026-06-22"}},"filters":{"projects":["project-a","project-b"],"teams":["team-a","team-b","team-c","team-d"]},"view":{"capacityViewMode":"team","condensedCards":false,"featureSortMode":"rank"}}}
curl -X GET -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/scenario
[{"id":"scen_1766146121427_4976","user":"[email protected]","shared":false}]
curl -X GET -s -H "X-Session-Id: $SESSION_ID" http://localhost:8000/api/scenario?id=scen_1766146121427_4976
{"id":"scen_1766146121427_4976","name":"12-19 Scenario 1","overrides":{"516154":{"start":"2025-10-24","end":"2025-11-23"},"516364":{"start":"2025-10-24","end":"2025-11-23"},"516412":{"start":"2025-10-24","end":"2025-11-23"},"516413":{"start":"2025-10-24","end":"2025-11-23"},"516419":{"start":"2025-10-24","end":"2025-11-23"},"534751":{"start":"2025-10-24","end":"2025-11-23"},"535825":{"start":"2025-10-24","end":"2025-11-23"},"682664":{"start":"2025-12-17","end":"2026-06-22"},"688048":{"start":"2026-04-19","end":"2026-05-19"},"688049":{"start":"2026-02-20","end":"2026-04-18"},"688050":{"start":"2025-12-26","end":"2026-02-19"},"688051":{"start":"2026-05-23","end":"2026-06-22"}},"filters":{"projects":["project-a","project-b"],"teams":["team-a","team-b","team-c","team-d"]},"view":{"capacityViewMode":"team","condensedCards":false,"featureSortMode":"rank"}}
python -m unittest tests/test_caching_client.py -v python -m unittest discover -s tests -p "test_*.py" -v
Use a template, here we use Debian 13.x as the base. If you are running on Proxmox 8, use this template:
https://cdn.gyptazy.com/proxmox/lxc_container/debian-13-standard_13.0-0_amd64.tar.zst Download this as a container teamplate.
Setup the LXC, give it the reasonable settings (or 2 CPU, 512 MB RAM, 8 GB disk, Static or DHCP IP)
Login and update the container apt update; apt upgrade; apt install nginx git python3-venv
Add the file nano /etc/nginx/sites-enabled/plannertool
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Remove the symlink for default site unlink /etc/nginx/sites-enabled/default and relad nginx systemctl reload nginx.
Add a non-root user to run the service adduser planner. Set a password, then su planner and go to the user home directory.
Clone the git repository git clone https://github.com/kpoppel/PlannerTool.git.
Setup the environment and run the service
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn planner:app
If this works, proceed to setting up the tool to automatically update and start. This step uses scripts/systemd_runner.sh and scripts/plannertool.service. Ensure the shell script is chmod +x
As root copy the plannertool.service to /etc/systemd/system/. Then reload and start it:
systemctl daemon-reload
systemctl enable plannertool
systemctl start plannertool
- Team backlogs. Can they be bundled up? Not without consistent use of team assignments.
Teams and projects in Azure is a floating thing. It is all tasks with an area path associated. Only the area path
sets projects and teams apart. In our context we should only put "Epics" in projects, and only "Features" (and below") in teams.
- Require clean up in Azure to streamline this.
- In the backend and frontend refactor so that "projects" are those with some id prefix (like 'project-') and teams with another (like "team-"). This is only to display them in different places. All other handling is the same. However it could also just be simplified to "Projects" with each team just also being a project, like it is now. A team can still participate on multiple Epics but could also more rarely participate on another team's Features.
- feature: Cost estimation
Using the capacity estimation calculate this:
- (/) Cost per feature/Epic
- Sum of cost per project (unfinished work)
- Sum of cost this fiscal year (configurable WSA 1/10-31/9)
- Sum of cost all time
- bug: Show Unplanned depends on Show Unassigned also being selected.
- feat: Enable flagging cost smells. Next:
Later:
- Feature: Make it possible to edit description in the UI
- Feature (convenience): Make a 'shrink-wrap' feature to pull in an Epic to fir the content (change start and end date)
- Feature (convenience): Make it possible to drag deft side of cards too
- Feature: Allow user side specification of projects so it is not server side
- Feature: Allow user side specification of teams so it is not server side
- Feature: Allow sharing and selecting which projects to load for a user (reducing load time)
- Feature (convenience) Export mountain view data to Excel format.
Solved:
- (/) bug: Changing scenarios does not refresh cost calculation data (this worked before) response: {"detail":"'float' object is not iterable"} GET works. Should simplify to use POST for any scenario including the baseline.
- (/) bug: After opening the Cost plugin, changing scenarios shows the spinner modal with the text "Loading Cost Data". Cost data is loaded for each scenario change from this point onwards. The plugin must unregister from the scenario change event when closed.
- (/) bug: When assigning capacity to a feature and moving or resizing it, the capacity override is lost. It does not matter if the feature already has a capacity allocation or not.
- (/) bug: The team selection popover in the details panel shows below the element. Redesigned this part.
- (/) feat:Add filter to sort away tasks without start/target dates. Reasoning: The iteration view in Azure is used in a way that items without iteration and start/target dates are not shown in the delivery plan page. Those without dates are not ready for primetime. TODO: Add field to the data signaling the data was originally without date, or don't add it from the server side and add it in the UI.
- (/) Feature: Make it possible to edit team load in the UI
- feat: Make a modal to configure team capacity spend. Iteration 1: Just output the text to put in Azure Devops manually Iteration 2: Make the change when syncing to Azure
- (/) feature: Cost estimation - first iteration
Cost per Epic/Feature is estimated on tasks where capacity estimation is present.
The estimation is opinionated (via feature flag) so that Epic estimates are ignored
if it has children assuming a breakdown is more precise even if there are gaps in date spans.
Based on the calendar people ledger we know the size of our teams and how many externals each team has.
Need to enrich the externals with their hourly cost
Need to enrich the data with worked hours: permanent: 116 h/month (could be different per site), externals 160h/month
First iteration: Use the team yaml file to avoid extra data sources. Use serverside configuration in a separate configuration file for working - (/) feat: Improve filter for task state to be additive not exclusive
- (/) Feature: Make the sidebar more nice to look at.
- (/) Feature: Page with mountain view large on it's own page with labels.
- (/) Feature count: 3 digits
- (/) Need to be able to fit all in the sidebar. Collapse or reduce font size?
- (/) Fix Azure link to point to UI
- (/) Bug: Changing end date of Epic and dragging it moved end date back to original date of latest child date? Not using the override date info on the Epic, and not using the override date of children?
- The problem is: Calculation is made on the baseline data. Before determining correct dates the overrides needs to be applied
- Solution: find all children in baseline, replace entries with override entries. Then calculate latest date.
- (/) Feature: If there is a dependency, show this on the board. (how to determine dependency? Data from Azure?)
- Determine link type to use: Related, Predecessor, or Successor (or all?)
- "Related" is simple at it does not imply a direction. The other two requires the maintainer to be vigilant.
- Details panel: Add Parents and children and pre/suc/rel links.
- (/) Bug: Team load calculation: when a team is 100% loaded, the graph should display 100% too when looking at the teams.
- Organisation load is good as it is.
- Perhaps switch to a line graph/piecewise linear representation when in team load mode.
- (/) Feature: If there are children with load data, use this data instead of the Epic. (empty spaces go to zero)
- Feature: If there are children propagate children data? May be really difficult as load will vary over time.
- Perhaps just turn off the Epic estimate in that case.
- What if the epics are shown alone? Load graph should still display propagated load as this is more accurate.
- If there are no children use the Epic estimates.
- NOTE: This is now a const variable to do either of these. Default is to ignore Epic if it has children.
- (/) Finish scenario save/load per user
- (/) doc: Add to README information about the workflow. Document the use the task states actively:
- New: Unplanned work
- Defined: Planned work, described adequately for further breakdown
- Active: Work in progress, developers assigned, time spent
- Resolved: Work completed, reviewed, demo, delivery processing
- Closed (not fetched): Task completed.
- (/) Bug: state.js line 229 hangs the browser. (Reason: an event handler was created again and again)
- (/) doc: Way of Working: How to organise data to get capacity graphs correctly displayed:
- Projects only contain Epics.
- Teams Do not have any Epics. They have Features (maybe someday Enabler type) and below.
- Project capacity spend is calculated from all Features which are children to those Epics.
- Team capacity spend is calculated from all Features where the team is mentioned.
- Right now:
- Projects and teams are more or less coincident.
- Team load should be able to calculate regardless.
- Project load more difficult.