Skip to content

About

Dashboard for team leads on a self-managed GitLab (Free tier): see what each person works on, catch stalled, overdue and unreviewed work, follow milestone progress without Premium, and report per team over weeks to quarters. SQLite cache keeps last-known-good data through outages. FastAPI, no JS build, Docker image on GHCR.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

GitLab Team Pulse

Local Pipeline Docker Image Latest tag GHCR image License: GPL v3 or later Python 3.12+ FastAPI 0.143.2 Coverage: 99% Conventional Commits

A people-centric dashboard for a self-managed GitLab instance. Pick a handful of GitLab accounts once, and everyone who opens the page sees what those people have recently worked on: assigned issues and merge requests in every state, their last twelve actions, seven days of activity and seven days of actual GitLab timelogs. It also shows exactly how fresh all of it is.

For whoever steers the team, it points out what is stalled, overdue or waiting for review, follows milestone progress, and reports on named teams over weeks, months and quarters. All of this works on GitLab Free (see steering signals).

Author: Marcel Petrick mail@marcelpetrick.it

License: GPLv3 or later. See LICENSE.

Note: project is generated with AI.

Dashboard (light)

New in 0.4: steering signals and teams

Needs attention. One list across everyone on the dashboard: stalled work (no update for a week means something is wrong), overdue and due-soon items, MRs waiting for review, MRs that can't be merged and MRs without a reviewer. Click a tile to filter.

Needs attention

Milestones. Progress, remaining estimate, unestimated and overdue issues and a burn-up line for the milestones your people work in. GitLab Free has no burndown API, so Team Pulse computes it from each milestone's issues.

Milestones

Team report: totals vs the previous period, share of effort, knowledge spread Team editor: members and project scope
Team report Team editor
About: version, author, license and dependencies Dark mode
About dialog Dashboard (dark)

Also in the box

People view Outage handling
People view Outage with diagnostics
Contribution calendar (2D) Contribution skyline (3D, rotate and zoom)
Contribution calendar Contribution skyline

When GitLab is unreachable, the dashboard keeps the last known good data, marks it with an icon and a text label (color is never the only signal), and explains the problem in the diagnostics drawer (screenshot above).

What it does

  • Complete user directory: every account the token can see, including bots, service accounts, and blocked or deactivated users. It refreshes hourly, supports a live filter while typing and six sort orders, and the selection survives filtering.
  • Global, persistent selection: stored in SQLite and shared by all viewers. A newly selected user is synchronized right away.
  • Per-person card:
    • Work: assigned issues, merge requests, MRs to review and epics (where the instance supports them, see GraphQL) in every state, sorted newest first. You can re-sort locally by update time, title, project, state, due date or type.
    • Activity: the five latest actions at a glance, with all twelve one click away.
    • Charts: a stacked seven-day activity chart (pushes, comments, issues, merge requests, other) and daily logged time with per-project totals.
    • Contributions: the last 12 months as a GitLab-style calendar, or as a 3D "skyline" you can rotate and zoom. Counts use GitLab's contribution rule, computed from events (see the feature notes).
  • Needs attention: one list across all monitored people of stalled items (no update for a week means something is wrong), overdue and due-soon work, MRs waiting for review, MRs that can't be merged, and MRs without a reviewer in the team. Open items stay visible however old they are; work items show spent / estimate from GitLab time tracking.
  • Milestones: progress, remaining estimate, unestimated and overdue issues and a burn-up line, computed from each milestone's issues (GitLab Free has no burndown API).
  • Teams: named, shared groups of people with an optional project scope. One click shows a team on the dashboard. The team report compares hours, merged MRs, closed issues, reviews, MR cycle time and estimate accuracy with the previous period, shows the share of effort per project and which projects depend on a single person, and copies last week as Markdown. Totals are team-level; a per-person table is optional (details).
  • Facts only: time comes from GitLab timelogs (one GraphQL query per user), never estimated. Nothing is scored, and there is no hard-coded 40-hour judgment. A week with no logged time is simply highlighted.
  • Cached first: the browser reads only the local SQLite cache and polls a compact /api/status for a data_version change. GitLab is crawled in the background: selected users every ten minutes, the directory hourly, and immediately on Refresh now. Duplicate refreshes are coalesced and refresh storms are throttled.
  • Last known good wins: a failed refresh never replaces good data. Retention cleanup keeps SQLite compact but never deletes the only good snapshot, even after days of outage.
  • Explicit freshness: exact timestamps plus "10 seconds ago" for everything. It distinguishes fresh, refreshing, stale, error and never-synced states, per user and per dataset (work, activity, timelogs fail independently).
  • Low GitLab load: detailed crawling only for selected users, project metadata only on demand, incremental activity sync, bounded concurrency, and retries with backoff and jitter. Authentication errors fail fast.
  • Self-explaining UI: every button, filter, number, badge and column header has a tooltip that says what it is, how it is counted and what clicking does (enforced by a browser test).
  • About dialog: version, author, license, source link and the runtime dependencies with their versions and licenses, read from the installed package.
  • Polished, dependency-free UI: plain HTML, CSS and ES modules served by FastAPI, native SVG charts, no CDN, and no Node.js build. It has light and dark themes (kept for the browser session), keyboard support, and all data text stays selectable and copyable.

Quick start: try it without a GitLab

uv sync
uv run gitlab-team-pulse demo        # http://127.0.0.1:8000, backed by a built-in fake GitLab

The demo starts a deterministic fake GitLab API (32 accounts, 6 projects, work, events and timelogs), pre-selects four people and serves the dashboard. With Docker:

docker run --rm -p 8000:8000 ghcr.io/marcelpetrick/gitlabteampulse:latest demo --host 0.0.0.0

Run it against your GitLab

TL;DR

  1. In GitLab: avatar → Edit profile → Access tokens → Add new token (https://<your-gitlab>/-/user_settings/personal_access_tokens). Tick only the read_api scope, then copy the glpat-… token.
  2. In a terminal:
git clone https://github.com/marcelpetrick/GitLabTeamPulse.git && cd GitLabTeamPulse
uv sync
export TEAMPULSE_GITLAB_URL=https://gitlab.example.com                              # base URL, no trailing path
read -rs "TEAMPULSE_GITLAB_TOKEN?GitLab token: " && export TEAMPULSE_GITLAB_TOKEN   # zsh; bash: read -rsp "GitLab token: " TEAMPULSE_GITLAB_TOKEN && export TEAMPULSE_GITLAB_TOKEN
export TEAMPULSE_TIMEZONE=Europe/Berlin                                             # optional: calendar days in local time
uv run gitlab-team-pulse doctor   # checks URL, token, admin rights and the timelogs query
uv run gitlab-team-pulse serve    # open http://127.0.0.1:8000
  1. In the browser: People → tick the colleagues to follow → Dashboard. The first sync starts right away; after that it refreshes every 10 minutes or on Refresh now. The first sync with the contribution calendar backfills a year of events per person (about 70 s for 6 people on a real instance); later refreshes are incremental.

Details

  • Token rights: a read_api token is enough; no write scope is ever needed. An administrator (or Auditor) account is recommended. Without it, GitLab hides blocked, deactivated and internal accounts, and the dashboard says so in Diagnostics instead of claiming completeness. The top-level GraphQL timelogs query may also need admin rights (see docs/GRAPHQL.md).

  • Keep the token in a file instead of the shell:

    mkdir -p ~/.config/gitlab-team-pulse
    ( umask 077; read -rs "t?GitLab token: "; printf '%s' "$t" > ~/.config/gitlab-team-pulse/token )
    export TEAMPULSE_GITLAB_TOKEN_FILE=~/.config/gitlab-team-pulse/token
  • Troubleshooting doctor:

    • Redirected (301/302): the URL scheme, host or path is off.
    • Certificate error: set TEAMPULSE_GITLAB_CA_BUNDLE=/path/to/company-ca.pem.
    • Timelogs unavailable: the account lacks the rights; everything else still works.
  • State: stored in ~/.local/share/gitlab-team-pulse/teampulse.db, so the selection and the cache survive restarts. The demo uses a separate demo.db.

  • Standalone install: make build && pipx install dist/gitlab_team_pulse-*.whl (or uv tool install .).

Docker / Compose (recommended for servers)

mkdir -p secrets && printf '%s' 'glpat-...' > secrets/gitlab_token && chmod 600 secrets/gitlab_token
TEAMPULSE_GITLAB_URL=https://gitlab.example.com docker compose up -d

compose.yaml mounts the token as a Docker secret (/run/secrets/gitlab_token, read automatically) and keeps SQLite on the teampulse-data volume. The container:

  • runs as the non-root user teampulse (uid 10001) with /data at mode 0700;
  • applies migrations as an explicit startup step, then runs Gunicorn with exactly one Uvicorn worker, so the in-process scheduler has a single owner;
  • logs human-readable lines to stdout and exposes a HEALTHCHECK on /api/health.

Images are published to ghcr.io/marcelpetrick/gitlabteampulse for linux/amd64 and linux/arm64, tagged latest, <version>, vX.Y tags and sha-<commit>.

Configuration

All settings are environment variables. None of them require a code change. make run / gitlab-team-pulse serve also reads an ignored .env file from the working directory; for another file use uv run --env-file path/to/file.env gitlab-team-pulse serve.

Variable Default Purpose
TEAMPULSE_GITLAB_URL — Base URL of the self-managed GitLab
TEAMPULSE_GITLAB_TOKEN — API token (fallback when no secret file exists)
TEAMPULSE_GITLAB_TOKEN_FILE /run/secrets/gitlab_token Secret file, preferred over the variable
TEAMPULSE_GITLAB_VERIFY_TLS true TLS verification
TEAMPULSE_GITLAB_CA_BUNDLE — Custom CA bundle for on-premise certificates
TEAMPULSE_GITLAB_CONCURRENCY 4 Maximum parallel GitLab requests
TEAMPULSE_GITLAB_TIMEOUT_SECONDS / _MAX_RETRIES 20 / 3 Request timeout and bounded retries
TEAMPULSE_DATABASE_PATH ~/.local/share/gitlab-team-pulse/teampulse.db SQLite file (container: /data/teampulse.db)
TEAMPULSE_HOST / TEAMPULSE_PORT 127.0.0.1 / 8000 Bind address (container: 0.0.0.0)
TEAMPULSE_LOG_LEVEL INFO DEBUG … CRITICAL
TEAMPULSE_SELECTED_REFRESH_INTERVAL_SECONDS 600 Selected-user refresh
TEAMPULSE_USER_REFRESH_INTERVAL_SECONDS 3600 Directory refresh
TEAMPULSE_UI_POLL_INTERVAL_SECONDS 20 Browser polling of the local backend
TEAMPULSE_STALE_GRACE_SECONDS 120 Grace before data is shown as stale
TEAMPULSE_MANUAL_REFRESH_MIN_INTERVAL_SECONDS 10 Refresh-storm protection
TEAMPULSE_RETENTION_HOURS / TEAMPULSE_ERROR_RETENTION_DAYS 24 / 7 Rolling cache and resolved-error retention
TEAMPULSE_WORK_WINDOW_DAYS 30 "Recently relevant" work window
TEAMPULSE_ACTIVITY_DAYS 7 Activity and timelog window
TEAMPULSE_CONTRIBUTIONS_REFRESH_MINUTES 60 How often the 12-month contribution calendar is refreshed
TEAMPULSE_TIMEZONE UTC Time zone for calendar-day buckets
TEAMPULSE_STALLED_DAYS 7 Open items without an update for this long are "stalled"
TEAMPULSE_DUE_SOON_DAYS 7 Due dates within this many days are "due soon"
TEAMPULSE_MILESTONES_MAX 12 Milestones whose progress is computed (0 turns it off)
TEAMPULSE_MAX_TRACKED_USERS 30 Selected users plus team members whose history is kept
TEAMPULSE_TEAMS_READONLY false Lock creating, editing and deleting teams
TEAMPULSE_TEAM_PERSON_BREAKDOWN false Show the per-person table in team reports

The token is never written to SQLite, logs (a redaction filter guards them), API responses or exception text shown in the UI. It is sent only to the configured GitLab host, and pagination links pointing elsewhere are refused.

Architecture

The full C4 architecture (system context, containers, components, the refresh sequence, the freshness state model, the data model and the deployment view, all as Mermaid diagrams) is in docs/ARCHITECTURE.md. In short:

Browser (HTML/CSS/ES modules, SVG charts)
   │  polls /api/status (data/users/teams versions) · reads /api/dashboard, /api/users, /api/teams · PATCH selection · POST refresh
FastAPI app (single worker) ── security headers (CSP, no CORS), cached reads only
   │
   ├── Scheduler (asyncio): directory hourly · selected users every 10 min · history and milestones hourly · cleanup hourly
   │        └── manual refresh coalescing and throttling, immediate sync for newly selected users
   ├── SyncService: per-user, per-dataset transactions; last-known-good; diagnostics
   │        └── GitLabClient (httpx): REST + GraphQL (timelogs, epics), pagination, retries, bounded concurrency
   └── SQLite (SQLAlchemy + Alembic, WAL, foreign keys) ── users, work items, events, timelogs,
            projects, milestones, teams, daily history aggregates, outcomes, sync state, runs, errors
Module Responsibility
gitlab/ The only code that speaks HTTP to GitLab (REST and GraphQL) or reads raw payloads; typed errors
sync.py, scheduler.py Synchronization, freshness bookkeeping, scheduling
store.py, models.py, migrations/ Persistence and schema history
retention.py Rolling cleanup that never removes the only good snapshot
dashboard.py, app.py Batched read model (no N+1 queries), attention list and HTTP API
teams.py Team payloads, team reports from the history aggregates, weekly digest
fake_gitlab.py Deterministic fake GitLab for demo, integration and browser tests

HTTP API: GET /api/health, /api/status, /api/users, /api/dashboard, /api/users/{id}/work|activity|time-summary, /api/errors, /api/projects, PATCH /api/users/{id}/selection, POST /api/refresh (202 or 429 with Retry-After), GET|POST /api/teams, PUT|DELETE /api/teams/{id}, POST /api/teams/{id}/apply, GET /api/teams/{id}/report?period=week|month|quarter&offset=N and GET /api/teams/{id}/digest and GET /api/about. Interactive docs are at /api/docs.

Why and how GitLab's GraphQL API is used (timelogs and epics) and what it saves compared with REST is explained in docs/GRAPHQL.md.

Development

make install     # uv sync --locked
make test        # unit + integration tests, coverage gate 95%
make e2e         # Playwright/Chromium browser tests against real HTTP servers
make lint typecheck format
make run         # serve against your GitLab (reads .env from the working directory)
make demo        # run against the fake GitLab
./localPipeline.sh   # everything CI runs, plus a Docker smoke test and an optional demo launch

make demo and make run both bind port 8000, so stop the demo before make run.

localPipeline.sh runs these stages: uv sync, Ruff lint, Ruff format check, strict mypy, a migration of a fresh database, tests with coverage, browser E2E, package build, and a wheel smoke test in a clean venv (serving the demo from the installed wheel). It then runs a Docker build with a container smoke test, opens the coverage report and launches the demo, and prints a summary. The GitHub Actions workflow and .gitlab-ci.yml run the same script. Docker Image publishes to GHCR after an end-to-end container smoke test that covers a Docker secret, a fake GitLab and a restart with a persistent volume.

Tests never need a live GitLab. They cover normalization, pagination, retries and rate limits, freshness, retention, last-known-good behaviour (including the VISION §37.3 outage scenario), migrations and drift, the API, the CLI and the browser flows from VISION §37.4.

Versioning and commits

Semantic Versioning: every commit bumps the patch version and major features bump the minor version (tools/bump_version.py). Commits follow Conventional Commits and are atomic, with the version appended, e.g. feat(docker): … (v0.1.0). See CHANGELOG.md.

Documentation

  • VISION.md: full product requirements and acceptance criteria.
  • AGENTS.md: how to work in this repository (commands, quality gate, architecture and frontend rules, release flow), written for AI coding agents and humans alike.
  • docs/ARCHITECTURE.md: C4 architecture overview with Mermaid diagrams (context, containers, components, dynamic, data and deployment views).
  • docs/GRAPHQL.md: what GraphQL is, why it is used for timelogs and epics, and how many requests it saves compared with REST.
  • docs/PERFORMANCE.md: profiling of startup, sync, API reads and the browser on real data, and what was changed or decided because of it.
  • docs/features/steering-signals.md: needs attention, milestones, teams and team reports: rules, counting, GitLab Free workarounds and load.
  • ideas20261015.md and plan.md: the reviewed feature ideas and the implementation plan behind v0.4.0.
  • CHANGELOG.md: release history.
  • SECURITY.md: how to report vulnerabilities and the deployment assumptions.

About

Dashboard for team leads on a self-managed GitLab (Free tier): see what each person works on, catch stalled, overdue and unreviewed work, follow milestone progress without Premium, and report per team over weeks to quarters. SQLite cache keeps last-known-good data through outages. FastAPI, no JS build, Docker image on GHCR.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages