@@ -14,6 +14,27 @@ export interface UserRow {
1414 fluency_score_json : string | null ;
1515}
1616
17+ export interface UserUsageSummary {
18+ user_id : number ;
19+ github_login : string ;
20+ github_name : string | null ;
21+ avatar_url : string | null ;
22+ is_admin : number ;
23+ total_input : number ;
24+ total_output : number ;
25+ total_interactions : number ;
26+ days_active : number ;
27+ last_upload_day : string | null ;
28+ }
29+
30+ export interface AdminDailyRow {
31+ day : string ;
32+ github_login : string ;
33+ input_tokens : number ;
34+ output_tokens : number ;
35+ interactions : number ;
36+ }
37+
1738export interface UploadRow {
1839 id : number ;
1940 user_id : number ;
@@ -208,6 +229,7 @@ function initSchema(db: DatabaseSync): void {
208229 db . exec ( `
209230 CREATE INDEX IF NOT EXISTS idx_uploads_user_day ON usage_uploads(user_id, day);
210231 CREATE INDEX IF NOT EXISTS idx_uploads_dataset ON usage_uploads(dataset_id, day);
232+ CREATE INDEX IF NOT EXISTS idx_uploads_day ON usage_uploads(day);
211233 ` ) ;
212234
213235 // Add fluency_json column if it doesn't exist (migration for existing DBs)
@@ -358,3 +380,48 @@ export function closeDb(): void {
358380 _db = undefined ;
359381 }
360382}
383+
384+ /**
385+ * Per-user aggregate stats for the admin dashboard.
386+ * LEFT JOIN ensures users with no uploads in the period still appear (with zeros).
387+ * `last_upload_day` reflects their all-time last upload, not filtered to the period.
388+ */
389+ export function getAdminUserSummaries ( days : number ) : UserUsageSummary [ ] {
390+ const cutoff = new Date ( ) ;
391+ cutoff . setUTCDate ( cutoff . getUTCDate ( ) - days ) ;
392+ const cutoffStr = cutoff . toISOString ( ) . slice ( 0 , 10 ) ;
393+ return getDb ( ) . prepare ( `
394+ SELECT
395+ u.id AS user_id,
396+ u.github_login,
397+ u.github_name,
398+ u.avatar_url,
399+ u.is_admin,
400+ COALESCE(SUM(CASE WHEN uu.day >= ? THEN uu.input_tokens ELSE 0 END), 0) AS total_input,
401+ COALESCE(SUM(CASE WHEN uu.day >= ? THEN uu.output_tokens ELSE 0 END), 0) AS total_output,
402+ COALESCE(SUM(CASE WHEN uu.day >= ? THEN uu.interactions ELSE 0 END), 0) AS total_interactions,
403+ COUNT(DISTINCT CASE WHEN uu.day >= ? THEN uu.day END) AS days_active,
404+ MAX(uu.day) AS last_upload_day
405+ FROM users u
406+ LEFT JOIN usage_uploads uu ON uu.user_id = u.id
407+ GROUP BY u.id
408+ ORDER BY total_input + total_output DESC
409+ ` ) . all ( cutoffStr , cutoffStr , cutoffStr , cutoffStr ) as unknown as UserUsageSummary [ ] ;
410+ }
411+
412+ /** Daily per-user token totals for the admin trend chart. */
413+ export function getAdminDailyTotals ( days : number ) : AdminDailyRow [ ] {
414+ return getDb ( ) . prepare ( `
415+ SELECT
416+ uu.day,
417+ u.github_login,
418+ SUM(uu.input_tokens) AS input_tokens,
419+ SUM(uu.output_tokens) AS output_tokens,
420+ SUM(uu.interactions) AS interactions
421+ FROM usage_uploads uu
422+ JOIN users u ON u.id = uu.user_id
423+ WHERE uu.day >= date('now', '-' || ? || ' days')
424+ GROUP BY uu.day, u.github_login
425+ ORDER BY uu.day, u.github_login
426+ ` ) . all ( days ) as unknown as AdminDailyRow [ ] ;
427+ }
0 commit comments