six-degrees 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +35 -7
  5. package/.next/standalone/.next/required-server-files.json +6 -1
  6. package/.next/standalone/.next/routes-manifest.json +18 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  22. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  24. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  26. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  28. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  32. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
  47. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  51. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  61. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  63. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/import.html +1 -1
  68. package/.next/standalone/.next/server/app/import.rsc +13 -13
  69. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  70. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  71. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
  72. package/.next/standalone/.next/server/app/index.html +1 -1
  73. package/.next/standalone/.next/server/app/index.rsc +13 -13
  74. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
  75. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  76. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  77. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/launch.html +2 -2
  80. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  81. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  82. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  83. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
  84. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  86. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  87. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  88. package/.next/standalone/.next/server/app/paths.html +1 -1
  89. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  90. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  91. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  92. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
  93. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  94. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  95. package/.next/standalone/.next/server/app/profile.html +1 -1
  96. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  97. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  98. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  99. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
  100. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  101. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  102. package/.next/standalone/.next/server/app/queue.html +1 -1
  103. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  104. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  105. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  106. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
  107. package/.next/standalone/.next/server/app/scores/page/app-paths-manifest.json +3 -0
  108. package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
  109. package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
  110. package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
  111. package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
  112. package/.next/standalone/.next/server/app/scores/page.js +13 -0
  113. package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
  114. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
  115. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
  116. package/.next/standalone/.next/server/app/scores.html +1 -0
  117. package/.next/standalone/.next/server/app/scores.meta +26 -0
  118. package/.next/standalone/.next/server/app/scores.rsc +24 -0
  119. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
  120. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
  121. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
  122. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  123. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  124. package/.next/standalone/.next/server/app/settings.html +1 -1
  125. package/.next/standalone/.next/server/app/settings.rsc +13 -13
  126. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
  127. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  128. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
  129. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  130. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  131. package/.next/standalone/.next/server/app/setup.html +1 -1
  132. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  133. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  134. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  135. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
  136. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +8 -0
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  159. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  162. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  163. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  164. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  165. package/.next/standalone/.next/server/chunks/_02twlca._.js +10 -0
  166. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  167. package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
  168. package/.next/standalone/.next/server/chunks/_1_fynia._.js +8 -6
  169. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
  170. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  171. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  172. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +13 -8
  173. package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
  174. package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
  175. package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
  176. package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
  177. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  178. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
  180. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
  181. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
  182. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
  183. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
  184. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
  185. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
  186. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
  187. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
  190. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
  192. package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
  193. package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
  195. package/.next/standalone/.next/server/chunks/ssr/_1cyxocj._.js +3 -0
  196. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
  198. package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +3 -0
  199. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
  200. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.js +3 -0
  201. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
  202. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  203. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
  204. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
  205. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  206. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  207. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +3 -0
  208. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  209. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  210. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
  211. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
  212. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
  213. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  214. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  215. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  216. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  217. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  218. package/.next/standalone/.next/server/pages/404.html +1 -1
  219. package/.next/standalone/.next/server/pages/500.html +1 -1
  220. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  221. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  222. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  223. package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  224. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  225. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
  226. package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  227. package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  228. package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
  229. package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  230. package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
  231. package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  232. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  233. package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  234. package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  235. package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  236. package/.next/standalone/CHANGELOG.md +286 -0
  237. package/.next/standalone/CONTRIBUTING.md +15 -4
  238. package/.next/standalone/README.md +50 -29
  239. package/.next/standalone/SECURITY.md +10 -0
  240. package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
  241. package/.next/standalone/app/api/company-scores/route.js +1 -1
  242. package/.next/standalone/app/api/ingest/route.js +23 -2
  243. package/.next/standalone/app/api/outreach/route.js +107 -44
  244. package/.next/standalone/app/api/scraper/route.js +146 -17
  245. package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
  246. package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
  247. package/.next/standalone/app/api/update-images/route.js +41 -7
  248. package/.next/standalone/app/avatars/[file]/route.js +6 -1
  249. package/.next/standalone/app/components/Avatar.js +5 -2
  250. package/.next/standalone/app/components/ChainView.js +659 -469
  251. package/.next/standalone/app/components/CompanyScores.js +16 -10
  252. package/.next/standalone/app/components/EmptyState.js +1 -1
  253. package/.next/standalone/app/components/FieldStep.js +94 -0
  254. package/.next/standalone/app/components/ForceGraph.js +183 -768
  255. package/.next/standalone/app/components/GridView.js +9 -7
  256. package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
  257. package/.next/standalone/app/components/LinkedInLimits.js +22 -7
  258. package/.next/standalone/app/components/ListView.js +6 -4
  259. package/.next/standalone/app/components/MappingProgress.js +5 -6
  260. package/.next/standalone/app/components/OnboardingGate.js +1 -1
  261. package/.next/standalone/app/components/OrbitGraph.js +33 -1
  262. package/.next/standalone/app/components/OutlinkQuest.js +5 -4
  263. package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
  264. package/.next/standalone/app/components/RingsView.js +9 -8
  265. package/.next/standalone/app/components/SeparationView.js +96 -14
  266. package/.next/standalone/app/components/Sidebar.js +366 -287
  267. package/.next/standalone/app/components/TheirCircle.js +300 -0
  268. package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
  269. package/.next/standalone/app/components/settings/SectorSection.js +21 -195
  270. package/.next/standalone/app/components/settings/TierSection.js +99 -0
  271. package/.next/standalone/app/components/useRequests.js +12 -0
  272. package/.next/standalone/app/components/useScanner.js +13 -0
  273. package/.next/standalone/app/components/views.js +3 -5
  274. package/.next/standalone/app/globals.css +7 -0
  275. package/.next/standalone/app/launch/page.js +6 -3
  276. package/.next/standalone/app/page.js +183 -74
  277. package/.next/standalone/app/paths/page.js +20 -14
  278. package/.next/standalone/app/profile/page.js +3 -3
  279. package/.next/standalone/app/queue/page.js +56 -60
  280. package/.next/standalone/app/scores/page.js +72 -0
  281. package/.next/standalone/app/settings/page.js +15 -3
  282. package/.next/standalone/app/setup/page.js +423 -260
  283. package/.next/standalone/db/schema.js +2 -1
  284. package/.next/standalone/eslint.config.mjs +3 -3
  285. package/.next/standalone/install.sh +3 -3
  286. package/.next/standalone/lib/chain-layout.js +139 -0
  287. package/.next/standalone/lib/circle.js +169 -0
  288. package/.next/standalone/lib/companies.js +1 -1
  289. package/.next/standalone/lib/csv.js +21 -10
  290. package/.next/standalone/lib/data-export.js +2 -1
  291. package/.next/standalone/lib/data-folder.js +4 -2
  292. package/.next/standalone/lib/db-client.js +1 -0
  293. package/.next/standalone/lib/galaxy.js +12 -0
  294. package/.next/standalone/lib/ingest.js +27 -8
  295. package/.next/standalone/lib/legacy-offer.js +7 -6
  296. package/.next/standalone/lib/legacy-scores.js +2 -2
  297. package/.next/standalone/lib/linkedin-limits.js +30 -8
  298. package/.next/standalone/lib/photos.js +47 -0
  299. package/.next/standalone/lib/quest.js +8 -3
  300. package/.next/standalone/lib/rarity.js +80 -0
  301. package/.next/standalone/lib/reach.js +113 -0
  302. package/.next/standalone/lib/requests-client.js +113 -0
  303. package/.next/standalone/lib/rpc.js +26 -15
  304. package/.next/standalone/lib/scan-state.js +1 -0
  305. package/.next/standalone/lib/scanner-setup.js +20 -1
  306. package/.next/standalone/lib/score-guess.js +60 -0
  307. package/.next/standalone/lib/scoring.js +1761 -69
  308. package/.next/standalone/lib/scraper-client.js +235 -17
  309. package/.next/standalone/lib/sector-directory.js +1 -1
  310. package/.next/standalone/lib/sector-focus.js +17 -3
  311. package/.next/standalone/lib/separation.js +4 -2
  312. package/.next/standalone/lib/settings-client.js +21 -0
  313. package/.next/standalone/lib/settings-effects.js +19 -1
  314. package/.next/standalone/lib/settings.js +7 -2
  315. package/.next/standalone/next.config.mjs +25 -3
  316. package/.next/standalone/package-lock.json +2 -2
  317. package/.next/standalone/package.json +2 -2
  318. package/.next/standalone/public/demo-data.json +1 -1
  319. package/.next/standalone/scripts/build-app.mjs +11 -2
  320. package/.next/standalone/scripts/check-node.mjs +1 -1
  321. package/.next/standalone/scripts/gen-synthetic.mjs +1 -1
  322. package/.next/standalone/scripts/image_store.py +77 -21
  323. package/.next/standalone/scripts/scrape.py +305 -29
  324. package/.next/standalone/server.js +1 -1
  325. package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  326. package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  327. package/.next/static/chunks/0l1568qff0efj.js +1 -0
  328. package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  329. package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  330. package/.next/static/chunks/135n87ydxjrra.js +2 -0
  331. package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  332. package/.next/static/chunks/28bu26ausqzmn.js +1 -0
  333. package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  334. package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  335. package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  336. package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  337. package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  338. package/CHANGELOG.md +286 -0
  339. package/README.md +50 -29
  340. package/SECURITY.md +10 -0
  341. package/db/schema.js +2 -1
  342. package/lib/chain-layout.js +139 -0
  343. package/lib/circle.js +169 -0
  344. package/lib/companies.js +1 -1
  345. package/lib/csv.js +21 -10
  346. package/lib/data-export.js +2 -1
  347. package/lib/data-folder.js +4 -2
  348. package/lib/db-client.js +1 -0
  349. package/lib/galaxy.js +12 -0
  350. package/lib/ingest.js +27 -8
  351. package/lib/legacy-offer.js +7 -6
  352. package/lib/legacy-scores.js +2 -2
  353. package/lib/linkedin-limits.js +30 -8
  354. package/lib/photos.js +47 -0
  355. package/lib/quest.js +8 -3
  356. package/lib/rarity.js +80 -0
  357. package/lib/reach.js +113 -0
  358. package/lib/requests-client.js +113 -0
  359. package/lib/rpc.js +26 -15
  360. package/lib/scan-state.js +1 -0
  361. package/lib/scanner-setup.js +20 -1
  362. package/lib/score-guess.js +60 -0
  363. package/lib/scoring.js +1761 -69
  364. package/lib/scraper-client.js +235 -17
  365. package/lib/sector-directory.js +1 -1
  366. package/lib/sector-focus.js +17 -3
  367. package/lib/separation.js +4 -2
  368. package/lib/settings-client.js +21 -0
  369. package/lib/settings-effects.js +19 -1
  370. package/lib/settings.js +7 -2
  371. package/package.json +2 -2
  372. package/public/demo-data.json +1 -1
  373. package/scripts/check-node.mjs +1 -1
  374. package/scripts/image_store.py +77 -21
  375. package/scripts/scrape.py +305 -29
  376. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
  377. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
  378. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
  379. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
  380. package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +0 -3
  381. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
  382. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
  383. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
  384. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
  385. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
  386. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
  387. package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +0 -3
  388. package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
  389. package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
  390. package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
  391. package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
  392. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
  393. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
  394. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
  395. package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
  396. package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
  397. package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  398. package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
  399. package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  400. package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
  401. package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
  402. package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +0 -44
  403. package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  404. package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
  405. package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  406. package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
  407. package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  408. package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  409. package/.next/standalone/app/components/BridgeRing.js +0 -753
  410. package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  411. package/.next/static/chunks/08k1crid-05dt.js +0 -1
  412. package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  413. package/.next/static/chunks/0q-_khndfxubv.js +0 -2
  414. package/.next/static/chunks/1c79veqedvubt.js +0 -1
  415. package/.next/static/chunks/1chp3nx45meg5.js +0 -44
  416. package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  417. package/.next/static/chunks/1uadqylrod5_v.js +0 -1
  418. package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  419. package/.next/static/chunks/289ktj97di6zg.js +0 -1
  420. package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  421. package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  422. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  423. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  424. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
  425. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  426. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  427. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
@@ -1,14 +1,25 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useMemo } from 'react';
4
- import { runScrape, scraperStatus, notReadyMessage } from '../../lib/scraper-client';
3
+ import { useState, useEffect, useMemo, useCallback } from 'react';
4
+ import Link from 'next/link';
5
+ import { beginScrape, scraperStatus, notReadyMessage, resumePoint, scansCircleOf, isCircleScan, busyReason } from '../../lib/scraper-client';
6
+ import useScanner from './useScanner';
7
+ import useRequests from './useRequests';
8
+ import TheirCircle from './TheirCircle';
9
+ import { hasRequest, markRequested, undoRequest } from '../../lib/requests-client';
5
10
  import { useUser } from './UserProvider';
6
11
  import { routeIndex, routesFor } from '../../lib/separation';
7
12
  import { topCompanies } from '../../lib/scoring';
13
+ import { reachIndex, reachState, circleState, readyToScan, circleScanCost } from '../../lib/reach';
8
14
  import Avatar from './Avatar';
15
+ import { localPhoto } from '../../lib/photos';
9
16
 
10
- export default function Sidebar({ selected, stats, tierColors, connections, degree2 = [], mode, collapsed, filter, pending = [], onToggle, onSelect, onSwitchMode, onFocusNode, onMarkSent, onUndoPending }) {
17
+ export default function Sidebar({ selected, stats, tierColors, connections, degree2 = [], mode, collapsed, filter, pending = [], onToggle, onSelect, onSwitchMode, onFocusNode, onMarkSent, onUndoPending, scanNotes, csvSource = null }) {
11
18
  const isDegreesMode = mode === 'degrees';
19
+ // 'sample' or 'csv' while the sample or a CSV import is open in this window,
20
+ // null for your own network. Only your own can be scanned: the scanner looks
21
+ // the person up in the database, and these two never reach it.
22
+ const canScan = !csvSource;
12
23
  const { userId, userProfile: ctxProfile } = useUser();
13
24
  const userProfile = ctxProfile || { name: 'User', sectors: [], goals: [] };
14
25
  const [searchQuery, setSearchQuery] = useState('');
@@ -26,9 +37,13 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
26
37
  }, [degree2]);
27
38
  // Who reaches whom, from the FULL lists this panel is given — so every route
28
39
  // shows even while a tier chip is narrowing the view, and a person picked in
29
- // Chains, Revolver or Galaxy gets every route too.
40
+ // Chains or Galaxy gets every route too.
30
41
  const routeIdx = useMemo(() => routeIndex(degree2), [degree2]);
31
42
  const bridgeById = useMemo(() => new Map(connections.map(c => [c.id, c])), [connections]);
43
+ // Who you've asked, shared with every view: a request sent here shows in
44
+ // Separation, the circle and the Outlink queue at once (lib/requests-client.js).
45
+ const requests = useRequests();
46
+ const isRequested = useCallback((row) => hasRequest(row, requests), [requests]);
32
47
 
33
48
  if (selected) {
34
49
  const isBridge = selected.degree === 1 && bridgeMap[selected.id];
@@ -86,9 +101,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
86
101
  </div>
87
102
  {/* Profile photo + name */}
88
103
  <div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 16 }}>
89
- {selected.profile_image_url ? (
104
+ {localPhoto(selected.profile_image_url) ? (
90
105
  <img
91
- src={selected.profile_image_url}
106
+ src={localPhoto(selected.profile_image_url)}
92
107
  alt={selected.name}
93
108
  style={{ width: 52, height: 52, borderRadius: '50%', objectFit: 'cover', border: `2px solid ${tierColors[selected.tier] || '#555'}`, flexShrink: 0 }}
94
109
  onError={(e) => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }}
@@ -97,7 +112,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
97
112
  <div style={{
98
113
  width: 52, height: 52, borderRadius: '50%', flexShrink: 0,
99
114
  background: tierColors[selected.tier] || '#555',
100
- display: selected.profile_image_url ? 'none' : 'flex',
115
+ display: localPhoto(selected.profile_image_url) ? 'none' : 'flex',
101
116
  alignItems: 'center', justifyContent: 'center',
102
117
  fontSize: 20, fontWeight: 700, color: selected.tier === 'S' ? '#000' : '#fff',
103
118
  }}>
@@ -114,6 +129,16 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
114
129
  </div>
115
130
  </div>
116
131
 
132
+ {/* Their circle, out to six degrees: who they know, who you added from
133
+ it, and who those people know (TheirCircle.js). Your connections only:
134
+ a 2nd-degree person's circle isn't yours to scan until they accept. */}
135
+ {selected.degree === 1 && (
136
+ <TheirCircle
137
+ person={selected} connections={connections} degree2={degree2}
138
+ tierColors={tierColors} onSelect={onSelect} isRequested={isRequested} canScan={canScan}
139
+ />
140
+ )}
141
+
117
142
  {selected.is_catalyst && (
118
143
  <div style={{
119
144
  background: 'rgba(0,255,136,0.08)', border: '1px solid rgba(0,255,136,0.3)',
@@ -239,8 +264,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
239
264
  onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
240
265
  onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.03)'}
241
266
  >
242
- {m.profile_image_url ? (
243
- <img src={m.profile_image_url} alt="" style={{
267
+ {localPhoto(m.profile_image_url) ? (
268
+ <img src={localPhoto(m.profile_image_url)} alt="" style={{
244
269
  width: 22, height: 22, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
245
270
  border: `1px solid ${tierColors[m.tier] || '#555'}`,
246
271
  }} onError={e => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }} />
@@ -248,7 +273,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
248
273
  <div style={{
249
274
  width: 22, height: 22, borderRadius: '50%', flexShrink: 0, fontSize: 9, fontWeight: 700,
250
275
  background: tierColors[m.tier] || '#555', color: m.tier === 'S' ? '#000' : '#fff',
251
- display: m.profile_image_url ? 'none' : 'flex', alignItems: 'center', justifyContent: 'center',
276
+ display: localPhoto(m.profile_image_url) ? 'none' : 'flex', alignItems: 'center', justifyContent: 'center',
252
277
  }}>{m.name?.charAt(0)}</div>
253
278
  <div style={{ flex: 1, minWidth: 0 }}>
254
279
  <div style={{ fontSize: 10, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.name}</div>
@@ -294,61 +319,67 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
294
319
  </button>
295
320
  )}
296
321
 
297
- {/* Connect to Unlock — for ALL degree-2 connections */}
298
- {(selected.degree === 2 || selected.source_connection_id) && selected.degree !== 1 && selected.unlock_status !== 'unlocked' && (
299
- <div style={{
300
- background: 'rgba(255,215,0,0.06)', border: '1px solid rgba(255,215,0,0.25)',
301
- borderRadius: 10, padding: 16, marginTop: 16, textAlign: 'center',
302
- }}>
303
- <div style={{ fontSize: 10, color: '#FFD700', fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>
304
- {selected.unlock_status === 'pending' ? 'CONNECTION PENDING' : 'LOCKED PATH'}
305
- </div>
306
- <div style={{ fontSize: 13, color: '#ccc', marginBottom: 12 }}>
307
- {selected.unlock_status === 'pending'
308
- ? `Waiting for ${selected.name} to accept. Run a scan to check.`
309
- : `Connect with ${selected.name} to unlock their network and extend your 6 degrees.`
310
- }
311
- </div>
322
+ {/* Connect to Unlock — for ALL degree-2 connections. A request sent here
323
+ is the person's, everywhere at once (lib/requests-client.js). */}
324
+ {(selected.degree === 2 || selected.source_connection_id) && selected.degree !== 1 && selected.unlock_status !== 'unlocked' && (() => {
325
+ const asked = isRequested(selected);
326
+ return (
312
327
  <div style={{
313
- width: '100%', height: 60, background: 'rgba(255,255,255,0.03)',
314
- borderRadius: 8, border: '1px dashed rgba(255,255,255,0.1)',
315
- display: 'flex', alignItems: 'center', justifyContent: 'center',
316
- fontSize: 11, color: '#444', marginBottom: 12,
328
+ background: 'rgba(255,215,0,0.06)', border: `1px ${asked ? 'dashed' : 'solid'} rgba(255,215,0,${asked ? 0.45 : 0.25})`,
329
+ borderRadius: 10, padding: 16, marginTop: 16, textAlign: 'center',
317
330
  }}>
318
- {selected.unlock_status === 'pending' ? '[ Cluster Pending... ]' : '[ Hidden Network ]'}
331
+ <div style={{ fontSize: 10, color: '#FFD700', fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>
332
+ {asked ? 'REQUEST SENT' : 'LOCKED PATH'}
333
+ </div>
334
+ <div style={{ fontSize: 13, color: '#ccc', marginBottom: 12 }}>
335
+ {asked
336
+ ? `Waiting for ${selected.name} to accept. The next scan of your own connections notices when they do.`
337
+ : `Connect with ${selected.name} to unlock their network and extend your six degrees.`
338
+ }
339
+ </div>
340
+ <div style={{
341
+ width: '100%', height: 60, background: 'rgba(255,255,255,0.03)',
342
+ borderRadius: 8, border: '1px dashed rgba(255,255,255,0.1)',
343
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
344
+ fontSize: 11, color: '#444', marginBottom: 12,
345
+ }}>
346
+ {asked ? '[ Cluster Pending... ]' : '[ Hidden Network ]'}
347
+ </div>
348
+ {asked ? (
349
+ <button
350
+ onClick={() => { undoRequest(selected).catch(() => {}); }}
351
+ style={{
352
+ background: 'none', border: 'none', color: '#888', fontSize: 11, fontWeight: 600, cursor: 'pointer',
353
+ }}
354
+ >
355
+ Didn&apos;t send it? Undo
356
+ </button>
357
+ ) : (
358
+ <a href={selected.profile_url} target="_blank" rel="noopener noreferrer"
359
+ style={{
360
+ display: 'block', padding: '10px 16px',
361
+ background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
362
+ color: '#000', borderRadius: 8, textDecoration: 'none',
363
+ fontWeight: 700, fontSize: 13,
364
+ }}
365
+ onClick={() => {
366
+ // Opens their profile to connect, and marks the request
367
+ // sent, through the bridge whose circle you found them in.
368
+ markRequested(selected, { bridgeId: selected.source_connection_id }).catch(() => {});
369
+ }}
370
+ >
371
+ Connect to Unlock Path
372
+ </a>
373
+ )}
319
374
  </div>
320
- {selected.unlock_status !== 'pending' && (
321
- <a href={selected.profile_url} target="_blank" rel="noopener noreferrer"
322
- style={{
323
- display: 'block', padding: '10px 16px',
324
- background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
325
- color: '#000', borderRadius: 8, textDecoration: 'none',
326
- fontWeight: 700, fontSize: 13,
327
- }}
328
- onClick={() => {
329
- // Mark as pending in unlock system
330
- fetch('/api/unlock', {
331
- method: 'POST',
332
- headers: { 'Content-Type': 'application/json' },
333
- body: JSON.stringify({ connectionId: selected.id }),
334
- }).catch(() => {});
335
- // Sync to outreach/pending system (Outlink)
336
- fetch('/api/outreach', {
337
- method: 'POST',
338
- headers: { 'Content-Type': 'application/json' },
339
- body: JSON.stringify({ action: 'mark-sent', connectionId: selected.id, profileUrl: selected.profile_url, userId }),
340
- }).catch(() => {});
341
- }}
342
- >
343
- Connect to Unlock Path
344
- </a>
345
- )}
346
- </div>
347
- )}
375
+ );
376
+ })()}
348
377
 
349
378
  {/* Create Cluster — for D1 connections OR accepted D2 (promoted, ready to bridge for D3) */}
350
379
  {(selected.degree === 1 || selected.outreach_status === 'accepted') && (
351
- <CreateClusterCard selected={selected} degree2={degree2} />
380
+ canScan
381
+ ? <CreateClusterCard key={selected.id} selected={selected} degree2={degree2} />
382
+ : <ScansNeedYourNetwork csvSource={csvSource} style={{ marginTop: 16 }} />
352
383
  )}
353
384
 
354
385
  {selected.profile_url && (
@@ -381,8 +412,10 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
381
412
  Your bridge connections and who they unlock.
382
413
  </p>
383
414
 
384
- {/* Auto-Bridge button */}
385
- <AutoBridgeButton connections={connections} degree2={degree2} />
415
+ {/* Who to scan next: people you reached, whose own circle is next */}
416
+ {canScan
417
+ ? <ReadyToScan connections={connections} degree2={degree2} scanNotes={scanNotes} tierColors={tierColors} />
418
+ : <ScansNeedYourNetwork csvSource={csvSource} style={{ marginBottom: 16 }} />}
386
419
 
387
420
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginBottom: 12 }}>
388
421
  <StatBox label="Degree-2 Found" value={degree2.length} />
@@ -435,8 +468,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
435
468
  onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.06)'}
436
469
  onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
437
470
  >
438
- {m.profile_image_url ? (
439
- <img src={m.profile_image_url} alt="" style={{ width: 24, height: 24, borderRadius: '50%', objectFit: 'cover' }} />
471
+ {localPhoto(m.profile_image_url) ? (
472
+ <img src={localPhoto(m.profile_image_url)} alt="" style={{ width: 24, height: 24, borderRadius: '50%', objectFit: 'cover' }} />
440
473
  ) : (
441
474
  <div style={{ width: 24, height: 24, borderRadius: '50%', background: tierColors[m.tier] || '#555', fontSize: 9, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>{m.name?.charAt(0)}</div>
442
475
  )}
@@ -471,8 +504,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
471
504
  border: '1px solid rgba(255,107,53,0.12)',
472
505
  }}>
473
506
  {/* Photo */}
474
- {p.profile_image_url ? (
475
- <img src={p.profile_image_url} alt="" style={{
507
+ {localPhoto(p.profile_image_url) ? (
508
+ <img src={localPhoto(p.profile_image_url)} alt="" style={{
476
509
  width: 32, height: 32, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
477
510
  border: `2px solid ${tierColors[p.tier] || '#555'}`,
478
511
  }} onError={e => { e.target.style.display = 'none'; }} />
@@ -653,8 +686,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
653
686
  onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
654
687
  onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
655
688
  >
656
- {c.profile_image_url ? (
657
- <img src={c.profile_image_url} alt="" style={{
689
+ {localPhoto(c.profile_image_url) ? (
690
+ <img src={localPhoto(c.profile_image_url)} alt="" style={{
658
691
  width: 24, height: 24, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
659
692
  border: `1.5px solid ${tierColors[c.tier] || '#555'}`,
660
693
  }} onError={e => { e.target.style.display = 'none'; }} />
@@ -683,6 +716,11 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
683
716
  {(() => {
684
717
  // Track per-tier rank numbering
685
718
  const tierRankCounter = {};
719
+ // Counted once. It used to be counted again for every person in the
720
+ // list: 900 million steps at 30,000 people, about 20 s on every render
721
+ // of the page while nothing was selected (opening the Filter panel, say).
722
+ const tierCounts = {};
723
+ allRanked.forEach(r => { tierCounts[r.tier] = (tierCounts[r.tier] || 0) + 1; });
686
724
  return allRanked.map((c, i) => {
687
725
  const prevTier = i > 0 ? allRanked[i - 1].tier : null;
688
726
  const showHeader = c.tier !== prevTier;
@@ -690,8 +728,6 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
690
728
  tierRankCounter[c.tier] = (tierRankCounter[c.tier] || 0) + 1;
691
729
  const tierRank = tierRankCounter[c.tier];
692
730
  const tierNames = { S: 'S-Tier — Elite', A: 'A-Tier — High Value', B: 'B-Tier — Notable', C: 'C-Tier — Standard', D: 'D-Tier — Entry' };
693
- const tierCounts = {};
694
- allRanked.forEach(r => { tierCounts[r.tier] = (tierCounts[r.tier] || 0) + 1; });
695
731
  return (
696
732
  <div key={c.id}>
697
733
  {showHeader && (
@@ -723,9 +759,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
723
759
  #{tierRank}
724
760
  </span>
725
761
  {/* Profile photo or initial */}
726
- {c.profile_image_url ? (
762
+ {localPhoto(c.profile_image_url) ? (
727
763
  <img
728
- src={c.profile_image_url}
764
+ src={localPhoto(c.profile_image_url)}
729
765
  alt=""
730
766
  style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
731
767
  border: `1.5px solid ${tierColors[c.tier] || '#555'}` }}
@@ -735,7 +771,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
735
771
  <div style={{
736
772
  width: 28, height: 28, borderRadius: '50%', flexShrink: 0,
737
773
  background: tierColors[c.tier] || '#555',
738
- display: c.profile_image_url ? 'none' : 'flex',
774
+ display: localPhoto(c.profile_image_url) ? 'none' : 'flex',
739
775
  alignItems: 'center', justifyContent: 'center',
740
776
  fontSize: 12, fontWeight: 700, color: c.tier === 'S' ? '#000' : '#fff',
741
777
  }}>
@@ -916,146 +952,181 @@ function PathBox({ routes, selected, tierColors, onSelect }) {
916
952
  );
917
953
  }
918
954
 
955
+ // Scan one person's circle, or carry on with it.
956
+ //
957
+ // LinkedIn lists someone else's connections in its own order, with no dates,
958
+ // so anyone new can turn up on any page: Rescan reads the whole list again from
959
+ // page 1. Resume carries on from the page the last read stopped at, for a list
960
+ // that was only partly read. Both show when there is something to resume.
961
+ //
962
+ // Whether a scan is running comes from the scanner (useScanner), not from this
963
+ // card: close the card mid-scan and open it again and the scan is still here,
964
+ // and every other card greys its buttons out until it ends.
919
965
  function CreateClusterCard({ selected, degree2 }) {
920
- const { userId } = useUser();
921
- const [status, setStatus] = useState('idle'); // idle, checking, scraping, done, error, offline
922
- const [log, setLog] = useState([]);
923
- const hasCluster = degree2?.some(d => d.source_connection_id === selected.id);
966
+ const scan = useScanner();
967
+ const [checking, setChecking] = useState(false);
968
+ const [problem, setProblem] = useState(null); // why a scan didn't start: { text, offline }
969
+ const [resume, setResume] = useState(undefined); // where Resume carries on; null: nothing to resume
924
970
  const clusterCount = degree2?.filter(d => d.source_connection_id === selected.id).length || 0;
925
-
926
- if (hasCluster && status === 'idle') {
927
- return (
928
- <div style={{
929
- background: 'rgba(0,255,136,0.06)', border: '1px solid rgba(0,255,136,0.2)',
930
- borderRadius: 10, padding: 16, marginTop: 16, textAlign: 'center',
931
- }}>
932
- <div style={{ fontSize: 10, color: '#00ff88', fontWeight: 700, letterSpacing: 1, marginBottom: 4 }}>
933
- CLUSTER ACTIVE
934
- </div>
935
- <div style={{ fontSize: 13, color: '#aaa' }}>
936
- {clusterCount} connections mapped in 6 Degrees
937
- </div>
938
- <button
939
- onClick={startScrape}
940
- style={{
941
- marginTop: 10, padding: '8px 16px', borderRadius: 6, border: 'none', cursor: 'pointer',
942
- background: 'rgba(0,255,136,0.12)', color: '#00ff88', fontWeight: 600, fontSize: 11,
943
- }}
944
- >
945
- Rescan Cluster
946
- </button>
947
- </div>
948
- );
949
- }
950
-
951
- async function startScrape() {
952
- setStatus('checking');
953
- setLog(['Checking the scanner...']);
971
+ const hasCluster = clusterCount > 0;
972
+
973
+ const mine = scan.running && scansCircleOf(scan, selected);
974
+ const busy = scan.running && !mine ? busyReason(scan) : null;
975
+ const last = scan.finished.find((j) => scansCircleOf(j, selected));
976
+
977
+ // Every read moves where Resume would carry on, so ask again whenever one ends.
978
+ const lastEnded = scan.finished[0]?.startedAt ?? null;
979
+ useEffect(() => {
980
+ let live = true;
981
+ resumePoint(selected.id).then((r) => { if (live) setResume(r); }, () => { if (live) setResume(null); });
982
+ return () => { live = false; };
983
+ }, [selected.id, lastEnded]);
984
+
985
+ async function start(action) {
986
+ setProblem(null);
987
+ setChecking(true);
954
988
  const blocked = notReadyMessage(await scraperStatus().catch(() => null));
989
+ setChecking(false);
955
990
  if (blocked) {
956
- setStatus('offline');
957
- setLog([blocked]);
991
+ setProblem({ text: blocked, offline: true });
958
992
  return;
959
993
  }
960
-
961
- setStatus('scraping');
962
- setLog(['Mapping the circle behind ' + selected.name + '...']);
963
-
964
994
  try {
965
- const final = await runScrape('bridge', { name: selected.name, onLog: setLog });
966
- if (final.exitCode === 0) {
967
- setStatus('done');
968
- setLog(prev => [...prev, 'Done. Refresh to see the cluster.']);
969
- } else {
970
- setStatus('error');
971
- }
972
- } catch {
973
- setStatus('error');
974
- setLog(['Failed to start the scan']);
995
+ await beginScrape(action, action === 'resume' ? { id: selected.id } : { name: selected.name, id: selected.id });
996
+ } catch (e) {
997
+ setProblem({ text: e.message || 'Could not start the scan.' });
975
998
  }
976
999
  }
977
1000
 
1001
+ const read = hasCluster || Boolean(resume);
1002
+ const accent = hasCluster ? '#00ff88' : '#9B59B6';
1003
+ const off = Boolean(busy) || checking;
1004
+ const button = (primary) => ({
1005
+ flex: 1, padding: '11px 8px', borderRadius: 8, fontWeight: 700, fontSize: primary ? 13 : 12,
1006
+ cursor: off ? 'not-allowed' : 'pointer', opacity: off ? 0.45 : 1,
1007
+ ...(primary
1008
+ ? { border: 'none', background: 'linear-gradient(135deg, #9B59B6, #3498DB)', color: '#fff' }
1009
+ : { border: `1px solid ${accent}55`, background: `${accent}14`, color: accent }),
1010
+ });
1011
+
978
1012
  return (
979
- <div style={{
980
- background: 'rgba(155,89,182,0.08)', border: '1px solid rgba(155,89,182,0.3)',
1013
+ <div id="scan-box" style={{
1014
+ background: hasCluster ? 'rgba(0,255,136,0.06)' : 'rgba(155,89,182,0.08)',
1015
+ border: `1px solid ${hasCluster ? 'rgba(0,255,136,0.2)' : 'rgba(155,89,182,0.3)'}`,
981
1016
  borderRadius: 10, padding: 16, marginTop: 16,
982
1017
  }}>
983
- <div style={{ fontSize: 10, color: '#9B59B6', fontWeight: 700, letterSpacing: 1, marginBottom: 6, textAlign: 'center' }}>
984
- CREATE CLUSTER
1018
+ <div style={{ fontSize: 10, color: accent, fontWeight: 700, letterSpacing: 1, marginBottom: 4, textAlign: 'center' }}>
1019
+ {hasCluster ? 'CLUSTER ACTIVE' : 'CREATE CLUSTER'}
985
1020
  </div>
986
- <div style={{ fontSize: 12, color: '#ccc', marginBottom: 12, textAlign: 'center' }}>
987
- Scan {selected.name}&apos;s connections to map their network
1021
+ <div style={{ fontSize: 12, color: '#aaa', marginBottom: 12, textAlign: 'center' }}>
1022
+ {hasCluster
1023
+ ? `${clusterCount} connections mapped in Six Degrees`
1024
+ : <>Scan {selected.name}&apos;s connections to map their network</>}
988
1025
  </div>
989
1026
 
990
- {status === 'idle' && (
991
- <button
992
- onClick={startScrape}
993
- style={{
994
- width: '100%', padding: '12px', borderRadius: 8, border: 'none', cursor: 'pointer',
995
- background: 'linear-gradient(135deg, #9B59B6, #3498DB)',
996
- color: '#fff', fontWeight: 700, fontSize: 14,
997
- }}
998
- >
999
- Scan {selected.name}&apos;s Network
1000
- </button>
1001
- )}
1002
-
1003
- {status === 'offline' && (
1027
+ {mine ? (
1028
+ <ScanLog title={scan.pending ? 'Starting…' : `Scanning ${selected.name}’s circle…`} log={scan.log} />
1029
+ ) : problem?.offline ? (
1004
1030
  <>
1005
1031
  <div style={{ padding: '10px', borderRadius: 8, background: 'rgba(255,80,80,0.1)', border: '1px solid rgba(255,80,80,0.3)', color: '#ff5050', fontSize: 12, marginBottom: 8, textAlign: 'center' }}>
1006
1032
  Scanner offline
1007
1033
  </div>
1008
1034
  <div style={{ fontSize: 10, color: '#888', textAlign: 'center', marginBottom: 8 }}>
1009
- Open the <strong>Scan</strong> page to finish setting the scanner up
1035
+ {problem.text} Open the <strong>Scan</strong> page to finish setting the scanner up.
1010
1036
  </div>
1011
- <button onClick={() => { setStatus('idle'); }} style={{
1037
+ <button onClick={() => setProblem(null)} style={{
1012
1038
  width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
1013
1039
  background: 'rgba(255,255,255,0.1)', color: '#aaa', fontWeight: 600, fontSize: 12,
1014
1040
  }}>
1015
1041
  Retry
1016
1042
  </button>
1017
1043
  </>
1018
- )}
1019
-
1020
- {(status === 'checking' || status === 'scraping') && (
1021
- <div>
1022
- <div style={{
1023
- padding: '10px', borderRadius: 8, background: 'rgba(155,89,182,0.1)',
1024
- border: '1px solid rgba(155,89,182,0.3)', marginBottom: 8,
1025
- }}>
1026
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
1027
- <div style={{ width: 8, height: 8, borderRadius: '50%', background: '#9B59B6', animation: 'pulse 1s infinite' }} />
1028
- <span style={{ fontSize: 11, fontWeight: 600, color: '#9B59B6' }}>Scanning...</span>
1044
+ ) : (
1045
+ <>
1046
+ {last && <LastRead job={last} />}
1047
+ <div style={{ display: 'flex', gap: 8 }}>
1048
+ {resume && (
1049
+ <button onClick={() => start('resume')} disabled={off} style={button(true)}>
1050
+ Resume from page {resume.nextPage}
1051
+ </button>
1052
+ )}
1053
+ <button onClick={() => start('bridge')} disabled={off} style={button(!resume && !hasCluster)}>
1054
+ {read ? 'Rescan from the start' : <>Scan {selected.name}&apos;s Network</>}
1055
+ </button>
1056
+ </div>
1057
+ {resume && (
1058
+ <div style={{ fontSize: 10, color: '#888', marginTop: 8, lineHeight: 1.45, textAlign: 'center' }}>
1059
+ Read to page {resume.pagesRead} so far. Resume carries on from page {resume.nextPage}. LinkedIn
1060
+ lists their connections in its own order, not by date, so Rescan reads them all again from page 1.
1061
+ </div>
1062
+ )}
1063
+ {hasCluster && resume === null && (
1064
+ <div style={{ fontSize: 10, color: '#888', marginTop: 8, lineHeight: 1.45, textAlign: 'center' }}>
1065
+ Their whole list has been read. LinkedIn doesn&apos;t date other people&apos;s connections, so a
1066
+ rescan reads it all again to find anyone new.
1029
1067
  </div>
1030
- <div style={{ fontFamily: 'monospace', fontSize: 10, color: '#888', maxHeight: 100, overflow: 'auto' }}>
1031
- {log.map((l, i) => <div key={i}>{l}</div>)}
1068
+ )}
1069
+ {busy && (
1070
+ <div style={{ fontSize: 10, color: '#bbb', marginTop: 8, textAlign: 'center' }}>
1071
+ {busy}. One scan at a time: this one can start when it finishes.
1032
1072
  </div>
1033
- </div>
1034
- </div>
1073
+ )}
1074
+ {problem && (
1075
+ <div style={{ fontSize: 11, color: '#ff8080', marginTop: 8, textAlign: 'center' }}>{problem.text}</div>
1076
+ )}
1077
+ </>
1035
1078
  )}
1079
+ </div>
1080
+ );
1081
+ }
1036
1082
 
1037
- {status === 'done' && (
1038
- <div style={{
1039
- padding: '10px', borderRadius: 8, background: 'rgba(0,255,136,0.1)',
1040
- border: '1px solid rgba(0,255,136,0.3)', color: '#00ff88', fontSize: 12, fontWeight: 600, textAlign: 'center',
1041
- }}>
1042
- Cluster created! Refresh the page to see it.
1043
- </div>
1044
- )}
1083
+ // In place of the Scan buttons while the sample or a CSV import is open. Both
1084
+ // live in this window only, so a scan could only fail: the scanner looks the
1085
+ // person up in the database, and says "Bridge '…' not found in database".
1086
+ function ScansNeedYourNetwork({ csvSource, style }) {
1087
+ return (
1088
+ <div style={{
1089
+ padding: '10px 12px', borderRadius: 8, fontSize: 11, color: '#999', lineHeight: 1.5, textAlign: 'center',
1090
+ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', ...style,
1091
+ }}>
1092
+ {csvSource === 'sample'
1093
+ ? 'Scanning needs your own network: everyone in the sample is invented.'
1094
+ : 'Scanning needs your own network: a CSV import lives only in this window. Close it (× at the top) and open Scan.'}
1095
+ </div>
1096
+ );
1097
+ }
1045
1098
 
1046
- {status === 'error' && (
1047
- <>
1048
- <div style={{ padding: '10px', borderRadius: 8, background: 'rgba(255,80,80,0.1)', border: '1px solid rgba(255,80,80,0.3)', color: '#ff5050', fontSize: 12, textAlign: 'center', marginBottom: 8 }}>
1049
- Scan failed — connections may be private
1050
- </div>
1051
- <button onClick={() => { setStatus('idle'); setLog([]); }} style={{
1052
- width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
1053
- background: 'rgba(255,255,255,0.1)', color: '#aaa', fontWeight: 600, fontSize: 12,
1054
- }}>
1055
- Try Again
1056
- </button>
1057
- </>
1058
- )}
1099
+ /** A running scan's log, newest at the bottom. */
1100
+ function ScanLog({ title, log, small = false }) {
1101
+ return (
1102
+ <div style={{
1103
+ padding: '10px', borderRadius: 8, background: 'rgba(155,89,182,0.1)', border: '1px solid rgba(155,89,182,0.3)',
1104
+ }}>
1105
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
1106
+ <div style={{ width: small ? 6 : 8, height: small ? 6 : 8, borderRadius: '50%', background: '#9B59B6', animation: 'pulse 1s infinite' }} />
1107
+ <span style={{ fontSize: 11, fontWeight: 600, color: '#9B59B6' }}>{title}</span>
1108
+ </div>
1109
+ <div style={{ fontFamily: 'monospace', fontSize: small ? 9 : 10, color: '#888', maxHeight: small ? 80 : 100, overflow: 'auto' }}>
1110
+ {log.slice(-40).map((l, i) => <div key={i}>{l}</div>)}
1111
+ </div>
1112
+ </div>
1113
+ );
1114
+ }
1115
+
1116
+ /** How a scan this page watched ended. */
1117
+ function LastRead({ job, who = null }) {
1118
+ const ok = job.exitCode === 0;
1119
+ const why = !ok && Array.isArray(job.failure) && job.failure.length ? job.failure[job.failure.length - 1] : null;
1120
+ return (
1121
+ <div style={{
1122
+ padding: '8px 10px', borderRadius: 8, marginBottom: 10, fontSize: 11, textAlign: 'center', lineHeight: 1.4,
1123
+ background: ok ? 'rgba(0,255,136,0.08)' : 'rgba(255,80,80,0.08)',
1124
+ border: `1px solid ${ok ? 'rgba(0,255,136,0.25)' : 'rgba(255,80,80,0.25)'}`,
1125
+ color: ok ? '#00ff88' : '#ff8080',
1126
+ }}>
1127
+ {ok
1128
+ ? `${who ? `${who}’s circle` : 'The scan'} finished. What it found is on the map.`
1129
+ : `${who ? `${who}’s scan` : 'The scan'} stopped before the end${why ? `: ${why}` : '.'}`}
1059
1130
  </div>
1060
1131
  );
1061
1132
  }
@@ -1076,8 +1147,8 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
1076
1147
  style={{ cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}
1077
1148
  >
1078
1149
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1079
- {cs.bridge.profile_image_url ? (
1080
- <img src={cs.bridge.profile_image_url} alt="" style={{
1150
+ {localPhoto(cs.bridge.profile_image_url) ? (
1151
+ <img src={localPhoto(cs.bridge.profile_image_url)} alt="" style={{
1081
1152
  width: 28, height: 28, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
1082
1153
  border: `1.5px solid ${tierColors[cs.bridge.tier] || '#555'}`,
1083
1154
  }} onError={e => { e.target.style.display = 'none'; }} />
@@ -1120,8 +1191,8 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
1120
1191
  {(cs.sTier.length > 0 || cs.aTier.length > 0) && (
1121
1192
  <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 8 }}>
1122
1193
  {[...cs.sTier, ...cs.aTier].slice(0, 4).map(m => (
1123
- m.profile_image_url ? (
1124
- <img key={m.id} src={m.profile_image_url} alt={m.name}
1194
+ localPhoto(m.profile_image_url) ? (
1195
+ <img key={m.id} src={localPhoto(m.profile_image_url)} alt={m.name}
1125
1196
  style={{ width: 22, height: 22, borderRadius: '50%', objectFit: 'cover',
1126
1197
  border: `1.5px solid ${tierColors[m.tier] || '#555'}` }}
1127
1198
  onError={(e) => { e.target.style.display = 'none'; }}
@@ -1171,8 +1242,8 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
1171
1242
  onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.06)'}
1172
1243
  onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
1173
1244
  >
1174
- {m.profile_image_url ? (
1175
- <img src={m.profile_image_url} alt="" style={{
1245
+ {localPhoto(m.profile_image_url) ? (
1246
+ <img src={localPhoto(m.profile_image_url)} alt="" style={{
1176
1247
  width: 20, height: 20, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
1177
1248
  border: `1px solid ${tierColors[m.tier] || '#555'}`,
1178
1249
  }} onError={(e) => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }} />
@@ -1180,7 +1251,7 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
1180
1251
  <div style={{
1181
1252
  width: 20, height: 20, borderRadius: '50%', flexShrink: 0, fontSize: 8, fontWeight: 700,
1182
1253
  background: tierColors[m.tier] || '#555', color: m.tier === 'S' ? '#000' : '#fff',
1183
- display: m.profile_image_url ? 'none' : 'flex',
1254
+ display: localPhoto(m.profile_image_url) ? 'none' : 'flex',
1184
1255
  alignItems: 'center', justifyContent: 'center',
1185
1256
  }}>{m.name?.charAt(0)}</div>
1186
1257
  <div style={{ minWidth: 0, flex: 1 }}>
@@ -1277,122 +1348,130 @@ function generateInsights(person, user, allConnections, degree2, routes = []) {
1277
1348
  return insights.slice(0, 5); // Max 5 insights
1278
1349
  }
1279
1350
 
1280
- function AutoBridgeButton({ connections, degree2 }) {
1281
- const [status, setStatus] = useState('idle');
1282
- const [log, setLog] = useState([]);
1283
- const [target, setTarget] = useState(null);
1284
-
1285
- // Find the top S-tier person without a cluster
1286
- const bridgedIds = new Set(degree2.map(d => d.source_connection_id).filter(Boolean));
1287
- const unbridged = (connections || [])
1288
- .filter(c => (c.tier === 'S' || c.tier === 'A') && !bridgedIds.has(c.id))
1289
- .sort((a, b) => (parseFloat(b.power_score) || 0) - (parseFloat(a.power_score) || 0));
1290
-
1291
- if (unbridged.length === 0 && status === 'idle') {
1292
- return (
1293
- <div style={{
1294
- padding: '10px', borderRadius: 8, marginBottom: 16,
1295
- background: 'rgba(0,255,136,0.06)', border: '1px solid rgba(0,255,136,0.2)',
1296
- color: '#00ff88', fontSize: 11, fontWeight: 600, textAlign: 'center',
1297
- }}>
1298
- All S/A-tier bridges mapped
1299
- </div>
1300
- );
1301
- }
1302
-
1303
- async function startAutoBridge() {
1304
- const next = unbridged[0];
1305
- if (!next) return;
1306
- setTarget(next);
1307
- setStatus('checking');
1308
- setLog([`Scanning ${next.name}'s network...`]);
1309
-
1310
- const blocked = notReadyMessage(await scraperStatus().catch(() => null));
1311
- if (blocked) {
1312
- setStatus('offline');
1313
- setLog([blocked]);
1314
- return;
1315
- }
1351
+ // Who to scan next, in the Degrees panel. It replaced "Auto-Bridge Next", which
1352
+ // started a scan of the top unmapped S/A connection in one click, by name, with
1353
+ // no cost shown, and offered hidden lists again after every run.
1354
+ //
1355
+ // This lists everyone you reached through a circle whose own circle can be
1356
+ // scanned now (lib/reach.js), strongest first, with the circle you found them
1357
+ // in and what a scan asks of LinkedIn. Each opens the Scan page with them
1358
+ // picked, and nothing starts until it's confirmed there, beside the budget and
1359
+ // the cooldown. Hidden lists stay in view, greyed, so nobody seems forgotten.
1360
+ // It's also the way to them from a keyboard or a screen reader: the orbs in
1361
+ // Bridge Chains aren't.
1362
+ function ReadyToScan({ connections, degree2, scanNotes, tierColors }) {
1363
+ const scan = useScanner();
1364
+ const [all, setAll] = useState(false);
1365
+ const reach = useMemo(() => reachIndex(connections, degree2, scanNotes), [connections, degree2, scanNotes]);
1366
+ const ready = useMemo(() => readyToScan(connections, reach), [connections, reach]);
1367
+ const hidden = useMemo(() => connections.filter((c) => reachState(c, reach) === 'hidden'), [connections, reach]);
1368
+ // Everyone else whose circle isn't scanned: the Scan page's batches.
1369
+ const others = useMemo(
1370
+ () => connections.filter((c) => reachState(c, reach) === null && circleState(c, reach) === 'todo').length,
1371
+ [connections, reach],
1372
+ );
1373
+ const byId = useMemo(() => new Map(connections.map((c) => [c.id, c])), [connections]);
1374
+ const cost = circleScanCost();
1316
1375
 
1317
- setStatus('scraping');
1318
- try {
1319
- const final = await runScrape('bridge', { name: next.name, onLog: setLog });
1320
- setStatus(final.exitCode === 0 ? 'done' : 'error');
1321
- } catch {
1322
- setStatus('error');
1323
- }
1324
- }
1376
+ const circle = scan.running && isCircleScan(scan);
1377
+ const busy = scan.running && !circle ? busyReason(scan) : null;
1378
+ const last = scan.finished.find(isCircleScan);
1379
+ const shown = all ? ready : ready.slice(0, 5);
1380
+ const origin = (p) => byId.get(p.unlocked_from_bridge_id)?.name || p.unlocked_from_name;
1381
+ const first = (name) => String(name || '').trim().split(/\s+/)[0];
1325
1382
 
1326
1383
  return (
1327
1384
  <div style={{
1328
- background: 'rgba(155,89,182,0.08)', border: '1px solid rgba(155,89,182,0.2)',
1385
+ background: 'rgba(0,255,136,0.05)', border: '1px solid rgba(0,255,136,0.2)',
1329
1386
  borderRadius: 10, padding: 14, marginBottom: 16,
1330
1387
  }}>
1331
- {status === 'idle' && (
1332
- <>
1333
- <div style={{ fontSize: 11, color: '#9B59B6', fontWeight: 700, marginBottom: 6 }}>
1334
- {unbridged.length} bridges unmapped
1335
- </div>
1336
- <div style={{ fontSize: 11, color: '#aaa', marginBottom: 8 }}>
1337
- Next: <strong>{unbridged[0]?.name}</strong> ({unbridged[0]?.tier}-tier, {parseFloat(unbridged[0]?.power_score).toFixed(1)})
1338
- </div>
1339
- <button onClick={startAutoBridge} style={{
1340
- width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
1341
- background: 'linear-gradient(135deg, #9B59B6, #FF6B35)',
1342
- color: '#fff', fontWeight: 700, fontSize: 13,
1343
- }}>
1344
- Auto-Bridge Next
1345
- </button>
1346
- </>
1347
- )}
1388
+ <div style={{ fontSize: 12, fontWeight: 800, color: '#00ff88', marginBottom: 4 }}>
1389
+ Ready to scan ({ready.length})
1390
+ </div>
1391
+ <div style={{ fontSize: 10.5, color: '#999', lineHeight: 1.5, marginBottom: 10 }}>
1392
+ People you added through a circle whose own circle isn&apos;t scanned yet. Scanning one
1393
+ brings in who they know: 3rd degree, through the circle you found them in.
1394
+ </div>
1348
1395
 
1349
- {status === 'offline' && (
1350
- <>
1351
- <div style={{ padding: '8px', borderRadius: 6, background: 'rgba(255,80,80,0.1)', color: '#ff5050', fontSize: 11, marginBottom: 6, textAlign: 'center' }}>
1352
- Scanner offline
1353
- </div>
1354
- <button onClick={() => setStatus('idle')} style={{
1355
- width: '100%', padding: '8px', borderRadius: 6, border: 'none', cursor: 'pointer',
1356
- background: 'rgba(255,255,255,0.1)', color: '#aaa', fontSize: 11,
1357
- }}>Retry</button>
1358
- </>
1396
+ {circle ? (
1397
+ <div style={{ marginBottom: 10 }}>
1398
+ <ScanLog small title={scan.pending ? 'Starting…' : `Scanning ${scan.target?.name || 'a circle'}…`} log={scan.log} />
1399
+ </div>
1400
+ ) : last && <LastRead job={last} who={last.target?.name} />}
1401
+
1402
+ {ready.length === 0 ? (
1403
+ <div style={{ fontSize: 11, color: '#aaa', lineHeight: 1.5 }}>
1404
+ Nobody yet. When someone you asked from a circle accepts, ↻ <b>Check for new</b> brings
1405
+ them into your connections, and they show here.
1406
+ </div>
1407
+ ) : (
1408
+ <div role="list" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
1409
+ {shown.map((p) => {
1410
+ const now = circle && scansCircleOf(scan, p);
1411
+ const from = origin(p);
1412
+ return (
1413
+ <Link key={p.id} role="listitem" href={`/setup?scan=${encodeURIComponent(p.id)}`}
1414
+ aria-label={`Scan ${p.name}’s circle: opens the Scan page to confirm`}
1415
+ style={{
1416
+ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', borderRadius: 8,
1417
+ textDecoration: 'none', color: '#fff',
1418
+ background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(0,255,136,0.18)',
1419
+ }}>
1420
+ <span style={{ width: 8, height: 8, borderRadius: '50%', flexShrink: 0, background: tierColors[p.tier] || '#888', boxShadow: '0 0 0 2px rgba(0,255,136,0.45)' }} />
1421
+ <span style={{ flex: 1, minWidth: 0 }}>
1422
+ <span style={{ fontSize: 12, fontWeight: 700 }}>{p.name}</span>
1423
+ <span style={{ fontSize: 10, color: tierColors[p.tier] || '#888', fontWeight: 700, marginLeft: 6 }}>{p.tier}</span>
1424
+ {from && (
1425
+ <span style={{ display: 'block', fontSize: 10, color: '#888', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
1426
+ was in {first(from)}&apos;s circle
1427
+ </span>
1428
+ )}
1429
+ </span>
1430
+ <span style={{ fontSize: 10.5, fontWeight: 700, color: '#00ff88', whiteSpace: 'nowrap' }}>
1431
+ {now ? 'Scanning…' : 'Scan… →'}
1432
+ </span>
1433
+ </Link>
1434
+ );
1435
+ })}
1436
+ {ready.length > 5 && (
1437
+ <button type="button" onClick={() => setAll((v) => !v)} style={{
1438
+ background: 'none', border: 'none', color: '#888', fontSize: 10.5, fontWeight: 600, cursor: 'pointer', padding: '2px 0', textAlign: 'left',
1439
+ }}>
1440
+ {all ? 'Show fewer' : `Show all ${ready.length}`}
1441
+ </button>
1442
+ )}
1443
+ </div>
1359
1444
  )}
1360
1445
 
1361
- {(status === 'checking' || status === 'scraping') && (
1362
- <div>
1363
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
1364
- <div style={{ width: 6, height: 6, borderRadius: '50%', background: '#9B59B6', animation: 'pulse 1s infinite' }} />
1365
- <span style={{ fontSize: 11, fontWeight: 600, color: '#9B59B6' }}>Scanning {target?.name}...</span>
1366
- </div>
1367
- <div style={{ fontFamily: 'monospace', fontSize: 9, color: '#777', maxHeight: 80, overflow: 'auto' }}>
1368
- {log.map((l, i) => <div key={i}>{l}</div>)}
1369
- </div>
1446
+ {ready.length > 0 && (
1447
+ <div style={{ fontSize: 10, color: '#888', lineHeight: 1.5, marginTop: 8 }}>
1448
+ Each scan: {cost.profileViews} profile view, then one LinkedIn search for every page of their
1449
+ list, up to {cost.searches} pages (about {cost.minutes} min for a whole list). Nothing starts
1450
+ until you confirm it on the Scan page.
1370
1451
  </div>
1371
1452
  )}
1372
1453
 
1373
- {status === 'done' && (
1374
- <div style={{ textAlign: 'center' }}>
1375
- <div style={{ padding: '8px', borderRadius: 6, background: 'rgba(0,255,136,0.1)', color: '#00ff88', fontSize: 11, fontWeight: 600, marginBottom: 6 }}>
1376
- {target?.name}&rsquo;s bridge mapped! Refresh to see.
1377
- </div>
1378
- <button onClick={() => { setStatus('idle'); setTarget(null); setLog([]); }} style={{
1379
- width: '100%', padding: '8px', borderRadius: 6, border: 'none', cursor: 'pointer',
1380
- background: 'linear-gradient(135deg, #9B59B6, #FF6B35)',
1381
- color: '#fff', fontWeight: 600, fontSize: 11,
1382
- }}>Bridge Next</button>
1454
+ {hidden.length > 0 && (
1455
+ <div style={{ marginTop: 10, fontSize: 10.5, color: '#777', lineHeight: 1.6 }}>
1456
+ {hidden.slice(0, 3).map((p) => (
1457
+ <div key={p.id} style={{ opacity: 0.7 }}>
1458
+ <span aria-hidden="true">🔒</span> {p.name} <span style={{ color: '#666' }}>· their list is hidden</span>
1459
+ </div>
1460
+ ))}
1461
+ {hidden.length > 3 && <div style={{ opacity: 0.7 }}>🔒 and {hidden.length - 3} more with hidden lists</div>}
1383
1462
  </div>
1384
1463
  )}
1385
1464
 
1386
- {status === 'error' && (
1387
- <>
1388
- <div style={{ padding: '8px', borderRadius: 6, background: 'rgba(255,80,80,0.1)', color: '#ff5050', fontSize: 11, marginBottom: 6, textAlign: 'center' }}>
1389
- Failed — connections may be private
1390
- </div>
1391
- <button onClick={() => { setStatus('idle'); setTarget(null); setLog([]); }} style={{
1392
- width: '100%', padding: '8px', borderRadius: 6, border: 'none', cursor: 'pointer',
1393
- background: 'rgba(255,255,255,0.1)', color: '#aaa', fontSize: 11,
1394
- }}>Try Next</button>
1395
- </>
1465
+ {others > 0 && (
1466
+ <div style={{ fontSize: 10.5, color: '#888', lineHeight: 1.5, marginTop: 10 }}>
1467
+ {others.toLocaleString('en-US')} of your other connections aren&apos;t scanned yet.{' '}
1468
+ <Link href="/setup" style={{ color: '#3498DB', textDecoration: 'none' }}>Map them in batches on the Scan page →</Link>
1469
+ </div>
1470
+ )}
1471
+ {busy && (
1472
+ <div style={{ fontSize: 10, color: '#bbb', marginTop: 8 }}>
1473
+ {busy}. One scan at a time.
1474
+ </div>
1396
1475
  )}
1397
1476
  </div>
1398
1477
  );