A lightweight read-only dashboard for visualizing sprint and backlog data from Gitea. Runs alongside Gitea on a headless VM, accessible via browser during planning sessions.
- Sprint tracking via
sprint/Nlabels in Gitea lacks visualization - No sprint-centric views (progress, backlog, ready queue)
- Planning sessions require switching between terminal (teax) and manual queries
- Need visual overview while discussing with AI assistant
FastAPI + HTMX dashboard that:
- Pulls data from Gitea API
- Provides sprint/backlog visualization
- Supports filtering and search
- Read-only (no writes to Gitea)
┌─────────────────────────────────────────────┐
│ headless VM │
│ ┌─────────┐ ┌─────────────┐ │
│ │ Gitea │◄───│ sprint-dash │◄──► Browser │
│ └─────────┘ └─────────────┘ (WSL) │
│ :3000 :8080 │
└─────────────────────────────────────────────┘
| Component | Choice | Rationale |
|---|---|---|
| Backend | FastAPI | Already familiar, async-ready |
| Frontend | HTMX + Jinja2 | No build step, server-rendered |
| Styling | Inline CSS | Minimal, dark theme |
| Data | Gitea API | No local storage (MVP) |
-
Home (
/)- Current sprint summary (open/closed/progress)
- Ready queue count
- Recent sprints list
-
Sprint Detail (
/sprints/{n})- Progress stats
- Issue list with state/type badges
-
Sprints List (
/sprints)- All sprints with progress bars
-
Backlog (
/backlog)- Ready queue (has
readylabel, no sprint) - Unscheduled (no sprint label)
- Ready queue (has
-
Search (
/search)- Live search with HTMX
- Filter by label (bug, feature, tech-debt)
- Filter by state (open, closed, all)
@dataclass(frozen=True)
class Issue:
number: int
title: str
state: str # open, closed
labels: tuple[str, ...]
created_at: str
updated_at: str
closed_at: str | None
body: str # For parsing size/effort from issue body
@dataclass(frozen=True)
class Sprint:
number: int
issues: tuple[Issue, ...]
lifecycle_state: str # "in_progress", "planned", "completed", "unknown"
# Derived: open_count, closed_count, progress_pct, lifecycle_indicator
@dataclass(frozen=True)
class Milestone:
id: int
title: str # "Sprint 45"
state: str # "open" or "closed"
open_issues: int
closed_issues: int
# Derived: sprint_number, lifecycle_state
@dataclass(frozen=True)
class CIHealth:
sha: str # short SHA of main branch HEAD
state: str # "success", "failure", "running", "pending", "unknown"
workflows: tuple[tuple[str, str], ...] # ((workflow_file, status), ...)
# Factory: CIHealth.from_workflows(sha, {workflow: status})
# Derived: workflow_abbrevs → [(abbrev, status, icon), ...]
@dataclass
class BoardIssue:
issue: Issue
blocked_by_count: int
blocks_count: int
blockers: list[tuple[int, str, int | None]] # (issue_num, state, sprint_num)
# Derived: is_blocked, open_blocker_count, blocker_context<input type="search"
hx-get="/issues"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"><button hx-get="/issues?label=bug" hx-target="#results">Bugs</button>Server checks HX-Request header and returns partial HTML instead of full page.
Configuration is loaded in order of precedence:
- Constructor arguments
- Environment variables (GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO)
- Tea CLI config (~/.config/tea/config.yml)
# .env (or use tea CLI config for URL/token)
GITEA_URL=https://gitea.example.com
GITEA_TOKEN=xxx
GITEA_OWNER=your_org
GITEA_REPO=your_repoThe dashboard shows CI pipeline status for the current sprint via the Gitea Actions Runs API.
Why Actions Runs API instead of Commit Status API:
- Commit status API (
/commits/{sha}/status) only reports statuses set byci.ymljobs - Downstream chained workflows (build, staging-deploy, staging-verify) don't set commit statuses on the original commit
- The Actions Runs API (
/actions/runs) shows all workflow runs and gives the full pipeline view
API pattern:
- Get main branch SHA:
GET /repos/{owner}/{repo}/branches/main→response.commit.id - Get recent runs:
GET /repos/{owner}/{repo}/actions/runs?limit=20 - Filter runs by SHA, group by workflow file (from
pathfield:"ci.yml@refs/heads/main"→"ci.yml") - Take latest run per workflow, map
status/conclusionto display state
Pipeline workflows tracked (PIPELINE_WORKFLOWS constant):
ci.yml→ C (Lint, Unit Tests, Integration Tests)build.yml→ B (Build and push Docker images)staging-deploy.yml→ D (Deploy to staging)staging-verify.yml→ V (Smoke, E2E, visual tests)
Display: Home page sprint card and board current sprint column show ✓/✗/⏳ with per-workflow breakdown.
| Feature | Storage Needed |
|---|---|
| Burndown charts | SQLite (daily snapshots) |
| Velocity trends | SQLite (sprint history) |
| Sprint dates/goals | SQLite (sprint metadata) |
| Cycle time | Event log parsing |
cd sprint-dash
pip install -e .
cp .env.example .env
# Edit .env with Gitea token
uvicorn app.main:app --host 0.0.0.0 --port 8080 --reloadBeyond MVP with:
app/gitea.py(~1100 lines) - Gitea API client with issues, sprints, milestones, dependencies, CI healthapp/main.py(~420 lines) - FastAPI routes with board, backlog, search, issue detail viewstemplates/- Jinja2 + HTMX templates with dark theme, board view, filters- CI pipeline health integration via Actions Runs API
- Dependency tracking with blocked/blocker indicators on board cards
- Milestone-based sprint lifecycle (via ADR-0017)
- Test with real Gitea connection
- Add
__init__.pyto app/ - Add loading indicators (htmx-indicator)
- Caching for Gitea API calls (60s TTL via cachetools)
- Error handling for API failures (GiteaError + error.html partial)
- Tea CLI config integration (falls back to ~/.config/tea/config.yml)
- Pagination improvements with max_pages limit and truncation warnings
- CI pipeline health on home and board views
- Issue dependency tracking (blocked-by / blocks)
- Milestone-based sprint lifecycle state
- Board view with Kanban columns, filters, epic grouping
- Deploy behind Caddy
- Read-only - Never modify Gitea data
- Server-rendered - No JS framework, HTMX for interactivity
- Minimal dependencies - FastAPI, httpx, Jinja2
- Dark theme - Matches terminal aesthetic
- Parse at boundaries - Gitea JSON → typed dataclasses immediately