A Django-based team velocity and task tracking dashboard that connects to JIRA and Azure DevOps to provide insights into your development process.
Transform your JIRA or Azure DevOps data into actionable development insights:
- 📋 Current Tasks: Real-time view of active work with completion forecasts
- 🚀 Team Velocity: Track story points and delivery trends over time with rolling averages
- 👨💻 Developer Velocity: Individual velocity metrics with seniority-level thresholds, rolling averages, and unfinished task inclusion
- 🔮 Task Forecasting: Predict completion dates based on team velocity, with completed vs remaining work breakdown
Choose your preferred setup method:
git clone <repository-url>
cd metricsCopy the example environment file:
cp .env.example .envEdit .env with your configuration (see Configuration Guide below).
Quick Start with JIRA (Single Command):
# Build the image
docker build -f ops/docker/Dockerfile -t metrics-dashboard .
# Run with minimal JIRA configuration (replace with your values)
docker run -p 8000:8000 \
-e METRICS_TASK_TRACKER=jira \
-e METRICS_JIRA_SERVER_URL=https://your-company.atlassian.net \
-e METRICS_JIRA_EMAIL=your-email@company.com \
-e METRICS_JIRA_API_TOKEN=your-api-token \
-e 'METRICS_PROJECT_KEYS=["PROJ"]' \
metrics-dashboardQuick Start with Azure DevOps:
# Build the image
docker build -f ops/docker/Dockerfile -t metrics-dashboard .
# Run with minimal Azure configuration (replace with your values)
docker run -p 8000:8000 \
-e METRICS_TASK_TRACKER=azure \
-e METRICS_AZURE_ORGANIZATION_URL=https://dev.azure.com/your-org \
-e METRICS_AZURE_PAT=your-personal-access-token \
-e METRICS_AZURE_PROJECT=YourProject \
metrics-dashboardUsing Environment File (Recommended for Multiple Variables):
# Run the container with environment variables from file
docker run -p 8000:8000 --env-file .env metrics-dashboardVisit http://localhost:8000 in your browser.
git clone <repository-url>
cd metrics
source ~/.virtualenvs/metrics/bin/activate # or your venv
pip install -r requirements.txtCopy the example environment file:
cp .env.example .envFor JIRA:
# Edit .env file
METRICS_TASK_TRACKER=jira
METRICS_JIRA_SERVER_URL=https://your-company.atlassian.net
METRICS_JIRA_EMAIL=your-email@company.com
METRICS_JIRA_API_TOKEN=your-api-token
METRICS_PROJECT_KEYS=["PROJ", "TEAM"]
METRICS_STORY_POINT_CUSTOM_FIELD_ID=customfield_10016For Azure DevOps:
# Edit .env file
METRICS_TASK_TRACKER=azure
METRICS_AZURE_ORGANIZATION_URL=https://dev.azure.com/your-org
METRICS_AZURE_PAT=your-personal-access-token
METRICS_AZURE_PROJECT=YourProject# Add to .env file
METRICS_MEMBERS={"John Doe": {"level": "senior", "member_groups": ["Team A"], "stages": ["Development"]}, "Jane Smith": {"level": "middle", "member_groups": ["Team B"], "stages": ["Testing"]}}python manage.py migrate
python manage.py check # Verify configuration
python manage.py runserver 8000Visit http://localhost:8000 in your browser.
Configure team members with skill levels for accurate velocity calculations:
METRICS_MEMBERS={
"John Doe": {
"level": "senior",
"member_groups": ["Team A"],
"stages": ["Development"]
},
"Jane Smith": {
"level": "middle",
"member_groups": ["Team B"],
"stages": ["Testing"]
}
}Skill Levels: junior, middle, senior
Match your team's workflow by customizing status mappings:
METRICS_IN_PROGRESS_STATUS_CODES=["Analysis", "Active", "In Progress", "In Development", "QA", "Validation", "Testing", "Review"]
METRICS_PENDING_STATUS_CODES=["Blocked", "On Hold", "Pending", "Waiting"]
METRICS_DONE_STATUS_CODES=["Done", "Closed", "Resolved"]METRICS_WORKING_DAYS_PER_MONTH=22
METRICS_IDEAL_HOURS_PER_DAY=4.0
METRICS_STORY_POINTS_TO_IDEAL_HOURS_CONVERTION_RATIO=1.0
METRICS_RECENTLY_FINISHED_TASKS_DAYS=14Adjust velocity multipliers based on experience levels:
METRICS_SENIORITY_LEVELS={"senior": 1.0, "middle": 2.0, "junior": 4.0}Filter data by task types, teams, or epics:
METRICS_GLOBAL_TASK_TYPES_FILTER=["Story", "Bug", "Task"]
METRICS_GLOBAL_TEAM_FILTER=["Team A", "Team B"]Override default assignee-based filtering with custom queries per member group:
# JIRA example - filter by parent epic
METRICS_MEMBER_GROUP_CUSTOM_FILTERS='{"TeamA": "parent in (PROJ-123, PROJ-456, PROJ-789)"}'
# Azure DevOps example - filter by parent work items
METRICS_MEMBER_GROUP_CUSTOM_FILTERS='{"TeamB": "[System.Parent] IN (174641, 176747, 179803)"}'When filtering by a specific member group, unassigned tasks normally appear under a separate "Unassigned" header. Enable this to relabel them under the filtered group's header instead:
METRICS_MERGE_UNASSIGNED_INTO_FILTERED_GROUP=trueThis only applies when viewing a specific member group — the "All Groups" view is unaffected.
Customize how tasks are sorted within each workflow stage:
# Default sorting criteria (applied to all stages unless overridden)
# Supported criteria: priority, assignee, health, spent_time
# Use '-' prefix for descending order (e.g., "-health" for worst health first)
METRICS_DEFAULT_SORT_CRITERIA=-health,-spent_time
# Stage-specific sorting (overrides default for specific stages)
# Example: Sort "Ready for Dev" stage by priority first, then assignee
METRICS_STAGE_SORT_OVERRIDES='{"Ready for Dev": "priority,assignee,-health"}'Available sort criteria:
priority- Task priority (ascending: 1, 2, 3...)assignee- Assignee name (alphabetical)health- Health status (ascending: GREEN → YELLOW → RED)spent_time- Time already spent on task
Sort direction:
- No prefix = ascending (e.g.,
priorityfor 1, 2, 3) -prefix = descending (e.g.,-healthfor RED, YELLOW, GREEN)
Configure fallback values when data is missing:
METRICS_DEFAULT_STORY_POINTS_VALUE_WHEN_MISSING=3
METRICS_DEFAULT_SENIORITY_LEVEL_WHEN_MISSING=middle
METRICS_DEFAULT_HEALTH_STATUS_WHEN_MISSING=GREEN
METRICS_MEMBER_GROUP_WHEN_MISSING="Unassigned"Error: JIRA connection failed
- Verify
METRICS_JIRA_SERVER_URLis correct (include https://) - Check
METRICS_JIRA_EMAILmatches your JIRA account - Ensure
METRICS_JIRA_API_TOKENis valid and has proper permissions - Test connection:
python manage.py check
Error: Story points not showing
- Find your custom field ID in JIRA → Settings → Issues → Custom Fields → Story Points → View (ID in URL)
- Update
METRICS_STORY_POINT_CUSTOM_FIELD_ID=customfield_XXXXX
Error: Azure DevOps connection issues
- Ensure your PAT has "Work Items (Read)" permissions
- Verify
METRICS_AZURE_ORGANIZATION_URLformat:https://dev.azure.com/your-org - Check
METRICS_AZURE_PROJECTmatches exact project name (case-sensitive)
No tasks appearing
- Check
METRICS_PROJECT_KEYSincludes your project(s) - Verify
METRICS_GLOBAL_TASK_TYPES_FILTERincludes relevant task types - Review status code mappings match your workflow
Velocity calculations seem wrong
- Verify team member levels in
METRICS_MEMBERS - Check seniority multipliers in
METRICS_SENIORITY_LEVELS - Ensure
METRICS_STORY_POINTS_TO_IDEAL_HOURS_CONVERTION_RATIOfits your team
Team members not showing up
- Add members to
METRICS_MEMBERSwith proper levels and groups - Check member names match exactly (case-sensitive)
- Verify
METRICS_MEMBER_GROUP_WHEN_MISSINGis configured
Dashboard loading slowly
- Reduce
METRICS_RECENTLY_FINISHED_TASKS_DAYSfor faster queries - Consider using
METRICS_GLOBAL_TEAM_FILTERto limit data scope - Check your task tracker API rate limits
Memory issues
- Increase server memory allocation
- Review
METRICS_GLOBAL_TASK_TYPES_FILTERto exclude unnecessary task types
After making changes, verify everything works:
python manage.py checkIf issues persist, check Django logs for detailed error messages.
- Review configuration examples in
.env.example - Check the Architecture Overview for technical details
- Verify all required environment variables are set
Protect your dashboard with username/password authentication:
METRICS_BASIC_AUTH_USERS='{"admin": "your-secure-password", "viewer": "another-password"}'If not set, the dashboard will be publicly accessible.
- Set
DEBUG=Falsein production - Configure
ALLOWED_HOSTSwith your domain:ALLOWED_HOSTS=your-domain.com,www.your-domain.com - Use strong, unique passwords for
METRICS_BASIC_AUTH_USERS - Store API tokens securely and rotate them regularly
-
Build Static Assets
python manage.py compress
-
Run with Production Server
gunicorn metrics.wsgi:application
-
Database Setup
python manage.py migrate
-
Health Check
python manage.py check --deploy
- Configure Caching: The app uses Django's cache framework for task search results
- Monitor Memory Usage: Large datasets may require additional memory
- API Rate Limits: Be aware of JIRA/Azure DevOps API rate limits
- Static File Serving: Use a reverse proxy (nginx) for static files in production
Essential production settings:
DEBUG=False
ALLOWED_HOSTS=your-domain.com,www.your-domain.com
METRICS_BASE_URL=https://your-domain.com
METRICS_BASIC_AUTH_USERS='{"admin": "secure-password"}'The application follows a Modular Monolith pattern with Hexagonal Architecture principles, providing deployment simplicity while maintaining clean architectural boundaries.
metrics/
├── tasks/ # Task management and tracking
├── velocity/ # Developer and team velocity calculations
├── forecast/ # Task completion forecasting
├── ui_web/ # Web interface and data federation
└── metrics/ # Django project configuration
Core Principles:
- Module Independence: Each module is self-contained with its own domain logic
- API-Only Communication: Modules communicate exclusively through public APIs in
app/api/ - Hexagonal Architecture: Clean separation between domain logic and external concerns
- Federation Gateway: UI module orchestrates data from other modules
Modules communicate through the API Repository Pattern, ensuring loose coupling:
# forecast/out/tasks_api_repository.py
class TasksApiRepository(TaskRepository):
def __init__(self, task_search_api):
self._task_search_api = task_search_api
async def search(self, criteria) -> List[Task]:
return await self._task_search_api.search(criteria)The application uses a simple, manual dependency injection (DI) pattern. Each module defines a Container class (e.g., TasksContainer) that is responsible for instantiating and wiring together the services within that module. A singleton instance of the container is created and imported by other modules to consume its public APIs, which are exposed as properties on the container.
This approach avoids complex DI frameworks and makes the dependency graph explicit and easy to trace.
The FederatedDataFetcher orchestrates concurrent data collection for views:
tasks = await (
FederatedDataFetcher
.for_(lambda: self._get_all_tasks())
.with_foreach_populator(self.forecast_api.populate_estimations)
.with_result_post_processor(self._sort_tasks_by_health_status)
.fetch()
)The UI is a dynamic, server-rendered application that leverages modern libraries to create a responsive and interactive experience without a complex JavaScript toolchain:
- Bulma: A lightweight, modern CSS framework used for all styling.
- HTMX: The core of the UI's interactivity. HTMX is used to perform AJAX requests and update parts of a page directly from HTML, enabling a dynamic, single-page application feel.
- Chart.js: Powers all data visualizations, rendering the charts and graphs for velocity and forecasting.
Each module follows identical structure:
{module_name}/
├── app/
│ ├── api/ # Public interfaces (ApiFor{Domain})
│ ├── domain/ # Business logic
│ │ ├── model/ # Domain models and config
│ │ └── {domain}_service.py # Core business services
│ └── spi/ # External dependencies
├── out/ # External integrations
├── config_loader.py # Module configuration
└── container.py # DI container and module interface
Tasks Module
TaskService: Core task operations and business logicApiForTaskSearch: Public interface for task queriesJiraTaskRepository/AzureTaskRepository: External data accessTasksConfig: Module configuration and settings
Velocity Module
VelocityService: Orchestrates velocity calculationsApiForVelocityCalculation: Public interface for velocity metricsUserVelocityCalculator: Individual developer metrics (from sd-metrics-lib)GeneralizedTeamVelocityCalculator: Team-wide metrics (from sd-metrics-lib)
Forecast Module
ForecastService: Business logic for forecasting algorithmsApiForForecast: Public interface for completion predictionsTaskEstimationCalculator: Completion time predictionsCapacityPlanningService: Resource allocation recommendations
UI Web Module
FederatedDataFetcher: Core data orchestration across modules- Component-based facades:
TeamVelocityFacade,TaskHealthFacade - Data classes:
ChartData,MemberGroupData,TaskHealthData - Convertors: Transform domain objects to UI-specific data structures
Domain models use nested objects reflecting business concepts:
@dataclass(slots=True)
class Task:
id: str
title: str
assignment: Assignment
time_tracking: TimeTracking
system_metadata: SystemMetadata
@dataclass(slots=True)
class Assignment:
assignee: Optional[User] = None
team: Optional[Team] = NoneThe application leverages sd-metrics-lib using the Calculators + Sources architecture:
- Configure
TaskProvider(JIRA/Azure viaJiraTaskProvider,AzureTaskProvider) - Configure extractors (
StoryPointExtractor,WorklogExtractor) - Provide to calculators (
UserVelocityCalculator,GeneralizedTeamVelocityCalculator)
This ensures battle-tested metric calculations while maintaining clean architectural boundaries.
- Python 3.9+
- JIRA or Azure DevOps access with API permissions
- Virtual environment (recommended)
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Built with Django 6.0, sd-metrics-lib 7.0, and modern web standards for sustainable software development metrics.


