Samyak000 commited on
Commit
ecfb294
·
verified ·
1 Parent(s): 63aeb06

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +505 -19
app.py CHANGED
@@ -4,7 +4,7 @@ FastAPI server for GitHub App integration
4
  Connects desktop app to GitHub securely
5
  """
6
 
7
- from fastapi import FastAPI, HTTPException, Request
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from pydantic import BaseModel
10
  from typing import Optional, List, Dict
@@ -12,6 +12,12 @@ import json
12
  import os
13
  from pathlib import Path
14
  from dotenv import load_dotenv
 
 
 
 
 
 
15
 
16
  from github_api import GitHubAppAuth, GitHubInsights, TokenManager
17
 
@@ -37,17 +43,30 @@ app.add_middleware(
37
 
38
  # Load environment variables
39
  APP_ID = os.getenv("GITHUB_APP_ID")
40
- PRIVATE_KEY = os.getenv("GITHUB_PRIVATE_KEY_PATH")
 
 
 
41
 
42
  # Initialize GitHub App authentication
43
- if APP_ID and PRIVATE_KEY:
44
- github_auth = GitHubAppAuth(APP_ID, PRIVATE_KEY)
 
 
 
45
  token_manager = TokenManager(github_auth)
46
  else:
47
  github_auth = None
48
  token_manager = None
49
  print("⚠️ WARNING: GitHub App credentials not configured")
50
 
 
 
 
 
 
 
 
51
  # Simple JSON storage for installations
52
  INSTALLATIONS_FILE = "installations.json"
53
 
@@ -65,6 +84,7 @@ class InstallationCallback(BaseModel):
65
 
66
  class InsightsRequest(BaseModel):
67
  """Request for repository insights from desktop app"""
 
68
  org: str
69
  repo: str
70
 
@@ -74,6 +94,12 @@ class TokenRefreshRequest(BaseModel):
74
  installation_id: int
75
 
76
 
 
 
 
 
 
 
77
  # ============================================================================
78
  # UTILITY FUNCTIONS
79
  # ============================================================================
@@ -108,6 +134,224 @@ def get_installation_id(org: str) -> Optional[int]:
108
  return None
109
 
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  # ============================================================================
112
  # API ENDPOINTS
113
  # ============================================================================
@@ -133,18 +377,25 @@ def health_check():
133
 
134
 
135
  @app.get("/github/callback")
136
- async def github_callback(request: Request):
137
  """
138
  Handle GitHub App installation callback
139
 
140
- After org installs Codesage, GitHub redirects here with installation_id
141
- This is the critical "make our product a member" step
 
142
  """
143
  # Get query parameters
144
  query_params = dict(request.query_params)
145
 
146
  installation_id = query_params.get("installation_id")
147
  setup_action = query_params.get("setup_action")
 
 
 
 
 
 
148
 
149
  if not installation_id:
150
  return {
@@ -184,11 +435,26 @@ async def github_callback(request: Request):
184
  # Save with org name if we got it, otherwise use placeholder
185
  if org_name:
186
  save_installation(org_name, installation_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  return {
188
  "success": True,
189
  "message": f"✅ Successfully installed Codesage for {org_name}",
190
  "org": org_name,
191
- "installation_id": installation_id
 
192
  }
193
  else:
194
  # Save with placeholder - user can update via /github/install
@@ -289,103 +555,287 @@ def list_org_repositories(org: str):
289
  @app.post("/insights/commits")
290
  def get_commits(data: InsightsRequest):
291
  """
292
- Get commit history for a repository
293
- Desktop app Backend GitHub
294
  """
295
  if not github_auth:
296
  raise HTTPException(status_code=500, detail="GitHub App not configured")
 
 
297
 
298
  installation_id = get_installation_id(data.org)
299
  if not installation_id:
300
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
301
 
302
  try:
 
303
  token = token_manager.get_token(installation_id)
304
  insights = GitHubInsights(token)
305
- commits = insights.get_commits(data.org, data.repo)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  return {
308
  "org": data.org,
309
  "repo": data.repo,
310
  "count": len(commits),
 
311
  "commits": commits
312
  }
313
  except Exception as e:
 
314
  raise HTTPException(status_code=500, detail=f"Failed to fetch commits: {str(e)}")
315
 
316
 
317
  @app.post("/insights/pull-requests")
318
  def get_pull_requests(data: InsightsRequest):
319
- """Get pull requests for a repository"""
 
 
320
  if not github_auth:
321
  raise HTTPException(status_code=500, detail="GitHub App not configured")
 
 
322
 
323
  installation_id = get_installation_id(data.org)
324
  if not installation_id:
325
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
326
 
327
  try:
 
328
  token = token_manager.get_token(installation_id)
329
  insights = GitHubInsights(token)
330
- prs = insights.get_pull_requests(data.org, data.repo)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
  return {
333
  "org": data.org,
334
  "repo": data.repo,
335
  "count": len(prs),
 
336
  "pull_requests": prs
337
  }
338
  except Exception as e:
 
339
  raise HTTPException(status_code=500, detail=f"Failed to fetch pull requests: {str(e)}")
340
 
341
 
342
  @app.post("/insights/issues")
343
  def get_issues(data: InsightsRequest):
344
- """Get issues for a repository"""
 
 
345
  if not github_auth:
346
  raise HTTPException(status_code=500, detail="GitHub App not configured")
 
 
347
 
348
  installation_id = get_installation_id(data.org)
349
  if not installation_id:
350
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
351
 
352
  try:
 
353
  token = token_manager.get_token(installation_id)
354
  insights = GitHubInsights(token)
355
- issues = insights.get_issues(data.org, data.repo)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
  return {
358
  "org": data.org,
359
  "repo": data.repo,
360
  "count": len(issues),
 
361
  "issues": issues
362
  }
363
  except Exception as e:
 
364
  raise HTTPException(status_code=500, detail=f"Failed to fetch issues: {str(e)}")
365
 
366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  @app.post("/insights/contributors")
368
  def get_contributors(data: InsightsRequest):
369
- """Get contributors and their stats"""
 
 
370
  if not github_auth:
371
  raise HTTPException(status_code=500, detail="GitHub App not configured")
 
 
372
 
373
  installation_id = get_installation_id(data.org)
374
  if not installation_id:
375
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
376
 
377
  try:
 
378
  token = token_manager.get_token(installation_id)
379
  insights = GitHubInsights(token)
380
  contributors = insights.get_contributors(data.org, data.repo)
381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  return {
383
  "org": data.org,
384
  "repo": data.repo,
385
  "count": len(contributors),
 
386
  "contributors": contributors
387
  }
388
  except Exception as e:
 
389
  raise HTTPException(status_code=500, detail=f"Failed to fetch contributors: {str(e)}")
390
 
391
 
@@ -445,11 +895,17 @@ def get_repository_activity(data: InsightsRequest):
445
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
446
 
447
  try:
 
448
  token = token_manager.get_token(installation_id)
449
  insights = GitHubInsights(token)
450
 
 
451
  code_frequency = insights.get_code_frequency(data.org, data.repo)
 
 
 
452
  commit_activity = insights.get_commit_activity(data.org, data.repo)
 
453
 
454
  return {
455
  "org": data.org,
@@ -458,12 +914,42 @@ def get_repository_activity(data: InsightsRequest):
458
  "commit_activity": commit_activity
459
  }
460
  except Exception as e:
 
461
  raise HTTPException(status_code=500, detail=f"Failed to fetch activity: {str(e)}")
462
 
463
 
464
- # ============================================================================
465
- # RUN SERVER
466
- # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
 
469
 
 
4
  Connects desktop app to GitHub securely
5
  """
6
 
7
+ from fastapi import FastAPI, HTTPException, Request, BackgroundTasks
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from pydantic import BaseModel
10
  from typing import Optional, List, Dict
 
12
  import os
13
  from pathlib import Path
14
  from dotenv import load_dotenv
15
+ from supabase import create_client, Client
16
+ import logging
17
+
18
+ # Configure logging
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
 
22
  from github_api import GitHubAppAuth, GitHubInsights, TokenManager
23
 
 
43
 
44
  # Load environment variables
45
  APP_ID = os.getenv("GITHUB_APP_ID")
46
+ PRIVATE_KEY_PATH = os.getenv("GITHUB_PRIVATE_KEY_PATH", "private-key.pem")
47
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
48
+ SUPABASE_SECRET_KEY = os.getenv("SUPABASE_SECRET_KEY")
49
+ DEFAULT_FIREBASE_ID = os.getenv("DEFAULT_FIREBASE_ID", "JDfZoVuGJhdLBEvR7rVCQKBG9r02")
50
 
51
  # Initialize GitHub App authentication
52
+ if APP_ID and os.path.exists(PRIVATE_KEY_PATH):
53
+ with open(PRIVATE_KEY_PATH, "r") as f:
54
+ private_key = f.read()
55
+
56
+ github_auth = GitHubAppAuth(APP_ID, private_key)
57
  token_manager = TokenManager(github_auth)
58
  else:
59
  github_auth = None
60
  token_manager = None
61
  print("⚠️ WARNING: GitHub App credentials not configured")
62
 
63
+ # Initialize Supabase client
64
+ if SUPABASE_URL and SUPABASE_SECRET_KEY:
65
+ supabase: Optional[Client] = create_client(SUPABASE_URL, SUPABASE_SECRET_KEY)
66
+ else:
67
+ supabase = None
68
+ print("⚠️ WARNING: Supabase credentials not configured")
69
+
70
  # Simple JSON storage for installations
71
  INSTALLATIONS_FILE = "installations.json"
72
 
 
84
 
85
  class InsightsRequest(BaseModel):
86
  """Request for repository insights from desktop app"""
87
+ firebase_id: str # Now required
88
  org: str
89
  repo: str
90
 
 
94
  installation_id: int
95
 
96
 
97
+ class GitHubSyncRequest(BaseModel):
98
+ """Request to sync GitHub data into Supabase"""
99
+ firebase_id: str
100
+ org: str
101
+
102
+
103
  # ============================================================================
104
  # UTILITY FUNCTIONS
105
  # ============================================================================
 
134
  return None
135
 
136
 
137
+ def ensure_user_exists(firebase_id: str):
138
+ if not supabase:
139
+ raise HTTPException(status_code=500, detail="Supabase not configured")
140
+
141
+ result = supabase.table("Users").select("firebase_id").eq("firebase_id", firebase_id).limit(1).execute()
142
+ if not result.data:
143
+ raise HTTPException(status_code=404, detail="User not found for firebase_id")
144
+
145
+
146
+ def build_repo_rollup(org: str, repo: str, insights: GitHubInsights) -> Dict:
147
+ repo_info = insights.get_repository_info(org, repo)
148
+ commits = insights.get_commits(org, repo, per_page=100)
149
+ pull_requests = insights.get_pull_requests(org, repo, state="all")
150
+ issues = insights.get_issues(org, repo, state="all")
151
+ contributors = insights.get_contributors(org, repo)
152
+ languages = insights.get_languages(org, repo)
153
+
154
+ return {
155
+ "owner": repo_info.get("owner", {}).get("login"),
156
+ "repo": repo_info.get("name"),
157
+ "full_name": repo_info.get("full_name"),
158
+ "private": repo_info.get("private"),
159
+ "default_branch": repo_info.get("default_branch"),
160
+ "language": repo_info.get("language"),
161
+ "html_url": repo_info.get("html_url"),
162
+ "updated_at": repo_info.get("updated_at"),
163
+ "pushed_at": repo_info.get("pushed_at"),
164
+ "description": repo_info.get("description"),
165
+ "stars": repo_info.get("stargazers_count", 0),
166
+ "forks": repo_info.get("forks_count", 0),
167
+ "open_issues": repo_info.get("open_issues_count", 0),
168
+ "created_at": repo_info.get("created_at"),
169
+ "size_kb": repo_info.get("size", 0),
170
+ "commit_count": len(commits),
171
+ "pr_count": len(pull_requests),
172
+ "issue_count": len(issues),
173
+ "contributor_count": len(contributors),
174
+ "languages_json": languages,
175
+ }
176
+
177
+
178
+ def sync_installation_to_supabase(firebase_id: str, org: str, installation_id: int) -> int:
179
+ if not github_auth:
180
+ logger.error("GitHub App not configured")
181
+ raise HTTPException(status_code=500, detail="GitHub App not configured")
182
+ if not supabase:
183
+ logger.error("Supabase not configured")
184
+ raise HTTPException(status_code=500, detail="Supabase not configured")
185
+
186
+ try:
187
+ logger.info(f"Starting sync for firebase_id={firebase_id}, org={org}, installation_id={installation_id}")
188
+
189
+ # Check if user exists
190
+ logger.info(f"Checking if user exists with firebase_id={firebase_id}")
191
+ result = supabase.table("Users").select("firebase_id").eq("firebase_id", firebase_id).limit(1).execute()
192
+ if not result.data:
193
+ logger.error(f"User not found in Supabase for firebase_id={firebase_id}")
194
+ return 0
195
+
196
+ token = token_manager.get_token(installation_id)
197
+ repos = github_auth.get_installation_repositories(token)
198
+ logger.info(f"Found {len(repos)} repositories for {org}")
199
+
200
+ insights = GitHubInsights(token)
201
+
202
+ # Build repo rollups
203
+ rows = []
204
+ for i, repo in enumerate(repos):
205
+ try:
206
+ repo_name = repo.get("name")
207
+ logger.info(f"Processing repo {i+1}/{len(repos)}: {repo_name}")
208
+ rollup = build_repo_rollup(org, repo_name, insights)
209
+ rollup["firebase_id"] = firebase_id
210
+ rollup["installation_id"] = installation_id
211
+ rows.append(rollup)
212
+ except Exception as e:
213
+ logger.warning(f"Failed to process repo {repo_name}: {str(e)}")
214
+ continue
215
+
216
+ if rows:
217
+ logger.info(f"Upserting {len(rows)} repos into github table")
218
+ supabase.table("github").upsert(rows, on_conflict="firebase_id,full_name").execute()
219
+ logger.info(f"Successfully synced {len(rows)} repos to github table")
220
+
221
+ # Now sync detail data for each repo
222
+ for i, repo in enumerate(repos):
223
+ try:
224
+ repo_name = repo.get("name")
225
+ full_name = f"{org}/{repo_name}"
226
+ logger.info(f"Syncing details for repo {i+1}/{len(repos)}: {repo_name}")
227
+
228
+ # Get github_id
229
+ github_result = supabase.table("github").select("id").eq("firebase_id", firebase_id).eq("full_name", full_name).limit(1).execute()
230
+ if not github_result.data:
231
+ logger.warning(f"Could not find {full_name} in github table, skipping details")
232
+ continue
233
+
234
+ github_id = github_result.data[0]["id"]
235
+
236
+ # Sync commits
237
+ try:
238
+ commits = insights.get_commits(org, repo_name, per_page=100)
239
+ commit_rows = []
240
+ for commit in commits:
241
+ commit_data = commit.get("commit", {})
242
+ author_data = commit_data.get("author", {})
243
+ commit_rows.append({
244
+ "github_id": github_id,
245
+ "firebase_id": firebase_id,
246
+ "sha": commit.get("sha"),
247
+ "message": commit_data.get("message"),
248
+ "author": author_data.get("name"),
249
+ "author_email": author_data.get("email"),
250
+ "committed_date": author_data.get("date"),
251
+ })
252
+ if commit_rows:
253
+ supabase.table("commits").upsert(commit_rows, on_conflict="firebase_id,sha").execute()
254
+ logger.info(f" Stored {len(commit_rows)} commits")
255
+ except Exception as e:
256
+ logger.warning(f" Failed to sync commits: {str(e)}")
257
+
258
+ # Sync issues
259
+ try:
260
+ issues = insights.get_issues(org, repo_name, state="all")
261
+ issue_rows = []
262
+ for issue in issues:
263
+ if "pull_request" in issue:
264
+ continue
265
+ issue_rows.append({
266
+ "github_id": github_id,
267
+ "firebase_id": firebase_id,
268
+ "issue_id": issue.get("id"),
269
+ "issue_number": issue.get("number"),
270
+ "title": issue.get("title"),
271
+ "body": issue.get("body"),
272
+ "state": issue.get("state"),
273
+ "author": issue.get("user", {}).get("login"),
274
+ "created_at": issue.get("created_at"),
275
+ "updated_at": issue.get("updated_at"),
276
+ "url": issue.get("html_url"),
277
+ "labels": [label.get("name") for label in issue.get("labels", [])],
278
+ "comments": issue.get("comments", 0),
279
+ "reactions": issue.get("reactions", {}).get("total_count", 0),
280
+ })
281
+ if issue_rows:
282
+ supabase.table("issues").upsert(issue_rows, on_conflict="firebase_id,issue_id").execute()
283
+ logger.info(f" Stored {len(issue_rows)} issues")
284
+ except Exception as e:
285
+ logger.warning(f" Failed to sync issues: {str(e)}")
286
+
287
+ # Sync pull requests
288
+ try:
289
+ prs = insights.get_pull_requests(org, repo_name, state="all")
290
+ pr_rows = []
291
+ for pr in prs:
292
+ pr_rows.append({
293
+ "github_id": github_id,
294
+ "firebase_id": firebase_id,
295
+ "pr_id": pr.get("id"),
296
+ "pr_number": pr.get("number"),
297
+ "title": pr.get("title"),
298
+ "body": pr.get("body"),
299
+ "state": pr.get("state"),
300
+ "author": pr.get("user", {}).get("login"),
301
+ "created_at": pr.get("created_at"),
302
+ "updated_at": pr.get("updated_at"),
303
+ "merged_at": pr.get("merged_at"),
304
+ "closed_at": pr.get("closed_at"),
305
+ "url": pr.get("html_url"),
306
+ "head_branch": pr.get("head", {}).get("ref"),
307
+ "base_branch": pr.get("base", {}).get("ref"),
308
+ "draft": pr.get("draft", False),
309
+ "mergeable": pr.get("mergeable"),
310
+ "comments": pr.get("comments", 0),
311
+ "commits": pr.get("commits", 0),
312
+ "additions": pr.get("additions", 0),
313
+ "deletions": pr.get("deletions", 0),
314
+ "changed_files": pr.get("changed_files", 0),
315
+ })
316
+ if pr_rows:
317
+ supabase.table("pull_requests").upsert(pr_rows, on_conflict="firebase_id,pr_id").execute()
318
+ logger.info(f" Stored {len(pr_rows)} pull requests")
319
+ except Exception as e:
320
+ logger.warning(f" Failed to sync pull requests: {str(e)}")
321
+
322
+ # Sync contributors
323
+ try:
324
+ contributors = insights.get_contributors(org, repo_name)
325
+ contributor_rows = []
326
+ for contributor in contributors:
327
+ contributor_rows.append({
328
+ "github_id": github_id,
329
+ "firebase_id": firebase_id,
330
+ "username": contributor.get("login"),
331
+ "contributions": contributor.get("contributions", 0),
332
+ "avatar_url": contributor.get("avatar_url"),
333
+ "profile_url": contributor.get("html_url"),
334
+ })
335
+ if contributor_rows:
336
+ supabase.table("contributors").upsert(contributor_rows, on_conflict="firebase_id,github_id,username").execute()
337
+ logger.info(f" Stored {len(contributor_rows)} contributors")
338
+ except Exception as e:
339
+ logger.warning(f" Failed to sync contributors: {str(e)}")
340
+
341
+ except Exception as e:
342
+ logger.warning(f"Failed to sync details for {repo_name}: {str(e)}")
343
+ continue
344
+
345
+ logger.info(f"Completed full sync for {len(rows)} repos")
346
+ else:
347
+ logger.warning("No repos to sync")
348
+
349
+ return len(rows)
350
+ except Exception as e:
351
+ logger.error(f"Sync failed: {str(e)}", exc_info=True)
352
+ return 0
353
+
354
+
355
  # ============================================================================
356
  # API ENDPOINTS
357
  # ============================================================================
 
377
 
378
 
379
  @app.get("/github/callback")
380
+ async def github_callback(request: Request, background_tasks: BackgroundTasks):
381
  """
382
  Handle GitHub App installation callback
383
 
384
+ After org installs Codesage, GitHub redirects here with installation_id and state (firebase_uid)
385
+ Frontend should redirect to: https://github.com/apps/{app_slug}/installations/new?state={firebase_uid}
386
+ GitHub redirects back with: /github/callback?installation_id=...&state={firebase_uid}
387
  """
388
  # Get query parameters
389
  query_params = dict(request.query_params)
390
 
391
  installation_id = query_params.get("installation_id")
392
  setup_action = query_params.get("setup_action")
393
+ firebase_id = query_params.get("state") # GitHub passes firebase_uid via state parameter
394
+
395
+ # Fallback to default if state not provided (for backward compatibility)
396
+ if not firebase_id:
397
+ logger.warning("No firebase_id in state parameter, using default")
398
+ firebase_id = DEFAULT_FIREBASE_ID
399
 
400
  if not installation_id:
401
  return {
 
435
  # Save with org name if we got it, otherwise use placeholder
436
  if org_name:
437
  save_installation(org_name, installation_id)
438
+ sync_result = None
439
+ if supabase:
440
+ # Run heavy sync in background so callback returns immediately
441
+ background_tasks.add_task(
442
+ sync_installation_to_supabase,
443
+ firebase_id, # Use firebase_id from state parameter
444
+ org_name,
445
+ installation_id
446
+ )
447
+ sync_result = {
448
+ "queued": True,
449
+ "firebase_id": firebase_id
450
+ }
451
+
452
  return {
453
  "success": True,
454
  "message": f"✅ Successfully installed Codesage for {org_name}",
455
  "org": org_name,
456
+ "installation_id": installation_id,
457
+ "supabase_sync": sync_result
458
  }
459
  else:
460
  # Save with placeholder - user can update via /github/install
 
555
  @app.post("/insights/commits")
556
  def get_commits(data: InsightsRequest):
557
  """
558
+ Get commit history for a repository and store in Supabase
559
+ firebase_id is required and will be passed in request body
560
  """
561
  if not github_auth:
562
  raise HTTPException(status_code=500, detail="GitHub App not configured")
563
+ if not supabase:
564
+ raise HTTPException(status_code=500, detail="Supabase not configured")
565
 
566
  installation_id = get_installation_id(data.org)
567
  if not installation_id:
568
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
569
 
570
  try:
571
+ # Fetch commits from GitHub
572
  token = token_manager.get_token(installation_id)
573
  insights = GitHubInsights(token)
574
+ commits = insights.get_commits(data.org, data.repo, per_page=100)
575
+
576
+ # Get github table id for this repo
577
+ full_name = f"{data.org}/{data.repo}"
578
+ github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute()
579
+
580
+ if not github_result.data:
581
+ raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user")
582
+
583
+ github_id = github_result.data[0]["id"]
584
+
585
+ # Transform and store commits
586
+ commit_rows = []
587
+ for commit in commits:
588
+ commit_data = commit.get("commit", {})
589
+ author_data = commit_data.get("author", {})
590
+
591
+ commit_rows.append({
592
+ "github_id": github_id,
593
+ "firebase_id": data.firebase_id,
594
+ "sha": commit.get("sha"),
595
+ "message": commit_data.get("message"),
596
+ "author": author_data.get("name"),
597
+ "author_email": author_data.get("email"),
598
+ "committed_date": author_data.get("date"),
599
+ })
600
+
601
+ if commit_rows:
602
+ supabase.table("commits").upsert(commit_rows, on_conflict="firebase_id,sha").execute()
603
+ logger.info(f"Stored {len(commit_rows)} commits for {full_name}")
604
 
605
  return {
606
  "org": data.org,
607
  "repo": data.repo,
608
  "count": len(commits),
609
+ "stored": len(commit_rows),
610
  "commits": commits
611
  }
612
  except Exception as e:
613
+ logger.error(f"Failed to fetch/store commits: {str(e)}")
614
  raise HTTPException(status_code=500, detail=f"Failed to fetch commits: {str(e)}")
615
 
616
 
617
  @app.post("/insights/pull-requests")
618
  def get_pull_requests(data: InsightsRequest):
619
+ """Get pull requests for a repository and store in Supabase
620
+ firebase_id is required and will be passed in request body
621
+ """
622
  if not github_auth:
623
  raise HTTPException(status_code=500, detail="GitHub App not configured")
624
+ if not supabase:
625
+ raise HTTPException(status_code=500, detail="Supabase not configured")
626
 
627
  installation_id = get_installation_id(data.org)
628
  if not installation_id:
629
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
630
 
631
  try:
632
+ # Fetch pull requests from GitHub
633
  token = token_manager.get_token(installation_id)
634
  insights = GitHubInsights(token)
635
+ prs = insights.get_pull_requests(data.org, data.repo, state="all")
636
+
637
+ # Get github table id for this repo
638
+ full_name = f"{data.org}/{data.repo}"
639
+ github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute()
640
+
641
+ if not github_result.data:
642
+ raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user")
643
+
644
+ github_id = github_result.data[0]["id"]
645
+
646
+ # Transform and store pull requests
647
+ pr_rows = []
648
+ for pr in prs:
649
+ pr_rows.append({
650
+ "github_id": github_id,
651
+ "firebase_id": data.firebase_id,
652
+ "pr_id": pr.get("id"),
653
+ "pr_number": pr.get("number"),
654
+ "title": pr.get("title"),
655
+ "body": pr.get("body"),
656
+ "state": pr.get("state"),
657
+ "author": pr.get("user", {}).get("login"),
658
+ "created_at": pr.get("created_at"),
659
+ "updated_at": pr.get("updated_at"),
660
+ "merged_at": pr.get("merged_at"),
661
+ "closed_at": pr.get("closed_at"),
662
+ "url": pr.get("html_url"),
663
+ "head_branch": pr.get("head", {}).get("ref"),
664
+ "base_branch": pr.get("base", {}).get("ref"),
665
+ "draft": pr.get("draft", False),
666
+ "mergeable": pr.get("mergeable"),
667
+ "comments": pr.get("comments", 0),
668
+ "commits": pr.get("commits", 0),
669
+ "additions": pr.get("additions", 0),
670
+ "deletions": pr.get("deletions", 0),
671
+ "changed_files": pr.get("changed_files", 0),
672
+ })
673
+
674
+ if pr_rows:
675
+ supabase.table("pull_requests").upsert(pr_rows, on_conflict="firebase_id,pr_id").execute()
676
+ logger.info(f"Stored {len(pr_rows)} pull requests for {full_name}")
677
 
678
  return {
679
  "org": data.org,
680
  "repo": data.repo,
681
  "count": len(prs),
682
+ "stored": len(pr_rows),
683
  "pull_requests": prs
684
  }
685
  except Exception as e:
686
+ logger.error(f"Failed to fetch/store pull requests: {str(e)}")
687
  raise HTTPException(status_code=500, detail=f"Failed to fetch pull requests: {str(e)}")
688
 
689
 
690
  @app.post("/insights/issues")
691
  def get_issues(data: InsightsRequest):
692
+ """Get issues for a repository and store in Supabase
693
+ firebase_id is required and will be passed in request body
694
+ """
695
  if not github_auth:
696
  raise HTTPException(status_code=500, detail="GitHub App not configured")
697
+ if not supabase:
698
+ raise HTTPException(status_code=500, detail="Supabase not configured")
699
 
700
  installation_id = get_installation_id(data.org)
701
  if not installation_id:
702
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
703
 
704
  try:
705
+ # Fetch issues from GitHub
706
  token = token_manager.get_token(installation_id)
707
  insights = GitHubInsights(token)
708
+ issues = insights.get_issues(data.org, data.repo, state="all")
709
+
710
+ # Get github table id for this repo
711
+ full_name = f"{data.org}/{data.repo}"
712
+ github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute()
713
+
714
+ if not github_result.data:
715
+ raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user")
716
+
717
+ github_id = github_result.data[0]["id"]
718
+
719
+ # Transform and store issues
720
+ issue_rows = []
721
+ for issue in issues:
722
+ # Skip pull requests (they appear in issues API too)
723
+ if "pull_request" in issue:
724
+ continue
725
+
726
+ issue_rows.append({
727
+ "github_id": github_id,
728
+ "firebase_id": data.firebase_id,
729
+ "issue_id": issue.get("id"),
730
+ "issue_number": issue.get("number"),
731
+ "title": issue.get("title"),
732
+ "body": issue.get("body"),
733
+ "state": issue.get("state"),
734
+ "author": issue.get("user", {}).get("login"),
735
+ "created_at": issue.get("created_at"),
736
+ "updated_at": issue.get("updated_at"),
737
+ "url": issue.get("html_url"),
738
+ "labels": [label.get("name") for label in issue.get("labels", [])],
739
+ "comments": issue.get("comments", 0),
740
+ "reactions": issue.get("reactions", {}).get("total_count", 0),
741
+ })
742
+
743
+ if issue_rows:
744
+ supabase.table("issues").upsert(issue_rows, on_conflict="firebase_id,issue_id").execute()
745
+ logger.info(f"Stored {len(issue_rows)} issues for {full_name}")
746
 
747
  return {
748
  "org": data.org,
749
  "repo": data.repo,
750
  "count": len(issues),
751
+ "stored": len(issue_rows),
752
  "issues": issues
753
  }
754
  except Exception as e:
755
+ logger.error(f"Failed to fetch/store issues: {str(e)}")
756
  raise HTTPException(status_code=500, detail=f"Failed to fetch issues: {str(e)}")
757
 
758
 
759
+ @app.post("/github/sync")
760
+ def sync_github_to_supabase(data: GitHubSyncRequest):
761
+ """
762
+ Sync GitHub repo data and rollup stats into Supabase
763
+ Trigger this after user creation with firebase_id
764
+ """
765
+ if not github_auth:
766
+ raise HTTPException(status_code=500, detail="GitHub App not configured")
767
+ if not supabase:
768
+ raise HTTPException(status_code=500, detail="Supabase not configured")
769
+
770
+ installation_id = get_installation_id(data.org)
771
+ if not installation_id:
772
+ raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
773
+
774
+ try:
775
+ synced = sync_installation_to_supabase(data.firebase_id, data.org, installation_id)
776
+ return {
777
+ "success": True,
778
+ "org": data.org,
779
+ "synced_repos": synced
780
+ }
781
+ except Exception as e:
782
+ raise HTTPException(status_code=500, detail=f"Failed to sync GitHub data: {str(e)}")
783
+
784
+
785
  @app.post("/insights/contributors")
786
  def get_contributors(data: InsightsRequest):
787
+ """Get contributors and their stats, store in Supabase
788
+ firebase_id is required and will be passed in request body
789
+ """
790
  if not github_auth:
791
  raise HTTPException(status_code=500, detail="GitHub App not configured")
792
+ if not supabase:
793
+ raise HTTPException(status_code=500, detail="Supabase not configured")
794
 
795
  installation_id = get_installation_id(data.org)
796
  if not installation_id:
797
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
798
 
799
  try:
800
+ # Fetch contributors from GitHub
801
  token = token_manager.get_token(installation_id)
802
  insights = GitHubInsights(token)
803
  contributors = insights.get_contributors(data.org, data.repo)
804
 
805
+ # Get github table id for this repo
806
+ full_name = f"{data.org}/{data.repo}"
807
+ github_result = supabase.table("github").select("id").eq("firebase_id", data.firebase_id).eq("full_name", full_name).limit(1).execute()
808
+
809
+ if not github_result.data:
810
+ raise HTTPException(status_code=404, detail=f"Repo {full_name} not found in github table for this user")
811
+
812
+ github_id = github_result.data[0]["id"]
813
+
814
+ # Transform and store contributors
815
+ contributor_rows = []
816
+ for contributor in contributors:
817
+ contributor_rows.append({
818
+ "github_id": github_id,
819
+ "firebase_id": data.firebase_id,
820
+ "username": contributor.get("login"),
821
+ "contributions": contributor.get("contributions", 0),
822
+ "avatar_url": contributor.get("avatar_url"),
823
+ "profile_url": contributor.get("html_url"),
824
+ })
825
+
826
+ if contributor_rows:
827
+ supabase.table("contributors").upsert(contributor_rows, on_conflict="firebase_id,github_id,username").execute()
828
+ logger.info(f"Stored {len(contributor_rows)} contributors for {full_name}")
829
+
830
  return {
831
  "org": data.org,
832
  "repo": data.repo,
833
  "count": len(contributors),
834
+ "stored": len(contributor_rows),
835
  "contributors": contributors
836
  }
837
  except Exception as e:
838
+ logger.error(f"Failed to fetch/store contributors: {str(e)}")
839
  raise HTTPException(status_code=500, detail=f"Failed to fetch contributors: {str(e)}")
840
 
841
 
 
895
  raise HTTPException(status_code=404, detail=f"No installation found for {data.org}")
896
 
897
  try:
898
+ logger.info(f"Fetching activity for {data.org}/{data.repo}")
899
  token = token_manager.get_token(installation_id)
900
  insights = GitHubInsights(token)
901
 
902
+ logger.info("Getting code frequency...")
903
  code_frequency = insights.get_code_frequency(data.org, data.repo)
904
+ logger.info(f"Code frequency returned: {len(code_frequency) if isinstance(code_frequency, list) else 'not a list'}")
905
+
906
+ logger.info("Getting commit activity...")
907
  commit_activity = insights.get_commit_activity(data.org, data.repo)
908
+ logger.info(f"Commit activity returned: {len(commit_activity) if isinstance(commit_activity, list) else 'not a list'}")
909
 
910
  return {
911
  "org": data.org,
 
914
  "commit_activity": commit_activity
915
  }
916
  except Exception as e:
917
+ logger.error(f"Failed to fetch activity: {str(e)}", exc_info=True)
918
  raise HTTPException(status_code=500, detail=f"Failed to fetch activity: {str(e)}")
919
 
920
 
921
+ @app.post("/github/sync/test")
922
+ def test_sync(data: GitHubSyncRequest):
923
+ """
924
+ Test endpoint to debug sync issues
925
+ Shows what's happening without background task
926
+ """
927
+ logger.info(f"Test sync called for firebase_id={data.firebase_id}, org={data.org}")
928
+
929
+ installation_id = get_installation_id(data.org)
930
+ if not installation_id:
931
+ return {
932
+ "success": False,
933
+ "error": f"No installation found for {data.org}",
934
+ "available_orgs": list(load_installations().keys())
935
+ }
936
+
937
+ try:
938
+ synced = sync_installation_to_supabase(data.firebase_id, data.org, installation_id)
939
+ return {
940
+ "success": True,
941
+ "org": data.org,
942
+ "firebase_id": data.firebase_id,
943
+ "synced_repos": synced
944
+ }
945
+ except Exception as e:
946
+ logger.error(f"Test sync failed: {str(e)}", exc_info=True)
947
+ return {
948
+ "success": False,
949
+ "error": str(e),
950
+ "org": data.org,
951
+ "firebase_id": data.firebase_id
952
+ }
953
 
954
 
955