six-degrees 0.4.0-beta.1 → 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 (355) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +5 -5
  4. package/.next/standalone/.next/required-server-files.json +6 -1
  5. package/.next/standalone/.next/routes-manifest.json +12 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  36. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  48. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  53. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  55. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  60. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/import.html +1 -1
  65. package/.next/standalone/.next/server/app/import.rsc +13 -13
  66. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  67. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  68. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
  69. package/.next/standalone/.next/server/app/index.html +1 -1
  70. package/.next/standalone/.next/server/app/index.rsc +13 -13
  71. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
  72. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  73. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  74. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/launch.html +2 -2
  77. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  78. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  79. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  80. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
  81. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  85. package/.next/standalone/.next/server/app/paths.html +1 -1
  86. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  87. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  88. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  89. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
  90. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/profile.html +1 -1
  93. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  94. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  95. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
  97. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/queue.html +1 -1
  100. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  101. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  102. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  103. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
  104. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/scores.html +1 -1
  107. package/.next/standalone/.next/server/app/scores.rsc +13 -13
  108. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +13 -13
  109. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  110. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +12 -12
  111. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/settings.html +1 -1
  114. package/.next/standalone/.next/server/app/settings.rsc +13 -13
  115. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
  116. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  117. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
  118. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/setup.html +1 -1
  121. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  122. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  123. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  124. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
  125. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
  128. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0rc27za._.js → [root-of-the-server]__0bat8nx._.js} +2 -2
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -6
  154. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  156. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +12 -8
  158. package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
  159. package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
  160. package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
  161. package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
  162. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  163. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
  165. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
  166. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
  167. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
  168. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
  169. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
  170. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
  172. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +1 -1
  173. package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
  174. package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
  175. package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
  176. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +1 -1
  178. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/ssr/{_0cu6-an._.js → _1vpfyd-._.js} +2 -2
  180. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
  181. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
  182. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
  184. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  186. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  187. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  189. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +1 -1
  190. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  191. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  192. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  193. package/.next/standalone/.next/server/pages/404.html +1 -1
  194. package/.next/standalone/.next/server/pages/500.html +1 -1
  195. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  196. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  197. package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  198. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  199. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
  200. package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  201. package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  202. package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
  203. package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  204. package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
  205. package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  206. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  207. package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  208. package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  209. package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  210. package/.next/standalone/CHANGELOG.md +152 -0
  211. package/.next/standalone/CONTRIBUTING.md +15 -4
  212. package/.next/standalone/README.md +21 -15
  213. package/.next/standalone/SECURITY.md +10 -0
  214. package/.next/standalone/app/api/ingest/route.js +6 -1
  215. package/.next/standalone/app/api/scraper/route.js +79 -11
  216. package/.next/standalone/app/api/update-images/route.js +41 -7
  217. package/.next/standalone/app/avatars/[file]/route.js +6 -1
  218. package/.next/standalone/app/components/Avatar.js +5 -2
  219. package/.next/standalone/app/components/ChainView.js +659 -469
  220. package/.next/standalone/app/components/EmptyState.js +1 -1
  221. package/.next/standalone/app/components/FieldStep.js +94 -0
  222. package/.next/standalone/app/components/ForceGraph.js +183 -768
  223. package/.next/standalone/app/components/GridView.js +9 -7
  224. package/.next/standalone/app/components/LinkedInLimits.js +22 -7
  225. package/.next/standalone/app/components/ListView.js +6 -4
  226. package/.next/standalone/app/components/MappingProgress.js +5 -6
  227. package/.next/standalone/app/components/OnboardingGate.js +1 -1
  228. package/.next/standalone/app/components/OrbitGraph.js +3 -1
  229. package/.next/standalone/app/components/OutlinkQuest.js +5 -4
  230. package/.next/standalone/app/components/RingsView.js +9 -8
  231. package/.next/standalone/app/components/Sidebar.js +172 -112
  232. package/.next/standalone/app/components/TheirCircle.js +18 -11
  233. package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
  234. package/.next/standalone/app/components/settings/SectorSection.js +17 -191
  235. package/.next/standalone/app/components/settings/TierSection.js +7 -12
  236. package/.next/standalone/app/components/views.js +2 -2
  237. package/.next/standalone/app/globals.css +7 -0
  238. package/.next/standalone/app/launch/page.js +2 -2
  239. package/.next/standalone/app/page.js +130 -25
  240. package/.next/standalone/app/paths/page.js +3 -2
  241. package/.next/standalone/app/queue/page.js +18 -10
  242. package/.next/standalone/app/setup/page.js +423 -260
  243. package/.next/standalone/db/schema.js +1 -1
  244. package/.next/standalone/eslint.config.mjs +3 -3
  245. package/.next/standalone/install.sh +3 -3
  246. package/.next/standalone/lib/chain-layout.js +139 -0
  247. package/.next/standalone/lib/csv.js +4 -6
  248. package/.next/standalone/lib/data-export.js +2 -1
  249. package/.next/standalone/lib/data-folder.js +4 -2
  250. package/.next/standalone/lib/galaxy.js +12 -0
  251. package/.next/standalone/lib/ingest.js +15 -8
  252. package/.next/standalone/lib/linkedin-limits.js +30 -8
  253. package/.next/standalone/lib/photos.js +47 -0
  254. package/.next/standalone/lib/quest.js +8 -3
  255. package/.next/standalone/lib/reach.js +113 -0
  256. package/.next/standalone/lib/scan-state.js +1 -0
  257. package/.next/standalone/lib/scanner-setup.js +20 -1
  258. package/.next/standalone/lib/scoring.js +769 -18
  259. package/.next/standalone/lib/scraper-client.js +27 -0
  260. package/.next/standalone/lib/sector-focus.js +13 -0
  261. package/.next/standalone/lib/separation.js +4 -2
  262. package/.next/standalone/lib/settings-client.js +21 -0
  263. package/.next/standalone/lib/settings.js +3 -1
  264. package/.next/standalone/next.config.mjs +25 -3
  265. package/.next/standalone/package-lock.json +2 -2
  266. package/.next/standalone/package.json +1 -1
  267. package/.next/standalone/scripts/build-app.mjs +11 -2
  268. package/.next/standalone/scripts/check-node.mjs +1 -1
  269. package/.next/standalone/scripts/image_store.py +77 -21
  270. package/.next/standalone/scripts/scrape.py +243 -29
  271. package/.next/standalone/server.js +1 -1
  272. package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  273. package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  274. package/.next/static/chunks/0l1568qff0efj.js +1 -0
  275. package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  276. package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  277. package/.next/static/chunks/135n87ydxjrra.js +2 -0
  278. package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  279. package/.next/static/chunks/28bu26ausqzmn.js +1 -0
  280. package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  281. package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  282. package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  283. package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  284. package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  285. package/CHANGELOG.md +152 -0
  286. package/README.md +21 -15
  287. package/SECURITY.md +10 -0
  288. package/db/schema.js +1 -1
  289. package/lib/chain-layout.js +139 -0
  290. package/lib/csv.js +4 -6
  291. package/lib/data-export.js +2 -1
  292. package/lib/data-folder.js +4 -2
  293. package/lib/galaxy.js +12 -0
  294. package/lib/ingest.js +15 -8
  295. package/lib/linkedin-limits.js +30 -8
  296. package/lib/photos.js +47 -0
  297. package/lib/quest.js +8 -3
  298. package/lib/reach.js +113 -0
  299. package/lib/scan-state.js +1 -0
  300. package/lib/scanner-setup.js +20 -1
  301. package/lib/scoring.js +769 -18
  302. package/lib/scraper-client.js +27 -0
  303. package/lib/sector-focus.js +13 -0
  304. package/lib/separation.js +4 -2
  305. package/lib/settings-client.js +21 -0
  306. package/lib/settings.js +3 -1
  307. package/package.json +1 -1
  308. package/scripts/check-node.mjs +1 -1
  309. package/scripts/image_store.py +77 -21
  310. package/scripts/scrape.py +243 -29
  311. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
  312. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
  313. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
  314. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
  315. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
  316. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
  317. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
  318. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
  319. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
  320. package/.next/standalone/.next/server/chunks/ssr/_0223v9s._.js +0 -3
  321. package/.next/standalone/.next/server/chunks/ssr/_0bcf13g._.js +0 -3
  322. package/.next/standalone/.next/server/chunks/ssr/_0wxbq78._.js +0 -3
  323. package/.next/standalone/.next/server/chunks/ssr/_0z88zcl._.js +0 -3
  324. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
  325. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
  326. package/.next/standalone/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  327. package/.next/standalone/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  328. package/.next/standalone/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  329. package/.next/standalone/.next/static/chunks/1d5hl1io798n2.js +0 -1
  330. package/.next/standalone/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  331. package/.next/standalone/.next/static/chunks/1roznk7hkjes1.js +0 -44
  332. package/.next/standalone/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  333. package/.next/standalone/.next/static/chunks/36gbwek52x_w-.js +0 -2
  334. package/.next/standalone/.next/static/chunks/3a55suosthntc.js +0 -1
  335. package/.next/standalone/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  336. package/.next/standalone/.next/static/chunks/3nbnio971zlji.js +0 -2
  337. package/.next/standalone/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  338. package/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  339. package/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  340. package/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  341. package/.next/static/chunks/1d5hl1io798n2.js +0 -1
  342. package/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  343. package/.next/static/chunks/1roznk7hkjes1.js +0 -44
  344. package/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  345. package/.next/static/chunks/36gbwek52x_w-.js +0 -2
  346. package/.next/static/chunks/3a55suosthntc.js +0 -1
  347. package/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  348. package/.next/static/chunks/3nbnio971zlji.js +0 -2
  349. package/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  350. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  351. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  352. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
  353. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  354. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  355. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useMemo, useCallback } from 'react';
4
+ import Link from 'next/link';
4
5
  import { beginScrape, scraperStatus, notReadyMessage, resumePoint, scansCircleOf, isCircleScan, busyReason } from '../../lib/scraper-client';
5
6
  import useScanner from './useScanner';
6
7
  import useRequests from './useRequests';
@@ -9,10 +10,16 @@ import { hasRequest, markRequested, undoRequest } from '../../lib/requests-clien
9
10
  import { useUser } from './UserProvider';
10
11
  import { routeIndex, routesFor } from '../../lib/separation';
11
12
  import { topCompanies } from '../../lib/scoring';
13
+ import { reachIndex, reachState, circleState, readyToScan, circleScanCost } from '../../lib/reach';
12
14
  import Avatar from './Avatar';
15
+ import { localPhoto } from '../../lib/photos';
13
16
 
14
- 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 }) {
15
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;
16
23
  const { userId, userProfile: ctxProfile } = useUser();
17
24
  const userProfile = ctxProfile || { name: 'User', sectors: [], goals: [] };
18
25
  const [searchQuery, setSearchQuery] = useState('');
@@ -94,9 +101,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
94
101
  </div>
95
102
  {/* Profile photo + name */}
96
103
  <div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 16 }}>
97
- {selected.profile_image_url ? (
104
+ {localPhoto(selected.profile_image_url) ? (
98
105
  <img
99
- src={selected.profile_image_url}
106
+ src={localPhoto(selected.profile_image_url)}
100
107
  alt={selected.name}
101
108
  style={{ width: 52, height: 52, borderRadius: '50%', objectFit: 'cover', border: `2px solid ${tierColors[selected.tier] || '#555'}`, flexShrink: 0 }}
102
109
  onError={(e) => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }}
@@ -105,7 +112,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
105
112
  <div style={{
106
113
  width: 52, height: 52, borderRadius: '50%', flexShrink: 0,
107
114
  background: tierColors[selected.tier] || '#555',
108
- display: selected.profile_image_url ? 'none' : 'flex',
115
+ display: localPhoto(selected.profile_image_url) ? 'none' : 'flex',
109
116
  alignItems: 'center', justifyContent: 'center',
110
117
  fontSize: 20, fontWeight: 700, color: selected.tier === 'S' ? '#000' : '#fff',
111
118
  }}>
@@ -128,7 +135,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
128
135
  {selected.degree === 1 && (
129
136
  <TheirCircle
130
137
  person={selected} connections={connections} degree2={degree2}
131
- tierColors={tierColors} onSelect={onSelect} isRequested={isRequested}
138
+ tierColors={tierColors} onSelect={onSelect} isRequested={isRequested} canScan={canScan}
132
139
  />
133
140
  )}
134
141
 
@@ -257,8 +264,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
257
264
  onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
258
265
  onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.03)'}
259
266
  >
260
- {m.profile_image_url ? (
261
- <img src={m.profile_image_url} alt="" style={{
267
+ {localPhoto(m.profile_image_url) ? (
268
+ <img src={localPhoto(m.profile_image_url)} alt="" style={{
262
269
  width: 22, height: 22, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
263
270
  border: `1px solid ${tierColors[m.tier] || '#555'}`,
264
271
  }} onError={e => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }} />
@@ -266,7 +273,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
266
273
  <div style={{
267
274
  width: 22, height: 22, borderRadius: '50%', flexShrink: 0, fontSize: 9, fontWeight: 700,
268
275
  background: tierColors[m.tier] || '#555', color: m.tier === 'S' ? '#000' : '#fff',
269
- display: m.profile_image_url ? 'none' : 'flex', alignItems: 'center', justifyContent: 'center',
276
+ display: localPhoto(m.profile_image_url) ? 'none' : 'flex', alignItems: 'center', justifyContent: 'center',
270
277
  }}>{m.name?.charAt(0)}</div>
271
278
  <div style={{ flex: 1, minWidth: 0 }}>
272
279
  <div style={{ fontSize: 10, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.name}</div>
@@ -327,7 +334,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
327
334
  <div style={{ fontSize: 13, color: '#ccc', marginBottom: 12 }}>
328
335
  {asked
329
336
  ? `Waiting for ${selected.name} to accept. The next scan of your own connections notices when they do.`
330
- : `Connect with ${selected.name} to unlock their network and extend your 6 degrees.`
337
+ : `Connect with ${selected.name} to unlock their network and extend your six degrees.`
331
338
  }
332
339
  </div>
333
340
  <div style={{
@@ -370,7 +377,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
370
377
 
371
378
  {/* Create Cluster — for D1 connections OR accepted D2 (promoted, ready to bridge for D3) */}
372
379
  {(selected.degree === 1 || selected.outreach_status === 'accepted') && (
373
- <CreateClusterCard key={selected.id} selected={selected} degree2={degree2} />
380
+ canScan
381
+ ? <CreateClusterCard key={selected.id} selected={selected} degree2={degree2} />
382
+ : <ScansNeedYourNetwork csvSource={csvSource} style={{ marginTop: 16 }} />
374
383
  )}
375
384
 
376
385
  {selected.profile_url && (
@@ -403,8 +412,10 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
403
412
  Your bridge connections and who they unlock.
404
413
  </p>
405
414
 
406
- {/* Auto-Bridge button */}
407
- <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 }} />}
408
419
 
409
420
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginBottom: 12 }}>
410
421
  <StatBox label="Degree-2 Found" value={degree2.length} />
@@ -457,8 +468,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
457
468
  onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.06)'}
458
469
  onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
459
470
  >
460
- {m.profile_image_url ? (
461
- <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' }} />
462
473
  ) : (
463
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>
464
475
  )}
@@ -493,8 +504,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
493
504
  border: '1px solid rgba(255,107,53,0.12)',
494
505
  }}>
495
506
  {/* Photo */}
496
- {p.profile_image_url ? (
497
- <img src={p.profile_image_url} alt="" style={{
507
+ {localPhoto(p.profile_image_url) ? (
508
+ <img src={localPhoto(p.profile_image_url)} alt="" style={{
498
509
  width: 32, height: 32, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
499
510
  border: `2px solid ${tierColors[p.tier] || '#555'}`,
500
511
  }} onError={e => { e.target.style.display = 'none'; }} />
@@ -675,8 +686,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
675
686
  onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
676
687
  onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
677
688
  >
678
- {c.profile_image_url ? (
679
- <img src={c.profile_image_url} alt="" style={{
689
+ {localPhoto(c.profile_image_url) ? (
690
+ <img src={localPhoto(c.profile_image_url)} alt="" style={{
680
691
  width: 24, height: 24, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
681
692
  border: `1.5px solid ${tierColors[c.tier] || '#555'}`,
682
693
  }} onError={e => { e.target.style.display = 'none'; }} />
@@ -705,6 +716,11 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
705
716
  {(() => {
706
717
  // Track per-tier rank numbering
707
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; });
708
724
  return allRanked.map((c, i) => {
709
725
  const prevTier = i > 0 ? allRanked[i - 1].tier : null;
710
726
  const showHeader = c.tier !== prevTier;
@@ -712,8 +728,6 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
712
728
  tierRankCounter[c.tier] = (tierRankCounter[c.tier] || 0) + 1;
713
729
  const tierRank = tierRankCounter[c.tier];
714
730
  const tierNames = { S: 'S-Tier — Elite', A: 'A-Tier — High Value', B: 'B-Tier — Notable', C: 'C-Tier — Standard', D: 'D-Tier — Entry' };
715
- const tierCounts = {};
716
- allRanked.forEach(r => { tierCounts[r.tier] = (tierCounts[r.tier] || 0) + 1; });
717
731
  return (
718
732
  <div key={c.id}>
719
733
  {showHeader && (
@@ -745,9 +759,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
745
759
  #{tierRank}
746
760
  </span>
747
761
  {/* Profile photo or initial */}
748
- {c.profile_image_url ? (
762
+ {localPhoto(c.profile_image_url) ? (
749
763
  <img
750
- src={c.profile_image_url}
764
+ src={localPhoto(c.profile_image_url)}
751
765
  alt=""
752
766
  style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
753
767
  border: `1.5px solid ${tierColors[c.tier] || '#555'}` }}
@@ -757,7 +771,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
757
771
  <div style={{
758
772
  width: 28, height: 28, borderRadius: '50%', flexShrink: 0,
759
773
  background: tierColors[c.tier] || '#555',
760
- display: c.profile_image_url ? 'none' : 'flex',
774
+ display: localPhoto(c.profile_image_url) ? 'none' : 'flex',
761
775
  alignItems: 'center', justifyContent: 'center',
762
776
  fontSize: 12, fontWeight: 700, color: c.tier === 'S' ? '#000' : '#fff',
763
777
  }}>
@@ -1006,7 +1020,7 @@ function CreateClusterCard({ selected, degree2 }) {
1006
1020
  </div>
1007
1021
  <div style={{ fontSize: 12, color: '#aaa', marginBottom: 12, textAlign: 'center' }}>
1008
1022
  {hasCluster
1009
- ? `${clusterCount} connections mapped in 6 Degrees`
1023
+ ? `${clusterCount} connections mapped in Six Degrees`
1010
1024
  : <>Scan {selected.name}&apos;s connections to map their network</>}
1011
1025
  </div>
1012
1026
 
@@ -1066,6 +1080,22 @@ function CreateClusterCard({ selected, degree2 }) {
1066
1080
  );
1067
1081
  }
1068
1082
 
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
+ }
1098
+
1069
1099
  /** A running scan's log, newest at the bottom. */
1070
1100
  function ScanLog({ title, log, small = false }) {
1071
1101
  return (
@@ -1095,7 +1125,7 @@ function LastRead({ job, who = null }) {
1095
1125
  color: ok ? '#00ff88' : '#ff8080',
1096
1126
  }}>
1097
1127
  {ok
1098
- ? `${who ? `${who}’s circle` : 'The scan'} finished. Refresh the page to see what it found.`
1128
+ ? `${who ? `${who}’s circle` : 'The scan'} finished. What it found is on the map.`
1099
1129
  : `${who ? `${who}’s scan` : 'The scan'} stopped before the end${why ? `: ${why}` : '.'}`}
1100
1130
  </div>
1101
1131
  );
@@ -1117,8 +1147,8 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
1117
1147
  style={{ cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}
1118
1148
  >
1119
1149
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1120
- {cs.bridge.profile_image_url ? (
1121
- <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={{
1122
1152
  width: 28, height: 28, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
1123
1153
  border: `1.5px solid ${tierColors[cs.bridge.tier] || '#555'}`,
1124
1154
  }} onError={e => { e.target.style.display = 'none'; }} />
@@ -1161,8 +1191,8 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
1161
1191
  {(cs.sTier.length > 0 || cs.aTier.length > 0) && (
1162
1192
  <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 8 }}>
1163
1193
  {[...cs.sTier, ...cs.aTier].slice(0, 4).map(m => (
1164
- m.profile_image_url ? (
1165
- <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}
1166
1196
  style={{ width: 22, height: 22, borderRadius: '50%', objectFit: 'cover',
1167
1197
  border: `1.5px solid ${tierColors[m.tier] || '#555'}` }}
1168
1198
  onError={(e) => { e.target.style.display = 'none'; }}
@@ -1212,8 +1242,8 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
1212
1242
  onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.06)'}
1213
1243
  onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
1214
1244
  >
1215
- {m.profile_image_url ? (
1216
- <img src={m.profile_image_url} alt="" style={{
1245
+ {localPhoto(m.profile_image_url) ? (
1246
+ <img src={localPhoto(m.profile_image_url)} alt="" style={{
1217
1247
  width: 20, height: 20, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
1218
1248
  border: `1px solid ${tierColors[m.tier] || '#555'}`,
1219
1249
  }} onError={(e) => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }} />
@@ -1221,7 +1251,7 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
1221
1251
  <div style={{
1222
1252
  width: 20, height: 20, borderRadius: '50%', flexShrink: 0, fontSize: 8, fontWeight: 700,
1223
1253
  background: tierColors[m.tier] || '#555', color: m.tier === 'S' ? '#000' : '#fff',
1224
- display: m.profile_image_url ? 'none' : 'flex',
1254
+ display: localPhoto(m.profile_image_url) ? 'none' : 'flex',
1225
1255
  alignItems: 'center', justifyContent: 'center',
1226
1256
  }}>{m.name?.charAt(0)}</div>
1227
1257
  <div style={{ minWidth: 0, flex: 1 }}>
@@ -1318,100 +1348,130 @@ function generateInsights(person, user, allConnections, degree2, routes = []) {
1318
1348
  return insights.slice(0, 5); // Max 5 insights
1319
1349
  }
1320
1350
 
1321
- // The bridge section's "map the next one" button. Any circle being read shows
1322
- // here, whoever started it; anything else running greys the button out.
1323
- function AutoBridgeButton({ connections, degree2 }) {
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 }) {
1324
1363
  const scan = useScanner();
1325
- const [checking, setChecking] = useState(false);
1326
- const [problem, setProblem] = useState(null);
1327
-
1328
- // Read since the page loaded, so their circle isn't on screen until a
1329
- // refresh: never offered as next again meanwhile.
1330
- const readHere = new Set(scan.finished.filter(isCircleScan).map((j) => j.target?.id).filter(Boolean));
1331
- // Find the top S-tier person without a cluster
1332
- const bridgedIds = new Set(degree2.map(d => d.source_connection_id).filter(Boolean));
1333
- const unbridged = (connections || [])
1334
- .filter(c => (c.tier === 'S' || c.tier === 'A') && !bridgedIds.has(c.id) && !readHere.has(c.id))
1335
- .sort((a, b) => (parseFloat(b.power_score) || 0) - (parseFloat(a.power_score) || 0));
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();
1336
1375
 
1337
1376
  const circle = scan.running && isCircleScan(scan);
1338
1377
  const busy = scan.running && !circle ? busyReason(scan) : null;
1339
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];
1340
1382
 
1341
- if (unbridged.length === 0 && !scan.running && !last) {
1342
- return (
1343
- <div style={{
1344
- padding: '10px', borderRadius: 8, marginBottom: 16,
1345
- background: 'rgba(0,255,136,0.06)', border: '1px solid rgba(0,255,136,0.2)',
1346
- color: '#00ff88', fontSize: 11, fontWeight: 600, textAlign: 'center',
1347
- }}>
1348
- All S/A-tier bridges mapped
1349
- </div>
1350
- );
1351
- }
1352
-
1353
- async function startAutoBridge() {
1354
- const next = unbridged[0];
1355
- if (!next) return;
1356
- setProblem(null);
1357
- setChecking(true);
1358
- const blocked = notReadyMessage(await scraperStatus().catch(() => null));
1359
- setChecking(false);
1360
- if (blocked) {
1361
- setProblem(blocked);
1362
- return;
1363
- }
1364
- try {
1365
- await beginScrape('bridge', { name: next.name, id: next.id });
1366
- } catch (e) {
1367
- setProblem(e.message || 'Could not start the scan.');
1368
- }
1369
- }
1370
-
1371
- const off = Boolean(busy) || checking;
1372
1383
  return (
1373
1384
  <div style={{
1374
- 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)',
1375
1386
  borderRadius: 10, padding: 14, marginBottom: 16,
1376
1387
  }}>
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>
1395
+
1377
1396
  {circle ? (
1378
- <ScanLog small title={scan.pending ? 'Starting…' : `Scanning ${scan.target?.name || 'a circle'}…`} log={scan.log} />
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>
1379
1407
  ) : (
1380
- <>
1381
- {last && <LastRead job={last} who={last.target?.name} />}
1382
- {unbridged.length > 0 ? (
1383
- <>
1384
- <div style={{ fontSize: 11, color: '#9B59B6', fontWeight: 700, marginBottom: 6 }}>
1385
- {unbridged.length} bridges unmapped
1386
- </div>
1387
- <div style={{ fontSize: 11, color: '#aaa', marginBottom: 8 }}>
1388
- Next: <strong>{unbridged[0].name}</strong> ({unbridged[0].tier}-tier, {parseFloat(unbridged[0].power_score).toFixed(1)})
1389
- </div>
1390
- <button onClick={startAutoBridge} disabled={off} style={{
1391
- width: '100%', padding: '10px', borderRadius: 8, border: 'none',
1392
- cursor: off ? 'not-allowed' : 'pointer', opacity: off ? 0.45 : 1,
1393
- background: 'linear-gradient(135deg, #9B59B6, #FF6B35)',
1394
- color: '#fff', fontWeight: 700, fontSize: 13,
1395
- }}>
1396
- Auto-Bridge Next
1397
- </button>
1398
- </>
1399
- ) : (
1400
- <div style={{ color: '#00ff88', fontSize: 11, fontWeight: 600, textAlign: 'center' }}>
1401
- All S/A-tier bridges mapped
1402
- </div>
1403
- )}
1404
- {busy && (
1405
- <div style={{ fontSize: 10, color: '#bbb', marginTop: 8, textAlign: 'center' }}>
1406
- {busy}. One scan at a time: this one can start when it finishes.
1407
- </div>
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>
1408
1442
  )}
1409
- {problem && (
1410
- <div style={{ padding: '8px', borderRadius: 6, background: 'rgba(255,80,80,0.1)', color: '#ff5050', fontSize: 11, marginTop: 8, textAlign: 'center' }}>
1411
- {problem}
1443
+ </div>
1444
+ )}
1445
+
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.
1451
+ </div>
1452
+ )}
1453
+
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>
1412
1459
  </div>
1413
- )}
1414
- </>
1460
+ ))}
1461
+ {hidden.length > 3 && <div style={{ opacity: 0.7 }}>🔒 and {hidden.length - 3} more with hidden lists</div>}
1462
+ </div>
1463
+ )}
1464
+
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>
1415
1475
  )}
1416
1476
  </div>
1417
1477
  );
@@ -23,7 +23,7 @@ const CLASSIC = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#B
23
23
  const TIERS = ['S', 'A', 'B', 'C', 'D'];
24
24
  const GREEN = '#00ff88';
25
25
 
26
- export default function TheirCircle({ person, connections = [], degree2 = [], tierColors = CLASSIC, onSelect, isRequested = requestedByDefault }) {
26
+ export default function TheirCircle({ person, connections = [], degree2 = [], tierColors = CLASSIC, onSelect, isRequested = requestedByDefault, canScan = true }) {
27
27
  const index = useMemo(() => circleIndex(connections, degree2), [connections, degree2]);
28
28
  const ways = useMemo(() => routeIndex(degree2), [degree2]);
29
29
  const { rings, totals } = useMemo(() => circleRings(person, index, { isRequested }), [person, index, isRequested]);
@@ -50,6 +50,8 @@ export default function TheirCircle({ person, connections = [], degree2 = [], ti
50
50
  const guess = useMemo(() => scoreGuess(person), [person]);
51
51
 
52
52
  if (!person) return null;
53
+ // With the sample or a CSV import open there is no scan box below to point
54
+ // at (Sidebar.js says why in its place), so this only says what is known.
53
55
  if (!rings.length) {
54
56
  const first = person.name?.split(' ')[0] || 'them';
55
57
  return (
@@ -57,22 +59,27 @@ export default function TheirCircle({ person, connections = [], degree2 = [], ti
57
59
  <Label>Their circle</Label>
58
60
  {guess ? (
59
61
  <>
60
- <div style={{ fontSize: 11.5, color: '#d8ccb0', lineHeight: 1.5, marginBottom: 9 }}>
61
- <b style={{ color: '#FFD700' }}>Their score is a guess.</b> {guess.reason} Scanning {first}&apos;s circle shows
62
- who you can reach through them, and a strong circle adds up to +2 to their score.
62
+ <div style={{ fontSize: 11.5, color: '#d8ccb0', lineHeight: 1.5, marginBottom: canScan ? 9 : 0 }}>
63
+ <b style={{ color: '#FFD700' }}>Their score is a guess.</b> {guess.reason}
64
+ {canScan && <> Scanning {first}&apos;s circle shows who you can reach through them, and a strong
65
+ circle adds up to +2 to their score.</>}
63
66
  </div>
64
- <button type="button" onClick={toScanBox} style={{
65
- width: '100%', padding: '8px 10px', borderRadius: 8, cursor: 'pointer', fontSize: 12, fontWeight: 700,
66
- border: '1px solid rgba(255,215,0,0.4)', background: 'rgba(255,215,0,0.08)', color: '#FFD700',
67
- }}>
68
- Scan their circle ↓
69
- </button>
67
+ {canScan && (
68
+ <button type="button" onClick={toScanBox} style={{
69
+ width: '100%', padding: '8px 10px', borderRadius: 8, cursor: 'pointer', fontSize: 12, fontWeight: 700,
70
+ border: '1px solid rgba(255,215,0,0.4)', background: 'rgba(255,215,0,0.08)', color: '#FFD700',
71
+ }}>
72
+ Scan their circle ↓
73
+ </button>
74
+ )}
70
75
  </>
71
- ) : (
76
+ ) : canScan ? (
72
77
  <div style={{ fontSize: 11, color: '#999', lineHeight: 1.5 }}>
73
78
  Not scanned yet. Scan {first}&apos;s circle below to see who they know, and
74
79
  everyone you reach through them after that.
75
80
  </div>
81
+ ) : (
82
+ <div style={{ fontSize: 11, color: '#999', lineHeight: 1.5 }}>Not scanned.</div>
76
83
  )}
77
84
  </div>
78
85
  );