A portfolio-level project health dashboard that pulls data from ServiceNow and displays it in a single-page web view. Built with Flask and Bootstrap 5.
- Portfolio view -- projects grouped by portfolio with per-portfolio and grand totals
- Financial tracking -- planned vs. actual CapEx and OpEx with progress bars (kCHF)
- RAG status indicators -- schedule, cost, and scope health from monthly status reports
- Timeline visualization -- Gantt-style bars with quarter labels and a "today" marker
- Key Project filter -- toggle to show only projects tagged
[Key Project] - Advanced view -- reveals PM column, per-project email reminders, and bulk "mail all PMs" link
- Dark / light theme -- toggle in the header, persisted in localStorage
- Excel export -- download all active project data as a formatted
.xlsx - Print-optimized -- landscape layout with forced light theme and proper page breaks
- Demo mode -- run the dashboard with realistic generated data, no ServiceNow instance required
Demo mode runs the full dashboard with 63 generated sample projects across 8 IT portfolios (Cloud & Infrastructure, Cybersecurity, Data & Analytics, Digital Workplace, ERP & Finance Systems, Manufacturing & OT, Customer Experience, Integration & API Platform). No ServiceNow instance or credentials are needed.
# 1. Clone the repository
git clone https://github.com/vincentmakes/ServiceNow-Project-Dashboard.git
cd ServiceNow-Project-Dashboard
# 2. (Recommended) Create a virtual environment
python -m venv venv
source venv/bin/activate # Linux / macOS
# venv\Scripts\activate # Windows
# 3. Install dependencies
pip install -r requirements.txt
# 4. Start in demo mode
DEMO_MODE=1 python app_oauth.pyOn Windows Command Prompt set the variable first:
set DEMO_MODE=1
python app_oauth.pyOn Windows PowerShell:
$env:DEMO_MODE="1"
python app_oauth.pyOpen http://localhost:5080 in your browser. You will see:
- 8 portfolio sections with 5-8 projects each
- ~20% of projects marked as Key Projects (filterable via toggle)
- Realistic financial data (CapEx / OpEx with varied spend ratios)
- RAG status indicators (green / yellow / red) on schedule, cost, and scope
- Timeline bars spanning across quarters
- Executive summaries in tooltips (hover over report dates)
- A few projects with stale reports (red dot) to demonstrate the email reminder feature
All interactive features work in demo mode: Key Projects filter, Advanced view, dark/light theme toggle, Excel export, and print layout.
# Build and start in demo mode — single command, nothing else needed
docker compose --profile demo up --buildOpen http://localhost:5080.
To stop: Ctrl+C or docker compose --profile demo down.
# Build the image
docker build -t project-dashboard .
# Run in demo mode
docker run -p 5080:5080 -e DEMO_MODE=1 project-dashboardLive mode connects to a ServiceNow instance via OAuth 2.0 and queries real
project data from the pm_project, pm_portfolio, sys_user,
project_status, and fm_expense_line tables.
- A ServiceNow instance with the Project Portfolio Management (PPM) plugin
- An OAuth 2.0 application registered in ServiceNow
- A service account with read access to the tables listed above
- Create a
.envfile in the project root:
SN_INSTANCE=yourco.service-now.com
SN_CLIENT_ID=your_oauth_client_id
SN_CLIENT_SECRET=your_oauth_client_secret
SN_SVC_USER=your_service_account_username
SN_SVC_PASSWORD=your_service_account_password
# Optional
FLASK_SECRET=a_random_secret_string
CACHE_TTL=300- Install and run:
pip install -r requirements.txt
python app_oauth.py-
Create the same
.envfile as above. -
Start with the live profile:
docker compose --profile live up --builddocker build -t project-dashboard .
docker run -p 5080:5080 \
-e SN_INSTANCE=yourco.service-now.com \
-e SN_CLIENT_ID=your_client_id \
-e SN_CLIENT_SECRET=your_client_secret \
-e SN_SVC_USER=your_username \
-e SN_SVC_PASSWORD=your_password \
project-dashboardOr mount a .env file:
docker run -p 5080:5080 --env-file .env project-dashboardapp_oauth.py Main application (Flask routes, SN integration, Excel export)
demo_data.py Demo data generator (~63 projects, 8 portfolios)
templates/
dashboard.html Jinja2 template (Inter font, Material Symbols)
static/
css/
dashboard-ui.css Main stylesheet (light/dark themes, design system)
print.css Print-optimized layout (landscape A4)
js/
dashboard.js Client-side logic (theme, filters, tooltips)
requirements.txt Python dependencies
Dockerfile Container image definition
docker-compose.yml Compose profiles for demo and live modes
.env Credentials for live mode (not committed)
CLAUDE.md Technical documentation for AI-assisted development
| Environment Variable | Required | Default | Description |
|---|---|---|---|
DEMO_MODE |
No | (unset) | Set to 1, true, or yes to use generated demo data |
SN_INSTANCE |
Live mode | -- | ServiceNow instance hostname (e.g. yourco.service-now.com) |
SN_CLIENT_ID |
Live mode | -- | ServiceNow OAuth client ID |
SN_CLIENT_SECRET |
Live mode | -- | ServiceNow OAuth client secret |
SN_SVC_USER |
Live mode | -- | Service account username |
SN_SVC_PASSWORD |
Live mode | -- | Service account password |
FLASK_SECRET |
No | random | Flask session secret key |
CACHE_TTL |
No | 300 |
Data cache TTL in seconds (live mode only) |
The dashboard reads from five ServiceNow tables. The service account used in live mode needs read access to all fields listed below.
| Field | Type | Description |
|---|---|---|
sys_id |
GUID | Unique project identifier |
number |
String | Project number (e.g. PRJ0012345) |
short_description |
String | Project name. Projects containing [Key Project] are flagged as key projects. Bracket tokens like [CT] cross-list the project into matching portfolios |
project_manager |
Reference → sys_user |
Project manager (sys_id) |
u_business_project_manager |
Reference → sys_user |
Business project manager (sys_id). This is a custom field — adjust if your instance uses a different column |
capex_cost |
Currency | Planned CapEx budget |
opex_cost |
Currency | Planned OpEx budget |
primary_portfolio |
Reference → pm_portfolio |
Portfolio the project belongs to (sys_id) |
start_date |
Date | Planned start date |
end_date |
Date | Planned end date |
work_end |
Date | Actual completion date (used for closed projects) |
active |
Boolean | Queried with active=true for active projects |
state |
Integer | Queried with stateIN3,7 for recently closed projects |
| Field | Type | Description |
|---|---|---|
sys_id |
GUID | Unique portfolio identifier |
name |
String | Portfolio display name (used as section header) |
| Field | Type | Description |
|---|---|---|
sys_id |
GUID | Unique user identifier |
name |
String | Display name (shown in PM / Business PM columns) |
email |
String | Email address (used for reminder mailto links) |
| Field | Type | Description |
|---|---|---|
sys_id |
GUID | Status report identifier (used for deep-link into ServiceNow) |
project |
Reference → pm_project |
Parent project (sys_id) |
as_on |
Date | Report "as of" date. Falls back to sys_updated_on if empty |
sys_updated_on |
DateTime | Last modification timestamp (fallback for report date) |
schedule |
String | Schedule RAG status (green, yellow, red) |
cost |
String | Cost RAG status (green, yellow, red) |
scope |
String | Scope RAG status (green, yellow, red) |
executive_summary |
String | Free-text summary (shown in tooltip on hover) |
| Field | Type | Description |
|---|---|---|
source_id |
Reference → pm_project |
Parent project (sys_id) |
amount |
Currency | Expense amount |
expense_type |
String | Expense category. Lines containing cap (case-insensitive) are counted as CapEx; all others as OpEx |
state |
String | Expense state. Lines with state=cancelled are excluded |
# Using gunicorn (Linux / macOS)
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5080 app_oauth:app
# Using waitress (Windows)
pip install waitress
waitress-serve --host=0.0.0.0 --port=5080 app_oauth:app
# Using Docker
docker build -t project-dashboard .
docker run -d -p 5080:5080 --env-file .env --restart unless-stopped project-dashboardConsider adding:
- A reverse proxy (nginx / Apache) with HTTPS
- An authentication layer (SSO, basic auth, or Azure AD)
- Cache TTL tuned to your data freshness needs (default: 5 minutes)
Internal use.