six-degrees 0.3.0 → 0.4.0-beta.1

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 (299) 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 +34 -6
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.rsc +10 -10
  17. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +10 -10
  18. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
  19. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +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.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
  37. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/import.html +1 -1
  52. package/.next/standalone/.next/server/app/import.rsc +12 -12
  53. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +12 -12
  54. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  55. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
  56. package/.next/standalone/.next/server/app/index.html +1 -1
  57. package/.next/standalone/.next/server/app/index.rsc +12 -12
  58. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
  59. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +12 -12
  60. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  61. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/launch.html +2 -2
  64. package/.next/standalone/.next/server/app/launch.rsc +12 -12
  65. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +12 -12
  66. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  67. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
  68. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/paths.html +1 -1
  73. package/.next/standalone/.next/server/app/paths.rsc +12 -12
  74. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +12 -12
  75. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  76. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
  77. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/profile.html +1 -1
  80. package/.next/standalone/.next/server/app/profile.rsc +12 -12
  81. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +12 -12
  82. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  83. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
  84. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  86. package/.next/standalone/.next/server/app/queue.html +1 -1
  87. package/.next/standalone/.next/server/app/queue.rsc +12 -12
  88. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +12 -12
  89. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  90. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
  91. package/.next/standalone/.next/server/app/scores/page/app-paths-manifest.json +3 -0
  92. package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
  93. package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
  94. package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
  95. package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
  96. package/.next/standalone/.next/server/app/scores/page.js +13 -0
  97. package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
  98. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
  99. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
  100. package/.next/standalone/.next/server/app/scores.html +1 -0
  101. package/.next/standalone/.next/server/app/scores.meta +26 -0
  102. package/.next/standalone/.next/server/app/scores.rsc +24 -0
  103. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
  104. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
  105. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
  106. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/app/settings.html +1 -1
  109. package/.next/standalone/.next/server/app/settings.rsc +12 -12
  110. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +12 -12
  111. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  112. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +11 -11
  113. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/app/setup.html +1 -1
  116. package/.next/standalone/.next/server/app/setup.rsc +12 -12
  117. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +12 -12
  118. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  119. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
  120. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  121. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rc27za._.js +8 -0
  122. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/_02twlca._.js +10 -0
  124. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -6
  127. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +3 -2
  130. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  131. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
  132. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
  133. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
  134. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
  135. package/.next/standalone/.next/server/chunks/ssr/_0223v9s._.js +3 -0
  136. package/.next/standalone/.next/server/chunks/ssr/_0bcf13g._.js +3 -0
  137. package/.next/standalone/.next/server/chunks/ssr/_0cu6-an._.js +3 -0
  138. package/.next/standalone/.next/server/chunks/ssr/_0wxbq78._.js +3 -0
  139. package/.next/standalone/.next/server/chunks/ssr/_0z88zcl._.js +3 -0
  140. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/ssr/_1cyxocj._.js +3 -0
  142. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
  143. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.js +3 -0
  144. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  145. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +4 -4
  146. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
  147. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  148. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  149. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +3 -0
  150. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  151. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
  152. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
  153. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
  154. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  155. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  156. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  157. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  158. package/.next/standalone/.next/server/pages/404.html +1 -1
  159. package/.next/standalone/.next/server/pages/500.html +1 -1
  160. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  161. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  162. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  163. package/.next/standalone/.next/static/chunks/0_yurn4q91vq8.js +16 -0
  164. package/.next/standalone/.next/static/chunks/0d_mh-06srh2k.js +1 -0
  165. package/.next/standalone/.next/static/chunks/0x3oadfikvb1c.js +1 -0
  166. package/.next/standalone/.next/static/chunks/1d5hl1io798n2.js +1 -0
  167. package/.next/standalone/.next/static/chunks/1qt67cvj9sjwi.js +1 -0
  168. package/.next/{static/chunks/1chp3nx45meg5.js → standalone/.next/static/chunks/1roznk7hkjes1.js} +1 -1
  169. package/.next/standalone/.next/static/chunks/2_bf2n4gstad9.js +33 -0
  170. package/.next/standalone/.next/static/chunks/36gbwek52x_w-.js +2 -0
  171. package/.next/standalone/.next/static/chunks/3a55suosthntc.js +1 -0
  172. package/.next/standalone/.next/static/chunks/3fq1zq-6cfwbc.js +1 -0
  173. package/.next/standalone/.next/static/chunks/3nbnio971zlji.js +2 -0
  174. package/.next/standalone/.next/static/chunks/3s6awp3qq9s7w.js +1 -0
  175. package/.next/standalone/CHANGELOG.md +134 -0
  176. package/.next/standalone/README.md +30 -15
  177. package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
  178. package/.next/standalone/app/api/company-scores/route.js +1 -1
  179. package/.next/standalone/app/api/ingest/route.js +17 -1
  180. package/.next/standalone/app/api/outreach/route.js +107 -44
  181. package/.next/standalone/app/api/scraper/route.js +69 -8
  182. package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
  183. package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
  184. package/.next/standalone/app/components/CompanyScores.js +16 -10
  185. package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
  186. package/.next/standalone/app/components/OrbitGraph.js +30 -0
  187. package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
  188. package/.next/standalone/app/components/SeparationView.js +96 -14
  189. package/.next/standalone/app/components/Sidebar.js +258 -239
  190. package/.next/standalone/app/components/TheirCircle.js +293 -0
  191. package/.next/standalone/app/components/settings/SectorSection.js +5 -5
  192. package/.next/standalone/app/components/settings/TierSection.js +104 -0
  193. package/.next/standalone/app/components/useRequests.js +12 -0
  194. package/.next/standalone/app/components/useScanner.js +13 -0
  195. package/.next/standalone/app/components/views.js +1 -3
  196. package/.next/standalone/app/launch/page.js +4 -1
  197. package/.next/standalone/app/page.js +55 -51
  198. package/.next/standalone/app/paths/page.js +17 -12
  199. package/.next/standalone/app/profile/page.js +3 -3
  200. package/.next/standalone/app/queue/page.js +38 -50
  201. package/.next/standalone/app/scores/page.js +72 -0
  202. package/.next/standalone/app/settings/page.js +15 -3
  203. package/.next/standalone/db/schema.js +1 -0
  204. package/.next/standalone/lib/circle.js +169 -0
  205. package/.next/standalone/lib/companies.js +1 -1
  206. package/.next/standalone/lib/csv.js +17 -4
  207. package/.next/standalone/lib/db-client.js +1 -0
  208. package/.next/standalone/lib/ingest.js +12 -0
  209. package/.next/standalone/lib/legacy-offer.js +7 -6
  210. package/.next/standalone/lib/legacy-scores.js +2 -2
  211. package/.next/standalone/lib/rarity.js +80 -0
  212. package/.next/standalone/lib/requests-client.js +113 -0
  213. package/.next/standalone/lib/rpc.js +26 -15
  214. package/.next/standalone/lib/score-guess.js +60 -0
  215. package/.next/standalone/lib/scoring.js +1002 -61
  216. package/.next/standalone/lib/scraper-client.js +208 -17
  217. package/.next/standalone/lib/sector-directory.js +1 -1
  218. package/.next/standalone/lib/sector-focus.js +4 -3
  219. package/.next/standalone/lib/settings-effects.js +19 -1
  220. package/.next/standalone/lib/settings.js +4 -1
  221. package/.next/standalone/package-lock.json +2 -2
  222. package/.next/standalone/package.json +2 -2
  223. package/.next/standalone/public/demo-data.json +1 -1
  224. package/.next/standalone/scripts/gen-synthetic.mjs +1 -1
  225. package/.next/standalone/scripts/scrape.py +62 -0
  226. package/.next/static/chunks/0_yurn4q91vq8.js +16 -0
  227. package/.next/static/chunks/0d_mh-06srh2k.js +1 -0
  228. package/.next/static/chunks/0x3oadfikvb1c.js +1 -0
  229. package/.next/static/chunks/1d5hl1io798n2.js +1 -0
  230. package/.next/static/chunks/1qt67cvj9sjwi.js +1 -0
  231. package/.next/{standalone/.next/static/chunks/1chp3nx45meg5.js → static/chunks/1roznk7hkjes1.js} +1 -1
  232. package/.next/static/chunks/2_bf2n4gstad9.js +33 -0
  233. package/.next/static/chunks/36gbwek52x_w-.js +2 -0
  234. package/.next/static/chunks/3a55suosthntc.js +1 -0
  235. package/.next/static/chunks/3fq1zq-6cfwbc.js +1 -0
  236. package/.next/static/chunks/3nbnio971zlji.js +2 -0
  237. package/.next/static/chunks/3s6awp3qq9s7w.js +1 -0
  238. package/CHANGELOG.md +134 -0
  239. package/README.md +30 -15
  240. package/db/schema.js +1 -0
  241. package/lib/circle.js +169 -0
  242. package/lib/companies.js +1 -1
  243. package/lib/csv.js +17 -4
  244. package/lib/db-client.js +1 -0
  245. package/lib/ingest.js +12 -0
  246. package/lib/legacy-offer.js +7 -6
  247. package/lib/legacy-scores.js +2 -2
  248. package/lib/rarity.js +80 -0
  249. package/lib/requests-client.js +113 -0
  250. package/lib/rpc.js +26 -15
  251. package/lib/score-guess.js +60 -0
  252. package/lib/scoring.js +1002 -61
  253. package/lib/scraper-client.js +208 -17
  254. package/lib/sector-directory.js +1 -1
  255. package/lib/sector-focus.js +4 -3
  256. package/lib/settings-effects.js +19 -1
  257. package/lib/settings.js +4 -1
  258. package/package.json +2 -2
  259. package/public/demo-data.json +1 -1
  260. package/scripts/scrape.py +62 -0
  261. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
  262. package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +0 -3
  263. package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +0 -3
  264. package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
  265. package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
  266. package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
  267. package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
  268. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
  269. package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
  270. package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
  271. package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  272. package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
  273. package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  274. package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
  275. package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
  276. package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  277. package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
  278. package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  279. package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
  280. package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  281. package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  282. package/.next/standalone/app/components/BridgeRing.js +0 -753
  283. package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  284. package/.next/static/chunks/08k1crid-05dt.js +0 -1
  285. package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  286. package/.next/static/chunks/0q-_khndfxubv.js +0 -2
  287. package/.next/static/chunks/1c79veqedvubt.js +0 -1
  288. package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  289. package/.next/static/chunks/1uadqylrod5_v.js +0 -1
  290. package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  291. package/.next/static/chunks/289ktj97di6zg.js +0 -1
  292. package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  293. package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  294. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_buildManifest.js +0 -0
  295. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_clientMiddlewareManifest.js +0 -0
  296. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_ssgManifest.js +0 -0
  297. /package/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_buildManifest.js +0 -0
  298. /package/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_clientMiddlewareManifest.js +0 -0
  299. /package/.next/static/{qqqcxvzBUizkECrT4swyK → NnWlxbDF24KhrU1eHiEnv}/_ssgManifest.js +0 -0
@@ -1,7 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useMemo } from 'react';
4
- import { runScrape, scraperStatus, notReadyMessage } from '../../lib/scraper-client';
3
+ import { useState, useEffect, useMemo, useCallback } from 'react';
4
+ import { beginScrape, scraperStatus, notReadyMessage, resumePoint, scansCircleOf, isCircleScan, busyReason } from '../../lib/scraper-client';
5
+ import useScanner from './useScanner';
6
+ import useRequests from './useRequests';
7
+ import TheirCircle from './TheirCircle';
8
+ import { hasRequest, markRequested, undoRequest } from '../../lib/requests-client';
5
9
  import { useUser } from './UserProvider';
6
10
  import { routeIndex, routesFor } from '../../lib/separation';
7
11
  import { topCompanies } from '../../lib/scoring';
@@ -26,9 +30,13 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
26
30
  }, [degree2]);
27
31
  // Who reaches whom, from the FULL lists this panel is given — so every route
28
32
  // shows even while a tier chip is narrowing the view, and a person picked in
29
- // Chains, Revolver or Galaxy gets every route too.
33
+ // Chains or Galaxy gets every route too.
30
34
  const routeIdx = useMemo(() => routeIndex(degree2), [degree2]);
31
35
  const bridgeById = useMemo(() => new Map(connections.map(c => [c.id, c])), [connections]);
36
+ // Who you've asked, shared with every view: a request sent here shows in
37
+ // Separation, the circle and the Outlink queue at once (lib/requests-client.js).
38
+ const requests = useRequests();
39
+ const isRequested = useCallback((row) => hasRequest(row, requests), [requests]);
32
40
 
33
41
  if (selected) {
34
42
  const isBridge = selected.degree === 1 && bridgeMap[selected.id];
@@ -114,6 +122,16 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
114
122
  </div>
115
123
  </div>
116
124
 
125
+ {/* Their circle, out to six degrees: who they know, who you added from
126
+ it, and who those people know (TheirCircle.js). Your connections only:
127
+ a 2nd-degree person's circle isn't yours to scan until they accept. */}
128
+ {selected.degree === 1 && (
129
+ <TheirCircle
130
+ person={selected} connections={connections} degree2={degree2}
131
+ tierColors={tierColors} onSelect={onSelect} isRequested={isRequested}
132
+ />
133
+ )}
134
+
117
135
  {selected.is_catalyst && (
118
136
  <div style={{
119
137
  background: 'rgba(0,255,136,0.08)', border: '1px solid rgba(0,255,136,0.3)',
@@ -294,61 +312,65 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
294
312
  </button>
295
313
  )}
296
314
 
297
- {/* Connect to Unlock — for ALL degree-2 connections */}
298
- {(selected.degree === 2 || selected.source_connection_id) && selected.degree !== 1 && selected.unlock_status !== 'unlocked' && (
299
- <div style={{
300
- background: 'rgba(255,215,0,0.06)', border: '1px solid rgba(255,215,0,0.25)',
301
- borderRadius: 10, padding: 16, marginTop: 16, textAlign: 'center',
302
- }}>
303
- <div style={{ fontSize: 10, color: '#FFD700', fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>
304
- {selected.unlock_status === 'pending' ? 'CONNECTION PENDING' : 'LOCKED PATH'}
305
- </div>
306
- <div style={{ fontSize: 13, color: '#ccc', marginBottom: 12 }}>
307
- {selected.unlock_status === 'pending'
308
- ? `Waiting for ${selected.name} to accept. Run a scan to check.`
309
- : `Connect with ${selected.name} to unlock their network and extend your 6 degrees.`
310
- }
311
- </div>
315
+ {/* Connect to Unlock — for ALL degree-2 connections. A request sent here
316
+ is the person's, everywhere at once (lib/requests-client.js). */}
317
+ {(selected.degree === 2 || selected.source_connection_id) && selected.degree !== 1 && selected.unlock_status !== 'unlocked' && (() => {
318
+ const asked = isRequested(selected);
319
+ return (
312
320
  <div style={{
313
- width: '100%', height: 60, background: 'rgba(255,255,255,0.03)',
314
- borderRadius: 8, border: '1px dashed rgba(255,255,255,0.1)',
315
- display: 'flex', alignItems: 'center', justifyContent: 'center',
316
- fontSize: 11, color: '#444', marginBottom: 12,
321
+ background: 'rgba(255,215,0,0.06)', border: `1px ${asked ? 'dashed' : 'solid'} rgba(255,215,0,${asked ? 0.45 : 0.25})`,
322
+ borderRadius: 10, padding: 16, marginTop: 16, textAlign: 'center',
317
323
  }}>
318
- {selected.unlock_status === 'pending' ? '[ Cluster Pending... ]' : '[ Hidden Network ]'}
324
+ <div style={{ fontSize: 10, color: '#FFD700', fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>
325
+ {asked ? 'REQUEST SENT' : 'LOCKED PATH'}
326
+ </div>
327
+ <div style={{ fontSize: 13, color: '#ccc', marginBottom: 12 }}>
328
+ {asked
329
+ ? `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.`
331
+ }
332
+ </div>
333
+ <div style={{
334
+ width: '100%', height: 60, background: 'rgba(255,255,255,0.03)',
335
+ borderRadius: 8, border: '1px dashed rgba(255,255,255,0.1)',
336
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
337
+ fontSize: 11, color: '#444', marginBottom: 12,
338
+ }}>
339
+ {asked ? '[ Cluster Pending... ]' : '[ Hidden Network ]'}
340
+ </div>
341
+ {asked ? (
342
+ <button
343
+ onClick={() => { undoRequest(selected).catch(() => {}); }}
344
+ style={{
345
+ background: 'none', border: 'none', color: '#888', fontSize: 11, fontWeight: 600, cursor: 'pointer',
346
+ }}
347
+ >
348
+ Didn&apos;t send it? Undo
349
+ </button>
350
+ ) : (
351
+ <a href={selected.profile_url} target="_blank" rel="noopener noreferrer"
352
+ style={{
353
+ display: 'block', padding: '10px 16px',
354
+ background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
355
+ color: '#000', borderRadius: 8, textDecoration: 'none',
356
+ fontWeight: 700, fontSize: 13,
357
+ }}
358
+ onClick={() => {
359
+ // Opens their profile to connect, and marks the request
360
+ // sent, through the bridge whose circle you found them in.
361
+ markRequested(selected, { bridgeId: selected.source_connection_id }).catch(() => {});
362
+ }}
363
+ >
364
+ Connect to Unlock Path
365
+ </a>
366
+ )}
319
367
  </div>
320
- {selected.unlock_status !== 'pending' && (
321
- <a href={selected.profile_url} target="_blank" rel="noopener noreferrer"
322
- style={{
323
- display: 'block', padding: '10px 16px',
324
- background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
325
- color: '#000', borderRadius: 8, textDecoration: 'none',
326
- fontWeight: 700, fontSize: 13,
327
- }}
328
- onClick={() => {
329
- // Mark as pending in unlock system
330
- fetch('/api/unlock', {
331
- method: 'POST',
332
- headers: { 'Content-Type': 'application/json' },
333
- body: JSON.stringify({ connectionId: selected.id }),
334
- }).catch(() => {});
335
- // Sync to outreach/pending system (Outlink)
336
- fetch('/api/outreach', {
337
- method: 'POST',
338
- headers: { 'Content-Type': 'application/json' },
339
- body: JSON.stringify({ action: 'mark-sent', connectionId: selected.id, profileUrl: selected.profile_url, userId }),
340
- }).catch(() => {});
341
- }}
342
- >
343
- Connect to Unlock Path
344
- </a>
345
- )}
346
- </div>
347
- )}
368
+ );
369
+ })()}
348
370
 
349
371
  {/* Create Cluster — for D1 connections OR accepted D2 (promoted, ready to bridge for D3) */}
350
372
  {(selected.degree === 1 || selected.outreach_status === 'accepted') && (
351
- <CreateClusterCard selected={selected} degree2={degree2} />
373
+ <CreateClusterCard key={selected.id} selected={selected} degree2={degree2} />
352
374
  )}
353
375
 
354
376
  {selected.profile_url && (
@@ -916,146 +938,165 @@ function PathBox({ routes, selected, tierColors, onSelect }) {
916
938
  );
917
939
  }
918
940
 
941
+ // Scan one person's circle, or carry on with it.
942
+ //
943
+ // LinkedIn lists someone else's connections in its own order, with no dates,
944
+ // so anyone new can turn up on any page: Rescan reads the whole list again from
945
+ // page 1. Resume carries on from the page the last read stopped at, for a list
946
+ // that was only partly read. Both show when there is something to resume.
947
+ //
948
+ // Whether a scan is running comes from the scanner (useScanner), not from this
949
+ // card: close the card mid-scan and open it again and the scan is still here,
950
+ // and every other card greys its buttons out until it ends.
919
951
  function CreateClusterCard({ selected, degree2 }) {
920
- const { userId } = useUser();
921
- const [status, setStatus] = useState('idle'); // idle, checking, scraping, done, error, offline
922
- const [log, setLog] = useState([]);
923
- const hasCluster = degree2?.some(d => d.source_connection_id === selected.id);
952
+ const scan = useScanner();
953
+ const [checking, setChecking] = useState(false);
954
+ const [problem, setProblem] = useState(null); // why a scan didn't start: { text, offline }
955
+ const [resume, setResume] = useState(undefined); // where Resume carries on; null: nothing to resume
924
956
  const clusterCount = degree2?.filter(d => d.source_connection_id === selected.id).length || 0;
925
-
926
- if (hasCluster && status === 'idle') {
927
- return (
928
- <div style={{
929
- background: 'rgba(0,255,136,0.06)', border: '1px solid rgba(0,255,136,0.2)',
930
- borderRadius: 10, padding: 16, marginTop: 16, textAlign: 'center',
931
- }}>
932
- <div style={{ fontSize: 10, color: '#00ff88', fontWeight: 700, letterSpacing: 1, marginBottom: 4 }}>
933
- CLUSTER ACTIVE
934
- </div>
935
- <div style={{ fontSize: 13, color: '#aaa' }}>
936
- {clusterCount} connections mapped in 6 Degrees
937
- </div>
938
- <button
939
- onClick={startScrape}
940
- style={{
941
- marginTop: 10, padding: '8px 16px', borderRadius: 6, border: 'none', cursor: 'pointer',
942
- background: 'rgba(0,255,136,0.12)', color: '#00ff88', fontWeight: 600, fontSize: 11,
943
- }}
944
- >
945
- Rescan Cluster
946
- </button>
947
- </div>
948
- );
949
- }
950
-
951
- async function startScrape() {
952
- setStatus('checking');
953
- setLog(['Checking the scanner...']);
957
+ const hasCluster = clusterCount > 0;
958
+
959
+ const mine = scan.running && scansCircleOf(scan, selected);
960
+ const busy = scan.running && !mine ? busyReason(scan) : null;
961
+ const last = scan.finished.find((j) => scansCircleOf(j, selected));
962
+
963
+ // Every read moves where Resume would carry on, so ask again whenever one ends.
964
+ const lastEnded = scan.finished[0]?.startedAt ?? null;
965
+ useEffect(() => {
966
+ let live = true;
967
+ resumePoint(selected.id).then((r) => { if (live) setResume(r); }, () => { if (live) setResume(null); });
968
+ return () => { live = false; };
969
+ }, [selected.id, lastEnded]);
970
+
971
+ async function start(action) {
972
+ setProblem(null);
973
+ setChecking(true);
954
974
  const blocked = notReadyMessage(await scraperStatus().catch(() => null));
975
+ setChecking(false);
955
976
  if (blocked) {
956
- setStatus('offline');
957
- setLog([blocked]);
977
+ setProblem({ text: blocked, offline: true });
958
978
  return;
959
979
  }
960
-
961
- setStatus('scraping');
962
- setLog(['Mapping the circle behind ' + selected.name + '...']);
963
-
964
980
  try {
965
- const final = await runScrape('bridge', { name: selected.name, onLog: setLog });
966
- if (final.exitCode === 0) {
967
- setStatus('done');
968
- setLog(prev => [...prev, 'Done. Refresh to see the cluster.']);
969
- } else {
970
- setStatus('error');
971
- }
972
- } catch {
973
- setStatus('error');
974
- setLog(['Failed to start the scan']);
981
+ await beginScrape(action, action === 'resume' ? { id: selected.id } : { name: selected.name, id: selected.id });
982
+ } catch (e) {
983
+ setProblem({ text: e.message || 'Could not start the scan.' });
975
984
  }
976
985
  }
977
986
 
987
+ const read = hasCluster || Boolean(resume);
988
+ const accent = hasCluster ? '#00ff88' : '#9B59B6';
989
+ const off = Boolean(busy) || checking;
990
+ const button = (primary) => ({
991
+ flex: 1, padding: '11px 8px', borderRadius: 8, fontWeight: 700, fontSize: primary ? 13 : 12,
992
+ cursor: off ? 'not-allowed' : 'pointer', opacity: off ? 0.45 : 1,
993
+ ...(primary
994
+ ? { border: 'none', background: 'linear-gradient(135deg, #9B59B6, #3498DB)', color: '#fff' }
995
+ : { border: `1px solid ${accent}55`, background: `${accent}14`, color: accent }),
996
+ });
997
+
978
998
  return (
979
- <div style={{
980
- background: 'rgba(155,89,182,0.08)', border: '1px solid rgba(155,89,182,0.3)',
999
+ <div id="scan-box" style={{
1000
+ background: hasCluster ? 'rgba(0,255,136,0.06)' : 'rgba(155,89,182,0.08)',
1001
+ border: `1px solid ${hasCluster ? 'rgba(0,255,136,0.2)' : 'rgba(155,89,182,0.3)'}`,
981
1002
  borderRadius: 10, padding: 16, marginTop: 16,
982
1003
  }}>
983
- <div style={{ fontSize: 10, color: '#9B59B6', fontWeight: 700, letterSpacing: 1, marginBottom: 6, textAlign: 'center' }}>
984
- CREATE CLUSTER
1004
+ <div style={{ fontSize: 10, color: accent, fontWeight: 700, letterSpacing: 1, marginBottom: 4, textAlign: 'center' }}>
1005
+ {hasCluster ? 'CLUSTER ACTIVE' : 'CREATE CLUSTER'}
985
1006
  </div>
986
- <div style={{ fontSize: 12, color: '#ccc', marginBottom: 12, textAlign: 'center' }}>
987
- Scan {selected.name}&apos;s connections to map their network
1007
+ <div style={{ fontSize: 12, color: '#aaa', marginBottom: 12, textAlign: 'center' }}>
1008
+ {hasCluster
1009
+ ? `${clusterCount} connections mapped in 6 Degrees`
1010
+ : <>Scan {selected.name}&apos;s connections to map their network</>}
988
1011
  </div>
989
1012
 
990
- {status === 'idle' && (
991
- <button
992
- onClick={startScrape}
993
- style={{
994
- width: '100%', padding: '12px', borderRadius: 8, border: 'none', cursor: 'pointer',
995
- background: 'linear-gradient(135deg, #9B59B6, #3498DB)',
996
- color: '#fff', fontWeight: 700, fontSize: 14,
997
- }}
998
- >
999
- Scan {selected.name}&apos;s Network
1000
- </button>
1001
- )}
1002
-
1003
- {status === 'offline' && (
1013
+ {mine ? (
1014
+ <ScanLog title={scan.pending ? 'Starting…' : `Scanning ${selected.name}’s circle…`} log={scan.log} />
1015
+ ) : problem?.offline ? (
1004
1016
  <>
1005
1017
  <div style={{ padding: '10px', borderRadius: 8, background: 'rgba(255,80,80,0.1)', border: '1px solid rgba(255,80,80,0.3)', color: '#ff5050', fontSize: 12, marginBottom: 8, textAlign: 'center' }}>
1006
1018
  Scanner offline
1007
1019
  </div>
1008
1020
  <div style={{ fontSize: 10, color: '#888', textAlign: 'center', marginBottom: 8 }}>
1009
- Open the <strong>Scan</strong> page to finish setting the scanner up
1021
+ {problem.text} Open the <strong>Scan</strong> page to finish setting the scanner up.
1010
1022
  </div>
1011
- <button onClick={() => { setStatus('idle'); }} style={{
1023
+ <button onClick={() => setProblem(null)} style={{
1012
1024
  width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
1013
1025
  background: 'rgba(255,255,255,0.1)', color: '#aaa', fontWeight: 600, fontSize: 12,
1014
1026
  }}>
1015
1027
  Retry
1016
1028
  </button>
1017
1029
  </>
1018
- )}
1019
-
1020
- {(status === 'checking' || status === 'scraping') && (
1021
- <div>
1022
- <div style={{
1023
- padding: '10px', borderRadius: 8, background: 'rgba(155,89,182,0.1)',
1024
- border: '1px solid rgba(155,89,182,0.3)', marginBottom: 8,
1025
- }}>
1026
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
1027
- <div style={{ width: 8, height: 8, borderRadius: '50%', background: '#9B59B6', animation: 'pulse 1s infinite' }} />
1028
- <span style={{ fontSize: 11, fontWeight: 600, color: '#9B59B6' }}>Scanning...</span>
1030
+ ) : (
1031
+ <>
1032
+ {last && <LastRead job={last} />}
1033
+ <div style={{ display: 'flex', gap: 8 }}>
1034
+ {resume && (
1035
+ <button onClick={() => start('resume')} disabled={off} style={button(true)}>
1036
+ Resume from page {resume.nextPage}
1037
+ </button>
1038
+ )}
1039
+ <button onClick={() => start('bridge')} disabled={off} style={button(!resume && !hasCluster)}>
1040
+ {read ? 'Rescan from the start' : <>Scan {selected.name}&apos;s Network</>}
1041
+ </button>
1042
+ </div>
1043
+ {resume && (
1044
+ <div style={{ fontSize: 10, color: '#888', marginTop: 8, lineHeight: 1.45, textAlign: 'center' }}>
1045
+ Read to page {resume.pagesRead} so far. Resume carries on from page {resume.nextPage}. LinkedIn
1046
+ lists their connections in its own order, not by date, so Rescan reads them all again from page 1.
1047
+ </div>
1048
+ )}
1049
+ {hasCluster && resume === null && (
1050
+ <div style={{ fontSize: 10, color: '#888', marginTop: 8, lineHeight: 1.45, textAlign: 'center' }}>
1051
+ Their whole list has been read. LinkedIn doesn&apos;t date other people&apos;s connections, so a
1052
+ rescan reads it all again to find anyone new.
1029
1053
  </div>
1030
- <div style={{ fontFamily: 'monospace', fontSize: 10, color: '#888', maxHeight: 100, overflow: 'auto' }}>
1031
- {log.map((l, i) => <div key={i}>{l}</div>)}
1054
+ )}
1055
+ {busy && (
1056
+ <div style={{ fontSize: 10, color: '#bbb', marginTop: 8, textAlign: 'center' }}>
1057
+ {busy}. One scan at a time: this one can start when it finishes.
1032
1058
  </div>
1033
- </div>
1034
- </div>
1059
+ )}
1060
+ {problem && (
1061
+ <div style={{ fontSize: 11, color: '#ff8080', marginTop: 8, textAlign: 'center' }}>{problem.text}</div>
1062
+ )}
1063
+ </>
1035
1064
  )}
1065
+ </div>
1066
+ );
1067
+ }
1036
1068
 
1037
- {status === 'done' && (
1038
- <div style={{
1039
- padding: '10px', borderRadius: 8, background: 'rgba(0,255,136,0.1)',
1040
- border: '1px solid rgba(0,255,136,0.3)', color: '#00ff88', fontSize: 12, fontWeight: 600, textAlign: 'center',
1041
- }}>
1042
- Cluster created! Refresh the page to see it.
1043
- </div>
1044
- )}
1069
+ /** A running scan's log, newest at the bottom. */
1070
+ function ScanLog({ title, log, small = false }) {
1071
+ return (
1072
+ <div style={{
1073
+ padding: '10px', borderRadius: 8, background: 'rgba(155,89,182,0.1)', border: '1px solid rgba(155,89,182,0.3)',
1074
+ }}>
1075
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
1076
+ <div style={{ width: small ? 6 : 8, height: small ? 6 : 8, borderRadius: '50%', background: '#9B59B6', animation: 'pulse 1s infinite' }} />
1077
+ <span style={{ fontSize: 11, fontWeight: 600, color: '#9B59B6' }}>{title}</span>
1078
+ </div>
1079
+ <div style={{ fontFamily: 'monospace', fontSize: small ? 9 : 10, color: '#888', maxHeight: small ? 80 : 100, overflow: 'auto' }}>
1080
+ {log.slice(-40).map((l, i) => <div key={i}>{l}</div>)}
1081
+ </div>
1082
+ </div>
1083
+ );
1084
+ }
1045
1085
 
1046
- {status === 'error' && (
1047
- <>
1048
- <div style={{ padding: '10px', borderRadius: 8, background: 'rgba(255,80,80,0.1)', border: '1px solid rgba(255,80,80,0.3)', color: '#ff5050', fontSize: 12, textAlign: 'center', marginBottom: 8 }}>
1049
- Scan failed — connections may be private
1050
- </div>
1051
- <button onClick={() => { setStatus('idle'); setLog([]); }} style={{
1052
- width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
1053
- background: 'rgba(255,255,255,0.1)', color: '#aaa', fontWeight: 600, fontSize: 12,
1054
- }}>
1055
- Try Again
1056
- </button>
1057
- </>
1058
- )}
1086
+ /** How a scan this page watched ended. */
1087
+ function LastRead({ job, who = null }) {
1088
+ const ok = job.exitCode === 0;
1089
+ const why = !ok && Array.isArray(job.failure) && job.failure.length ? job.failure[job.failure.length - 1] : null;
1090
+ return (
1091
+ <div style={{
1092
+ padding: '8px 10px', borderRadius: 8, marginBottom: 10, fontSize: 11, textAlign: 'center', lineHeight: 1.4,
1093
+ background: ok ? 'rgba(0,255,136,0.08)' : 'rgba(255,80,80,0.08)',
1094
+ border: `1px solid ${ok ? 'rgba(0,255,136,0.25)' : 'rgba(255,80,80,0.25)'}`,
1095
+ color: ok ? '#00ff88' : '#ff8080',
1096
+ }}>
1097
+ {ok
1098
+ ? `${who ? `${who}’s circle` : 'The scan'} finished. Refresh the page to see what it found.`
1099
+ : `${who ? `${who}’s scan` : 'The scan'} stopped before the end${why ? `: ${why}` : '.'}`}
1059
1100
  </div>
1060
1101
  );
1061
1102
  }
@@ -1277,18 +1318,27 @@ function generateInsights(person, user, allConnections, degree2, routes = []) {
1277
1318
  return insights.slice(0, 5); // Max 5 insights
1278
1319
  }
1279
1320
 
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.
1280
1323
  function AutoBridgeButton({ connections, degree2 }) {
1281
- const [status, setStatus] = useState('idle');
1282
- const [log, setLog] = useState([]);
1283
- const [target, setTarget] = useState(null);
1324
+ const scan = useScanner();
1325
+ const [checking, setChecking] = useState(false);
1326
+ const [problem, setProblem] = useState(null);
1284
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));
1285
1331
  // Find the top S-tier person without a cluster
1286
1332
  const bridgedIds = new Set(degree2.map(d => d.source_connection_id).filter(Boolean));
1287
1333
  const unbridged = (connections || [])
1288
- .filter(c => (c.tier === 'S' || c.tier === 'A') && !bridgedIds.has(c.id))
1334
+ .filter(c => (c.tier === 'S' || c.tier === 'A') && !bridgedIds.has(c.id) && !readHere.has(c.id))
1289
1335
  .sort((a, b) => (parseFloat(b.power_score) || 0) - (parseFloat(a.power_score) || 0));
1290
1336
 
1291
- if (unbridged.length === 0 && status === 'idle') {
1337
+ const circle = scan.running && isCircleScan(scan);
1338
+ const busy = scan.running && !circle ? busyReason(scan) : null;
1339
+ const last = scan.finished.find(isCircleScan);
1340
+
1341
+ if (unbridged.length === 0 && !scan.running && !last) {
1292
1342
  return (
1293
1343
  <div style={{
1294
1344
  padding: '10px', borderRadius: 8, marginBottom: 16,
@@ -1303,95 +1353,64 @@ function AutoBridgeButton({ connections, degree2 }) {
1303
1353
  async function startAutoBridge() {
1304
1354
  const next = unbridged[0];
1305
1355
  if (!next) return;
1306
- setTarget(next);
1307
- setStatus('checking');
1308
- setLog([`Scanning ${next.name}'s network...`]);
1309
-
1356
+ setProblem(null);
1357
+ setChecking(true);
1310
1358
  const blocked = notReadyMessage(await scraperStatus().catch(() => null));
1359
+ setChecking(false);
1311
1360
  if (blocked) {
1312
- setStatus('offline');
1313
- setLog([blocked]);
1361
+ setProblem(blocked);
1314
1362
  return;
1315
1363
  }
1316
-
1317
- setStatus('scraping');
1318
1364
  try {
1319
- const final = await runScrape('bridge', { name: next.name, onLog: setLog });
1320
- setStatus(final.exitCode === 0 ? 'done' : 'error');
1321
- } catch {
1322
- setStatus('error');
1365
+ await beginScrape('bridge', { name: next.name, id: next.id });
1366
+ } catch (e) {
1367
+ setProblem(e.message || 'Could not start the scan.');
1323
1368
  }
1324
1369
  }
1325
1370
 
1371
+ const off = Boolean(busy) || checking;
1326
1372
  return (
1327
1373
  <div style={{
1328
1374
  background: 'rgba(155,89,182,0.08)', border: '1px solid rgba(155,89,182,0.2)',
1329
1375
  borderRadius: 10, padding: 14, marginBottom: 16,
1330
1376
  }}>
1331
- {status === 'idle' && (
1332
- <>
1333
- <div style={{ fontSize: 11, color: '#9B59B6', fontWeight: 700, marginBottom: 6 }}>
1334
- {unbridged.length} bridges unmapped
1335
- </div>
1336
- <div style={{ fontSize: 11, color: '#aaa', marginBottom: 8 }}>
1337
- Next: <strong>{unbridged[0]?.name}</strong> ({unbridged[0]?.tier}-tier, {parseFloat(unbridged[0]?.power_score).toFixed(1)})
1338
- </div>
1339
- <button onClick={startAutoBridge} style={{
1340
- width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
1341
- background: 'linear-gradient(135deg, #9B59B6, #FF6B35)',
1342
- color: '#fff', fontWeight: 700, fontSize: 13,
1343
- }}>
1344
- Auto-Bridge Next
1345
- </button>
1346
- </>
1347
- )}
1348
-
1349
- {status === 'offline' && (
1377
+ {circle ? (
1378
+ <ScanLog small title={scan.pending ? 'Starting…' : `Scanning ${scan.target?.name || 'a circle'}…`} log={scan.log} />
1379
+ ) : (
1350
1380
  <>
1351
- <div style={{ padding: '8px', borderRadius: 6, background: 'rgba(255,80,80,0.1)', color: '#ff5050', fontSize: 11, marginBottom: 6, textAlign: 'center' }}>
1352
- Scanner offline
1353
- </div>
1354
- <button onClick={() => setStatus('idle')} style={{
1355
- width: '100%', padding: '8px', borderRadius: 6, border: 'none', cursor: 'pointer',
1356
- background: 'rgba(255,255,255,0.1)', color: '#aaa', fontSize: 11,
1357
- }}>Retry</button>
1358
- </>
1359
- )}
1360
-
1361
- {(status === 'checking' || status === 'scraping') && (
1362
- <div>
1363
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
1364
- <div style={{ width: 6, height: 6, borderRadius: '50%', background: '#9B59B6', animation: 'pulse 1s infinite' }} />
1365
- <span style={{ fontSize: 11, fontWeight: 600, color: '#9B59B6' }}>Scanning {target?.name}...</span>
1366
- </div>
1367
- <div style={{ fontFamily: 'monospace', fontSize: 9, color: '#777', maxHeight: 80, overflow: 'auto' }}>
1368
- {log.map((l, i) => <div key={i}>{l}</div>)}
1369
- </div>
1370
- </div>
1371
- )}
1372
-
1373
- {status === 'done' && (
1374
- <div style={{ textAlign: 'center' }}>
1375
- <div style={{ padding: '8px', borderRadius: 6, background: 'rgba(0,255,136,0.1)', color: '#00ff88', fontSize: 11, fontWeight: 600, marginBottom: 6 }}>
1376
- {target?.name}&rsquo;s bridge mapped! Refresh to see.
1377
- </div>
1378
- <button onClick={() => { setStatus('idle'); setTarget(null); setLog([]); }} style={{
1379
- width: '100%', padding: '8px', borderRadius: 6, border: 'none', cursor: 'pointer',
1380
- background: 'linear-gradient(135deg, #9B59B6, #FF6B35)',
1381
- color: '#fff', fontWeight: 600, fontSize: 11,
1382
- }}>Bridge Next</button>
1383
- </div>
1384
- )}
1385
-
1386
- {status === 'error' && (
1387
- <>
1388
- <div style={{ padding: '8px', borderRadius: 6, background: 'rgba(255,80,80,0.1)', color: '#ff5050', fontSize: 11, marginBottom: 6, textAlign: 'center' }}>
1389
- Failed — connections may be private
1390
- </div>
1391
- <button onClick={() => { setStatus('idle'); setTarget(null); setLog([]); }} style={{
1392
- width: '100%', padding: '8px', borderRadius: 6, border: 'none', cursor: 'pointer',
1393
- background: 'rgba(255,255,255,0.1)', color: '#aaa', fontSize: 11,
1394
- }}>Try Next</button>
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
+ )}
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}
1412
+ </div>
1413
+ )}
1395
1414
  </>
1396
1415
  )}
1397
1416
  </div>