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,11 +1,15 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback, useRef } from 'react';
3
+ import { Suspense, useState, useEffect, useCallback, useRef } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
4
5
  import OnboardingGate from '../components/OnboardingGate';
5
6
  import Link from 'next/link';
6
- import { stopScrape } from '../../lib/scraper-client';
7
- import { setupStep } from '../../lib/scanner-setup';
7
+ import { stopScrape, pickedPerson } from '../../lib/scraper-client';
8
+ import { setupStep, askForField } from '../../lib/scanner-setup';
9
+ import { IS_DEMO } from '../../lib/demo';
10
+ import { circleScanCost } from '../../lib/reach';
8
11
  import { BudgetBox, CooldownBanner, PausedList } from '../components/LinkedInLimits';
12
+ import FieldStep, { FieldAnswer } from '../components/FieldStep';
9
13
 
10
14
  // Everything here runs through /api/scraper. There is deliberately no second
11
15
  // server and no command to copy: the step where people gave up was starting a
@@ -15,6 +19,7 @@ const BG = '#0a0a1a';
15
19
  const LINE = '1px solid rgba(255,255,255,0.1)';
16
20
 
17
21
  const ACTION_LABELS = {
22
+ photos: 'Saving profile photos to this computer',
18
23
  install: 'Installing the scanner',
19
24
  setup: 'Setting up the scanner',
20
25
  login: 'Waiting for you to sign in',
@@ -27,7 +32,9 @@ const ACTION_LABELS = {
27
32
  };
28
33
 
29
34
  export default function SetupPage() {
30
- return <OnboardingGate><SetupInner /></OnboardingGate>;
35
+ // Suspense because SetupInner reads the address's ?scan= (useSearchParams),
36
+ // which Next requires to sit inside one for the page to build.
37
+ return <OnboardingGate><Suspense><SetupInner /></Suspense></OnboardingGate>;
31
38
  }
32
39
 
33
40
  function SetupInner() {
@@ -60,6 +67,28 @@ function SetupInner() {
60
67
  const [finish, setFinish] = useRemembered('six-degrees-bridge-finish', true);
61
68
  const [error, setError] = useState(null);
62
69
  const logRef = useRef(null);
70
+ // What's saved, for the question about your field: undefined while it
71
+ // loads, null when it couldn't be read. And the answer given here, if any.
72
+ const [settings, setSettings] = useState(undefined);
73
+ const [field, setField] = useState(null);
74
+ // Someone sent here to have their circle scanned (Bridge Chains, the Degrees
75
+ // panel's Ready to scan): /setup?scan=<id>. Nothing starts until it's confirmed.
76
+ // The router's search params rather than window.location: every way here is a
77
+ // click, and the address bar only changes after this page has rendered, so the
78
+ // pick was lost on all of them and only a reload showed it.
79
+ const pickId = useSearchParams().get('scan');
80
+ const [pick, setPick] = useState(null);
81
+ useEffect(() => {
82
+ if (!pickId) return undefined;
83
+ let live = true;
84
+ pickedPerson(pickId).then((d) => { if (live) setPick(d?.person ? d : null); }, () => {});
85
+ return () => { live = false; };
86
+ }, [pickId]);
87
+ // Next's router follows history.replaceState, so pickId drops ?scan= as well.
88
+ function unpick() {
89
+ setPick(null);
90
+ try { window.history.replaceState(null, '', window.location.pathname); } catch { /* the link stays */ }
91
+ }
63
92
 
64
93
  const poll = useCallback(async () => {
65
94
  try {
@@ -76,6 +105,16 @@ function SetupInner() {
76
105
  return () => clearInterval(t);
77
106
  }, [poll]);
78
107
 
108
+ useEffect(() => {
109
+ if (IS_DEMO) return;
110
+ let off = false;
111
+ fetch('/api/settings')
112
+ .then((r) => (r.ok ? r.json() : null))
113
+ .then((d) => { if (!off) setSettings(d?.settings || null); })
114
+ .catch(() => { if (!off) setSettings(null); });
115
+ return () => { off = true; };
116
+ }, []);
117
+
79
118
  useEffect(() => {
80
119
  if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
81
120
  }, [s?.log?.length]);
@@ -86,6 +125,13 @@ function SetupInner() {
86
125
  poll();
87
126
  }
88
127
 
128
+ // The question is answered: the steps take its place, from the top, since
129
+ // it may have been answered from far down the picker.
130
+ function answered(sectors) {
131
+ setField({ sectors });
132
+ window.scrollTo(0, 0);
133
+ }
134
+
89
135
  async function run(action, extra = {}) {
90
136
  setError(null);
91
137
  setBusy(true);
@@ -123,6 +169,9 @@ function SetupInner() {
123
169
  // you know, then the people found through them, then company scans.
124
170
  const net = s?.network || { first: 0, second: 0, third: 0 };
125
171
  const mapped = net.first;
172
+ // Your field, asked once before the first scan (lib/scanner-setup.js
173
+ // askForField). The steps wait while that isn't known yet (null).
174
+ const askField = IS_DEMO || field ? false : askForField(s, settings);
126
175
  // A run that ended badly, and the line that says why — the last thing it
127
176
  // printed before stopping. Shown as a box, not left for someone to find in the log.
128
177
  const failed = s && !running && s.exitCode != null && s.exitCode !== 0;
@@ -157,276 +206,318 @@ function SetupInner() {
157
206
  This reads your own LinkedIn connections in a real Chrome window on this
158
207
  machine and saves them here. Nothing leaves your computer, and you will never
159
208
  be asked for your password — you sign in yourself, once.
160
- {!mapped && ' Three steps and your galaxy appears; each one ticks itself off.'}
209
+ {!mapped && (askField
210
+ ? ' One optional question, then three steps and your galaxy appears.'
211
+ : ' Three steps and your galaxy appears; each one ticks itself off.')}
161
212
  </p>
162
213
 
163
- {mapped > 0 && !running && (
164
- <Box>
165
- <div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
166
- <div style={{ flex: 1, minWidth: 200 }}>
167
- <b>{mapped.toLocaleString()} connections</b>
168
- {net.second > 0 && <>, plus {net.second.toLocaleString()} people in their circles</>}
169
- {net.third > 0 && <> and {net.third.toLocaleString()} from company scans</>}
170
- .{' '}
171
- <span style={{ color: '#9aa' }}>Your galaxy is ready.</span>
214
+ {/* The first look at the scanner can take a while (it looks for Python): say so, never a blank page. */}
215
+ {askField === null && <p style={{ color: '#778', fontSize: 13 }}>Checking your setup…</p>}
216
+ {askField && <FieldStep onDone={answered} />}
217
+ {field && <FieldAnswer sectors={field.sectors} />}
218
+
219
+ {askField === false && <>
220
+ {/* By id: the server finds them by their profile, since two connections can share a name. */}
221
+ {pick?.person.id === pickId && (
222
+ <ScanOne
223
+ pick={pick} pages={pages} setPages={setPages} li={li} running={running} s={s}
224
+ canSearch={canSearch} busy={busy} onUnpick={unpick}
225
+ onStart={() => run('bridge', { id: pick.person.id, maxPages: pages })}
226
+ />
227
+ )}
228
+
229
+ {mapped > 0 && !running && (
230
+ <Box>
231
+ <div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
232
+ <div style={{ flex: 1, minWidth: 200 }}>
233
+ <b>{mapped.toLocaleString()} connections</b>
234
+ {net.second > 0 && <>, plus {net.second.toLocaleString()} people in their circles</>}
235
+ {net.third > 0 && <> and {net.third.toLocaleString()} from company scans</>}
236
+ .{' '}
237
+ <span style={{ color: '#9aa' }}>Your galaxy is ready.</span>
238
+ </div>
239
+ <Link href="/" style={{
240
+ padding: '9px 18px', borderRadius: 7, fontSize: 13.5, fontWeight: 700,
241
+ color: '#0a0a1a', textDecoration: 'none',
242
+ background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
243
+ }}>See your network →</Link>
172
244
  </div>
173
- <Link href="/" style={{
174
- padding: '9px 18px', borderRadius: 7, fontSize: 13.5, fontWeight: 700,
175
- color: '#0a0a1a', textDecoration: 'none',
176
- background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
177
- }}>See your network →</Link>
178
- </div>
179
- </Box>
180
- )}
181
-
182
- {failed && (
183
- <Box tone="bad">
184
- <b>The last run stopped before it finished.</b>
185
- {failReason && (
186
- <div style={{
187
- marginTop: 6, color: '#e8c4c4', whiteSpace: 'pre-wrap',
188
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12.5,
189
- }}>{failReason}</div>
190
- )}
191
- <div style={{ color: '#9aa', marginTop: 6 }}>The full log is below.</div>
192
- </Box>
193
- )}
194
-
195
- {notFound && (
196
- <Box tone="bad">
197
- <b>Can’t find the scanner files.</b><br />
198
- <span style={{ color: '#9aa' }}>
199
- Expected <code style={code}>scripts/scrape.py</code> next to the app. If you
200
- downloaded a zip, run the app from inside the project folder.
201
- </span>
202
- </Box>
203
- )}
204
-
205
- {needsChrome && (
206
- <Box tone="bad">
207
- <b>Google Chrome isn’t installed.</b><br />
208
- <span style={{ color: '#9aa' }}>
209
- The scanner drives your real Chrome. Install it from{' '}
210
- <a href="https://www.google.com/chrome/" target="_blank" rel="noreferrer"
211
- style={{ color: '#3498DB' }}>google.com/chrome</a>, then reload this page.
212
- </span>
213
- </Box>
214
- )}
215
-
216
- {/* The Python inside the app didn't work: what happened, and what to do instead.
217
- Red only while there is something to do; once another Python runs the
218
- scanner it is just news. */}
219
- {s && step1.note && (
220
- <Box tone={step1.done ? undefined : 'bad'}>{step1.note}</Box>
221
- )}
222
-
223
- {/* ---- step 1 : the scanner itself ---- */}
224
- <Step
225
- n={1}
226
- done={step1.done}
227
- title="Set up the scanner"
228
- body={s ? step1.text : ''}
229
- action={
230
- s && step1.button && (
231
- <Btn onClick={() => run(step1.button.action)} disabled={busy || running}>
232
- {step1.button.label}
233
- </Btn>
234
- )
235
- }
236
- />
237
-
238
- {/* ---- step 2 : sign in ---- */}
239
- <Step
240
- n={2}
241
- done={!!c.signedIn}
242
- title="Sign into LinkedIn"
243
- body={
244
- c.signedIn
245
- ? 'Signed in on this machine. If LinkedIn ever asks for a security check, or a scan says you were signed out, open LinkedIn here and finish it by hand.'
246
- : 'Opens a Chrome window. Sign in with your email and password — “Continue with Google” cannot work here, because Google blocks its sign-in inside automated browsers.'
247
- }
248
- action={
249
- // Shown after sign-in too: a security check survives the session
250
- // cookie, so "signed in" can still need a person (TRAPS §35).
251
- c.dependencies && (
252
- <Btn onClick={() => run('login')} disabled={busy || running}>
253
- {running && s.action === 'login' ? 'Waiting for you…' : 'Open LinkedIn'}
254
- </Btn>
255
- )
256
- }
257
- />
258
-
259
- {/* ---- step 3 : the people you know ---- */}
260
- <Step
261
- n={3}
262
- done={mapped > 0}
263
- title="1st degree — the people you know"
264
- body="The first scan walks your whole connections list, about a minute and a half for 750 people. After that, Check for new only looks at what has been added since."
265
- action={
266
- <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
267
- <Btn onClick={() => run('full')} disabled={!canSearch} primary>
268
- {running && s.action === 'full' ? 'Scanning…' : 'Scan my whole network'}
269
- </Btn>
270
- <Btn onClick={() => run('refresh')} disabled={!canSearch}>
271
- {running && s.action === 'refresh' ? 'Checking…' : 'Check for new'}
272
- </Btn>
273
- </div>
274
- }
275
- />
276
-
277
- {/* ---- step 4 : who they know ---- */}
278
- <Step
279
- n={4}
280
- done={false}
281
- title="2nd degree — the people they know"
282
- body={
283
- <>
284
- This is what fills <b>Degrees</b> and <b>Outlink</b>: it opens each of your
285
- connections in turn and reads who <i>they</i> know. Most people hide their
286
- connections — those are noted and never tried again.
287
- <br /><br />
288
- It is slow on purpose, about two minutes between each person, because
289
- this is the part LinkedIn notices. During development a real account was
290
- temporarily restricted after roughly <b>19 people in one sitting</b>.
291
- Run a batch, leave it for a day, run another — and stop the moment
292
- LinkedIn mentions unusual activity. It always picks up where it left
293
- off: anyone still without a mapped circle, in the order you choose below —
294
- your newest connections first, or highest tier first. Each person&rsquo;s
295
- list is read to the end, saving every 10 pages, and a read that is stopped
296
- carries on from the same page next time.
297
- </>
298
- }
299
- action={
300
- <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
301
- <CooldownBanner
302
- cooldown={li?.cooldown}
303
- disabled={busy}
304
- onLift={() => run('lift-cooldown')}
305
- />
306
- <BudgetBox li={li} disabled={busy} onSetLimits={(l) => run('set-limits', l)} />
307
- {order === 'score' && <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
308
- <span style={{ fontSize: 12, color: '#8b9a9a' }}>Work through</span>
309
- {['S', 'A', 'B', 'C', 'D'].map((t) => {
310
- const on = tiers.includes(t);
311
- return (
312
- <button
313
- key={t}
314
- onClick={() => setTiers((v) => (v.includes(t) ? v.filter((x) => x !== t) : [...v, t]))}
315
- disabled={running}
316
- style={{
317
- width: 30, height: 28, borderRadius: 7, fontSize: 12, fontWeight: 700,
318
- cursor: running ? 'not-allowed' : 'pointer', border: LINE,
319
- background: on ? 'rgba(52,152,219,0.22)' : 'rgba(255,255,255,0.05)',
320
- color: on ? '#cfe6f7' : '#667',
321
- }}
322
- >{t}</button>
323
- );
324
- })}
325
- <span style={{ fontSize: 11.5, color: '#667' }}>
326
- {tiers.length ? '' : 'pick at least one'}
327
- </span>
328
- </div>}
329
- {order === 'newest' && (
330
- <div style={{ fontSize: 12, color: '#8b9a9a', lineHeight: 1.6 }}>
331
- Goes by the date you connected, newest first, across every tier. Run
332
- {' '}<b>Check for new</b> first so your latest connections are in the list.
245
+ </Box>
246
+ )}
247
+
248
+ {/* Photos an older version kept as links to LinkedIn. The app shows
249
+ only photos saved here (lib/photos.js), so until then those people
250
+ show initials. Every scan saves them at its end; this is the way
251
+ without scanning. */}
252
+ {s?.photosWaiting > 0 && !running && (
253
+ <Box>
254
+ <div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
255
+ <div style={{ flex: 1, minWidth: 200 }}>
256
+ <b>{s.photosWaiting.toLocaleString()} {s.photosWaiting === 1 ? 'photo isn’t' : 'photos aren’t'} saved on this computer yet.</b>{' '}
257
+ <span style={{ color: '#9aa' }}>
258
+ An older version kept them as links to LinkedIn. The app doesn’t load
259
+ photos from LinkedIn while you browse, so those people show initials until
260
+ the photos are saved here: now, or at the end of your next scan. An expired
261
+ link can’t be saved; that person’s photo comes back when they’re next scanned.
262
+ </span>
263
+ {s && !c.dependencies && (
264
+ <div style={{ color: '#8b9a9a', fontSize: 12.5, marginTop: 6 }}>Set up the scanner first (step 1 below): it saves them.</div>
265
+ )}
333
266
  </div>
267
+ <Btn onClick={() => run('photos')} disabled={busy || !c.dependencies}>Save photos</Btn>
268
+ </div>
269
+ </Box>
270
+ )}
271
+
272
+ {failed && (
273
+ <Box tone="bad">
274
+ <b>The last run stopped before it finished.</b>
275
+ {failReason && (
276
+ <div style={{
277
+ marginTop: 6, color: '#e8c4c4', whiteSpace: 'pre-wrap',
278
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12.5,
279
+ }}>{failReason}</div>
334
280
  )}
281
+ <div style={{ color: '#9aa', marginTop: 6 }}>The full log is below.</div>
282
+ </Box>
283
+ )}
284
+
285
+ {notFound && (
286
+ <Box tone="bad">
287
+ <b>Can’t find the scanner files.</b><br />
288
+ <span style={{ color: '#9aa' }}>
289
+ Expected <code style={code}>scripts/scrape.py</code> next to the app. If you
290
+ downloaded a zip, run the app from inside the project folder.
291
+ </span>
292
+ </Box>
293
+ )}
294
+
295
+ {needsChrome && (
296
+ <Box tone="bad">
297
+ <b>Google Chrome isn’t installed.</b><br />
298
+ <span style={{ color: '#9aa' }}>
299
+ The scanner drives your real Chrome. Install it from{' '}
300
+ <a href="https://www.google.com/chrome/" target="_blank" rel="noreferrer"
301
+ style={{ color: '#3498DB' }}>google.com/chrome</a>, then reload this page.
302
+ </span>
303
+ </Box>
304
+ )}
305
+
306
+ {/* The Python inside the app didn't work: what happened, and what to do instead.
307
+ Red only while there is something to do; once another Python runs the
308
+ scanner it is just news. */}
309
+ {s && step1.note && (
310
+ <Box tone={step1.done ? undefined : 'bad'}>{step1.note}</Box>
311
+ )}
312
+
313
+ {/* ---- step 1 : the scanner itself ---- */}
314
+ <Step
315
+ n={1}
316
+ done={step1.done}
317
+ title="Set up the scanner"
318
+ body={s ? step1.text : ''}
319
+ action={
320
+ s && step1.button && (
321
+ <Btn onClick={() => run(step1.button.action)} disabled={busy || running}>
322
+ {step1.button.label}
323
+ </Btn>
324
+ )
325
+ }
326
+ />
327
+
328
+ {/* ---- step 2 : sign in ---- */}
329
+ <Step
330
+ n={2}
331
+ done={!!c.signedIn}
332
+ title="Sign into LinkedIn"
333
+ body={
334
+ c.signedIn
335
+ ? 'Signed in on this machine. If LinkedIn ever asks for a security check, or a scan says you were signed out, open LinkedIn here and finish it by hand.'
336
+ : 'Opens a Chrome window. Sign in with your email and password — “Continue with Google” cannot work here, because Google blocks its sign-in inside automated browsers.'
337
+ }
338
+ action={
339
+ // Shown after sign-in too: a security check survives the session
340
+ // cookie, so "signed in" can still need a person (TRAPS §35).
341
+ c.dependencies && (
342
+ <Btn onClick={() => run('login')} disabled={busy || running}>
343
+ {running && s.action === 'login' ? 'Waiting for you…' : 'Open LinkedIn'}
344
+ </Btn>
345
+ )
346
+ }
347
+ />
348
+
349
+ {/* ---- step 3 : the people you know ---- */}
350
+ <Step
351
+ n={3}
352
+ done={mapped > 0}
353
+ title="1st degree — the people you know"
354
+ body="The first scan walks your whole connections list, about a minute and a half for 750 people. After that, Check for new only looks at what has been added since."
355
+ action={
356
+ <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
357
+ <Btn onClick={() => run('full')} disabled={!canSearch} primary>
358
+ {running && s.action === 'full' ? 'Scanning…' : 'Scan my whole network'}
359
+ </Btn>
360
+ <Btn onClick={() => run('refresh')} disabled={!canSearch}>
361
+ {running && s.action === 'refresh' ? 'Checking…' : 'Check for new'}
362
+ </Btn>
363
+ </div>
364
+ }
365
+ />
366
+
367
+ {/* ---- step 4 : who they know ---- */}
368
+ <Step
369
+ n={4}
370
+ done={false}
371
+ title="2nd degree — the people they know"
372
+ body={
373
+ <>
374
+ This is what fills <b>Degrees</b> and <b>Outlink</b>: it opens each of your
375
+ connections in turn and reads who <i>they</i> know. Most people hide their
376
+ connections — those are noted and never tried again.
377
+ <br /><br />
378
+ It is slow on purpose, about two minutes between each person, because
379
+ this is the part LinkedIn notices. During development a real account was
380
+ temporarily restricted after roughly <b>19 people in one sitting</b>.
381
+ Run a batch, leave it for a day, run another — and stop the moment
382
+ LinkedIn mentions unusual activity. It always picks up where it left
383
+ off: anyone still without a mapped circle, in the order you choose below —
384
+ your newest connections first, or highest tier first. Each person&rsquo;s
385
+ list is read to the end, saving every 10 pages, and a read that is stopped
386
+ carries on from the same page next time.
387
+ </>
388
+ }
389
+ action={
390
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
391
+ <CooldownBanner
392
+ cooldown={li?.cooldown}
393
+ disabled={busy}
394
+ onLift={() => run('lift-cooldown')}
395
+ />
396
+ <BudgetBox li={li} disabled={busy} onSetLimits={(l) => run('set-limits', l)} />
397
+ {order === 'score' && <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
398
+ <span style={{ fontSize: 12, color: '#8b9a9a' }}>Work through</span>
399
+ {['S', 'A', 'B', 'C', 'D'].map((t) => {
400
+ const on = tiers.includes(t);
401
+ return (
402
+ <button
403
+ key={t}
404
+ onClick={() => setTiers((v) => (v.includes(t) ? v.filter((x) => x !== t) : [...v, t]))}
405
+ disabled={running}
406
+ style={{
407
+ width: 30, height: 28, borderRadius: 7, fontSize: 12, fontWeight: 700,
408
+ cursor: running ? 'not-allowed' : 'pointer', border: LINE,
409
+ background: on ? 'rgba(52,152,219,0.22)' : 'rgba(255,255,255,0.05)',
410
+ color: on ? '#cfe6f7' : '#667',
411
+ }}
412
+ >{t}</button>
413
+ );
414
+ })}
415
+ <span style={{ fontSize: 11.5, color: '#667' }}>
416
+ {tiers.length ? '' : 'pick at least one'}
417
+ </span>
418
+ </div>}
419
+ {order === 'newest' && (
420
+ <div style={{ fontSize: 12, color: '#8b9a9a', lineHeight: 1.6 }}>
421
+ Goes by the date you connected, newest first, across every tier. Run
422
+ {' '}<b>Check for new</b> first so your latest connections are in the list.
423
+ </div>
424
+ )}
425
+ <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
426
+ <span style={{ fontSize: 12, color: '#8b9a9a' }}>Start with</span>
427
+ <select value={order} onChange={(e) => setOrder(e.target.value)} disabled={running} style={selectStyle}>
428
+ <option value="newest">Newest connections first</option>
429
+ <option value="score">Highest tier first</option>
430
+ </select>
431
+ <span style={{ fontSize: 12, color: '#8b9a9a' }}>Read up to</span>
432
+ <select value={pages} onChange={(e) => setPages(Number(e.target.value))} disabled={running} style={selectStyle}>
433
+ <option value={100}>every page, to the end of their list</option>
434
+ <option value={50}>50 pages each</option>
435
+ <option value={25}>25 pages each</option>
436
+ <option value={10}>10 pages (~100 people) each</option>
437
+ </select>
438
+ </div>
439
+ <label style={{ display: 'flex', gap: 8, alignItems: 'center', fontSize: 12.5, color: '#cfd8d8', cursor: running ? 'not-allowed' : 'pointer' }}>
440
+ <input type="checkbox" checked={finish} onChange={(e) => setFinish(e.target.checked)} disabled={running} />
441
+ Also finish people already mapped, from the page each one stopped at
442
+ </label>
443
+ <div style={{ fontSize: 12, color: '#FFD700', lineHeight: 1.6 }}>
444
+ Every page is a LinkedIn search, so it rests 20 seconds before each one and a
445
+ minute after every 10, and a long list can take
446
+ {' '}about {circleScanCost(pages).minutes} minutes a person.
447
+ LinkedIn shows 100 pages of anyone&rsquo;s connections at most. Free accounts
448
+ have a monthly search limit: if LinkedIn says it has been reached, the scan saves
449
+ what it read and stops, and carries on from that page next time. Keep batches small.
450
+ </div>
335
451
  <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
336
- <span style={{ fontSize: 12, color: '#8b9a9a' }}>Start with</span>
337
- <select value={order} onChange={(e) => setOrder(e.target.value)} disabled={running} style={selectStyle}>
338
- <option value="newest">Newest connections first</option>
339
- <option value="score">Highest tier first</option>
340
- </select>
341
- <span style={{ fontSize: 12, color: '#8b9a9a' }}>Read up to</span>
342
- <select value={pages} onChange={(e) => setPages(Number(e.target.value))} disabled={running} style={selectStyle}>
343
- <option value={100}>every page, to the end of their list</option>
344
- <option value={50}>50 pages each</option>
345
- <option value={25}>25 pages each</option>
346
- <option value={10}>10 pages (~100 people) each</option>
452
+ <Btn onClick={() => run('auto-bridge', { maxBridges: batch, tiers: order === 'score' ? tiers : [], order, maxPages: pages, deeper: finish })} disabled={!canSearch || (order === 'score' && !tiers.length)} primary>
453
+ {running && s.action === 'auto-bridge' ? 'Mapping…' : 'Map 2nd degree'}
454
+ </Btn>
455
+ <select
456
+ value={batch}
457
+ onChange={(e) => setBatch(Number(e.target.value))}
458
+ disabled={running}
459
+ style={selectStyle}
460
+ >
461
+ <option value={5}>5 people</option>
462
+ <option value={10}>10 people</option>
463
+ <option value={25}>25 people</option>
464
+ <option value={0}>everyone — not advised</option>
347
465
  </select>
466
+ <Btn onClick={() => run('auto-bridge-retry', { maxBridges: batch, tiers: order === 'score' ? tiers : [], order, maxPages: pages, deeper: finish })} disabled={!canSearch || (order === 'score' && !tiers.length)}>
467
+ Retry hidden ones
468
+ </Btn>
348
469
  </div>
349
- <label style={{ display: 'flex', gap: 8, alignItems: 'center', fontSize: 12.5, color: '#cfd8d8', cursor: running ? 'not-allowed' : 'pointer' }}>
350
- <input type="checkbox" checked={finish} onChange={(e) => setFinish(e.target.checked)} disabled={running} />
351
- Also finish people already mapped, from the page each one stopped at
352
- </label>
353
- <div style={{ fontSize: 12, color: '#FFD700', lineHeight: 1.6 }}>
354
- Every page is a LinkedIn search, so it rests 20 seconds before each one and a
355
- minute after every 10, and a long list can take
356
- {' '}{pages >= 100 ? 'about 55 minutes a person' : `about ${Math.max(5, Math.round(pages * 0.55))} minutes a person`}.
357
- LinkedIn shows 100 pages of anyone&rsquo;s connections at most. Free accounts
358
- have a monthly search limit: if LinkedIn says it has been reached, the scan saves
359
- what it read and stops, and carries on from that page next time. Keep batches small.
470
+ <PausedList
471
+ paused={s?.paused || []}
472
+ disabled={!canSearch}
473
+ onResume={(p) => run('resume', { id: p.id })}
474
+ onResumeAll={() => run('resume-all', { maxBridges: batch })}
475
+ />
360
476
  </div>
361
- <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
362
- <Btn onClick={() => run('auto-bridge', { maxBridges: batch, tiers: order === 'score' ? tiers : [], order, maxPages: pages, deeper: finish })} disabled={!canSearch || (order === 'score' && !tiers.length)} primary>
363
- {running && s.action === 'auto-bridge' ? 'Mapping…' : 'Map 2nd degree'}
364
- </Btn>
365
- <select
366
- value={batch}
367
- onChange={(e) => setBatch(Number(e.target.value))}
368
- disabled={running}
369
- style={selectStyle}
370
- >
371
- <option value={5}>5 people</option>
372
- <option value={10}>10 people</option>
373
- <option value={25}>25 people</option>
374
- <option value={0}>everyone — not advised</option>
375
- </select>
376
- <Btn onClick={() => run('auto-bridge-retry', { maxBridges: batch, tiers: order === 'score' ? tiers : [], order, maxPages: pages, deeper: finish })} disabled={!canSearch || (order === 'score' && !tiers.length)}>
377
- Retry hidden ones
378
- </Btn>
379
- </div>
380
- <PausedList
381
- paused={s?.paused || []}
382
- disabled={!canSearch}
383
- onResume={(p) => run('resume', { id: p.id })}
384
- onResumeAll={() => run('resume-all', { maxBridges: batch })}
385
- />
386
- </div>
387
- }
388
- />
477
+ }
478
+ />
389
479
 
390
- {/* ---- stop: one control, always where the log is ---- */}
391
- {running && (
392
- <div style={{
393
- display: 'flex', alignItems: 'center', gap: 12, marginTop: 20,
394
- padding: '14px 16px', borderRadius: 8,
395
- background: 'rgba(255,255,255,0.05)', border: LINE,
396
- }}>
397
- <Spinner />
398
- <div style={{ flex: 1, fontSize: 13.5 }}>
399
- <b>{ACTION_LABELS[s.action] || 'Working'}</b>
400
- {s.progress && <Progress p={s.progress} action={s.action} />}
401
- <div style={{ color: '#8b9a9a', fontSize: 12.5, marginTop: 2 }}>
402
- Stopping closes the browser cleanly and keeps everything found so far.
480
+ {/* ---- stop: one control, always where the log is ---- */}
481
+ {running && (
482
+ <div style={{
483
+ display: 'flex', alignItems: 'center', gap: 12, marginTop: 20,
484
+ padding: '14px 16px', borderRadius: 8,
485
+ background: 'rgba(255,255,255,0.05)', border: LINE,
486
+ }}>
487
+ <Spinner />
488
+ <div style={{ flex: 1, fontSize: 13.5 }}>
489
+ <b>{ACTION_LABELS[s.action] || 'Working'}</b>
490
+ {s.progress && <Progress p={s.progress} action={s.action} />}
491
+ <div style={{ color: '#8b9a9a', fontSize: 12.5, marginTop: 2 }}>
492
+ Stopping closes the browser cleanly and keeps everything found so far.
493
+ </div>
403
494
  </div>
495
+ <Btn onClick={stop} tone="bad">Stop</Btn>
404
496
  </div>
405
- <Btn onClick={stop} tone="bad">Stop</Btn>
406
- </div>
407
- )}
497
+ )}
408
498
 
409
- {error && <Box tone="bad">{error}</Box>}
499
+ {error && <Box tone="bad">{error}</Box>}
410
500
 
411
- {(s?.log?.length > 0) && (
412
- <>
413
- <div style={{
414
- display: 'flex', alignItems: 'center', gap: 10,
415
- margin: '28px 0 8px', fontSize: 12, color: '#788',
416
- textTransform: 'uppercase', letterSpacing: 0.6,
417
- }}>
418
- {running && <Spinner />}
419
- {running ? 'Working' : 'Last run'}
420
- </div>
421
- <pre ref={logRef} style={{
422
- background: 'rgba(0,0,0,0.45)', border: LINE, borderRadius: 8,
423
- padding: 14, maxHeight: 280, overflow: 'auto', margin: 0,
424
- fontSize: 12.5, lineHeight: 1.7, color: '#b9c6c6',
425
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
426
- whiteSpace: 'pre-wrap', wordBreak: 'break-word',
427
- }}>{s.log.join('\n')}</pre>
428
- </>
429
- )}
501
+ {(s?.log?.length > 0) && (
502
+ <>
503
+ <div style={{
504
+ display: 'flex', alignItems: 'center', gap: 10,
505
+ margin: '28px 0 8px', fontSize: 12, color: '#788',
506
+ textTransform: 'uppercase', letterSpacing: 0.6,
507
+ }}>
508
+ {running && <Spinner />}
509
+ {running ? 'Working' : 'Last run'}
510
+ </div>
511
+ <pre ref={logRef} style={{
512
+ background: 'rgba(0,0,0,0.45)', border: LINE, borderRadius: 8,
513
+ padding: 14, maxHeight: 280, overflow: 'auto', margin: 0,
514
+ fontSize: 12.5, lineHeight: 1.7, color: '#b9c6c6',
515
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
516
+ whiteSpace: 'pre-wrap', wordBreak: 'break-word',
517
+ }}>{s.log.join('\n')}</pre>
518
+ </>
519
+ )}
520
+ </>}
430
521
 
431
522
  <div style={{ marginTop: 32, paddingTop: 20, borderTop: LINE, fontSize: 13.5, color: '#8b9a9a' }}>
432
523
  Checking for updates has moved to{' '}
@@ -447,6 +538,78 @@ function SetupInner() {
447
538
  );
448
539
  }
449
540
 
541
+ // One person's circle, picked elsewhere and scanned only once it's confirmed
542
+ // here, beside what it costs and what the budget has left (backlog 2.4). Their
543
+ // read goes as deep as "Read up to" below says; Bridge Chains shows it filling in.
544
+ function ScanOne({ pick, pages, setPages, li, running, s, canSearch, busy, onStart, onUnpick }) {
545
+ const { person, circle } = pick;
546
+ const first = String(person.name || '').trim().split(/\s+/)[0] || 'them';
547
+ const cost = circleScanCost(pages);
548
+ const theirs = running && s?.target?.id === person.id;
549
+ const short = li?.leftToday != null && li.leftToday < cost.searches;
550
+ return (
551
+ <div style={{
552
+ margin: '16px 0', padding: '16px 18px', borderRadius: 10, fontSize: 13.5, lineHeight: 1.6,
553
+ background: 'rgba(0,255,136,0.05)', border: '1px solid rgba(0,255,136,0.3)',
554
+ }}>
555
+ <div style={{ fontSize: 11, fontWeight: 700, color: '#00ff88', letterSpacing: 0.6, textTransform: 'uppercase', marginBottom: 4 }}>
556
+ Scan one circle
557
+ </div>
558
+ <div style={{ fontSize: 16, fontWeight: 700 }}>
559
+ {person.name} <span style={{ fontSize: 12, color: TIER[person.tier] || '#888' }}>{person.tier}-tier</span>
560
+ </div>
561
+ {person.unlocked_from_name && (
562
+ <div style={{ fontSize: 12.5, color: '#9aa' }}>Was in {person.unlocked_from_name}&rsquo;s circle</div>
563
+ )}
564
+ {theirs ? (
565
+ <div style={{ marginTop: 10 }}>
566
+ <b>Scanning {first}&rsquo;s circle.</b> It saves every 10 pages, and{' '}
567
+ <Link href={`/?chain=${encodeURIComponent(person.id)}`} style={{ color: '#00ff88' }}>Bridge Chains shows it filling in →</Link>
568
+ </div>
569
+ ) : (
570
+ <>
571
+ <div style={{ color: '#b8c4c4', marginTop: 10 }}>
572
+ It costs <b>{cost.profileViews} profile view</b> to find their list, then <b>one LinkedIn search for
573
+ every page</b> of it: up to {cost.searches} {cost.searches === 1 ? 'page' : 'pages'}, about {cost.minutes} minutes.
574
+ {li?.leftToday != null && <> <b>{li.leftToday}</b> of your {li.limits?.daily} searches a day are left{short ? '; the scan stops when they run out, and can carry on from that page later' : ''}.</>}
575
+ </div>
576
+ {circle === 'scanned' && (
577
+ <div style={{ color: '#FFD700', fontSize: 12.5, marginTop: 6 }}>
578
+ Their circle is already scanned. This reads their whole list again from page 1; to carry on
579
+ from where a read stopped, use Resume on their card or in the Paused list below.
580
+ </div>
581
+ )}
582
+ {circle === 'hidden' && (
583
+ <div style={{ color: '#FFD700', fontSize: 12.5, marginTop: 6 }}>
584
+ Their list was hidden last time it was tried. Trying again costs a profile view.
585
+ </div>
586
+ )}
587
+ <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap', marginTop: 12 }}>
588
+ <Btn onClick={onStart} disabled={!canSearch || busy} primary>Scan {first}&rsquo;s circle</Btn>
589
+ <select value={pages} onChange={(e) => setPages(Number(e.target.value))} disabled={running} style={selectStyle}
590
+ aria-label="How many pages of their list to read">
591
+ <option value={100}>every page, to the end of their list</option>
592
+ <option value={50}>up to 50 pages</option>
593
+ <option value={25}>up to 25 pages</option>
594
+ <option value={10}>up to 10 pages (~100 people)</option>
595
+ </select>
596
+ <Btn onClick={onUnpick}>Not now</Btn>
597
+ </div>
598
+ {!canSearch && (
599
+ <div style={{ fontSize: 12.5, color: '#8b9a9a', marginTop: 8 }}>
600
+ {running ? 'Something else is running. One scan at a time: this one can start when it finishes.'
601
+ : li?.cooldown ? 'Scanning is paused for now (see below).'
602
+ : 'The scanner isn’t ready yet: finish the steps below first.'}
603
+ </div>
604
+ )}
605
+ </>
606
+ )}
607
+ </div>
608
+ );
609
+ }
610
+
611
+ const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
612
+
450
613
  // A setting this browser remembers between visits: the order and depth someone
451
614
  // picked should not reset every time the page opens. localStorage can be missing
452
615
  // or throw (private windows), and then it is simply not remembered.