Skip to content

About

People-centric dashboard for self-managed GitLab: pick a few accounts and see their issues, MRs and epics in any state, last 12 actions, 7-day activity and real timelogs, served from a SQLite cache that 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.141.1 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.

Author: Marcel Petrick mail@marcelpetrick.it

License: GPLv3 or later. See LICENSE.

Note: project is generated with AI.

Dashboard (light)

Dark mode People view
Dashboard (dark) People view
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:

Outage with diagnostics

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).
  • 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.
  • 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

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_version) · reads /api/dashboard, /api/users · 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 · 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, 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) and HTTP API
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, PATCH /api/users/{id}/selection, POST /api/refresh (202 or 429 with Retry-After). 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.
  • 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.
  • CHANGELOG.md: release history.
  • SECURITY.md: how to report vulnerabilities and the deployment assumptions.

About

People-centric dashboard for self-managed GitLab: pick a few accounts and see their issues, MRs and epics in any state, last 12 actions, 7-day activity and real timelogs, served from a SQLite cache that 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