···100100 atproto_handle TEXT NOT NULL,
101101 atproto_display_name TEXT,
102102 atproto_avatar TEXT,
103103+ post_count INTEGER,
104104+ follower_count INTEGER,
103105 match_score INTEGER NOT NULL,
104106 found_at TIMESTAMP DEFAULT NOW(),
105107 last_verified TIMESTAMP,
···153155 await sql`CREATE INDEX IF NOT EXISTS idx_notification_queue_pending ON notification_queue(sent, created_at) WHERE sent = false`;
154156155157 // NEW: Enhanced indexes for common query patterns
158158+159159+ // For sorting
160160+ await sql`CREATE INDEX IF NOT EXISTS idx_atproto_matches_stats ON atproto_matches(source_account_id, found_at DESC, post_count DESC, follower_count DESC)`;
156161157162 // For session lookups (most frequent query)
158163await sql`CREATE INDEX IF NOT EXISTS idx_user_sessions_did ON user_sessions(did)`;
+33-9
netlify/functions/get-upload-details.ts
···8484 am.atproto_display_name,
8585 am.atproto_avatar,
8686 am.match_score,
8787+ am.post_count,
8888+ am.follower_count,
8989+ am.found_at,
8790 ums.followed,
8888- ums.dismissed
9191+ ums.dismissed,
9292+ -- Calculate if this is a new match (found after upload creation)
9393+ CASE WHEN am.found_at > uu.created_at THEN 1 ELSE 0 END as is_new_match
8994 FROM user_source_follows usf
9095 JOIN source_accounts sa ON usf.source_account_id = sa.id
9191- LEFT JOIN atproto_matches am ON sa.id = am.source_account_id
9696+ JOIN user_uploads uu ON usf.upload_id = uu.upload_id
9797+ LEFT JOIN atproto_matches am ON sa.id = am.source_account_id AND am.is_active = true
9298 LEFT JOIN user_match_status ums ON am.id = ums.atproto_match_id AND ums.did = ${userSession.did}
9399 WHERE usf.upload_id = ${uploadId}
9494- ORDER BY sa.source_username
100100+ ORDER BY
101101+ -- 1. Users with matches first
102102+ CASE WHEN am.atproto_did IS NOT NULL THEN 0 ELSE 1 END,
103103+ -- 2. New matches (found after initial upload)
104104+ is_new_match DESC,
105105+ -- 3. Highest post count
106106+ am.post_count DESC NULLS LAST,
107107+ -- 4. Highest follower count
108108+ am.follower_count DESC NULLS LAST,
109109+ -- 5. Username as tiebreaker
110110+ sa.source_username
95111 LIMIT ${pageSize}
96112 OFFSET ${offset}
97113 `;
9811499115 // Group results by source username
100100- const groupedResults: any = {};
116116+ const groupedResults = new Map<string, any>();
101117102118 (results as any[]).forEach((row: any) => {
103119 const username = row.source_username;
104120105105- if (!groupedResults[username]) {
106106- groupedResults[username] = {
107107- tiktokUser: {
121121+ // Get or create the entry for this username
122122+ let userResult = groupedResults.get(username);
123123+124124+ if (!userResult) {
125125+ userResult = {
126126+ sourceUser: {
108127 username: username,
109128 date: row.source_date || '',
110129 },
111130 atprotoMatches: [],
112131 };
132132+ groupedResults.set(username, userResult); // Add to map, this preserves the order
113133 }
114134135135+ // Add the match (if it exists) to the array
115136 if (row.atproto_did) {
116116- groupedResults[username].atprotoMatches.push({
137137+ userResult.atprotoMatches.push({
117138 did: row.atproto_did,
118139 handle: row.atproto_handle,
119140 displayName: row.atproto_display_name,
120141 avatar: row.atproto_avatar,
121142 matchScore: row.match_score,
143143+ postCount: row.post_count,
144144+ followerCount: row.follower_count,
145145+ foundAt: row.found_at,
122146 followed: row.followed || false,
123147 dismissed: row.dismissed || false,
124148 });
125149 }
126150 });
127151128128- const searchResults = Object.values(groupedResults);
152152+ const searchResults = Array.from(groupedResults.values());
129153130154 return {
131155 statusCode: 200,