diff --git a/app/main.py b/app/main.py index 617c509..51a3341 100644 --- a/app/main.py +++ b/app/main.py @@ -58,6 +58,7 @@ from app.models import ( get_metrics, get_notes, get_picks, + get_project_activity, get_stats, init_db, log_event, @@ -600,6 +601,24 @@ async def dashboard(request: Request): repo_info["starred"] = full_name in starred_names for repo_info in inactive: repo_info["starred"] = repo_info["full_name"] in starred_names + activity_by_repo = get_project_activity() + today_utc = datetime.now(timezone.utc).date() + for repo_info in active + inactive + archived: + act = activity_by_repo.get(repo_info["full_name"]) + if act is None: + repo_info["activity"] = None + continue + idle_days = None + if act["last_at"]: + last_dt = _parse_datetime(act["last_at"]) + if last_dt is not None: + idle_days = (today_utc - last_dt.date()).days + repo_info["activity"] = { + "closed": act["closed"], + "moved": act["moved"], + "idle_days": idle_days, + "last_at": act["last_at"] or "", + } for repo_info in active: repo_info["home_tab"] = "active" for repo_info in inactive: @@ -2305,13 +2324,17 @@ async def api_capture(request: Request): @app.get("/capture", response_class=HTMLResponse) -async def capture_page(request: Request): - """Quick-capture page: form, status and usage instructions.""" +async def capture_page(request: Request, repo: str = ""): + """Quick-capture page: form, status and usage instructions. + + `?repo=Owner/name` pre-fills the target repo (issue #23: the + project card's quick actions capture straight into a project). + """ settings = get_settings() return render( request, "capture.html", - capture_repo=settings.capture_repo, + capture_repo=repo.strip() or settings.capture_repo, api_enabled=bool(settings.capture_token), ntfy_topic=settings.capture_ntfy_topic, ntfy_url=settings.ntfy_url, diff --git a/app/models.py b/app/models.py index 25f17a1..642f778 100644 --- a/app/models.py +++ b/app/models.py @@ -368,6 +368,38 @@ def column_entry_times(repo_full_name: str) -> dict[int, str]: return {row["issue_number"]: row["entered_at"] for row in rows} +def get_project_activity(days: int = 7) -> dict: + """Per-repo journal activity in ONE aggregated pass (issue #19). + + For every repo present in the journal: closures and moves of + the last `days` days (journal stamps are UTC strings), plus + the most recent event ever seen for that repo. + """ + from datetime import datetime, timedelta, timezone + + cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).strftime( + "%Y-%m-%d %H:%M:%S" + ) + with get_db() as conn: + rows = conn.execute( + """ + SELECT repo_full_name, + SUM(CASE WHEN event_type = 'closed' AND created_at >= ? + THEN 1 ELSE 0 END), + SUM(CASE WHEN event_type = 'moved' AND created_at >= ? + THEN 1 ELSE 0 END), + MAX(created_at) + FROM board_events + GROUP BY repo_full_name + """, + (cutoff, cutoff), + ).fetchall() + return { + r[0]: {"closed": r[1] or 0, "moved": r[2] or 0, "last_at": r[3]} + for r in rows + } + + def get_metrics(repo_full_name: str | None = None, window_days: int = 30) -> dict: """Throughput and cycle time computed from the journal.