sprint-dash: a sprint dashboard for Gitea

2026-02-26 · #gitea #fastapi #htmx #open-source

The problem

Gitea is great for issues. It's not great for sprints.

I started tracking sprints with sprint/N labels on issues. That works fine right up until you want to see a sprint as a unit. How many issues are open? What's blocked? What carried over from last sprint? There's no sprint-centric view. You're scrolling issue lists, filtering by label, mentally assembling the picture.

Milestones helped a bit. They give you a progress bar and a due date. But milestones don't carry over unfinished work. They don't show you a board. They don't snapshot what you planned versus what you delivered. And they don't know about dependencies.

For a while I worked around it. Filtered issue lists. Terminal queries with teax. Markdown note, briefly. You know how it goes, right?

Then I got tired of workarounds.

What I built

sprint-dash is a read-only dashboard that sits alongside Gitea. It pulls issue data from Gitea's API and manages sprint structure in its own SQLite database. The dashboard never writes to Gitea.

That separation is the key design decision. Gitea owns issues. sprint-dash owns sprints. Each system does what it's good at. It's the same principle I've seen in vendor management: when one entity holds all the authority, conflicts of interest are inevitable. Separation of concerns works in code and in organisations.

Sprint structure in SQLite

Sprint membership, lifecycle state, dates, and goals live in three SQLite tables: sprints, sprint_issues, and sprint_snapshots. Issue metadata—titles, labels, assignees, state—comes from Gitea's API with a 60-second cache.

This means sprint operations are instant. Creating a sprint, adding issues, closing with carry-over—none of it touches the API. It's just SQLite writes. The only network calls are for issue details, and those are cached.

# Sprint operations are local SQLite writes
sd-cli sprint create 48 --start 2026-03-09 --end 2026-03-23 --goal "Feature X"
sd-cli sprint start 48
sd-cli sprint close 47 --carry-over-to 48

Board view

The main view is a Kanban board with columns for each issue state. Drag-and-drop via Sortable.js and HTMX. Filter by label, group by epic. It's what I have on a second monitor during planning sessions.

Each card shows dependency indicators—which issues block which, how many open blockers remain. When you're deciding what to work on next, that context matters.

Sprint lifecycle

Sprints have a lifecycle: planned → in progress → completed. When you start a sprint, it takes a snapshot of the planned issues. When you close it, it snapshots again. The delta between those two snapshots is your planning-versus-execution data.

Carry-over is automatic. Close sprint 47, tell it to carry over to 48, and every open issue moves. No manual relabelling. No forgetting that one ticket.

Backlog

The backlog view shows two lists: issues with a ready label that aren't assigned to a sprint (the ready queue), and everything else that's unscheduled. When you're planning the next sprint, this is where you shop.

The stack

FastAPI + HTMX + Jinja2. No JavaScript framework. No build step.

The entire frontend is server-rendered HTML. Routes check the HX-Request header and return either a full page (direct navigation) or an HTML partial (HTMX swap). One set of templates serves both cases.

Styling is inline CSS with a dark theme. It's a planning tool, not a portfolio piece. It needs to be readable on a second monitor at arm's length. Dark background, high contrast text, colour-coded labels.

Component Choice Why
Backend FastAPI Async, fast, typed
Frontend HTMX + Jinja2 No build step, server-rendered
Sprint data SQLite Local, instant, zero config
Issue data Gitea API Single source of truth for issues
Interactivity Sortable.js Drag-and-drop board columns

Why HTMX

I considered React. For about thirty seconds.

This is a dashboard for one person on a local network. It doesn't need client-side routing, state management, or a build pipeline. HTMX gives me live search, partial page updates, and drag-and-drop with zero JavaScript framework overhead. The server renders HTML. The browser displays it. That's the whole architecture.

Live search is a good example. Type in the search box, HTMX fires a request after 300ms of inactivity, server returns filtered HTML, browser swaps it in. No client-side filtering logic. No state to manage. The server already has the data.

<input type="search"
       hx-get="/search"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#results">

CI pipeline health

If you run Woodpecker CI, sprint-dash shows pipeline health on the home page and board view. Four stages tracked: CI (lint + tests), Build (Docker image), Deploy (staging), Verify (smoke tests). Displayed as compact indicators next to the sprint header.

C:✓ B:✓ D:✓ V:✓

If Woodpecker isn't configured, CI indicators are silently omitted. No errors, no empty space. It just works or it doesn't show up.

This is the same pipeline health I show in my tmux status bar via teax. Having it on the dashboard too means I see build status whether I'm in the terminal or the browser.

The CLI

sprint-dash has a companion CLI called sd-cli. It talks to the dashboard over HTTP, so sprint operations work from the terminal without opening a browser.

sd-cli sprint list                          # List sprints
sd-cli sprint show 47                       # Sprint details + issues
sd-cli sprint current                       # What sprint am I in?
sd-cli issue add 48 101 102 103             # Add issues to a sprint
sd-cli sprint close 47 --carry-over-to 48   # Close and carry over

sd-cli works in two modes. HTTP mode talks to a running sprint-dash server. Direct mode reads and writes SQLite, for when you're on the same machine or inside the Docker container. Same commands, different backend.

There's also a standalone package that strips the SQLite dependency. Install it on your workstation and point it at your sprint-dash server. No database drivers needed.

Deployment

sprint-dash runs in Docker. A Woodpecker pipeline handles the full cycle: lint and test in parallel, Docker build, Trivy security scan, push to Gitea's built-in container registry, pull and restart on the target machine. Total time: about 60 seconds.

# Pipeline stages
ci.yml        → Ruff lint + pytest (parallel)     ~13s
build.yml     → Docker build + Trivy scan + push   ~45s
deploy.yml    → Pull + restart + health check       ~3s

All workflows use Woodpecker's local backend—they run directly on the host, not Docker-in-Docker. SQLite data persists via a Docker volume mount.

What it doesn't do

sprint-dash doesn't replace Gitea for issue management. Create issues in Gitea, manage sprints in sprint-dash. The boundary is clear.

It doesn't do velocity charts yet. The snapshot data is there—sprint start and end captures—but I haven't built the visualisation. That's next.

It's built for solo dev or small team use. I haven't tested it with hundreds of issues in a single sprint. It would probably work, but the board view would get crowded.

And it's Gitea-specific. The API client speaks Gitea. Forgejo would likely work (it's API-compatible), but I haven't tested it. GitHub and GitLab would need a different client.

The ecosystem

sprint-dash is part of a small set of tools I've built around Gitea:

  • teax — CLI companion for tea, filling gaps in issue editing, bulk operations, dependencies, and CI visibility
  • sprint-dash — Sprint dashboard with board view, backlog, and lifecycle management
  • sd-cli — Terminal-based sprint operations, ships with sprint-dash

Together with the release process I described previously, this is the toolchain that makes trunk-based solo development work. teax for CLI operations, sprint-dash for visual planning, Woodpecker for CI/CD, and a process that skips the ceremony but keeps the safety.

GitHub: github.com/simoninglis/sprint-dash

Issues and PRs welcome. If you're running Gitea and hitting the same sprint tracking wall, give it a look.