Skip to content

Commit d6b1c49

Browse files
committed
Feat: metrics show
1 parent 3389172 commit d6b1c49

2 files changed

Lines changed: 109 additions & 3 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
## 🚀 Features
4646

4747
### Git Hosting & Collaboration
48+
4849
- **Full Git Support** - SSH and HTTP/HTTPS protocols with authentication
4950
- **Repository Management** - Create, fork, archive, and transfer repositories
5051
- **Branch Protection** - Enforce PR requirements, approvals, and code review policies
@@ -54,13 +55,15 @@
5455
- **Organizations** - Team management with role-based access control (RBAC)
5556

5657
### CI/CD & Automation
58+
5759
- **GitHub Actions Compatible** - Run workflows defined in `.github/workflows/`
5860
- **Self-hosted Runners** - Docker-based job execution with artifact support
5961
- **Merge Queue** - Automated PR merge management with conflict detection
6062
- **Webhooks** - Real-time event notifications to external services
6163
- **Secrets Management** - Encrypted storage for CI/CD credentials
6264

6365
### Security Features ✅
66+
6467
- **Rate Limiting** - Prevents brute force attacks (5 login attempts per 15min)
6568
- **CSRF Protection** - Double-submit cookie pattern for all state-changing operations
6669
- **Input Validation** - Zod schemas prevent injection attacks
@@ -69,6 +72,7 @@
6972
- **Audit Logging** - Complete activity tracking for compliance
7073

7174
### Modern Tech Stack
75+
7276
- **Astro + React** - Fast, modern frontend with island architecture
7377
- **TailwindCSS + shadcn/ui** - Beautiful, accessible UI components
7478
- **Universal Database Adapter** - PostgreSQL, MySQL, SQLite, MongoDB, Turso, PlanetScale
@@ -78,7 +82,6 @@
7882

7983
---
8084

81-
8285
---
8386

8487
## 🏗 Architecture
@@ -100,6 +103,7 @@ OpenCodeHub is built on a modern, scalable architecture designed for performance
100103
## 📦 Quick Start
101104

102105
### Prerequisites
106+
103107
- **Node.js** 18+ or **Bun** 1.0+
104108
- **Database**: PostgreSQL 14+ (recommended) / MySQL 8+ / SQLite 3.35+
105109
- **Git** 2.30+
@@ -237,8 +241,6 @@ och status # Show stack status (alias: st)
237241

238242
Each PR builds on the previous one. When #123 merges, #124 automatically rebases.
239243

240-
Each PR builds on the previous one. When #123 merges, #124 automatically rebases.
241-
242244
#### Creating a Stack
243245

244246
**Using CLI:**

src/pages/api/user/metrics.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
2+
import type { APIRoute } from "astro";
3+
import { getDatabase, schema } from "@/db";
4+
import { eq, and, sql, desc, gte } from "drizzle-orm";
5+
import { withErrorHandler } from "@/lib/errors";
6+
import { success, unauthorized } from "@/lib/api";
7+
8+
import { getUserFromRequest } from "@/lib/auth";
9+
10+
export const GET: APIRoute = withErrorHandler(async ({ request, locals }) => {
11+
// Try locals first (session auth), then fallback to header token (CLI auth)
12+
let userId = locals.user?.id;
13+
14+
if (!userId) {
15+
const tokenPayload = await getUserFromRequest(request);
16+
if (!tokenPayload) {
17+
return unauthorized();
18+
}
19+
userId = tokenPayload.userId;
20+
}
21+
22+
const url = new URL(request.url);
23+
const weeks = parseInt(url.searchParams.get("weeks") || "4");
24+
25+
// Calculate date range
26+
const now = new Date();
27+
const startDate = new Date();
28+
startDate.setDate(now.getDate() - (weeks * 7));
29+
30+
const db = getDatabase();
31+
32+
// 1. Authored PRs Stats
33+
const authoredPrs = await db.query.pullRequests.findMany({
34+
where: and(
35+
eq(schema.pullRequests.authorId, userId),
36+
gte(schema.pullRequests.createdAt, startDate)
37+
)
38+
});
39+
40+
const mergedPrs = authoredPrs.filter(pr => pr.isMerged);
41+
42+
// Calculate avg time to merge
43+
let totalMergeTime = 0;
44+
mergedPrs.forEach(pr => {
45+
if (pr.mergedAt && pr.createdAt) {
46+
totalMergeTime += (pr.mergedAt.getTime() - pr.createdAt.getTime());
47+
}
48+
});
49+
50+
// Convert to hours
51+
const avgTimeToMerge = mergedPrs.length > 0
52+
? (totalMergeTime / mergedPrs.length) / (1000 * 60 * 60)
53+
: 0;
54+
55+
// 2. Review Stats
56+
const reviews = await db.query.pullRequestReviews.findMany({
57+
where: and(
58+
eq(schema.pullRequestReviews.reviewerId, userId),
59+
gte(schema.pullRequestReviews.createdAt, startDate)
60+
)
61+
});
62+
63+
const approvals = reviews.filter(r => r.state === "approved").length;
64+
const changesRequested = reviews.filter(r => r.state === "changes_requested").length;
65+
66+
// 3. Calculate Trends (Weekly)
67+
const trends = [];
68+
for (let i = 0; i < weeks; i++) {
69+
const weekStart = new Date();
70+
weekStart.setDate(now.getDate() - ((i + 1) * 7));
71+
const weekEnd = new Date();
72+
weekEnd.setDate(now.getDate() - (i * 7));
73+
74+
const weekLabel = `${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()}`;
75+
76+
const weekAuthored = authoredPrs.filter(
77+
pr => pr.createdAt >= weekStart && pr.createdAt < weekEnd
78+
).length;
79+
80+
const weekReviewed = reviews.filter(
81+
r => r.createdAt >= weekStart && r.createdAt < weekEnd
82+
).length;
83+
84+
trends.unshift({
85+
week: weekLabel,
86+
authored: weekAuthored,
87+
reviewed: weekReviewed
88+
});
89+
}
90+
91+
return success({
92+
authored: {
93+
total: authoredPrs.length,
94+
merged: mergedPrs.length,
95+
avgTimeToMerge // in hours
96+
},
97+
reviewed: {
98+
total: reviews.length,
99+
approvals,
100+
changesRequested
101+
},
102+
trends
103+
});
104+
});

0 commit comments

Comments
 (0)