{"id":51465127,"url":"https://github.com/omaciel/sprint-pulse","last_synced_at":"2026-07-06T10:01:22.855Z","repository":{"id":364630769,"uuid":"1257227689","full_name":"omaciel/sprint-pulse","owner":"omaciel","description":"Team availability dashboard — FastAPI + SQLite desktop \u0026 web app (PTO, sprints, Jira metrics)","archived":false,"fork":false,"pushed_at":"2026-06-30T14:22:22.000Z","size":2392,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-30T16:19:36.446Z","etag":null,"topics":["dashboard","fastapi","htmx","pto-tracker","python","sprint-planning","sqlite"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/omaciel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-02T13:36:05.000Z","updated_at":"2026-06-30T14:24:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/omaciel/sprint-pulse","commit_stats":null,"previous_names":["omaciel/sprint-pulse"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/omaciel/sprint-pulse","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaciel%2Fsprint-pulse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaciel%2Fsprint-pulse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaciel%2Fsprint-pulse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaciel%2Fsprint-pulse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/omaciel","download_url":"https://codeload.github.com/omaciel/sprint-pulse/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omaciel%2Fsprint-pulse/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35185688,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dashboard","fastapi","htmx","pto-tracker","python","sprint-planning","sqlite"],"created_at":"2026-07-06T10:01:21.693Z","updated_at":"2026-07-06T10:01:22.844Z","avatar_url":"https://github.com/omaciel.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sprint Pulse ⚡\n\n[![tests](https://github.com/omaciel/sprint-pulse/actions/workflows/test.yml/badge.svg)](https://github.com/omaciel/sprint-pulse/actions/workflows/test.yml)\n[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\nA team availability dashboard you can **install**. Sprint Pulse combines team\ntime-off tracking, live Jira sprint metrics (tickets done/total, story points\ndone/total), and release-calendar events into a single availability heatmap.\n\nIt's a **FastAPI + SQLite** app that runs three ways from one codebase:\n\n- **Desktop app** — a native window (pywebview) on macOS and Linux. 100% Python.\n- **Local web app** — the same app in your browser (great for development).\n- **Containerized web app** — browse to it from anywhere; data on a volume.\n\nFirst run walks you through a setup wizard (settings → import sprints → team), or\nyou can import existing `data/*.yaml` in one click. Metrics refresh on a schedule\nyou control.\n\n## What it shows\n\n- Sprint navigator with state badges (active / future / closed)\n- One sprint heatmap at a time — team members × working days\n- Cell vocabulary: a **configurable set of absence types** (PTO, regional/company\n  holidays, partial, tentative by default), plus **Excluded** members who are shown\n  gray and don't count toward capacity\n- Release-event row driven by **configurable event types** (git tags, Go/No-Go,\n  GA, freeze, testathon by default)\n- A **Types** page to manage both vocabularies — add / rename / recolor (from a\n  fixed palette) / delete event \u0026 absence types\n- Per-sprint availability % and a per-associate summary\n\nTime off is managed **per person**: open **Team → a name** for that member's\navailability **calendar** — pick an absence type (the defaults, or any you've added\non the **Types** page) and click weekdays to mark or clear them. Sprints derive their own\n\"who's out\" list automatically from those dates, so editing a sprint is just its\ndates and release events.\n\n## Screenshots\n\n\u003e Captured from `make demo-desktop` — the native desktop window with fictional\n\u003e sample data (no Jira or credentials required).\n\n**Dashboard** — one sprint's availability heatmap: team members × working days,\ncolored absence cells (from the configurable absence types), the release-event\nrow, the per-sprint availability %, and a sidebar with the sprint navigator and\nlegend.\n\n![Sprint Pulse dashboard — a sprint availability heatmap with legend and sprint navigator](docs/images/dashboard.png)\n\n**First-run setup** — a guided wizard (app settings → Jira connection → team\nroster), or import an existing `data/*.yaml` roster, sprints, and time off in one\nclick.\n\n![Sprint Pulse first-run welcome screen with guided setup and YAML import](docs/images/welcome.png)\n\n**Team** — manage the roster, toggle **Excluded** (shown gray and not counted\ntoward capacity), and click any name to open that member's availability\ncalendar.\n\n![Sprint Pulse Team page showing the roster with excluded-member toggles](docs/images/team.png)\n\n**Sprints** — import sprints from Jira or add them manually; each row shows its\ndates, Jira state, and cached ticket / story-point metrics. Archive a sprint to\ndrop it off the dashboard without deleting it.\n\n![Sprint Pulse Sprints page listing sprints with dates, state, and metrics](docs/images/sprints.png)\n\n**Schedule** — refresh Jira metrics automatically on an interval or cron\nexpression, or hit **Refresh metrics now** on demand; the last-run status shows\nwhat was updated.\n\n![Sprint Pulse Schedule page with refresh cadence and run-now controls](docs/images/schedule.png)\n\n**Types** — manage the event \u0026 absence type vocabularies: each type's label, its\nabbreviation (the letter shown in a cell), and its color (picked from a fixed\npalette). Add, rename, recolor, or delete types — a type still in use can't be\ndeleted.\n\n![Sprint Pulse Types page managing event and absence types with color swatches](docs/images/types.png)\n\n## Requirements\n\n- **Python 3.13+**\n- Dependencies declared in `pyproject.toml` and locked in `uv.lock` (FastAPI,\n  SQLModel, Jinja2, APScheduler, pywebview, …), managed with\n  [uv](https://docs.astral.sh/uv/). The desktop shell + packaging deps live in\n  the optional `desktop` extra; Linux desktop also needs the **WebKitGTK** system\n  package (`gir1.2-webkit2-4.1` / `webkit2gtk`). The container path needs neither.\n- A Jira API token with read access to your Scrum board (for live metrics).\n\n## Setup\n\n**No manual install step needed.** Any `make` target that runs the app or\ntests creates the `.venv` and syncs dependencies on demand (via `uv`), so you\ncan jump straight to `make demo`. The sync re-runs only when `pyproject.toml`\nor `uv.lock` change. You just need [uv](https://docs.astral.sh/uv/) on your\n`PATH` — the Makefile points you to its installer if it's missing.\n\nTo populate the environment up front (e.g. before going offline):\n\n```bash\nmake install        # runtime + desktop deps\nmake install-dev    # runtime + desktop + test/lint tooling\n# …or directly:  uv sync --extra desktop\n```\n\n\u003e `make` targets use `.venv/bin/python` automatically — no activation needed.\n\u003e Override with `make \u003ctarget\u003e PYTHON=/path/to/python` if you keep the venv\n\u003e elsewhere.\n\n## Ways to run\n\n### 1. Desktop app (native window)\n\n```bash\nmake dev-desktop\n```\n\nOpens a native window (WebKit). Uses your local database (see *Where your data\nlives*). The Jira token is stored in your OS keychain.\n\n### 2. Local web app (browser)\n\n```bash\nmake dev                      # http://localhost:8765  (auto-reloads on edits)\n```\n\nYour browser opens automatically once the server is accepting connections.\nChange the port with `PORT=9000 make dev`.\n\n### 3. Containerized web app (browser)\n\n```bash\nmake container-build          # build the image (podman)\nmake container-run            # run it; http://localhost:8765\n\n# …or run it directly with podman or docker:\npodman run -p 8765:8765 -v sprint-pulse-data:/data \\\n  -e JIRA_USERNAME=you@example.com -e JIRA_API_TOKEN=xxxx sprint-pulse\ndocker  run -p 8765:8765 -v sprint-pulse-data:/data \\\n  -e JIRA_USERNAME=you@example.com -e JIRA_API_TOKEN=xxxx sprint-pulse\n```\n\nThe DB lives on the `/data` volume and survives restarts. In the container the\nJira token comes from `JIRA_API_TOKEN` (the DB only ever stores a reference, never\nthe token). Override the published port with `PORT=9000 make container-run`.\n\n### 4. Run the server directly (advanced)\n\nThe web app is a standard ASGI module — handy for a custom host/port/DB:\n\n```bash\nSPRINT_PULSE_HOST=0.0.0.0 SPRINT_PULSE_PORT=9000 \\\nSPRINT_PULSE_DB=/path/to/my.db \\\npython -m sprint_pulse.web\n```\n\n### 5. Demo mode — no live Jira needed\n\nTry the whole app offline with fictional example data and a **mocked Jira**:\n\n```bash\nmake demo            # browser at http://localhost:8765  (fresh throwaway DB)\nmake demo-desktop    # same, in the native window\n```\n\nOn a fresh checkout this is all you need: `make demo` installs dependencies if\nneeded, starts a fresh `.demo.db`, points the wizard's YAML import at\n`examples/`, sets `SPRINT_PULSE_DEMO=1` so the Jira features use canned data\ninstead of a real instance, and opens your browser once the server is ready.\nFrom the wizard you can:\n\n- **Import manually** — \"Import from YAML\" loads the example team + sprints.\n- **Import automatically** — \"Import from Jira\" lists mock board sprints to pick\n  from, and \"Test connection\" / \"Refresh metrics now\" return mock data.\n\nNo credentials or VPN required. Delete `.demo.db` to start over (`make demo`\ndoes this for you each run).\n\n### 6. Build a distributable desktop app\n\n```bash\nmake build-desktop            # PyInstaller bundle\n# → dist/SprintPulse.app (macOS) / dist/SprintPulse/ (Linux)\n```\n\nOn first launch the bundle shows the setup wizard and writes to the same default\ndatabase location as the dev app.\n\n## Where your data lives\n\nEverything — your roster, sprints, time off, **and** configuration (team name,\nworking days, Jira site/board/username, scheduler settings) — lives in a single\n**SQLite file**. The Jira API token is *not* in it (keychain on desktop,\n`JIRA_API_TOKEN` in the container).\n\n**Resolution order** (first match wins):\n\n1. **`SPRINT_PULSE_DB`** — an exact path you set. Always wins.\n   ```bash\n   SPRINT_PULSE_DB=/path/to/team.db make dev\n   ```\n2. **`XDG_DATA_HOME`** — if set, uses `$XDG_DATA_HOME/sprint-pulse/sprint-pulse.db`\n   (honored on every OS, including macOS).\n3. **Otherwise, search then create** — look for an existing DB in the XDG default\n   first, then the OS-native location, and use whichever exists. If neither\n   exists, create one at the XDG-default path:\n\n   | OS | XDG-default (checked first) | OS-native (checked next) |\n   |----|------------------------------|--------------------------|\n   | macOS | `~/.local/share/sprint-pulse/sprint-pulse.db` | `~/Library/Application Support/sprint-pulse/sprint-pulse.db` |\n   | Linux | `~/.local/share/sprint-pulse/sprint-pulse.db` | (same as XDG-default) |\n   | Windows | `~/.local/share/sprint-pulse/sprint-pulse.db` | `%APPDATA%\\sprint-pulse\\sprint-pulse.db` |\n   | Container | `/data/sprint-pulse.db` (set via `SPRINT_PULSE_DB`) | — |\n\nSymlinks are followed, so you can keep the real file anywhere (e.g. in a dotfiles\nrepo) and symlink it into the location above.\n\n**See the resolved path any time:**\n\n```bash\nmake db-path\n# → /Users/you/.local/share/sprint-pulse/sprint-pulse.db\n```\n\n## Importing existing YAML\n\nHave a `config.yaml` + `sprints/*.yaml` directory (the bundled `examples/`, or your\nown)? Import it once:\n\n```bash\npython migrate_yaml_to_sqlite.py --data examples            # import the samples\npython migrate_yaml_to_sqlite.py --data /path/to/mine       # your own dir\npython migrate_yaml_to_sqlite.py --data examples --db /tmp/x.db   # to a specific DB\npython migrate_yaml_to_sqlite.py --data examples --force    # overwrite a populated DB\n# …or click \"Import from YAML\" in the first-run wizard (set SPRINT_PULSE_SEED_DIR)\n```\n\nAfter import, SQLite is the source of truth — the YAML is no longer edited. Your own\nteam data is yours to keep outside the repo; only the fictional `examples/` ship here.\n\n## Environment variables\n\n| Variable | Used by | Purpose |\n|----------|---------|---------|\n| `SPRINT_PULSE_DB` | all | Exact path to the SQLite file (highest priority). |\n| `XDG_DATA_HOME` | all | If set, data lives under `$XDG_DATA_HOME/sprint-pulse/`. |\n| `SPRINT_PULSE_HOST` | web/container | Bind address (default `127.0.0.1`; container sets `0.0.0.0`). |\n| `SPRINT_PULSE_PORT` | web/container | Port (default `8765`). |\n| `SPRINT_PULSE_HEADLESS` | container | `1` → use the env token backend, not the keychain. |\n| `JIRA_USERNAME` | container/headless | Jira account email (desktop sets this in the UI). |\n| `JIRA_API_TOKEN` | container/headless | Jira API token (desktop stores it in the keychain instead). |\n| `JIRA_API_TOKEN_FILE` | container/headless | Path to a file containing the token (alternative to `JIRA_API_TOKEN`). |\n| `SPRINT_PULSE_DEMO` | all | `1` → use mocked Jira data (no live instance/creds). See *Demo mode*. |\n| `SPRINT_PULSE_SEED_DIR` | wizard | YAML dir for \"Import from YAML\" (default `data/`; demo uses `examples/`). |\n\n## Layout\n\n```\nsprint-pulse/\n├── sprint_pulse/\n│   ├── config.py sprints.py jira.py render.py   # domain core (reused)\n│   ├── db/          # SQLModel models + engine (DB path resolution)\n│   ├── services/    # validated reads/writes, secrets, refresh pipeline\n│   ├── web/         # FastAPI app, routers, Jinja2 + HTMX templates, scheduler\n│   └── desktop.py   # pywebview shell\n├── migrate_yaml_to_sqlite.py   # one-time YAML → SQLite import\n├── Containerfile               # browser deployment\n├── packaging/sprint_pulse.spec # desktop bundle (PyInstaller)\n├── examples/                   # fictional sample data for demos (make demo)\n├── tests/                      # pytest suite\n└── .claude/skills/             # maintain-time-off-report, refresh-sprint-metrics\n```\n\n## Development\n\n```bash\nmake test     # pytest\nmake lint     # ruff\nmake check    # lint + tests (the CI gate)\nmake hooks    # install the pre-push hook (runs `make check` before each push)\nmake help     # list every target\n```\n\n\u003e Run `make hooks` once after cloning. It points `core.hooksPath` at `.githooks/`,\n\u003e so every `git push` runs `make check` first and a push that would fail CI is\n\u003e blocked locally (bypass in an emergency with `git push --no-verify`).\n\n## Working with Claude Code\n\nTwo project-scoped skills auto-load in `.claude/skills/`:\n\n- **maintain-time-off-report** — data model, validation, and how to edit via the app\n- **refresh-sprint-metrics** — refreshing Jira metrics via the scheduler\n\nDescribe what you want in plain language (\"add PTO for Alice on May 7\", \"refresh the\nmetrics\") and the matching skill kicks in.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomaciel%2Fsprint-pulse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fomaciel%2Fsprint-pulse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomaciel%2Fsprint-pulse/lists"}