@theihtisham/dev-pulse 1.0.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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/devpulse/__init__.py +4 -0
- package/devpulse/api/__init__.py +1 -0
- package/devpulse/api/app.py +371 -0
- package/devpulse/cli/__init__.py +1 -0
- package/devpulse/cli/dashboard.py +131 -0
- package/devpulse/cli/main.py +678 -0
- package/devpulse/cli/render.py +175 -0
- package/devpulse/core/__init__.py +34 -0
- package/devpulse/core/analytics.py +487 -0
- package/devpulse/core/config.py +77 -0
- package/devpulse/core/database.py +612 -0
- package/devpulse/core/github_client.py +281 -0
- package/devpulse/core/models.py +142 -0
- package/devpulse/core/report_generator.py +454 -0
- package/devpulse/static/.gitkeep +1 -0
- package/devpulse/templates/report.html +64 -0
- package/jest.config.js +7 -0
- package/package.json +35 -0
- package/pyproject.toml +80 -0
- package/requirements.txt +14 -0
- package/tests/__init__.py +1 -0
- package/tests/conftest.py +208 -0
- package/tests/test_analytics.py +284 -0
- package/tests/test_api.py +313 -0
- package/tests/test_cli.py +204 -0
- package/tests/test_config.py +47 -0
- package/tests/test_database.py +255 -0
- package/tests/test_models.py +107 -0
- package/tests/test_report_generator.py +173 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DevPulse Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Dev Pulse
|
|
2
|
+
|
|
3
|
+
Developer productivity metrics dashboard
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @theihtisham/dev-pulse
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { } from '@theihtisham/dev-pulse';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- Written in TypeScript with full type definitions
|
|
20
|
+
- Zero external dependencies (minimal footprint)
|
|
21
|
+
- Event-driven architecture
|
|
22
|
+
- Comprehensive test suite
|
|
23
|
+
|
|
24
|
+
## API
|
|
25
|
+
|
|
26
|
+
### Quick Start
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { } from '@theihtisham/dev-pulse';
|
|
30
|
+
|
|
31
|
+
// Initialize and use the package
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
Configuration options and their defaults:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const config = {
|
|
40
|
+
// Add configuration options here
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
See the `tests/` directory for usage examples.
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Install dependencies
|
|
52
|
+
npm install
|
|
53
|
+
|
|
54
|
+
# Build
|
|
55
|
+
npm run build
|
|
56
|
+
|
|
57
|
+
# Run tests
|
|
58
|
+
npm test
|
|
59
|
+
|
|
60
|
+
# Lint
|
|
61
|
+
npm run lint
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
67
|
+
|
|
68
|
+
## Author
|
|
69
|
+
|
|
70
|
+
**ihtisham**
|
|
71
|
+
|
|
72
|
+
- GitHub: [https://github.com/ihtisham](https://github.com/ihtisham)
|
|
73
|
+
- npm: [@theihtisham](https://www.npmjs.com/~theihtisham)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastAPI web API package for DevPulse."""
|
|
@@ -0,0 +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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package for DevPulse."""
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Interactive terminal dashboard for DevPulse."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.layout import Layout
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from devpulse.core.database import Database
|
|
12
|
+
from devpulse.core.analytics import AnalyticsEngine
|
|
13
|
+
from devpulse.core.report_generator import ReportGenerator
|
|
14
|
+
from devpulse.cli.render import render_header, render_heatmap, render_insight_card
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dashboard_cmd(
|
|
18
|
+
author: Optional[str] = None,
|
|
19
|
+
days: int = 30,
|
|
20
|
+
repo: Optional[str] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Launch the full interactive terminal dashboard."""
|
|
23
|
+
console = Console()
|
|
24
|
+
db = Database()
|
|
25
|
+
engine = AnalyticsEngine(db=db)
|
|
26
|
+
report_gen = ReportGenerator(db=db)
|
|
27
|
+
|
|
28
|
+
render_header(console)
|
|
29
|
+
|
|
30
|
+
# ── Section 1: Summary ───────────────────────────────────────────
|
|
31
|
+
console.rule("[bold cyan]Overview[/bold cyan]")
|
|
32
|
+
|
|
33
|
+
daily = report_gen.daily_report(author=author, repo=repo)
|
|
34
|
+
console.print(f"\n Date: [bold]{daily['date']}[/bold]")
|
|
35
|
+
console.print(f" {daily['summary']}\n")
|
|
36
|
+
|
|
37
|
+
# ── Section 2: Team Metrics ──────────────────────────────────────
|
|
38
|
+
console.rule("[bold cyan]Team Metrics[/bold cyan]")
|
|
39
|
+
console.print()
|
|
40
|
+
|
|
41
|
+
team = engine.team_metrics(days=days, repo=repo)
|
|
42
|
+
if team:
|
|
43
|
+
table = Table(show_header=True, header_style="bold cyan", expand=True)
|
|
44
|
+
table.add_column("Developer", style="white", min_width=15)
|
|
45
|
+
table.add_column("Commits", justify="right")
|
|
46
|
+
table.add_column("C/D", justify="right", style="dim")
|
|
47
|
+
table.add_column("PRs", justify="right")
|
|
48
|
+
table.add_column("Reviews", justify="right")
|
|
49
|
+
table.add_column("Merge(h)", justify="right")
|
|
50
|
+
table.add_column("+/- Lines", justify="right")
|
|
51
|
+
|
|
52
|
+
for m in team[:10]:
|
|
53
|
+
table.add_row(
|
|
54
|
+
m.author,
|
|
55
|
+
str(m.commits_count),
|
|
56
|
+
str(m.commits_per_day),
|
|
57
|
+
str(m.prs_created),
|
|
58
|
+
str(m.reviews_given),
|
|
59
|
+
str(m.avg_pr_merge_time_hours),
|
|
60
|
+
f"+{m.lines_added}/-{m.lines_removed}",
|
|
61
|
+
)
|
|
62
|
+
console.print(table)
|
|
63
|
+
else:
|
|
64
|
+
console.print("[dim]No team data yet. Run 'devpulse sync' to fetch data.[/dim]")
|
|
65
|
+
|
|
66
|
+
# ── Section 3: Team Health ───────────────────────────────────────
|
|
67
|
+
console.rule("[bold cyan]Team Health[/bold cyan]")
|
|
68
|
+
console.print()
|
|
69
|
+
|
|
70
|
+
health = engine.team_health(days=days, repo=repo)
|
|
71
|
+
score_color = "green" if health.overall_score >= 70 else "yellow" if health.overall_score >= 40 else "red"
|
|
72
|
+
console.print(f" Health Score: [{score_color}]{health.overall_score}/100[/{score_color}]")
|
|
73
|
+
console.print(f" Workload Balance: {health.workload_balance:.0%}")
|
|
74
|
+
console.print(f" Collaboration: {health.collaboration_score:.0%}")
|
|
75
|
+
console.print(f" Velocity: {health.velocity_trend}")
|
|
76
|
+
|
|
77
|
+
if health.burnout_risk:
|
|
78
|
+
console.print("\n [bold]Burnout Risk:[/bold]")
|
|
79
|
+
for name, risk in sorted(health.burnout_risk.items(), key=lambda x: x[1], reverse=True):
|
|
80
|
+
color = "red" if risk >= 0.6 else "yellow" if risk >= 0.3 else "green"
|
|
81
|
+
bar_len = int(risk * 20)
|
|
82
|
+
bar = "#" * bar_len + "-" * (20 - bar_len)
|
|
83
|
+
console.print(f" {name:<15} [{color}]{bar}[/{color}] {risk:.0%}")
|
|
84
|
+
|
|
85
|
+
# ── Section 4: AI Insights ───────────────────────────────────────
|
|
86
|
+
console.rule("[bold cyan]AI Insights[/bold cyan]")
|
|
87
|
+
console.print()
|
|
88
|
+
|
|
89
|
+
insights = engine.generate_insights(days=days, repo=repo)
|
|
90
|
+
for i, insight in enumerate(insights, 1):
|
|
91
|
+
render_insight_card(console, insight, i)
|
|
92
|
+
|
|
93
|
+
# ── Section 5: Goals ─────────────────────────────────────────────
|
|
94
|
+
goals = db.get_goals(status="active")
|
|
95
|
+
if goals:
|
|
96
|
+
console.rule("[bold cyan]Active Goals[/bold cyan]")
|
|
97
|
+
console.print()
|
|
98
|
+
goal_table = Table(show_header=True, header_style="bold cyan")
|
|
99
|
+
goal_table.add_column("Goal", style="white")
|
|
100
|
+
goal_table.add_column("Metric")
|
|
101
|
+
goal_table.add_column("Progress", justify="right")
|
|
102
|
+
goal_table.add_column("Deadline")
|
|
103
|
+
for g in goals:
|
|
104
|
+
pct = (g["current_value"] / g["target_value"] * 100) if g["target_value"] > 0 else 0
|
|
105
|
+
color = "green" if pct >= 75 else "yellow" if pct >= 25 else "red"
|
|
106
|
+
goal_table.add_row(
|
|
107
|
+
g["title"],
|
|
108
|
+
g["metric"],
|
|
109
|
+
f"[{color}]{pct:.0%}[/{color}]",
|
|
110
|
+
g.get("deadline", "-"),
|
|
111
|
+
)
|
|
112
|
+
console.print(goal_table)
|
|
113
|
+
|
|
114
|
+
# ── Section 6: Heatmap ───────────────────────────────────────────
|
|
115
|
+
console.rule("[bold cyan]Activity Heatmap[/bold cyan]")
|
|
116
|
+
console.print()
|
|
117
|
+
|
|
118
|
+
heatmap_data = engine.activity_heatmap(author=author, days=90)
|
|
119
|
+
if heatmap_data:
|
|
120
|
+
render_heatmap(console, heatmap_data, days=90)
|
|
121
|
+
else:
|
|
122
|
+
console.print("[dim]No activity data. Run 'devpulse sync' to fetch commits.[/dim]")
|
|
123
|
+
|
|
124
|
+
# ── Section 7: Recommendations ───────────────────────────────────
|
|
125
|
+
console.rule("[bold cyan]Recommendations[/bold cyan]")
|
|
126
|
+
console.print()
|
|
127
|
+
for rec in health.recommendations:
|
|
128
|
+
console.print(f" -> {rec}")
|
|
129
|
+
|
|
130
|
+
console.print()
|
|
131
|
+
console.print("[dim]Run 'devpulse sync' to refresh data | 'devpulse --help' for all commands[/dim]")
|