Spaces:
Sleeping
Sleeping
Commit ·
2cc8683
1
Parent(s): 8a2fb1b
apply changes
Browse files- src/app/api/sync/run/route.ts +102 -29
- src/app/api/sync/status/route.ts +51 -0
- src/db/migrations/add_sync_status.sql +16 -0
- src/db/schema.ts +3 -0
src/app/api/sync/run/route.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
| 8 |
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
import { runFullSync, runMaintainerSync, runContributorSync, reconcileOpenTriageIssue1, syncContributorPRsDirect, SYNC_INTERVAL_MS } from "@/lib/sync/github-sync";
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
export async function POST(request: NextRequest) {
|
| 13 |
try {
|
|
@@ -20,37 +23,107 @@ export async function POST(request: NextRequest) {
|
|
| 20 |
return NextResponse.json({ error: "GitHub access token not found" }, { status: 400 });
|
| 21 |
}
|
| 22 |
|
| 23 |
-
//
|
| 24 |
-
const
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
} else {
|
| 32 |
-
// Contributors sync only their authored PRs
|
| 33 |
-
// First: Run search-based sync (may have indexing delay)
|
| 34 |
-
stats = await runContributorSync(user.id, user.username, user.githubAccessToken);
|
| 35 |
-
|
| 36 |
-
// Second: Direct fetch from repos where user has existing PRs (bypasses search delay)
|
| 37 |
-
directSync = await syncContributorPRsDirect(user.id, user.username, user.githubAccessToken);
|
| 38 |
}
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
} catch (error) {
|
| 55 |
console.error("POST /api/sync/run error:", error);
|
| 56 |
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
|
|
| 8 |
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
import { runFullSync, runMaintainerSync, runContributorSync, reconcileOpenTriageIssue1, syncContributorPRsDirect, SYNC_INTERVAL_MS } from "@/lib/sync/github-sync";
|
| 11 |
+
import { db } from "@/db";
|
| 12 |
+
import { users } from "@/db/schema";
|
| 13 |
+
import { eq } from "drizzle-orm";
|
| 14 |
|
| 15 |
export async function POST(request: NextRequest) {
|
| 16 |
try {
|
|
|
|
| 23 |
return NextResponse.json({ error: "GitHub access token not found" }, { status: 400 });
|
| 24 |
}
|
| 25 |
|
| 26 |
+
// Get current user record to check sync status
|
| 27 |
+
const userRecord = await db.select()
|
| 28 |
+
.from(users)
|
| 29 |
+
.where(eq(users.id, user.id))
|
| 30 |
+
.limit(1);
|
| 31 |
+
|
| 32 |
+
if (!userRecord || userRecord.length === 0) {
|
| 33 |
+
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
+
const currentSyncStatus = userRecord[0].syncStatus;
|
| 37 |
+
|
| 38 |
+
// Circuit breaker: Check if sync already in progress
|
| 39 |
+
if (currentSyncStatus === 'SYNCING') {
|
| 40 |
+
console.log(`[SyncRun] User ${user.id} sync already in progress`);
|
| 41 |
+
return NextResponse.json({
|
| 42 |
+
error: "Sync already in progress",
|
| 43 |
+
status: "SYNCING",
|
| 44 |
+
message: "Please wait for the current sync to complete"
|
| 45 |
+
}, { status: 429 });
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if (currentSyncStatus === 'PENDING') {
|
| 49 |
+
console.log(`[SyncRun] User ${user.id} sync is pending`);
|
| 50 |
+
return NextResponse.json({
|
| 51 |
+
error: "Sync is queued",
|
| 52 |
+
status: "PENDING",
|
| 53 |
+
message: "Sync request is queued and will start shortly"
|
| 54 |
+
}, { status: 202 });
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Set status to SYNCING before starting
|
| 58 |
+
await db.update(users)
|
| 59 |
+
.set({
|
| 60 |
+
syncStatus: 'SYNCING',
|
| 61 |
+
syncError: null
|
| 62 |
+
})
|
| 63 |
+
.where(eq(users.id, user.id));
|
| 64 |
+
|
| 65 |
+
console.log(`[SyncRun] Starting sync for user ${user.id}`);
|
| 66 |
+
|
| 67 |
+
try {
|
| 68 |
+
// Determine sync type based on user role
|
| 69 |
+
const userRole = user.role?.toUpperCase();
|
| 70 |
+
let stats;
|
| 71 |
+
let directSync = null;
|
| 72 |
+
|
| 73 |
+
if (userRole === "MAINTAINER") {
|
| 74 |
+
// Maintainers sync all open issues/PRs
|
| 75 |
+
stats = await runMaintainerSync(user.id, user.githubAccessToken);
|
| 76 |
+
} else {
|
| 77 |
+
// Contributors sync only their authored PRs
|
| 78 |
+
// First: Run search-based sync (may have indexing delay)
|
| 79 |
+
stats = await runContributorSync(user.id, user.username, user.githubAccessToken);
|
| 80 |
+
|
| 81 |
+
// Second: Direct fetch from repos where user has existing PRs (bypasses search delay)
|
| 82 |
+
directSync = await syncContributorPRsDirect(user.id, user.username, user.githubAccessToken);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Always reconcile the critical openTriage#1 issue to ensure immediate state sync
|
| 86 |
+
const reconcileResult = await reconcileOpenTriageIssue1(user.githubAccessToken);
|
| 87 |
+
|
| 88 |
+
// Mark sync as COMPLETED
|
| 89 |
+
await db.update(users)
|
| 90 |
+
.set({
|
| 91 |
+
syncStatus: 'COMPLETED',
|
| 92 |
+
lastSyncAt: new Date().toISOString(),
|
| 93 |
+
syncError: null
|
| 94 |
+
})
|
| 95 |
+
.where(eq(users.id, user.id));
|
| 96 |
+
|
| 97 |
+
console.log(`[SyncRun] Sync completed for user ${user.id}`);
|
| 98 |
+
|
| 99 |
+
return NextResponse.json({
|
| 100 |
+
success: true,
|
| 101 |
+
message: "Sync completed",
|
| 102 |
+
role: userRole,
|
| 103 |
+
stats,
|
| 104 |
+
directSync, // Include direct sync results for contributors
|
| 105 |
+
reconcile: {
|
| 106 |
+
openTriageIssue1: reconcileResult,
|
| 107 |
+
},
|
| 108 |
+
nextSyncIntervalMs: SYNC_INTERVAL_MS,
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
} catch (syncError: any) {
|
| 112 |
+
// Mark sync as FAILED with error message
|
| 113 |
+
await db.update(users)
|
| 114 |
+
.set({
|
| 115 |
+
syncStatus: 'FAILED',
|
| 116 |
+
syncError: syncError.message || 'Unknown error'
|
| 117 |
+
})
|
| 118 |
+
.where(eq(users.id, user.id));
|
| 119 |
+
|
| 120 |
+
console.error(`[SyncRun] Sync failed for user ${user.id}:`, syncError);
|
| 121 |
+
|
| 122 |
+
return NextResponse.json({
|
| 123 |
+
error: "Sync failed",
|
| 124 |
+
message: syncError.message || "Internal server error"
|
| 125 |
+
}, { status: 500 });
|
| 126 |
+
}
|
| 127 |
} catch (error) {
|
| 128 |
console.error("POST /api/sync/run error:", error);
|
| 129 |
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
src/app/api/sync/status/route.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Sync Status API Route
|
| 3 |
+
*
|
| 4 |
+
* GET /api/sync/status - Check current sync status for the authenticated user
|
| 5 |
+
* Used by frontend to poll sync progress without triggering new syncs
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { db } from "@/db";
|
| 11 |
+
import { users } from "@/db/schema";
|
| 12 |
+
import { eq } from "drizzle-orm";
|
| 13 |
+
|
| 14 |
+
export async function GET(request: NextRequest) {
|
| 15 |
+
try {
|
| 16 |
+
const user = await getCurrentUser(request);
|
| 17 |
+
if (!user) {
|
| 18 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Get user's sync status
|
| 22 |
+
const userRecord = await db.select({
|
| 23 |
+
syncStatus: users.syncStatus,
|
| 24 |
+
lastSyncAt: users.lastSyncAt,
|
| 25 |
+
syncError: users.syncError,
|
| 26 |
+
})
|
| 27 |
+
.from(users)
|
| 28 |
+
.where(eq(users.id, user.id))
|
| 29 |
+
.limit(1);
|
| 30 |
+
|
| 31 |
+
if (!userRecord || userRecord.length === 0) {
|
| 32 |
+
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const { syncStatus, lastSyncAt, syncError } = userRecord[0];
|
| 36 |
+
|
| 37 |
+
return NextResponse.json({
|
| 38 |
+
status: syncStatus || 'IDLE',
|
| 39 |
+
lastSyncAt: lastSyncAt || null,
|
| 40 |
+
error: syncError || null,
|
| 41 |
+
isIdl: syncStatus === 'IDLE' || syncStatus === 'COMPLETED',
|
| 42 |
+
isSyncing: syncStatus === 'SYNCING',
|
| 43 |
+
isPending: syncStatus === 'PENDING',
|
| 44 |
+
hasFailed: syncStatus === 'FAILED',
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
} catch (error) {
|
| 48 |
+
console.error("GET /api/sync/status error:", error);
|
| 49 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 50 |
+
}
|
| 51 |
+
}
|
src/db/migrations/add_sync_status.sql
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- Migration: Add sync status tracking to users table
|
| 2 |
+
-- Date: 2026-02-12
|
| 3 |
+
-- Purpose: Prevent concurrent sync requests and track sync history
|
| 4 |
+
|
| 5 |
+
-- Add sync status columns to users table
|
| 6 |
+
ALTER TABLE users ADD COLUMN sync_status TEXT DEFAULT 'IDLE';
|
| 7 |
+
ALTER TABLE users ADD COLUMN last_sync_at TEXT;
|
| 8 |
+
ALTER TABLE users ADD COLUMN sync_error TEXT;
|
| 9 |
+
|
| 10 |
+
-- Create index for faster sync status queries
|
| 11 |
+
CREATE INDEX IF NOT EXISTS idx_users_sync_status ON users(sync_status);
|
| 12 |
+
|
| 13 |
+
-- Comments
|
| 14 |
+
-- sync_status: IDLE | PENDING | SYNCING | COMPLETED | FAILED
|
| 15 |
+
-- last_sync_at: ISO 8601 timestamp of last successful sync
|
| 16 |
+
-- sync_error: Error message if last sync failed
|
src/db/schema.ts
CHANGED
|
@@ -38,6 +38,9 @@ export const users = sqliteTable("users", {
|
|
| 38 |
avatarUrl: text("avatar_url").notNull(),
|
| 39 |
role: text("role"), // UserRole enum
|
| 40 |
githubAccessToken: text("github_access_token"),
|
|
|
|
|
|
|
|
|
|
| 41 |
createdAt: text("created_at").notNull(),
|
| 42 |
updatedAt: text("updated_at").notNull(),
|
| 43 |
});
|
|
|
|
| 38 |
avatarUrl: text("avatar_url").notNull(),
|
| 39 |
role: text("role"), // UserRole enum
|
| 40 |
githubAccessToken: text("github_access_token"),
|
| 41 |
+
syncStatus: text("sync_status").default("IDLE"), // IDLE, PENDING, SYNCING, COMPLETED, FAILED
|
| 42 |
+
lastSyncAt: text("last_sync_at"), // Timestamp of last successful sync
|
| 43 |
+
syncError: text("sync_error"), // Error message if sync failed
|
| 44 |
createdAt: text("created_at").notNull(),
|
| 45 |
updatedAt: text("updated_at").notNull(),
|
| 46 |
});
|