An open API service indexing awesome lists of open source software.

https://github.com/omaciel/sprint-pulse

Team availability dashboard — FastAPI + SQLite desktop & web app (PTO, sprints, Jira metrics)
https://github.com/omaciel/sprint-pulse

dashboard fastapi htmx pto-tracker python sprint-planning sqlite

Last synced: about 2 hours ago
JSON representation

Team availability dashboard — FastAPI + SQLite desktop & web app (PTO, sprints, Jira metrics)

Awesome Lists containing this project

README

          

# Sprint Pulse ⚡

[![tests](https://github.com/omaciel/sprint-pulse/actions/workflows/test.yml/badge.svg)](https://github.com/omaciel/sprint-pulse/actions/workflows/test.yml)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

A team availability dashboard you can **install**. Sprint Pulse combines team
time-off tracking, live Jira sprint metrics (tickets done/total, story points
done/total), and release-calendar events into a single availability heatmap.

It's a **FastAPI + SQLite** app that runs three ways from one codebase:

- **Desktop app** — a native window (pywebview) on macOS and Linux. 100% Python.
- **Local web app** — the same app in your browser (great for development).
- **Containerized web app** — browse to it from anywhere; data on a volume.

First run walks you through a setup wizard (settings → import sprints → team), or
you can import existing `data/*.yaml` in one click. Metrics refresh on a schedule
you control.

## What it shows

- Sprint navigator with state badges (active / future / closed)
- One sprint heatmap at a time — team members × working days
- Cell vocabulary: a **configurable set of absence types** (PTO, regional/company
holidays, partial, tentative by default), plus **Excluded** members who are shown
gray and don't count toward capacity
- Release-event row driven by **configurable event types** (git tags, Go/No-Go,
GA, freeze, testathon by default)
- A **Types** page to manage both vocabularies — add / rename / recolor (from a
fixed palette) / delete event & absence types
- Per-sprint availability % and a per-associate summary

Time off is managed **per person**: open **Team → a name** for that member's
availability **calendar** — pick an absence type (the defaults, or any you've added
on the **Types** page) and click weekdays to mark or clear them. Sprints derive their own
"who's out" list automatically from those dates, so editing a sprint is just its
dates and release events.

## Screenshots

> Captured from `make demo-desktop` — the native desktop window with fictional
> sample data (no Jira or credentials required).

**Dashboard** — one sprint's availability heatmap: team members × working days,
colored absence cells (from the configurable absence types), the release-event
row, the per-sprint availability %, and a sidebar with the sprint navigator and
legend.

![Sprint Pulse dashboard — a sprint availability heatmap with legend and sprint navigator](docs/images/dashboard.png)

**First-run setup** — a guided wizard (app settings → Jira connection → team
roster), or import an existing `data/*.yaml` roster, sprints, and time off in one
click.

![Sprint Pulse first-run welcome screen with guided setup and YAML import](docs/images/welcome.png)

**Team** — manage the roster, toggle **Excluded** (shown gray and not counted
toward capacity), and click any name to open that member's availability
calendar.

![Sprint Pulse Team page showing the roster with excluded-member toggles](docs/images/team.png)

**Sprints** — import sprints from Jira or add them manually; each row shows its
dates, Jira state, and cached ticket / story-point metrics. Archive a sprint to
drop it off the dashboard without deleting it.

![Sprint Pulse Sprints page listing sprints with dates, state, and metrics](docs/images/sprints.png)

**Schedule** — refresh Jira metrics automatically on an interval or cron
expression, or hit **Refresh metrics now** on demand; the last-run status shows
what was updated.

![Sprint Pulse Schedule page with refresh cadence and run-now controls](docs/images/schedule.png)

**Types** — manage the event & absence type vocabularies: each type's label, its
abbreviation (the letter shown in a cell), and its color (picked from a fixed
palette). Add, rename, recolor, or delete types — a type still in use can't be
deleted.

![Sprint Pulse Types page managing event and absence types with color swatches](docs/images/types.png)

## Requirements

- **Python 3.13+**
- Dependencies declared in `pyproject.toml` and locked in `uv.lock` (FastAPI,
SQLModel, Jinja2, APScheduler, pywebview, …), managed with
[uv](https://docs.astral.sh/uv/). The desktop shell + packaging deps live in
the optional `desktop` extra; Linux desktop also needs the **WebKitGTK** system
package (`gir1.2-webkit2-4.1` / `webkit2gtk`). The container path needs neither.
- A Jira API token with read access to your Scrum board (for live metrics).

## Setup

**No manual install step needed.** Any `make` target that runs the app or
tests creates the `.venv` and syncs dependencies on demand (via `uv`), so you
can jump straight to `make demo`. The sync re-runs only when `pyproject.toml`
or `uv.lock` change. You just need [uv](https://docs.astral.sh/uv/) on your
`PATH` — the Makefile points you to its installer if it's missing.

To populate the environment up front (e.g. before going offline):

```bash
make install # runtime + desktop deps
make install-dev # runtime + desktop + test/lint tooling
# …or directly: uv sync --extra desktop
```

> `make` targets use `.venv/bin/python` automatically — no activation needed.
> Override with `make PYTHON=/path/to/python` if you keep the venv
> elsewhere.

## Ways to run

### 1. Desktop app (native window)

```bash
make dev-desktop
```

Opens a native window (WebKit). Uses your local database (see *Where your data
lives*). The Jira token is stored in your OS keychain.

### 2. Local web app (browser)

```bash
make dev # http://localhost:8765 (auto-reloads on edits)
```

Your browser opens automatically once the server is accepting connections.
Change the port with `PORT=9000 make dev`.

### 3. Containerized web app (browser)

```bash
make container-build # build the image (podman)
make container-run # run it; http://localhost:8765

# …or run it directly with podman or docker:
podman run -p 8765:8765 -v sprint-pulse-data:/data \
-e JIRA_USERNAME=you@example.com -e JIRA_API_TOKEN=xxxx sprint-pulse
docker run -p 8765:8765 -v sprint-pulse-data:/data \
-e JIRA_USERNAME=you@example.com -e JIRA_API_TOKEN=xxxx sprint-pulse
```

The DB lives on the `/data` volume and survives restarts. In the container the
Jira token comes from `JIRA_API_TOKEN` (the DB only ever stores a reference, never
the token). Override the published port with `PORT=9000 make container-run`.

### 4. Run the server directly (advanced)

The web app is a standard ASGI module — handy for a custom host/port/DB:

```bash
SPRINT_PULSE_HOST=0.0.0.0 SPRINT_PULSE_PORT=9000 \
SPRINT_PULSE_DB=/path/to/my.db \
python -m sprint_pulse.web
```

### 5. Demo mode — no live Jira needed

Try the whole app offline with fictional example data and a **mocked Jira**:

```bash
make demo # browser at http://localhost:8765 (fresh throwaway DB)
make demo-desktop # same, in the native window
```

On a fresh checkout this is all you need: `make demo` installs dependencies if
needed, starts a fresh `.demo.db`, points the wizard's YAML import at
`examples/`, sets `SPRINT_PULSE_DEMO=1` so the Jira features use canned data
instead of a real instance, and opens your browser once the server is ready.
From the wizard you can:

- **Import manually** — "Import from YAML" loads the example team + sprints.
- **Import automatically** — "Import from Jira" lists mock board sprints to pick
from, and "Test connection" / "Refresh metrics now" return mock data.

No credentials or VPN required. Delete `.demo.db` to start over (`make demo`
does this for you each run).

### 6. Build a distributable desktop app

```bash
make build-desktop # PyInstaller bundle
# → dist/SprintPulse.app (macOS) / dist/SprintPulse/ (Linux)
```

On first launch the bundle shows the setup wizard and writes to the same default
database location as the dev app.

## Where your data lives

Everything — your roster, sprints, time off, **and** configuration (team name,
working days, Jira site/board/username, scheduler settings) — lives in a single
**SQLite file**. The Jira API token is *not* in it (keychain on desktop,
`JIRA_API_TOKEN` in the container).

**Resolution order** (first match wins):

1. **`SPRINT_PULSE_DB`** — an exact path you set. Always wins.
```bash
SPRINT_PULSE_DB=/path/to/team.db make dev
```
2. **`XDG_DATA_HOME`** — if set, uses `$XDG_DATA_HOME/sprint-pulse/sprint-pulse.db`
(honored on every OS, including macOS).
3. **Otherwise, search then create** — look for an existing DB in the XDG default
first, then the OS-native location, and use whichever exists. If neither
exists, create one at the XDG-default path:

| OS | XDG-default (checked first) | OS-native (checked next) |
|----|------------------------------|--------------------------|
| macOS | `~/.local/share/sprint-pulse/sprint-pulse.db` | `~/Library/Application Support/sprint-pulse/sprint-pulse.db` |
| Linux | `~/.local/share/sprint-pulse/sprint-pulse.db` | (same as XDG-default) |
| Windows | `~/.local/share/sprint-pulse/sprint-pulse.db` | `%APPDATA%\sprint-pulse\sprint-pulse.db` |
| Container | `/data/sprint-pulse.db` (set via `SPRINT_PULSE_DB`) | — |

Symlinks are followed, so you can keep the real file anywhere (e.g. in a dotfiles
repo) and symlink it into the location above.

**See the resolved path any time:**

```bash
make db-path
# → /Users/you/.local/share/sprint-pulse/sprint-pulse.db
```

## Importing existing YAML

Have a `config.yaml` + `sprints/*.yaml` directory (the bundled `examples/`, or your
own)? Import it once:

```bash
python migrate_yaml_to_sqlite.py --data examples # import the samples
python migrate_yaml_to_sqlite.py --data /path/to/mine # your own dir
python migrate_yaml_to_sqlite.py --data examples --db /tmp/x.db # to a specific DB
python migrate_yaml_to_sqlite.py --data examples --force # overwrite a populated DB
# …or click "Import from YAML" in the first-run wizard (set SPRINT_PULSE_SEED_DIR)
```

After import, SQLite is the source of truth — the YAML is no longer edited. Your own
team data is yours to keep outside the repo; only the fictional `examples/` ship here.

## Environment variables

| Variable | Used by | Purpose |
|----------|---------|---------|
| `SPRINT_PULSE_DB` | all | Exact path to the SQLite file (highest priority). |
| `XDG_DATA_HOME` | all | If set, data lives under `$XDG_DATA_HOME/sprint-pulse/`. |
| `SPRINT_PULSE_HOST` | web/container | Bind address (default `127.0.0.1`; container sets `0.0.0.0`). |
| `SPRINT_PULSE_PORT` | web/container | Port (default `8765`). |
| `SPRINT_PULSE_HEADLESS` | container | `1` → use the env token backend, not the keychain. |
| `JIRA_USERNAME` | container/headless | Jira account email (desktop sets this in the UI). |
| `JIRA_API_TOKEN` | container/headless | Jira API token (desktop stores it in the keychain instead). |
| `JIRA_API_TOKEN_FILE` | container/headless | Path to a file containing the token (alternative to `JIRA_API_TOKEN`). |
| `SPRINT_PULSE_DEMO` | all | `1` → use mocked Jira data (no live instance/creds). See *Demo mode*. |
| `SPRINT_PULSE_SEED_DIR` | wizard | YAML dir for "Import from YAML" (default `data/`; demo uses `examples/`). |

## Layout

```
sprint-pulse/
├── sprint_pulse/
│ ├── config.py sprints.py jira.py render.py # domain core (reused)
│ ├── db/ # SQLModel models + engine (DB path resolution)
│ ├── services/ # validated reads/writes, secrets, refresh pipeline
│ ├── web/ # FastAPI app, routers, Jinja2 + HTMX templates, scheduler
│ └── desktop.py # pywebview shell
├── migrate_yaml_to_sqlite.py # one-time YAML → SQLite import
├── Containerfile # browser deployment
├── packaging/sprint_pulse.spec # desktop bundle (PyInstaller)
├── examples/ # fictional sample data for demos (make demo)
├── tests/ # pytest suite
└── .claude/skills/ # maintain-time-off-report, refresh-sprint-metrics
```

## Development

```bash
make test # pytest
make lint # ruff
make check # lint + tests (the CI gate)
make hooks # install the pre-push hook (runs `make check` before each push)
make help # list every target
```

> Run `make hooks` once after cloning. It points `core.hooksPath` at `.githooks/`,
> so every `git push` runs `make check` first and a push that would fail CI is
> blocked locally (bypass in an emergency with `git push --no-verify`).

## Working with Claude Code

Two project-scoped skills auto-load in `.claude/skills/`:

- **maintain-time-off-report** — data model, validation, and how to edit via the app
- **refresh-sprint-metrics** — refreshing Jira metrics via the scheduler

Describe what you want in plain language ("add PTO for Alice on May 7", "refresh the
metrics") and the matching skill kicks in.