ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

fix extension flow: create user_source_follows, auto-search, time display

Backend (extension-import.ts):
- Now creates user_source_follows entries linking upload to source accounts
- Without these, get-upload-details returned empty (queries FROM user_source_follows)
- Uses bulkCreate return value (Map<username, id>) to create links

Frontend (App.tsx):
- handleLoadUpload now detects if upload has no matches yet
- Sets isSearching: true for new uploads
- Automatically triggers searchAllUsers for new uploads
- Saves results after search completes
- Changed platform from hardcoded "tiktok" to "twitter"

Frontend (HistoryTab.tsx):
- Fixed time display: removed "Uploaded" prefix
- Now shows "about 5 hours ago" instead of "Uploaded in about 5 hours"
- formatRelativeTime with addSuffix already provides complete sentence

Resolves:
- Empty results on page load
- No automatic searching
- History navigation not working (will work after search)
- Grammatically incorrect time display

byarielm.fyi 6ced3f0b b6b8c396

verified
Changed files
+161 -23
docs
packages
functions
web
src
components
+15 -15
docs/git-history.json
··· 8 8 "files_changed": 4 9 9 }, 10 10 { 11 - "hash": "9ca734749fbaa014828f8437afc5e515610afd31", 12 - "short_hash": "9ca7347", 11 + "hash": "581ed00fec3c0c5f472c6ff92e00bf4ed5b27e9a", 12 + "short_hash": "581ed00", 13 13 "author": "Ariel M. Lighty", 14 - "date": "2025-12-26T13:37:24-05:00", 15 - "message": "update documentation: extension ready for testing after API response fix", 14 + "date": "2025-12-26T13:47:37-05:00", 15 + "message": "fix extension import: use bulkCreate and handle uploadId param\n\nBackend fixes:\n- Use SourceAccountRepository.bulkCreate() instead of non-existent upsertSourceAccount()\n- Change redirectUrl from /results?uploadId= to /?uploadId=\n- More efficient bulk insert instead of loop\n\nFrontend fixes:\n- Add useEffect to load results when uploadId param present\n- Calls loadUploadResults(uploadId) automatically on page load\n- Cleans up URL param after loading\n\nResolves:\n- \"sourceAccountRepo.upsertSourceAccount is not a function\" error\n- \"No routes matched location /results?uploadId=...\" routing error", 16 16 "files_changed": 4 17 17 }, 18 18 { ··· 48 48 "files_changed": 1 49 49 }, 50 50 { 51 - "hash": "b00ecad7a2c576962cb78de5b50f301e4664830e", 52 - "short_hash": "b00ecad", 51 + "hash": "a8a4b0a819297dec7b92d02036699cc86b2523ce", 52 + "short_hash": "a8a4b0a", 53 53 "author": "Ariel M. Lighty", 54 - "date": "2025-12-26T00:26:09-05:00", 55 - "message": "refactor extension to require authentication and use proper upload flow\n\nRemoved temporary storage approach and implemented proper authentication flow:\n\nExtension changes:\n- Added session check to popup init flow (checkSession in api-client)\n- Added \"not logged in\" state with login prompts\n- Updated uploadToATlast to include credentials for cookie-based auth\n- Extension now requires user to be logged in BEFORE scanning\n\nBackend changes:\n- Converted extension-import to AuthenticatedHandler (requires auth)\n- Now creates upload records immediately (no temporary storage)\n- Removed extension_imports table from database schema\n- Deleted get-extension-import function (no longer needed)\n- Deleted import-store utility (temporary approach removed)\n\nFrontend changes:\n- Removed ExtensionImport page and /import/:id route\n- Extension uploads now use same flow as file uploads\n\nThis matches the correct user flow: user logs in to ATlast first, then\nextension creates permanent upload records directly (same as file uploads).\n\nBuilt extension successfully for dev environment.", 56 - "files_changed": 12 54 + "date": "2025-12-26T00:33:21-05:00", 55 + "message": "fix extension-import: add missing matchedUsers parameter to createUpload\n\nThe createUpload method expects 5 parameters but we were only passing 4,\ncausing NaN to be inserted for unmatched_users calculation. Now passing 0\nfor matchedUsers (will be updated after search is performed).", 56 + "files_changed": 3 57 57 }, 58 58 { 59 59 "hash": "d0bcf337b6d223a86443f6f67767e87b74e4dd7d", ··· 80 80 "files_changed": 3 81 81 }, 82 82 { 83 - "hash": "ba29fd68872913ba0a587aa7f29f97b3d373a732", 84 - "short_hash": "ba29fd6", 83 + "hash": "32cdee3aeac7ef986df47e0fff786b5f7471e55b", 84 + "short_hash": "32cdee3", 85 85 "author": "Ariel M. Lighty", 86 86 "date": "2025-12-25T13:22:32-05:00", 87 87 "message": "configure Netlify dev for monorepo with --filter flag\n\nFixed Netlify CLI monorepo detection issue by using --filter flag:\n- Updated root package.json scripts to use 'npx netlify-cli dev --filter @atlast/web'\n- Updated netlify.toml [dev] section to use npm with --prefix for framework command\n- Added monorepo development instructions to CLAUDE.md\n- Documented Windows Git Bash compatibility issue with netlify command\n\nSolution: Use 'npx netlify-cli dev --filter @atlast/web' to bypass monorepo\nproject selection prompt and specify which workspace package to run.\n\nDev server now runs successfully at http://localhost:8888 with all backend\nfunctions loaded.", 88 - "files_changed": 5 88 + "files_changed": 4 89 89 }, 90 90 { 91 - "hash": "32cdee3aeac7ef986df47e0fff786b5f7471e55b", 92 - "short_hash": "32cdee3", 91 + "hash": "ba29fd68872913ba0a587aa7f29f97b3d373a732", 92 + "short_hash": "ba29fd6", 93 93 "author": "Ariel M. Lighty", 94 94 "date": "2025-12-25T13:22:32-05:00", 95 95 "message": "configure Netlify dev for monorepo with --filter flag\n\nFixed Netlify CLI monorepo detection issue by using --filter flag:\n- Updated root package.json scripts to use 'npx netlify-cli dev --filter @atlast/web'\n- Updated netlify.toml [dev] section to use npm with --prefix for framework command\n- Added monorepo development instructions to CLAUDE.md\n- Documented Windows Git Bash compatibility issue with netlify command\n\nSolution: Use 'npx netlify-cli dev --filter @atlast/web' to bypass monorepo\nproject selection prompt and specify which workspace package to run.\n\nDev server now runs successfully at http://localhost:8888 with all backend\nfunctions loaded.", 96 - "files_changed": 4 96 + "files_changed": 5 97 97 }, 98 98 { 99 99 "hash": "c3e7afad396d130791d801a85cbfc9643bcd6309",
+110
docs/graph-data.json
··· 3343 3343 "created_at": "2025-12-26T13:51:52.256909300-05:00", 3344 3344 "updated_at": "2025-12-26T13:51:52.256909300-05:00", 3345 3345 "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3346 + }, 3347 + { 3348 + "id": 305, 3349 + "change_id": "8ad6ef53-29a2-442e-b88f-9e0541634950", 3350 + "node_type": "goal", 3351 + "title": "Fix extension flow: auto-search after load, history navigation, time formatting", 3352 + "description": null, 3353 + "status": "pending", 3354 + "created_at": "2025-12-26T14:05:53.798547500-05:00", 3355 + "updated_at": "2025-12-26T14:05:53.798547500-05:00", 3356 + "metadata_json": "{\"branch\":\"master\",\"confidence\":90}" 3357 + }, 3358 + { 3359 + "id": 306, 3360 + "change_id": "481942f8-5905-4948-a1cb-ee320a98271b", 3361 + "node_type": "observation", 3362 + "title": "handleLoadUpload expects existing results but extension creates empty upload - need to load source accounts and trigger search", 3363 + "description": null, 3364 + "status": "pending", 3365 + "created_at": "2025-12-26T14:06:18.067673100-05:00", 3366 + "updated_at": "2025-12-26T14:06:18.067673100-05:00", 3367 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3368 + }, 3369 + { 3370 + "id": 307, 3371 + "change_id": "ae01acc1-f5ff-481b-823f-de2d4f1843a2", 3372 + "node_type": "observation", 3373 + "title": "Extension-import creates upload and source_accounts but NOT user_source_follows - get-upload-details returns empty because it queries FROM user_source_follows", 3374 + "description": null, 3375 + "status": "pending", 3376 + "created_at": "2025-12-26T14:08:57.918421600-05:00", 3377 + "updated_at": "2025-12-26T14:08:57.918421600-05:00", 3378 + "metadata_json": "{\"branch\":\"master\",\"confidence\":100}" 3379 + }, 3380 + { 3381 + "id": 308, 3382 + "change_id": "2368cae0-9ae1-4ca0-9ace-8c3555f9e679", 3383 + "node_type": "action", 3384 + "title": "Add user_source_follows creation to extension-import endpoint", 3385 + "description": null, 3386 + "status": "pending", 3387 + "created_at": "2025-12-26T14:09:03.035871-05:00", 3388 + "updated_at": "2025-12-26T14:09:03.035871-05:00", 3389 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3390 + }, 3391 + { 3392 + "id": 309, 3393 + "change_id": "cd9b88e7-fe8d-4ee0-a187-e99eef0b7e64", 3394 + "node_type": "outcome", 3395 + "title": "Fixed all extension flow issues: added user_source_follows creation, auto-search after load, time formatting", 3396 + "description": null, 3397 + "status": "pending", 3398 + "created_at": "2025-12-26T14:11:09.055850200-05:00", 3399 + "updated_at": "2025-12-26T14:11:09.055850200-05:00", 3400 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3346 3401 } 3347 3402 ], 3348 3403 "edges": [ ··· 6568 6623 "weight": 1.0, 6569 6624 "rationale": "Implementation complete", 6570 6625 "created_at": "2025-12-26T13:51:54.561857100-05:00" 6626 + }, 6627 + { 6628 + "id": 294, 6629 + "from_node_id": 304, 6630 + "to_node_id": 305, 6631 + "from_change_id": "dff4aef7-8732-4aae-a6be-f44fb42b4941", 6632 + "to_change_id": "8ad6ef53-29a2-442e-b88f-9e0541634950", 6633 + "edge_type": "leads_to", 6634 + "weight": 1.0, 6635 + "rationale": "New issues found in testing", 6636 + "created_at": "2025-12-26T14:05:56.045966300-05:00" 6637 + }, 6638 + { 6639 + "id": 295, 6640 + "from_node_id": 305, 6641 + "to_node_id": 306, 6642 + "from_change_id": "8ad6ef53-29a2-442e-b88f-9e0541634950", 6643 + "to_change_id": "481942f8-5905-4948-a1cb-ee320a98271b", 6644 + "edge_type": "leads_to", 6645 + "weight": 1.0, 6646 + "rationale": "Root cause identified", 6647 + "created_at": "2025-12-26T14:06:20.512128100-05:00" 6648 + }, 6649 + { 6650 + "id": 296, 6651 + "from_node_id": 306, 6652 + "to_node_id": 307, 6653 + "from_change_id": "481942f8-5905-4948-a1cb-ee320a98271b", 6654 + "to_change_id": "ae01acc1-f5ff-481b-823f-de2d4f1843a2", 6655 + "edge_type": "leads_to", 6656 + "weight": 1.0, 6657 + "rationale": "Root cause found", 6658 + "created_at": "2025-12-26T14:09:00.202061-05:00" 6659 + }, 6660 + { 6661 + "id": 297, 6662 + "from_node_id": 307, 6663 + "to_node_id": 308, 6664 + "from_change_id": "ae01acc1-f5ff-481b-823f-de2d4f1843a2", 6665 + "to_change_id": "2368cae0-9ae1-4ca0-9ace-8c3555f9e679", 6666 + "edge_type": "leads_to", 6667 + "weight": 1.0, 6668 + "rationale": "Action to fix", 6669 + "created_at": "2025-12-26T14:09:05.743608600-05:00" 6670 + }, 6671 + { 6672 + "id": 298, 6673 + "from_node_id": 308, 6674 + "to_node_id": 309, 6675 + "from_change_id": "2368cae0-9ae1-4ca0-9ace-8c3555f9e679", 6676 + "to_change_id": "cd9b88e7-fe8d-4ee0-a187-e99eef0b7e64", 6677 + "edge_type": "leads_to", 6678 + "weight": 1.0, 6679 + "rationale": "Implementation complete", 6680 + "created_at": "2025-12-26T14:11:11.543447500-05:00" 6571 6681 } 6572 6682 ] 6573 6683 }
+11 -2
packages/functions/src/extension-import.ts
··· 60 60 61 61 console.log(`[extension-import] Created upload ${uploadId} for user ${context.did}`); 62 62 63 - // Save source accounts using bulk insert 63 + // Save source accounts using bulk insert and link to upload 64 64 try { 65 - await sourceAccountRepo.bulkCreate( 65 + const sourceAccountIdMap = await sourceAccountRepo.bulkCreate( 66 66 validatedData.platform, 67 67 validatedData.usernames 68 68 ); 69 69 console.log(`[extension-import] Saved ${validatedData.usernames.length} source accounts`); 70 + 71 + // Link source accounts to this upload 72 + const links = Array.from(sourceAccountIdMap.values()).map(sourceAccountId => ({ 73 + sourceAccountId, 74 + sourceDate: validatedData.metadata.scrapedAt 75 + })); 76 + 77 + await sourceAccountRepo.linkUserToAccounts(uploadId, context.did, links); 78 + console.log(`[extension-import] Linked ${links.length} source accounts to upload`); 70 79 } catch (error) { 71 80 console.error('[extension-import] Error saving source accounts:', error); 72 81 // Continue anyway - upload is created, frontend can still search
+24 -5
packages/web/src/App.tsx
··· 177 177 return; 178 178 } 179 179 180 - const platform = "tiktok"; 180 + // Detect platform from first result's username or default to twitter for extension imports 181 + const platform = "twitter"; // Extension imports are always from Twitter for now 181 182 setCurrentPlatform(platform); 182 183 184 + // Check if this is a new upload with no matches yet 185 + const hasMatches = data.results.some(r => r.atprotoMatches.length > 0); 186 + 183 187 const loadedResults: SearchResult[] = data.results.map((result) => ({ 184 - ...result, 188 + sourceUser: result.sourceUser.username, 185 189 sourcePlatform: platform, 186 - isSearching: false, 190 + isSearching: !hasMatches, // Search if no matches exist yet 191 + atprotoMatches: result.atprotoMatches || [], 187 192 selectedMatches: new Set<string>( 188 - result.atprotoMatches 193 + (result.atprotoMatches || []) 189 194 .filter( 190 195 (match) => 191 196 !match.followStatus || ··· 198 203 199 204 setSearchResults(loadedResults); 200 205 setCurrentStep("results"); 206 + 207 + // If no matches yet, trigger search 208 + if (!hasMatches) { 209 + setStatusMessage("Starting search for matches..."); 210 + const followLexicon = ATPROTO_APPS[currentDestinationAppId]?.followLexicon; 211 + await searchAllUsers(loadedResults, followLexicon); 212 + 213 + // Save results after search completes 214 + const updatedResults = loadedResults.filter(r => !r.isSearching); 215 + if (updatedResults.length > 0) { 216 + await saveResults(uploadId, platform, updatedResults); 217 + } 218 + } 219 + 201 220 // Announce to screen readers only - visual feedback is navigation to results page 202 221 setAriaAnnouncement( 203 222 `Loaded ${loadedResults.length} results from previous upload`, ··· 208 227 setCurrentStep("home"); 209 228 } 210 229 }, 211 - [setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error], 230 + [setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error, currentDestinationAppId, searchAllUsers, saveResults], 212 231 ); 213 232 214 233 // Login handler
+1 -1
packages/web/src/components/HistoryTab.tsx
··· 148 148 {upload.totalUsers === 1 ? "user found" : "users found"} 149 149 </Badge> 150 150 <Badge variant="info"> 151 - Uploaded {formatRelativeTime(upload.createdAt)} 151 + {formatRelativeTime(upload.createdAt)} 152 152 </Badge> 153 153 </> 154 154 }