@theihtisham/dev-pulse 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +43 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  5. package/.github/dependabot.yml +16 -0
  6. package/.github/workflows/ci.yml +33 -0
  7. package/CODE_OF_CONDUCT.md +27 -0
  8. package/Dockerfile +8 -0
  9. package/LICENSE +21 -21
  10. package/README.md +135 -39
  11. package/SECURITY.md +22 -0
  12. package/devpulse/__init__.py +4 -4
  13. package/devpulse/api/__init__.py +1 -1
  14. package/devpulse/api/app.py +371 -371
  15. package/devpulse/cli/__init__.py +1 -1
  16. package/devpulse/cli/dashboard.py +131 -131
  17. package/devpulse/cli/main.py +678 -678
  18. package/devpulse/cli/render.py +175 -175
  19. package/devpulse/core/__init__.py +34 -34
  20. package/devpulse/core/analytics.py +487 -487
  21. package/devpulse/core/config.py +77 -77
  22. package/devpulse/core/database.py +612 -612
  23. package/devpulse/core/github_client.py +281 -281
  24. package/devpulse/core/models.py +142 -142
  25. package/devpulse/core/report_generator.py +454 -454
  26. package/devpulse/static/.gitkeep +1 -1
  27. package/devpulse/templates/report.html +64 -64
  28. package/package.json +35 -35
  29. package/pyproject.toml +80 -80
  30. package/requirements.txt +14 -14
  31. package/tests/__init__.py +1 -1
  32. package/tests/conftest.py +208 -208
  33. package/tests/test_analytics.py +284 -284
  34. package/tests/test_api.py +313 -313
  35. package/tests/test_cli.py +204 -204
  36. package/tests/test_config.py +47 -47
  37. package/tests/test_database.py +255 -255
  38. package/tests/test_models.py +107 -107
  39. package/tests/test_report_generator.py +173 -173
  40. package/jest.config.js +0 -7
@@ -1,371 +1,371 @@
1
- """FastAPI application for DevPulse REST API."""
2
-
3
- import os
4
- from datetime import datetime
5
- from typing import Any, Optional
6
-
7
- from fastapi import FastAPI, HTTPException, Query
8
- from fastapi.responses import HTMLResponse, JSONResponse
9
- from fastapi.middleware.cors import CORSMiddleware
10
- from pydantic import BaseModel
11
-
12
- from devpulse.core.database import Database
13
- from devpulse.core.analytics import AnalyticsEngine
14
- from devpulse.core.report_generator import ReportGenerator
15
-
16
- app = FastAPI(
17
- title="DevPulse API",
18
- description="AI-powered developer productivity dashboard with GitHub integration.",
19
- version="1.0.0",
20
- docs_url="/docs",
21
- redoc_url="/redoc",
22
- )
23
-
24
- app.add_middleware(
25
- CORSMiddleware,
26
- allow_origins=["*"],
27
- allow_credentials=True,
28
- allow_methods=["*"],
29
- allow_headers=["*"],
30
- )
31
-
32
-
33
- def _get_db() -> Database:
34
- return Database()
35
-
36
-
37
- def _get_engine() -> AnalyticsEngine:
38
- return AnalyticsEngine(db=_get_db())
39
-
40
-
41
- def _get_report_gen() -> ReportGenerator:
42
- return ReportGenerator(db=_get_db())
43
-
44
-
45
- # ── Request/Response models ──────────────────────────────────────────
46
-
47
- class GoalCreate(BaseModel):
48
- title: str
49
- description: str = ""
50
- target_value: float
51
- metric: str
52
- deadline: Optional[str] = None
53
-
54
-
55
- class SprintSnapshot(BaseModel):
56
- sprint_name: str
57
- total_points: float
58
- completed_points: float
59
- added_points: float = 0.0
60
-
61
-
62
- class QualitySnapshot(BaseModel):
63
- repo: str
64
- test_coverage: float = 0.0
65
- open_bugs: int = 0
66
- tech_debt_score: float = 0.0
67
- lines_added: int = 0
68
- lines_removed: int = 0
69
- files_changed: int = 0
70
-
71
-
72
- class HealthResponse(BaseModel):
73
- overall_score: float
74
- workload_balance: float
75
- burnout_risk: dict[str, float]
76
- collaboration_score: float
77
- velocity_trend: str
78
- recommendations: list[str]
79
-
80
-
81
- class MessageResponse(BaseModel):
82
- message: str
83
- detail: Optional[str] = None
84
-
85
-
86
- # ── Health check ─────────────────────────────────────────────────────
87
-
88
- @app.get("/", tags=["health"])
89
- def root() -> dict[str, str]:
90
- """API health check."""
91
- return {
92
- "name": "DevPulse API",
93
- "version": "1.0.0",
94
- "status": "healthy",
95
- "timestamp": datetime.utcnow().isoformat(),
96
- }
97
-
98
-
99
- # ── Commits ──────────────────────────────────────────────────────────
100
-
101
- @app.get("/api/commits", tags=["commits"])
102
- def list_commits(
103
- repo: Optional[str] = Query(None),
104
- author: Optional[str] = Query(None),
105
- since: Optional[str] = Query(None),
106
- limit: int = Query(100, ge=1, le=1000),
107
- ) -> list[dict[str, Any]]:
108
- """List commits with optional filters."""
109
- db = _get_db()
110
- return db.get_commits(repo=repo, author=author, since=since, limit=limit)
111
-
112
-
113
- @app.get("/api/commits/heatmap", tags=["commits"])
114
- def commit_heatmap(
115
- author: Optional[str] = Query(None),
116
- days: int = Query(365, ge=7, le=730),
117
- ) -> list[dict[str, Any]]:
118
- """Get daily commit counts for heatmap visualization."""
119
- db = _get_db()
120
- return db.get_commit_count_by_day(author=author, days=days)
121
-
122
-
123
- # ── Pull Requests ────────────────────────────────────────────────────
124
-
125
- @app.get("/api/pull-requests", tags=["pull-requests"])
126
- def list_pull_requests(
127
- repo: Optional[str] = Query(None),
128
- author: Optional[str] = Query(None),
129
- state: Optional[str] = Query(None),
130
- since: Optional[str] = Query(None),
131
- limit: int = Query(100, ge=1, le=500),
132
- ) -> list[dict[str, Any]]:
133
- """List pull requests with optional filters."""
134
- db = _get_db()
135
- return db.get_pull_requests(repo=repo, author=author, state=state, since=since, limit=limit)
136
-
137
-
138
- # ── Issues ───────────────────────────────────────────────────────────
139
-
140
- @app.get("/api/issues", tags=["issues"])
141
- def list_issues(
142
- repo: Optional[str] = Query(None),
143
- state: Optional[str] = Query(None),
144
- since: Optional[str] = Query(None),
145
- limit: int = Query(100, ge=1, le=500),
146
- ) -> list[dict[str, Any]]:
147
- """List issues with optional filters."""
148
- db = _get_db()
149
- return db.get_issues(repo=repo, state=state, since=since, limit=limit)
150
-
151
-
152
- # ── Reviews ──────────────────────────────────────────────────────────
153
-
154
- @app.get("/api/reviews", tags=["reviews"])
155
- def list_reviews(
156
- repo: Optional[str] = Query(None),
157
- author: Optional[str] = Query(None),
158
- since: Optional[str] = Query(None),
159
- limit: int = Query(100, ge=1, le=500),
160
- ) -> list[dict[str, Any]]:
161
- """List code reviews with optional filters."""
162
- db = _get_db()
163
- return db.get_reviews(repo=repo, author=author, since=since, limit=limit)
164
-
165
-
166
- # ── Metrics ──────────────────────────────────────────────────────────
167
-
168
- @app.get("/api/metrics/{author}", tags=["metrics"])
169
- def get_developer_metrics(
170
- author: str,
171
- days: int = Query(30, ge=1, le=365),
172
- repo: Optional[str] = Query(None),
173
- ) -> dict[str, Any]:
174
- """Get comprehensive metrics for a developer."""
175
- engine = _get_engine()
176
- metrics = engine.developer_metrics(author=author, days=days, repo=repo)
177
- return metrics.model_dump()
178
-
179
-
180
- @app.get("/api/metrics", tags=["metrics"])
181
- def get_team_metrics(
182
- days: int = Query(30, ge=1, le=365),
183
- repo: Optional[str] = Query(None),
184
- ) -> list[dict[str, Any]]:
185
- """Get metrics for all team members."""
186
- engine = _get_engine()
187
- team = engine.team_metrics(days=days, repo=repo)
188
- return [m.model_dump() for m in team]
189
-
190
-
191
- # ── Team Health ──────────────────────────────────────────────────────
192
-
193
- @app.get("/api/health", tags=["health"], response_model=HealthResponse)
194
- def team_health(
195
- days: int = Query(30, ge=1, le=365),
196
- repo: Optional[str] = Query(None),
197
- ) -> HealthResponse:
198
- """Analyze team health and burnout risk."""
199
- engine = _get_engine()
200
- health = engine.team_health(days=days, repo=repo)
201
- return HealthResponse(
202
- overall_score=health.overall_score,
203
- workload_balance=health.workload_balance,
204
- burnout_risk=health.burnout_risk,
205
- collaboration_score=health.collaboration_score,
206
- velocity_trend=health.velocity_trend,
207
- recommendations=health.recommendations,
208
- )
209
-
210
-
211
- # ── AI Insights ──────────────────────────────────────────────────────
212
-
213
- @app.get("/api/insights", tags=["insights"])
214
- def get_insights(
215
- days: int = Query(30, ge=1, le=365),
216
- repo: Optional[str] = Query(None),
217
- ) -> list[dict[str, str]]:
218
- """Get AI-powered insights and recommendations."""
219
- engine = _get_engine()
220
- return engine.generate_insights(days=days, repo=repo)
221
-
222
-
223
- # ── Goals ────────────────────────────────────────────────────────────
224
-
225
- @app.get("/api/goals", tags=["goals"])
226
- def list_goals(status: Optional[str] = Query(None)) -> list[dict[str, Any]]:
227
- """List goals."""
228
- db = _get_db()
229
- return db.get_goals(status=status)
230
-
231
-
232
- @app.post("/api/goals", tags=["goals"], status_code=201)
233
- def create_goal(goal: GoalCreate) -> dict[str, Any]:
234
- """Create a new goal."""
235
- db = _get_db()
236
- gid = db.upsert_goal(goal.model_dump())
237
- return {"id": gid, **goal.model_dump()}
238
-
239
-
240
- @app.delete("/api/goals/{goal_id}", tags=["goals"])
241
- def delete_goal(goal_id: int) -> MessageResponse:
242
- """Delete a goal."""
243
- db = _get_db()
244
- if db.delete_goal(goal_id):
245
- return MessageResponse(message=f"Goal {goal_id} deleted.")
246
- raise HTTPException(status_code=404, detail=f"Goal {goal_id} not found.")
247
-
248
-
249
- @app.get("/api/goals/coach", tags=["goals"])
250
- def goal_coaching(goal_id: Optional[int] = Query(None)) -> list[dict[str, Any]]:
251
- """Get AI coaching for goals."""
252
- engine = _get_engine()
253
- return engine.goal_coaching(goal_id=goal_id)
254
-
255
-
256
- # ── Sprints ──────────────────────────────────────────────────────────
257
-
258
- @app.post("/api/sprints/snapshot", tags=["sprints"], status_code=201)
259
- def create_sprint_snapshot(snapshot: SprintSnapshot) -> MessageResponse:
260
- """Record a sprint snapshot."""
261
- db = _get_db()
262
- db.save_sprint_snapshot({
263
- "sprint_name": snapshot.sprint_name,
264
- "total_points": snapshot.total_points,
265
- "completed_points": snapshot.completed_points,
266
- "remaining_points": snapshot.total_points - snapshot.completed_points,
267
- "added_points": snapshot.added_points,
268
- })
269
- return MessageResponse(message=f"Sprint snapshot recorded for '{snapshot.sprint_name}'.")
270
-
271
-
272
- @app.get("/api/sprints/{sprint_name}/burndown", tags=["sprints"])
273
- def sprint_burndown(sprint_name: str) -> list[dict[str, Any]]:
274
- """Get burndown data for a sprint."""
275
- engine = _get_engine()
276
- return engine.sprint_burndown(sprint_name)
277
-
278
-
279
- @app.get("/api/sprints/{sprint_name}/predict", tags=["sprints"])
280
- def predict_sprint(
281
- sprint_name: str,
282
- total: float = Query(...),
283
- elapsed: int = Query(...),
284
- duration: int = Query(...),
285
- ) -> dict[str, Any]:
286
- """Predict sprint completion."""
287
- engine = _get_engine()
288
- return engine.predict_sprint_completion(sprint_name, total, elapsed, duration)
289
-
290
-
291
- # ── Code Quality ─────────────────────────────────────────────────────
292
-
293
- @app.post("/api/quality/snapshot", tags=["quality"], status_code=201)
294
- def create_quality_snapshot(snapshot: QualitySnapshot) -> MessageResponse:
295
- """Record a code quality snapshot."""
296
- db = _get_db()
297
- db.save_quality_snapshot(snapshot.model_dump())
298
- return MessageResponse(message=f"Quality snapshot recorded for '{snapshot.repo}'.")
299
-
300
-
301
- @app.get("/api/quality", tags=["quality"])
302
- def code_quality_trends(
303
- repo: Optional[str] = Query(None),
304
- days: int = Query(90, ge=1, le=365),
305
- ) -> list[dict[str, Any]]:
306
- """Get code quality trends."""
307
- engine = _get_engine()
308
- trends = engine.code_quality_trend(repo=repo, days=days)
309
- return [t.model_dump() for t in trends]
310
-
311
-
312
- @app.get("/api/quality/score", tags=["quality"])
313
- def quality_score(
314
- repo: str = Query(..., description="Repository full name (e.g. owner/repo)"),
315
- days: int = Query(30, ge=1, le=365),
316
- ) -> dict[str, Any]:
317
- """Compute aggregate quality score for a repo."""
318
- engine = _get_engine()
319
- return engine.compute_quality_score(repo=repo, days=days)
320
-
321
-
322
- # ── Reports ──────────────────────────────────────────────────────────
323
-
324
- @app.get("/api/reports/daily", tags=["reports"])
325
- def daily_report(
326
- date: Optional[str] = Query(None),
327
- author: Optional[str] = Query(None),
328
- repo: Optional[str] = Query(None),
329
- format: str = Query("json", regex="^(json|markdown|html)$"),
330
- ) -> Any:
331
- """Generate a daily report."""
332
- gen = _get_report_gen()
333
- data = gen.daily_report(target_date=date, author=author, repo=repo)
334
- if format == "html":
335
- return HTMLResponse(content=gen.to_html(data, "daily"))
336
- if format == "markdown":
337
- return JSONResponse(content={"markdown": gen.to_markdown(data, "daily")})
338
- return data
339
-
340
-
341
- @app.get("/api/reports/weekly", tags=["reports"])
342
- def weekly_report(
343
- week_start: Optional[str] = Query(None),
344
- repo: Optional[str] = Query(None),
345
- format: str = Query("json", regex="^(json|markdown|html)$"),
346
- ) -> Any:
347
- """Generate a weekly report."""
348
- gen = _get_report_gen()
349
- data = gen.weekly_report(week_start=week_start, repo=repo)
350
- if format == "html":
351
- return HTMLResponse(content=gen.to_html(data, "weekly"))
352
- if format == "markdown":
353
- return JSONResponse(content={"markdown": gen.to_markdown(data, "weekly")})
354
- return data
355
-
356
-
357
- @app.get("/api/reports/monthly", tags=["reports"])
358
- def monthly_report(
359
- year: Optional[int] = Query(None),
360
- month: Optional[int] = Query(None),
361
- repo: Optional[str] = Query(None),
362
- format: str = Query("json", regex="^(json|markdown|html)$"),
363
- ) -> Any:
364
- """Generate a monthly report."""
365
- gen = _get_report_gen()
366
- data = gen.monthly_report(year=year, month=month, repo=repo)
367
- if format == "html":
368
- return HTMLResponse(content=gen.to_html(data, "monthly"))
369
- if format == "markdown":
370
- return JSONResponse(content={"markdown": gen.to_markdown(data, "monthly")})
371
- return data
1
+ """FastAPI application for DevPulse REST API."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from typing import Any, Optional
6
+
7
+ from fastapi import FastAPI, HTTPException, Query
8
+ from fastapi.responses import HTMLResponse, JSONResponse
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from pydantic import BaseModel
11
+
12
+ from devpulse.core.database import Database
13
+ from devpulse.core.analytics import AnalyticsEngine
14
+ from devpulse.core.report_generator import ReportGenerator
15
+
16
+ app = FastAPI(
17
+ title="DevPulse API",
18
+ description="AI-powered developer productivity dashboard with GitHub integration.",
19
+ version="1.0.0",
20
+ docs_url="/docs",
21
+ redoc_url="/redoc",
22
+ )
23
+
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"],
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+
33
+ def _get_db() -> Database:
34
+ return Database()
35
+
36
+
37
+ def _get_engine() -> AnalyticsEngine:
38
+ return AnalyticsEngine(db=_get_db())
39
+
40
+
41
+ def _get_report_gen() -> ReportGenerator:
42
+ return ReportGenerator(db=_get_db())
43
+
44
+
45
+ # ── Request/Response models ──────────────────────────────────────────
46
+
47
+ class GoalCreate(BaseModel):
48
+ title: str
49
+ description: str = ""
50
+ target_value: float
51
+ metric: str
52
+ deadline: Optional[str] = None
53
+
54
+
55
+ class SprintSnapshot(BaseModel):
56
+ sprint_name: str
57
+ total_points: float
58
+ completed_points: float
59
+ added_points: float = 0.0
60
+
61
+
62
+ class QualitySnapshot(BaseModel):
63
+ repo: str
64
+ test_coverage: float = 0.0
65
+ open_bugs: int = 0
66
+ tech_debt_score: float = 0.0
67
+ lines_added: int = 0
68
+ lines_removed: int = 0
69
+ files_changed: int = 0
70
+
71
+
72
+ class HealthResponse(BaseModel):
73
+ overall_score: float
74
+ workload_balance: float
75
+ burnout_risk: dict[str, float]
76
+ collaboration_score: float
77
+ velocity_trend: str
78
+ recommendations: list[str]
79
+
80
+
81
+ class MessageResponse(BaseModel):
82
+ message: str
83
+ detail: Optional[str] = None
84
+
85
+
86
+ # ── Health check ─────────────────────────────────────────────────────
87
+
88
+ @app.get("/", tags=["health"])
89
+ def root() -> dict[str, str]:
90
+ """API health check."""
91
+ return {
92
+ "name": "DevPulse API",
93
+ "version": "1.0.0",
94
+ "status": "healthy",
95
+ "timestamp": datetime.utcnow().isoformat(),
96
+ }
97
+
98
+
99
+ # ── Commits ──────────────────────────────────────────────────────────
100
+
101
+ @app.get("/api/commits", tags=["commits"])
102
+ def list_commits(
103
+ repo: Optional[str] = Query(None),
104
+ author: Optional[str] = Query(None),
105
+ since: Optional[str] = Query(None),
106
+ limit: int = Query(100, ge=1, le=1000),
107
+ ) -> list[dict[str, Any]]:
108
+ """List commits with optional filters."""
109
+ db = _get_db()
110
+ return db.get_commits(repo=repo, author=author, since=since, limit=limit)
111
+
112
+
113
+ @app.get("/api/commits/heatmap", tags=["commits"])
114
+ def commit_heatmap(
115
+ author: Optional[str] = Query(None),
116
+ days: int = Query(365, ge=7, le=730),
117
+ ) -> list[dict[str, Any]]:
118
+ """Get daily commit counts for heatmap visualization."""
119
+ db = _get_db()
120
+ return db.get_commit_count_by_day(author=author, days=days)
121
+
122
+
123
+ # ── Pull Requests ────────────────────────────────────────────────────
124
+
125
+ @app.get("/api/pull-requests", tags=["pull-requests"])
126
+ def list_pull_requests(
127
+ repo: Optional[str] = Query(None),
128
+ author: Optional[str] = Query(None),
129
+ state: Optional[str] = Query(None),
130
+ since: Optional[str] = Query(None),
131
+ limit: int = Query(100, ge=1, le=500),
132
+ ) -> list[dict[str, Any]]:
133
+ """List pull requests with optional filters."""
134
+ db = _get_db()
135
+ return db.get_pull_requests(repo=repo, author=author, state=state, since=since, limit=limit)
136
+
137
+
138
+ # ── Issues ───────────────────────────────────────────────────────────
139
+
140
+ @app.get("/api/issues", tags=["issues"])
141
+ def list_issues(
142
+ repo: Optional[str] = Query(None),
143
+ state: Optional[str] = Query(None),
144
+ since: Optional[str] = Query(None),
145
+ limit: int = Query(100, ge=1, le=500),
146
+ ) -> list[dict[str, Any]]:
147
+ """List issues with optional filters."""
148
+ db = _get_db()
149
+ return db.get_issues(repo=repo, state=state, since=since, limit=limit)
150
+
151
+
152
+ # ── Reviews ──────────────────────────────────────────────────────────
153
+
154
+ @app.get("/api/reviews", tags=["reviews"])
155
+ def list_reviews(
156
+ repo: Optional[str] = Query(None),
157
+ author: Optional[str] = Query(None),
158
+ since: Optional[str] = Query(None),
159
+ limit: int = Query(100, ge=1, le=500),
160
+ ) -> list[dict[str, Any]]:
161
+ """List code reviews with optional filters."""
162
+ db = _get_db()
163
+ return db.get_reviews(repo=repo, author=author, since=since, limit=limit)
164
+
165
+
166
+ # ── Metrics ──────────────────────────────────────────────────────────
167
+
168
+ @app.get("/api/metrics/{author}", tags=["metrics"])
169
+ def get_developer_metrics(
170
+ author: str,
171
+ days: int = Query(30, ge=1, le=365),
172
+ repo: Optional[str] = Query(None),
173
+ ) -> dict[str, Any]:
174
+ """Get comprehensive metrics for a developer."""
175
+ engine = _get_engine()
176
+ metrics = engine.developer_metrics(author=author, days=days, repo=repo)
177
+ return metrics.model_dump()
178
+
179
+
180
+ @app.get("/api/metrics", tags=["metrics"])
181
+ def get_team_metrics(
182
+ days: int = Query(30, ge=1, le=365),
183
+ repo: Optional[str] = Query(None),
184
+ ) -> list[dict[str, Any]]:
185
+ """Get metrics for all team members."""
186
+ engine = _get_engine()
187
+ team = engine.team_metrics(days=days, repo=repo)
188
+ return [m.model_dump() for m in team]
189
+
190
+
191
+ # ── Team Health ──────────────────────────────────────────────────────
192
+
193
+ @app.get("/api/health", tags=["health"], response_model=HealthResponse)
194
+ def team_health(
195
+ days: int = Query(30, ge=1, le=365),
196
+ repo: Optional[str] = Query(None),
197
+ ) -> HealthResponse:
198
+ """Analyze team health and burnout risk."""
199
+ engine = _get_engine()
200
+ health = engine.team_health(days=days, repo=repo)
201
+ return HealthResponse(
202
+ overall_score=health.overall_score,
203
+ workload_balance=health.workload_balance,
204
+ burnout_risk=health.burnout_risk,
205
+ collaboration_score=health.collaboration_score,
206
+ velocity_trend=health.velocity_trend,
207
+ recommendations=health.recommendations,
208
+ )
209
+
210
+
211
+ # ── AI Insights ──────────────────────────────────────────────────────
212
+
213
+ @app.get("/api/insights", tags=["insights"])
214
+ def get_insights(
215
+ days: int = Query(30, ge=1, le=365),
216
+ repo: Optional[str] = Query(None),
217
+ ) -> list[dict[str, str]]:
218
+ """Get AI-powered insights and recommendations."""
219
+ engine = _get_engine()
220
+ return engine.generate_insights(days=days, repo=repo)
221
+
222
+
223
+ # ── Goals ────────────────────────────────────────────────────────────
224
+
225
+ @app.get("/api/goals", tags=["goals"])
226
+ def list_goals(status: Optional[str] = Query(None)) -> list[dict[str, Any]]:
227
+ """List goals."""
228
+ db = _get_db()
229
+ return db.get_goals(status=status)
230
+
231
+
232
+ @app.post("/api/goals", tags=["goals"], status_code=201)
233
+ def create_goal(goal: GoalCreate) -> dict[str, Any]:
234
+ """Create a new goal."""
235
+ db = _get_db()
236
+ gid = db.upsert_goal(goal.model_dump())
237
+ return {"id": gid, **goal.model_dump()}
238
+
239
+
240
+ @app.delete("/api/goals/{goal_id}", tags=["goals"])
241
+ def delete_goal(goal_id: int) -> MessageResponse:
242
+ """Delete a goal."""
243
+ db = _get_db()
244
+ if db.delete_goal(goal_id):
245
+ return MessageResponse(message=f"Goal {goal_id} deleted.")
246
+ raise HTTPException(status_code=404, detail=f"Goal {goal_id} not found.")
247
+
248
+
249
+ @app.get("/api/goals/coach", tags=["goals"])
250
+ def goal_coaching(goal_id: Optional[int] = Query(None)) -> list[dict[str, Any]]:
251
+ """Get AI coaching for goals."""
252
+ engine = _get_engine()
253
+ return engine.goal_coaching(goal_id=goal_id)
254
+
255
+
256
+ # ── Sprints ──────────────────────────────────────────────────────────
257
+
258
+ @app.post("/api/sprints/snapshot", tags=["sprints"], status_code=201)
259
+ def create_sprint_snapshot(snapshot: SprintSnapshot) -> MessageResponse:
260
+ """Record a sprint snapshot."""
261
+ db = _get_db()
262
+ db.save_sprint_snapshot({
263
+ "sprint_name": snapshot.sprint_name,
264
+ "total_points": snapshot.total_points,
265
+ "completed_points": snapshot.completed_points,
266
+ "remaining_points": snapshot.total_points - snapshot.completed_points,
267
+ "added_points": snapshot.added_points,
268
+ })
269
+ return MessageResponse(message=f"Sprint snapshot recorded for '{snapshot.sprint_name}'.")
270
+
271
+
272
+ @app.get("/api/sprints/{sprint_name}/burndown", tags=["sprints"])
273
+ def sprint_burndown(sprint_name: str) -> list[dict[str, Any]]:
274
+ """Get burndown data for a sprint."""
275
+ engine = _get_engine()
276
+ return engine.sprint_burndown(sprint_name)
277
+
278
+
279
+ @app.get("/api/sprints/{sprint_name}/predict", tags=["sprints"])
280
+ def predict_sprint(
281
+ sprint_name: str,
282
+ total: float = Query(...),
283
+ elapsed: int = Query(...),
284
+ duration: int = Query(...),
285
+ ) -> dict[str, Any]:
286
+ """Predict sprint completion."""
287
+ engine = _get_engine()
288
+ return engine.predict_sprint_completion(sprint_name, total, elapsed, duration)
289
+
290
+
291
+ # ── Code Quality ─────────────────────────────────────────────────────
292
+
293
+ @app.post("/api/quality/snapshot", tags=["quality"], status_code=201)
294
+ def create_quality_snapshot(snapshot: QualitySnapshot) -> MessageResponse:
295
+ """Record a code quality snapshot."""
296
+ db = _get_db()
297
+ db.save_quality_snapshot(snapshot.model_dump())
298
+ return MessageResponse(message=f"Quality snapshot recorded for '{snapshot.repo}'.")
299
+
300
+
301
+ @app.get("/api/quality", tags=["quality"])
302
+ def code_quality_trends(
303
+ repo: Optional[str] = Query(None),
304
+ days: int = Query(90, ge=1, le=365),
305
+ ) -> list[dict[str, Any]]:
306
+ """Get code quality trends."""
307
+ engine = _get_engine()
308
+ trends = engine.code_quality_trend(repo=repo, days=days)
309
+ return [t.model_dump() for t in trends]
310
+
311
+
312
+ @app.get("/api/quality/score", tags=["quality"])
313
+ def quality_score(
314
+ repo: str = Query(..., description="Repository full name (e.g. owner/repo)"),
315
+ days: int = Query(30, ge=1, le=365),
316
+ ) -> dict[str, Any]:
317
+ """Compute aggregate quality score for a repo."""
318
+ engine = _get_engine()
319
+ return engine.compute_quality_score(repo=repo, days=days)
320
+
321
+
322
+ # ── Reports ──────────────────────────────────────────────────────────
323
+
324
+ @app.get("/api/reports/daily", tags=["reports"])
325
+ def daily_report(
326
+ date: Optional[str] = Query(None),
327
+ author: Optional[str] = Query(None),
328
+ repo: Optional[str] = Query(None),
329
+ format: str = Query("json", regex="^(json|markdown|html)$"),
330
+ ) -> Any:
331
+ """Generate a daily report."""
332
+ gen = _get_report_gen()
333
+ data = gen.daily_report(target_date=date, author=author, repo=repo)
334
+ if format == "html":
335
+ return HTMLResponse(content=gen.to_html(data, "daily"))
336
+ if format == "markdown":
337
+ return JSONResponse(content={"markdown": gen.to_markdown(data, "daily")})
338
+ return data
339
+
340
+
341
+ @app.get("/api/reports/weekly", tags=["reports"])
342
+ def weekly_report(
343
+ week_start: Optional[str] = Query(None),
344
+ repo: Optional[str] = Query(None),
345
+ format: str = Query("json", regex="^(json|markdown|html)$"),
346
+ ) -> Any:
347
+ """Generate a weekly report."""
348
+ gen = _get_report_gen()
349
+ data = gen.weekly_report(week_start=week_start, repo=repo)
350
+ if format == "html":
351
+ return HTMLResponse(content=gen.to_html(data, "weekly"))
352
+ if format == "markdown":
353
+ return JSONResponse(content={"markdown": gen.to_markdown(data, "weekly")})
354
+ return data
355
+
356
+
357
+ @app.get("/api/reports/monthly", tags=["reports"])
358
+ def monthly_report(
359
+ year: Optional[int] = Query(None),
360
+ month: Optional[int] = Query(None),
361
+ repo: Optional[str] = Query(None),
362
+ format: str = Query("json", regex="^(json|markdown|html)$"),
363
+ ) -> Any:
364
+ """Generate a monthly report."""
365
+ gen = _get_report_gen()
366
+ data = gen.monthly_report(year=year, month=month, repo=repo)
367
+ if format == "html":
368
+ return HTMLResponse(content=gen.to_html(data, "monthly"))
369
+ if format == "markdown":
370
+ return JSONResponse(content={"markdown": gen.to_markdown(data, "monthly")})
371
+ return data