@@ -3,6 +3,8 @@ import yaml from "js-yaml";
33import { fetchGitHubUserData } from "@/lib/github" ;
44import { calculateUserScore } from "@/lib/score" ;
55import { createCacheStore , getCacheConfigFromEnv } from "@/lib/cache-store" ;
6+ import { getDatabaseStore } from "@/lib/db-store" ;
7+ import { detectCountry } from "@/lib/location-detector" ;
68
79export const runtime = "nodejs" ;
810
@@ -48,6 +50,25 @@ function buildLeaderboardCacheKey(country: string, namespace: string): string {
4850 return `${ namespace } :leaderboard:${ country . trim ( ) . toLowerCase ( ) } ` ;
4951}
5052
53+ function getEnvInt ( key : string , fallback : number ) : number {
54+ const raw = process . env [ key ] ?. trim ( ) ;
55+ if ( ! raw ) return fallback ;
56+ const parsed = Number . parseInt ( raw , 10 ) ;
57+ return Number . isFinite ( parsed ) && parsed > 0 ? parsed : fallback ;
58+ }
59+
60+ function getStaleDays ( ) : number {
61+ return getEnvInt ( "LEADERBOARD_USER_STALE_DAYS" , 30 ) ;
62+ }
63+
64+ function getSeedLimit ( ) : number {
65+ return getEnvInt ( "LEADERBOARD_SEED_LIMIT" , 256 ) ;
66+ }
67+
68+ function getRefreshLimit ( ) : number {
69+ return getEnvInt ( "LEADERBOARD_REFRESH_LIMIT" , 500 ) ;
70+ }
71+
5172// ─── POST handler ──────────────────────────────────────────────────────
5273
5374export async function POST ( request : Request ) {
@@ -61,6 +82,10 @@ export async function POST(request: Request) {
6182 ) ;
6283 }
6384
85+ // Ensure database schema exists
86+ const db = getDatabaseStore ( ) ;
87+ await db . initializeSchema ( ) ;
88+
6489 // ── 1. Fetch committers from committers.top ──────────────────────────
6590 let committersData : {
6691 title : string ;
@@ -110,63 +135,144 @@ export async function POST(request: Request) {
110135 ) ;
111136 }
112137
113- // ── 2. Calculate scores for all users ────────────────────────────────
114- const scored : ScoredEntry [ ] = [ ] ;
138+ // ── 2a. Seed NEW users from committers.top ───────────────────────────
115139 const errors : string [ ] = [ ] ;
140+ const seedLimit = getSeedLimit ( ) ;
141+ const usersToSeed = committersData . users . slice ( 0 , seedLimit ) ;
142+ let newUsersCount = 0 ;
143+ let skippedExistingCount = 0 ;
116144
117- // Build rank lookup from original committers data
118- const rankMap = new Map (
119- committersData . users . map ( ( u ) => [
120- u . login ,
121- { rank : u . rank , contributions : u . contributions } ,
122- ] ) ,
123- ) ;
124-
125- for ( const user of committersData . users ) {
145+ for ( const user of usersToSeed ) {
126146 try {
147+ const exists = await db . userExists ( user . login ) ;
148+ if ( exists ) {
149+ skippedExistingCount += 1 ;
150+ continue ;
151+ }
152+
127153 const data = await fetchGitHubUserData ( user . login ) ;
128154 const score = calculateUserScore ( data , user . login ) ;
129- const original = rankMap . get ( user . login ) ?? { rank : 0 , contributions : 0 } ;
130- scored . push ( {
131- username : user . login ,
155+ const countryDetected = detectCountry ( data . location ) ;
156+
157+ await db . upsertUser ( {
158+ username : data . login ,
132159 name : data . name ,
133160 avatarUrl : data . avatarUrl ,
161+ location : data . location ,
162+ country : countryDetected ,
163+ rawData : data ,
164+ scores : score ,
134165 repoScore : Math . round ( score . repoScore ) ,
135166 prScore : Math . round ( score . prScore ) ,
136167 contributionScore : Math . round ( score . contributionScore ) ,
137168 finalScore : Math . round ( score . finalScore ) ,
138- originalRank : original . rank ,
139- originalContributions : original . contributions ,
140- impactRank : 0 ,
169+ staleDays : getStaleDays ( ) ,
141170 } ) ;
171+
172+ newUsersCount += 1 ;
142173 } catch {
143174 errors . push ( user . login ) ;
144175 }
145176 }
146177
147- // Sort by finalScore descending and assign impactRank
178+ // ── 2b. Refresh STALE users from DB top N ───────────────────────────
179+ const refreshLimit = getRefreshLimit ( ) ;
180+ const topUsers = await db . getTopUsers ( country , refreshLimit ) ;
181+ let refreshedCount = 0 ;
182+
183+ for ( const row of topUsers ) {
184+ // Check if stale
185+ if ( row . stale_after >= new Date ( ) ) {
186+ continue ; // still fresh
187+ }
188+
189+ try {
190+ const data = await fetchGitHubUserData ( row . username ) ;
191+ const score = calculateUserScore ( data , row . username ) ;
192+ const countryDetected = detectCountry ( data . location ) ;
193+
194+ await db . upsertUser ( {
195+ username : data . login ,
196+ name : data . name ,
197+ avatarUrl : data . avatarUrl ,
198+ location : data . location ,
199+ country : countryDetected ,
200+ rawData : data ,
201+ scores : score ,
202+ repoScore : Math . round ( score . repoScore ) ,
203+ prScore : Math . round ( score . prScore ) ,
204+ contributionScore : Math . round ( score . contributionScore ) ,
205+ finalScore : Math . round ( score . finalScore ) ,
206+ staleDays : getStaleDays ( ) ,
207+ } ) ;
208+
209+ refreshedCount += 1 ;
210+ } catch {
211+ errors . push ( row . username ) ;
212+ }
213+ }
214+
215+ // ── 3. Build & cache the leaderboard result ──────────────────────────
216+ const allUsers = await db . getLeaderboard ( country , getEnvInt ( "LEADERBOARD_DISPLAY_LIMIT" , 500 ) ) ;
217+ const totalCount = await db . getLeaderboardCount ( country ) ;
218+
219+ // Build rank lookup from original committers data
220+ const rankMap = new Map (
221+ committersData . users . map ( ( u ) => [
222+ u . login ,
223+ { rank : u . rank , contributions : u . contributions } ,
224+ ] ) ,
225+ ) ;
226+
227+ const scored : ScoredEntry [ ] = allUsers . map ( ( row ) => {
228+ const original = rankMap . get ( row . username ) ?? { rank : 0 , contributions : 0 } ;
229+ return {
230+ username : row . username ,
231+ name : row . name ,
232+ avatarUrl : row . avatar_url ,
233+ repoScore : row . repo_score ,
234+ prScore : row . pr_score ,
235+ contributionScore : row . contribution_score ,
236+ finalScore : row . final_score ,
237+ originalRank : original . rank ,
238+ originalContributions : original . contributions ,
239+ impactRank : 0 ,
240+ } ;
241+ } ) ;
242+
243+ // Sort and assign impactRank
148244 scored . sort ( ( a , b ) => b . finalScore - a . finalScore ) ;
149245 scored . forEach ( ( u , idx ) => ( u . impactRank = idx + 1 ) ) ;
150246
151247 const result : LeaderboardResult = {
152248 title : committersData . title ,
153- totalFromSource : committersData . totalFromSource ,
249+ totalFromSource : totalCount ,
154250 scored,
155- errors,
251+ errors : [ ... new Set ( errors ) ] , // deduplicate
156252 } ;
157253
158- // ── 3. Store in Redis cache ───────────────────────────── ─────────────
254+ // ── 4. Invalidate Redis cache (lazy rebuild on next GET) ─────────────
159255 try {
160256 const cacheConfig = getCacheConfigFromEnv ( ) ;
161257 const cacheStore = createCacheStore ( cacheConfig ) ;
162- if ( cacheStore . enabled ) {
258+ if ( cacheStore . enabled && cacheStore . del ) {
163259 const cacheKey = buildLeaderboardCacheKey ( country , cacheConfig . namespace ) ;
164- await cacheStore . set ( cacheKey , result , cacheConfig . ttlSeconds ) ;
260+ await cacheStore . del ( cacheKey ) ;
165261 }
166262 } catch {
167- // Non-fatal: cache write failure should not block the response
168- console . warn ( "Failed to cache leaderboard result " ) ;
263+ // Non-fatal: cache invalidation failure should not block the response
264+ console . warn ( "Failed to invalidate leaderboard cache " ) ;
169265 }
170266
171- return NextResponse . json ( { success : true , ...result } ) ;
172- }
267+ return NextResponse . json ( {
268+ success : true ,
269+ ...result ,
270+ _meta : {
271+ newUsers : newUsersCount ,
272+ refreshedUsers : refreshedCount ,
273+ skippedExisting : skippedExistingCount ,
274+ errors : errors . length ,
275+ totalInDb : totalCount ,
276+ } ,
277+ } ) ;
278+ }
0 commit comments