@@ -2,7 +2,72 @@ import { NextResponse } from "next/server";
22import { createClient } from "../../../lib/supabase/server" ;
33import { getUserWithProfile } from "@/app/lib/supabase/help/user" ;
44
5+ function formatDateYMD ( date : Date ) {
6+ const y = date . getFullYear ( ) ;
7+ const m = String ( date . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
8+ const d = String ( date . getDate ( ) ) . padStart ( 2 , "0" ) ;
9+ return `${ y } -${ m } -${ d } ` ;
10+ }
11+
12+ function toDateKey ( value : string ) {
13+ return value . slice ( 0 , 10 ) ;
14+ }
15+
16+ type DailyStat = {
17+ date : string ;
18+ total_seconds : number ;
19+ } ;
20+
21+ function buildSnapshotMetrics ( dailyStats : DailyStat [ ] ) {
22+ const normalized = [ ...dailyStats ]
23+ . map ( ( entry ) => ( {
24+ date : toDateKey ( entry . date ) ,
25+ total_seconds : Math . max ( 0 , Math . floor ( entry . total_seconds || 0 ) ) ,
26+ } ) )
27+ . sort ( ( a , b ) => a . date . localeCompare ( b . date ) ) ;
28+
29+ const last7 = normalized . slice ( - 7 ) ;
30+ const totalSeconds7d = last7 . reduce ( ( sum , day ) => sum + day . total_seconds , 0 ) ;
31+ const activeDays7d = last7 . filter ( ( day ) => day . total_seconds > 0 ) . length ;
32+
33+ const activeByDay = normalized . map ( ( day ) => day . total_seconds > 0 ) ;
34+ const activeDays = activeByDay . filter ( Boolean ) . length ;
35+ const consistencyPercent =
36+ normalized . length > 0
37+ ? Math . round ( ( activeDays / normalized . length ) * 100 )
38+ : 0 ;
39+
40+ let bestStreak = 0 ;
41+ let runningStreak = 0 ;
42+ for ( const isActive of activeByDay ) {
43+ runningStreak = isActive ? runningStreak + 1 : 0 ;
44+ if ( runningStreak > bestStreak ) bestStreak = runningStreak ;
45+ }
46+
47+ let currentStreak = 0 ;
48+ for ( let i = activeByDay . length - 1 ; i >= 0 ; i -= 1 ) {
49+ if ( ! activeByDay [ i ] ) break ;
50+ currentStreak += 1 ;
51+ }
52+
53+ const peakDay = last7 . reduce (
54+ ( max , day ) => ( day . total_seconds > max . total_seconds ? day : max ) ,
55+ { date : "" , total_seconds : 0 } ,
56+ ) ;
57+
58+ return {
59+ totalSeconds7d,
60+ activeDays7d,
61+ consistencyPercent,
62+ currentStreak,
63+ bestStreak,
64+ peakDayDate : peakDay . date || null ,
65+ peakDaySeconds : peakDay . total_seconds ,
66+ } ;
67+ }
68+
569export async function GET ( request : Request ) {
70+ const CONSISTENCY_DAYS = 365 ;
671 const supabase = await createClient ( ) ;
772 const { user, profile } = await getUserWithProfile ( ) ;
873 const { searchParams } = new URL ( request . url ) ;
@@ -45,21 +110,29 @@ export async function GET(request: Request) {
45110
46111 const now = new Date ( ) ;
47112 const sixHours = 6 * 60 * 60 * 1000 ;
113+ const existingDailyStats = Array . isArray ( existing ?. daily_stats )
114+ ? existing . daily_stats
115+ : [ ] ;
48116
49117 if ( existing ?. last_fetched_at ) {
50118 const lastFetch = new Date ( existing . last_fetched_at ) . getTime ( ) ;
51- if ( now . getTime ( ) - lastFetch < sixHours ) {
119+ if (
120+ now . getTime ( ) - lastFetch < sixHours &&
121+ existingDailyStats . length >= CONSISTENCY_DAYS
122+ ) {
52123 return NextResponse . json ( { success : true , data : existing } ) ;
53124 }
54125 }
55126 }
56127
57128 // Fetch from WakaTime API endpoints
58129 const endDate = new Date ( ) ;
130+ endDate . setHours ( 0 , 0 , 0 , 0 ) ;
59131 const startDate = new Date ( ) ;
60- startDate . setDate ( endDate . getDate ( ) - 6 ) ;
61- const endStr = endDate . toISOString ( ) . split ( "T" ) [ 0 ] ;
62- const startStr = startDate . toISOString ( ) . split ( "T" ) [ 0 ] ;
132+ startDate . setHours ( 0 , 0 , 0 , 0 ) ;
133+ startDate . setDate ( endDate . getDate ( ) - ( CONSISTENCY_DAYS - 1 ) ) ;
134+ const endStr = formatDateYMD ( endDate ) ;
135+ const startStr = formatDateYMD ( startDate ) ;
63136
64137 const authHeader = `Basic ${ Buffer . from ( profile$ . wakatime_api_key ) . toString ( "base64" ) } ` ;
65138
@@ -94,11 +167,17 @@ export async function GET(request: Request) {
94167 range : { date : string } ;
95168 grand_total : { total_seconds : number } ;
96169 } ) => ( {
97- date : day . range . date ,
98- total_seconds : day . grand_total . total_seconds ,
170+ date : toDateKey ( day . range . date ) ,
171+ total_seconds : Math . floor ( day . grand_total . total_seconds || 0 ) ,
99172 } ) ,
100173 ) ;
101174
175+ const snapshotMetrics = buildSnapshotMetrics ( daily_stats ) ;
176+ const topLanguage =
177+ Array . isArray ( wakaStats . languages ) && wakaStats . languages . length > 0
178+ ? wakaStats . languages [ 0 ]
179+ : null ;
180+
102181 if ( apiKey ) {
103182 const { error } = await supabase
104183 . from ( "profiles" )
@@ -158,6 +237,35 @@ export async function GET(request: Request) {
158237 projects : projectsResult ?. projects || [ ] ,
159238 } ;
160239
240+ const { error : snapshotError } = await supabase
241+ . from ( "user_dashboard_snapshots" )
242+ . upsert (
243+ {
244+ user_id : user . id ,
245+ snapshot_date : endStr ,
246+ total_seconds_7d : snapshotMetrics . totalSeconds7d ,
247+ active_days_7d : snapshotMetrics . activeDays7d ,
248+ consistency_percent : snapshotMetrics . consistencyPercent ,
249+ current_streak : snapshotMetrics . currentStreak ,
250+ best_streak : snapshotMetrics . bestStreak ,
251+ peak_day : snapshotMetrics . peakDayDate ,
252+ peak_day_seconds : snapshotMetrics . peakDaySeconds ,
253+ top_language : topLanguage ?. name || null ,
254+ top_language_percent :
255+ typeof topLanguage ?. percent === "number"
256+ ? Number ( topLanguage . percent . toFixed ( 2 ) )
257+ : null ,
258+ updated_at : new Date ( ) . toISOString ( ) ,
259+ } ,
260+ {
261+ onConflict : "user_id,snapshot_date" ,
262+ } ,
263+ ) ;
264+
265+ if ( snapshotError ) {
266+ console . error ( "Failed to upsert user dashboard snapshot" , snapshotError ) ;
267+ }
268+
161269 return NextResponse . json ( {
162270 success : ! ! statsResult && ! statsError && ! projectsError ,
163271 data : mergedResult ,
0 commit comments