six-degrees 0.7.0 → 1.0.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 (360) 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 +32 -4
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +16 -16
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/backup/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/csv/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/restore/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 +4 -4
  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/social/crm/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/route.js +1 -1
  60. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  62. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  65. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  67. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/import.html +1 -1
  72. package/.next/standalone/.next/server/app/import.rsc +18 -18
  73. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +18 -18
  74. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  75. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +16 -16
  76. package/.next/standalone/.next/server/app/index.html +1 -1
  77. package/.next/standalone/.next/server/app/index.rsc +19 -19
  78. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +16 -16
  79. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +19 -19
  80. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  81. package/.next/standalone/.next/server/app/insights/page/app-paths-manifest.json +3 -0
  82. package/.next/standalone/.next/server/app/insights/page/build-manifest.json +20 -0
  83. package/.next/standalone/.next/server/app/insights/page/next-font-manifest.json +11 -0
  84. package/.next/standalone/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
  85. package/.next/standalone/.next/server/app/insights/page/server-reference-manifest.json +4 -0
  86. package/.next/standalone/.next/server/app/insights/page.js +13 -0
  87. package/.next/standalone/.next/server/app/insights/page.js.map +5 -0
  88. package/.next/standalone/.next/server/app/insights/page.js.nft.json +1 -0
  89. package/.next/standalone/.next/server/app/insights/page_client-reference-manifest.js +3 -0
  90. package/.next/standalone/.next/server/app/insights.html +1 -0
  91. package/.next/standalone/.next/server/app/insights.meta +26 -0
  92. package/.next/standalone/.next/server/app/insights.rsc +35 -0
  93. package/.next/standalone/.next/server/app/insights.segments/_full.segment.rsc +35 -0
  94. package/.next/standalone/.next/server/app/insights.segments/_tree.segment.rsc +4 -0
  95. package/.next/standalone/.next/server/app/insights.segments/insights/__PAGE__.segment.rsc +33 -0
  96. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  97. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  98. package/.next/standalone/.next/server/app/launch.html +2 -2
  99. package/.next/standalone/.next/server/app/launch.rsc +18 -18
  100. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +18 -18
  101. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  102. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +16 -16
  103. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  104. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  105. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  106. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  107. package/.next/standalone/.next/server/app/paths.html +1 -1
  108. package/.next/standalone/.next/server/app/paths.rsc +19 -19
  109. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +19 -19
  110. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  111. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +16 -16
  112. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  113. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  114. package/.next/standalone/.next/server/app/profile.html +1 -1
  115. package/.next/standalone/.next/server/app/profile.rsc +19 -19
  116. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +19 -19
  117. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  118. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +16 -16
  119. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  120. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  121. package/.next/standalone/.next/server/app/queue.html +1 -1
  122. package/.next/standalone/.next/server/app/queue.rsc +19 -19
  123. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +19 -19
  124. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  125. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +21 -20
  126. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  127. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  128. package/.next/standalone/.next/server/app/scores.html +1 -1
  129. package/.next/standalone/.next/server/app/scores.rsc +18 -18
  130. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +18 -18
  131. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  132. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +16 -16
  133. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  134. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  135. package/.next/standalone/.next/server/app/settings.html +1 -1
  136. package/.next/standalone/.next/server/app/settings.rsc +19 -19
  137. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +19 -19
  138. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  139. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +16 -16
  140. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  141. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  142. package/.next/standalone/.next/server/app/setup.html +1 -1
  143. package/.next/standalone/.next/server/app/setup.rsc +19 -19
  144. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +19 -19
  145. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  146. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +16 -16
  147. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  148. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  149. package/.next/standalone/.next/server/app/social.html +1 -1
  150. package/.next/standalone/.next/server/app/social.rsc +18 -18
  151. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +18 -18
  152. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  153. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +16 -16
  154. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  155. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__063s0x7._.js → [root-of-the-server]__0r-0t-9._.js} +1 -1
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ra6xyg._.js +15 -0
  157. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20rwfdw._.js → [root-of-the-server]__0rcpjad._.js} +5 -2
  158. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  159. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/lib_02iz_9q._.js +2 -2
  161. package/.next/standalone/.next/server/chunks/lib_05agvk3._.js +2 -2
  162. package/.next/standalone/.next/server/chunks/lib_0v84u66._.js +2 -2
  163. package/.next/standalone/.next/server/chunks/lib_db-client_0_dch35.js +9 -9
  164. package/.next/standalone/.next/server/chunks/{lib_rpc_0jnfp2e.js → lib_rpc_00nndai.js} +1 -1
  165. package/.next/standalone/.next/server/chunks/{lib_rpc_0b17r6v.js → lib_rpc_03ibb-x.js} +1 -1
  166. package/.next/standalone/.next/server/chunks/{lib_rpc_0doq0t-.js → lib_rpc_03ri3ji.js} +1 -1
  167. package/.next/standalone/.next/server/chunks/{lib_rpc_0fd2rlw.js → lib_rpc_078ce48.js} +1 -1
  168. package/.next/standalone/.next/server/chunks/lib_rpc_0a69mgr.js +3 -0
  169. package/.next/standalone/.next/server/chunks/{lib_rpc_005abye.js → lib_rpc_0cej3t8.js} +1 -1
  170. package/.next/standalone/.next/server/chunks/lib_rpc_0cmh4g6.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_0nx2_pc.js +3 -0
  172. package/.next/standalone/.next/server/chunks/lib_rpc_0r79-w1.js +3 -0
  173. package/.next/standalone/.next/server/chunks/lib_rpc_0yx8ynd.js +3 -0
  174. package/.next/standalone/.next/server/chunks/lib_rpc_12p-o9-.js +3 -0
  175. package/.next/standalone/.next/server/chunks/lib_rpc_1466dnb.js +3 -0
  176. package/.next/standalone/.next/server/chunks/{lib_rpc_0is4qoc.js → lib_rpc_1hjjjr4.js} +1 -1
  177. package/.next/standalone/.next/server/chunks/lib_rpc_1sryxv6.js +3 -0
  178. package/.next/standalone/.next/server/chunks/lib_rpc_1trn-x5.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_1ygu_mv.js +3 -0
  180. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egamyq._.js +3 -0
  181. package/.next/standalone/.next/server/chunks/ssr/_04am_h8._.js +3 -0
  182. package/.next/standalone/.next/server/chunks/ssr/_07zb7lu._.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/_0_79idq._.js +1 -1
  184. package/.next/standalone/.next/server/chunks/ssr/_0gds903._.js +3 -0
  185. package/.next/standalone/.next/server/chunks/ssr/_0klqt9z._.js +3 -0
  186. package/.next/standalone/.next/server/chunks/ssr/{_1k9qfsk._.js → _14jak5s._.js} +6 -2
  187. package/.next/standalone/.next/server/chunks/ssr/_14nxd3q._.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/_1cj2yog._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/_1fm1ih_._.js +1 -1
  190. package/.next/standalone/.next/server/chunks/ssr/_1hdqw0-._.js +7 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_1k2nmbw._.js +1 -1
  192. package/.next/standalone/.next/server/chunks/ssr/{_0gy11ar._.js → _1r62six._.js} +6 -2
  193. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_insights_page_actions_0-0tf75.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/app_components_0uuff77._.js +2 -2
  195. package/.next/standalone/.next/server/chunks/ssr/app_components_1j3q-h5._.js +2 -2
  196. package/.next/standalone/.next/server/chunks/ssr/{_01o3i5y._.js → app_components_settings_SectorPicker_1c-29qw.js} +2 -6
  197. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  198. package/.next/standalone/.next/server/chunks/ssr/app_insights_page_1d_chja.js +3 -0
  199. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +14 -14
  200. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +3 -3
  201. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  202. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +4 -4
  203. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  204. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  205. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  206. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  207. package/.next/standalone/.next/server/pages/404.html +1 -1
  208. package/.next/standalone/.next/server/pages/500.html +1 -1
  209. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  210. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  211. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  212. package/.next/standalone/.next/static/chunks/02wnf8dkxegxg.js +34 -0
  213. package/.next/standalone/.next/static/chunks/0hggugbh84m4t.js +21 -0
  214. package/.next/{static/chunks/2eo9pqq9gay3r.js → standalone/.next/static/chunks/0ka4prpk3w9wn.js} +4 -4
  215. package/.next/standalone/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
  216. package/.next/standalone/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
  217. package/.next/{static/chunks/44yn0tcd9dfk3.js → standalone/.next/static/chunks/1bkt_hhjzvbh8.js} +1 -1
  218. package/.next/standalone/.next/static/chunks/1cmphb3kh2ers.css +3 -0
  219. package/.next/{static/chunks/1qbjermbf68xj.js → standalone/.next/static/chunks/1d8yv3k_atka7.js} +1 -1
  220. package/.next/standalone/.next/static/chunks/1ods5h86raivz.js +1 -0
  221. package/.next/standalone/.next/static/chunks/23hc51oa4vkhc.js +5 -0
  222. package/.next/standalone/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
  223. package/.next/standalone/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
  224. package/.next/{static/chunks/2zf4jmz9l3li2.js → standalone/.next/static/chunks/2w-y83mjlpnse.js} +1 -1
  225. package/.next/standalone/.next/static/chunks/2ws1xf293j-tm.js +5 -0
  226. package/.next/standalone/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
  227. package/.next/standalone/.next/static/chunks/3opgg1efrx8q6.js +1 -0
  228. package/.next/standalone/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
  229. package/.next/{static/chunks/0an_b6zi1r_qq.js → standalone/.next/static/chunks/3y40z6of3r9py.js} +1 -5
  230. package/.next/standalone/.next/static/chunks/403dpflln8ah5.js +1 -0
  231. package/.next/standalone/CHANGELOG.md +83 -0
  232. package/.next/standalone/README.md +40 -14
  233. package/.next/standalone/SECURITY.md +17 -4
  234. package/.next/standalone/app/api/outreach/route.js +13 -73
  235. package/.next/standalone/app/api/scraper/route.js +106 -5
  236. package/.next/standalone/app/components/AppHeader.js +22 -6
  237. package/.next/standalone/app/components/AppTabs.js +4 -0
  238. package/.next/standalone/app/components/AutoConnect.js +286 -0
  239. package/.next/standalone/app/components/ChainView.js +75 -2
  240. package/.next/standalone/app/components/FilterPanel.js +8 -1
  241. package/.next/standalone/app/components/LinkedInLimits.js +38 -13
  242. package/.next/standalone/app/components/OutlinkQuest.js +32 -11
  243. package/.next/standalone/app/components/ScanStatusBar.js +18 -3
  244. package/.next/standalone/app/components/ScanTrail.js +13 -3
  245. package/.next/standalone/app/components/SeparationView.js +6 -22
  246. package/.next/standalone/app/components/Sidebar.js +147 -38
  247. package/.next/standalone/app/components/insights/Boards.js +275 -0
  248. package/.next/standalone/app/components/insights/PeopleView.js +266 -0
  249. package/.next/standalone/app/components/insights/Rail.js +156 -0
  250. package/.next/standalone/app/components/insights/Report.js +382 -0
  251. package/.next/standalone/app/components/insights/parts.js +239 -0
  252. package/.next/standalone/app/components/settings/UsageSection.js +50 -3
  253. package/.next/standalone/app/globals.css +2 -3
  254. package/.next/standalone/app/insights/page.js +220 -0
  255. package/.next/standalone/app/page.js +1 -17
  256. package/.next/standalone/app/queue/page.js +30 -18
  257. package/.next/standalone/app/setup/page.js +71 -13
  258. package/.next/standalone/install.sh +7 -6
  259. package/.next/standalone/lib/auto-connect.js +145 -0
  260. package/.next/standalone/lib/companies.js +1 -1
  261. package/.next/standalone/lib/in-progress.js +46 -0
  262. package/.next/standalone/lib/insights-board.js +613 -0
  263. package/.next/standalone/lib/linkedin-limits.js +36 -11
  264. package/.next/standalone/lib/requests-client.js +10 -0
  265. package/.next/standalone/lib/requests.js +124 -0
  266. package/.next/standalone/lib/scan-state.js +1 -0
  267. package/.next/standalone/lib/scanner-setup.js +82 -3
  268. package/.next/standalone/lib/scraper-client.js +18 -5
  269. package/.next/standalone/lib/settings.js +6 -0
  270. package/.next/standalone/lib/usage.js +5 -0
  271. package/.next/standalone/package-lock.json +2 -2
  272. package/.next/standalone/package.json +1 -1
  273. package/.next/standalone/scripts/build-app.mjs +167 -11
  274. package/.next/standalone/scripts/build-desktop.mjs +3 -2
  275. package/.next/standalone/scripts/ci-mac-signing.sh +128 -0
  276. package/.next/standalone/scripts/entitlements/electron.plist +23 -0
  277. package/.next/standalone/scripts/entitlements/node.plist +27 -0
  278. package/.next/standalone/scripts/mac-sign.mjs +563 -0
  279. package/.next/standalone/scripts/make-dmg-background.mjs +2 -1
  280. package/.next/standalone/scripts/scrape.py +569 -9
  281. package/.next/standalone/scripts/set-signing-secrets.sh +96 -0
  282. package/.next/static/chunks/02wnf8dkxegxg.js +34 -0
  283. package/.next/static/chunks/0hggugbh84m4t.js +21 -0
  284. package/.next/{standalone/.next/static/chunks/2eo9pqq9gay3r.js → static/chunks/0ka4prpk3w9wn.js} +4 -4
  285. package/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
  286. package/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
  287. package/.next/{standalone/.next/static/chunks/44yn0tcd9dfk3.js → static/chunks/1bkt_hhjzvbh8.js} +1 -1
  288. package/.next/static/chunks/1cmphb3kh2ers.css +3 -0
  289. package/.next/{standalone/.next/static/chunks/1qbjermbf68xj.js → static/chunks/1d8yv3k_atka7.js} +1 -1
  290. package/.next/static/chunks/1ods5h86raivz.js +1 -0
  291. package/.next/static/chunks/23hc51oa4vkhc.js +5 -0
  292. package/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
  293. package/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
  294. package/.next/{standalone/.next/static/chunks/2zf4jmz9l3li2.js → static/chunks/2w-y83mjlpnse.js} +1 -1
  295. package/.next/static/chunks/2ws1xf293j-tm.js +5 -0
  296. package/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
  297. package/.next/static/chunks/3opgg1efrx8q6.js +1 -0
  298. package/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
  299. package/.next/{standalone/.next/static/chunks/0an_b6zi1r_qq.js → static/chunks/3y40z6of3r9py.js} +1 -5
  300. package/.next/static/chunks/403dpflln8ah5.js +1 -0
  301. package/CHANGELOG.md +83 -0
  302. package/README.md +40 -14
  303. package/SECURITY.md +17 -4
  304. package/lib/auto-connect.js +145 -0
  305. package/lib/companies.js +1 -1
  306. package/lib/in-progress.js +46 -0
  307. package/lib/insights-board.js +613 -0
  308. package/lib/linkedin-limits.js +36 -11
  309. package/lib/requests-client.js +10 -0
  310. package/lib/requests.js +124 -0
  311. package/lib/scan-state.js +1 -0
  312. package/lib/scanner-setup.js +82 -3
  313. package/lib/scraper-client.js +18 -5
  314. package/lib/settings.js +6 -0
  315. package/lib/usage.js +5 -0
  316. package/package.json +1 -1
  317. package/scripts/scrape.py +569 -9
  318. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_4y8z7._.js +0 -8
  319. package/.next/standalone/.next/server/chunks/_02twlca._.js +0 -10
  320. package/.next/standalone/.next/server/chunks/lib_rpc_0nef_n7.js +0 -3
  321. package/.next/standalone/.next/server/chunks/lib_rpc_0plrzra.js +0 -3
  322. package/.next/standalone/.next/server/chunks/lib_rpc_13a4m0c.js +0 -3
  323. package/.next/standalone/.next/server/chunks/lib_rpc_1if17c-.js +0 -3
  324. package/.next/standalone/.next/server/chunks/lib_rpc_1j-zw7v.js +0 -3
  325. package/.next/standalone/.next/server/chunks/lib_rpc_1sngt-q.js +0 -3
  326. package/.next/standalone/.next/server/chunks/lib_rpc_1v2sgme.js +0 -3
  327. package/.next/standalone/.next/server/chunks/lib_rpc_1vh-tgx.js +0 -3
  328. package/.next/standalone/.next/server/chunks/lib_rpc_1zaubz7.js +0 -3
  329. package/.next/standalone/.next/server/chunks/ssr/_02sband._.js +0 -7
  330. package/.next/standalone/.next/server/chunks/ssr/_189g8lg._.js +0 -3
  331. package/.next/standalone/.next/server/chunks/ssr/_1f_n00p._.js +0 -3
  332. package/.next/standalone/.next/server/chunks/ssr/app_components_0t908t-._.js +0 -3
  333. package/.next/standalone/.next/static/chunks/0e3g0sil79cus.js +0 -21
  334. package/.next/standalone/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
  335. package/.next/standalone/.next/static/chunks/163b-20vwa06i.js +0 -34
  336. package/.next/standalone/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
  337. package/.next/standalone/.next/static/chunks/23_1z47al92nz.js +0 -57
  338. package/.next/standalone/.next/static/chunks/260bomtr214m8.js +0 -34
  339. package/.next/standalone/.next/static/chunks/2xk2ra4piiuod.js +0 -34
  340. package/.next/standalone/.next/static/chunks/3b0-14cxqsprt.js +0 -1
  341. package/.next/standalone/.next/static/chunks/3jarus1_38s2z.js +0 -1
  342. package/.next/standalone/.next/static/chunks/3ssu341hav_sq.css +0 -3
  343. package/.next/standalone/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
  344. package/.next/static/chunks/0e3g0sil79cus.js +0 -21
  345. package/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
  346. package/.next/static/chunks/163b-20vwa06i.js +0 -34
  347. package/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
  348. package/.next/static/chunks/23_1z47al92nz.js +0 -57
  349. package/.next/static/chunks/260bomtr214m8.js +0 -34
  350. package/.next/static/chunks/2xk2ra4piiuod.js +0 -34
  351. package/.next/static/chunks/3b0-14cxqsprt.js +0 -1
  352. package/.next/static/chunks/3jarus1_38s2z.js +0 -1
  353. package/.next/static/chunks/3ssu341hav_sq.css +0 -3
  354. package/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
  355. /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_buildManifest.js +0 -0
  356. /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_clientMiddlewareManifest.js +0 -0
  357. /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_ssgManifest.js +0 -0
  358. /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_buildManifest.js +0 -0
  359. /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_clientMiddlewareManifest.js +0 -0
  360. /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_ssgManifest.js +0 -0
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useMemo, useCallback } from 'react';
3
+ import { useState, useEffect, useMemo, useCallback, useSyncExternalStore } from 'react';
4
4
  import Link from 'next/link';
5
- import { beginScrape, scraperStatus, notReadyMessage, resumePoint, scansCircleOf, isCircleScan, busyReason } from '../../lib/scraper-client';
5
+ import { beginScrape, scraperStatus, notReadyMessage, resumePoint, scansCircleOf, isCircleScan, busyReason, watchScanner, scannerNow } from '../../lib/scraper-client';
6
+ import { scanBoxTitle, stoppedLine } from '../../lib/in-progress';
6
7
  import useScanner from './useScanner';
7
8
  import useRequests from './useRequests';
8
9
  import TheirCircle from './TheirCircle';
@@ -17,6 +18,7 @@ import Avatar from './Avatar';
17
18
  import NoteDetail from './NoteDetail';
18
19
  import CirclePanel from './CirclePanel';
19
20
  import { localPhoto } from '../../lib/photos';
21
+ import ConnectChoice, { secondaryLook } from './AutoConnect';
20
22
 
21
23
  export default function Sidebar({ selected, stats, tierColors, connections, degree2 = [], mode, collapsed, filter, pending = [], onToggle, onSelect, onSwitchMode, onFocusNode, onMarkSent, onUndoPending, scanNotes, csvSource = null, onOpenCircle, onShowInSeparation, note = null, onCloseNote, circle = null }) {
22
24
  const isDegreesMode = mode === 'degrees';
@@ -54,6 +56,12 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
54
56
  // Separation, the circle and the Outlink queue at once (lib/requests-client.js).
55
57
  const requests = useRequests();
56
58
  const isRequested = useCallback((row) => hasRequest(row, requests), [requests]);
59
+ // Where Resume would carry on with the person open, asked once for the whole
60
+ // card: the line by their name and the scan box below both show it. Only for
61
+ // whoever gets a scan box (the same test as below). Above the early returns,
62
+ // so the hook order never changes.
63
+ const scansHere = Boolean(selected) && canScan && (selected.degree === 1 || selected.outreach_status === 'accepted');
64
+ const { resume, scanning: scanningThem } = useResume(scansHere ? selected : null);
57
65
 
58
66
  // A notification someone clicked takes the panel over until they go back (app/components/NoteDetail.js).
59
67
  if (note) {
@@ -151,6 +159,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
151
159
  </div>
152
160
  </div>
153
161
 
162
+ {/* Their scan stopped partway: said by their name, where it's seen without scrolling. */}
163
+ {scansHere && resume && <StoppedPartway resume={resume} scanning={scanningThem} />}
164
+
154
165
  {/* Their circle, out to six degrees: who they know, who you added from
155
166
  it, and who those people know (TheirCircle.js). Your connections only:
156
167
  a 2nd-degree person's circle isn't yours to scan until they accept. */}
@@ -224,9 +235,15 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
224
235
  <div style={row}>
225
236
  <span style={label}>Scanned</span>
226
237
  <span style={{ flex: 1 }}>{scanned}</span>
227
- {i.bars !== 5 && canScan && (
238
+ {/* Stopped partway: carry on from that page, the line by their name's own
239
+ Resume. The Scan page's Scan one circle reads the list again from page 1,
240
+ which spends the searches already spent; that stays for anyone else. */}
241
+ {i.bars !== 5 && canScan && (scansHere && resume ? (
242
+ <button type="button" style={act} title={`Resume from page ${resume.nextPage}`}
243
+ onClick={() => resumeInScanBox(scanningThem)}>Resume →</button>
244
+ ) : (resume === null || i.bars == null || !scansHere) && (
228
245
  <a href={`/setup?scan=${encodeURIComponent(selected.id)}`} style={act}>{i.bars == null ? 'Scan →' : 'Finish →'}</a>
229
- )}
246
+ ))}
230
247
  </div>
231
248
  {(() => {
232
249
  const since = selected.connected_date ? new Date(selected.connected_date) : null;
@@ -468,21 +485,27 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
468
485
  Didn&apos;t send it? Undo
469
486
  </button>
470
487
  ) : (
471
- <a href={selected.profile_url} target="_blank" rel="noopener noreferrer"
472
- style={{
473
- display: 'block', padding: '10px 16px',
474
- background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
475
- color: '#000', borderRadius: 8, textDecoration: 'none',
476
- fontWeight: 700, fontSize: 13,
477
- }}
478
- onClick={() => {
479
- // Opens their profile to connect, and marks the request
480
- // sent, through the bridge whose circle you found them in.
481
- markRequested(selected, { bridgeId: selected.source_connection_id }).catch(() => {});
482
- }}
483
- >
484
- Connect to Unlock Path
485
- </a>
488
+ // Auto first, the better pick; Connect beside it as an outline
489
+ // (AutoConnect.js; Blake, 2026-10-03). Your own network only.
490
+ <ConnectChoice person={selected} canAuto={canScan} size="card" connect={(second) => (
491
+ <a href={selected.profile_url} target="_blank" rel="noopener noreferrer"
492
+ title={second ? 'Opens their LinkedIn profile, to connect yourself' : undefined}
493
+ style={second ? { ...secondaryLook('card'), flex: 1 } : {
494
+ display: 'block', padding: '10px 16px',
495
+ background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
496
+ color: '#000', borderRadius: 8, textDecoration: 'none',
497
+ fontWeight: 700, fontSize: 13,
498
+ }}
499
+ onClick={() => {
500
+ // Opens their profile to connect, and marks the request
501
+ // sent, through the bridge whose circle you found them in.
502
+ markRequested(selected, { bridgeId: selected.source_connection_id }).catch(() => {});
503
+ }}
504
+ >
505
+ {/* Beside Auto there's room for one word (a 320 px panel). */}
506
+ {second ? 'Connect' : 'Connect to Unlock Path'}
507
+ </a>
508
+ )} />
486
509
  )}
487
510
  </div>
488
511
  );
@@ -491,7 +514,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
491
514
  {/* Create Cluster — for D1 connections OR accepted D2 (promoted, ready to bridge for D3) */}
492
515
  {(selected.degree === 1 || selected.outreach_status === 'accepted') && (
493
516
  canScan
494
- ? <CreateClusterCard key={selected.id} selected={selected} degree2={degree2} />
517
+ ? <CreateClusterCard key={selected.id} selected={selected} degree2={degree2} resume={resume} />
495
518
  : <ScansNeedYourNetwork csvSource={csvSource} style={{ marginTop: 16 }} />
496
519
  )}
497
520
 
@@ -1075,6 +1098,87 @@ function PathBox({ routes, selected, tierColors, onSelect }) {
1075
1098
  );
1076
1099
  }
1077
1100
 
1101
+ const noWatch = () => () => {};
1102
+
1103
+ /**
1104
+ * Where Resume would carry on with this person ({ nextPage, pagesRead, legacy };
1105
+ * null when there is nothing to carry on with; undefined until known), and
1106
+ * whether their circle is being scanned right now. One question to the server
1107
+ * for the whole card (GET /api/scraper?resume=<id>), asked again whenever a
1108
+ * scan ends, since every read moves it. Only those two facts are taken from the
1109
+ * scanner, never its whole answer: that changes every time a running scan's log
1110
+ * grows, and this panel would redraw with it.
1111
+ */
1112
+ function useResume(person) {
1113
+ const id = person?.id ?? null;
1114
+ const subscribe = id ? watchScanner : noWatch;
1115
+ const lastEnded = useSyncExternalStore(subscribe, () => scannerNow().finished[0]?.startedAt ?? null, () => null);
1116
+ const scanning = useSyncExternalStore(subscribe, () => {
1117
+ const now = scannerNow();
1118
+ return Boolean(now.running && person && scansCircleOf(now, person));
1119
+ }, () => false);
1120
+ const [got, setGot] = useState({ id: null, resume: undefined });
1121
+ useEffect(() => {
1122
+ if (!id) return undefined;
1123
+ let live = true;
1124
+ resumePoint(id).then((r) => { if (live) setGot({ id, resume: r }); }, () => { if (live) setGot({ id, resume: null }); });
1125
+ return () => { live = false; };
1126
+ }, [id, lastEnded]);
1127
+ // Another person's answer never shows on this one's card while theirs is asked.
1128
+ return { resume: id && got.id === id ? got.resume : undefined, scanning: Boolean(id) && scanning };
1129
+ }
1130
+
1131
+ /**
1132
+ * Resume, from anywhere on the card: bring the scan box into view and press its
1133
+ * own Resume, so the same checks run and the scan's log or any problem shows
1134
+ * where it's seen. While their scan runs (or Resume is greyed out) it only
1135
+ * brings the box into view, which says why.
1136
+ */
1137
+ function resumeInScanBox(scanning = false) {
1138
+ const box = document.getElementById('scan-box');
1139
+ if (!box) return;
1140
+ box.scrollIntoView({ behavior: 'smooth', block: 'center' });
1141
+ box.animate?.([{ boxShadow: '0 0 0 3px rgba(255,215,0,0.75)' }, { boxShadow: '0 0 0 0 rgba(255,215,0,0)' }], { duration: 1600 });
1142
+ if (!scanning) box.querySelector('button[data-resume]:not([disabled])')?.click();
1143
+ }
1144
+
1145
+ /**
1146
+ * The line by their name when the read of their list stopped partway (Blake,
1147
+ * 2026-10-03: "if someone was scanned but not finished it needs to show that in
1148
+ * their profile card"). The scan box is far down the card, so this says it where
1149
+ * it's seen, and Resume here presses the box's own Resume: the same checks, and
1150
+ * the box, brought into view, is where the scan's log and any problem show.
1151
+ * While their scan runs it says so, and only takes you to the box.
1152
+ */
1153
+ function StoppedPartway({ resume, scanning }) {
1154
+ const gold = 'var(--sd-gold, #FFD700)';
1155
+ return (
1156
+ <button type="button" onClick={() => resumeInScanBox(scanning)}
1157
+ aria-label={scanning ? 'Scanning their circle now: go to the scan' : `${stoppedLine(resume)}. Resume from page ${resume.nextPage}`}
1158
+ style={{
1159
+ display: 'flex', alignItems: 'center', gap: 9, width: '100%', boxSizing: 'border-box', margin: '-6px 0 16px',
1160
+ padding: '8px 11px', borderRadius: 8, cursor: 'pointer', textAlign: 'left', font: 'inherit',
1161
+ background: 'rgba(255,215,0,0.07)', border: '1px solid rgba(255,215,0,0.32)', color: 'var(--sd-fg-2, #ddd)',
1162
+ }}>
1163
+ {/* A pause sign while stopped; a pulsing dot while it runs. */}
1164
+ {scanning ? (
1165
+ // ScanLog's keyframes: the box below shows its log while their scan runs.
1166
+ <span aria-hidden="true" className="scanlog-dot" style={{ width: 8, height: 8, borderRadius: '50%', background: gold, flexShrink: 0, animation: 'scanlogPulse 1s ease-in-out infinite' }} />
1167
+ ) : (
1168
+ <span aria-hidden="true" style={{ display: 'flex', gap: 2, flexShrink: 0 }}>
1169
+ <span style={{ width: 3, height: 10, borderRadius: 1, background: gold }} />
1170
+ <span style={{ width: 3, height: 10, borderRadius: 1, background: gold }} />
1171
+ </span>
1172
+ )}
1173
+ <span style={{ flex: 1, minWidth: 0, fontSize: 11.5, lineHeight: 1.35 }}>
1174
+ <b style={{ color: gold }}>{scanning ? 'Scanning their circle now' : `${stoppedLine(resume)}.`}</b>
1175
+ {!scanning && <span style={{ color: 'var(--sd-fg-3, #999)' }}> Their list isn&apos;t finished.</span>}
1176
+ </span>
1177
+ <span style={{ fontSize: 11.5, fontWeight: 800, color: gold, whiteSpace: 'nowrap' }}>{scanning ? 'See it ↓' : 'Resume ↓'}</span>
1178
+ </button>
1179
+ );
1180
+ }
1181
+
1078
1182
  // Scan one person's circle, or carry on with it.
1079
1183
  //
1080
1184
  // LinkedIn lists someone else's connections in its own order, with no dates,
@@ -1085,11 +1189,18 @@ function PathBox({ routes, selected, tierColors, onSelect }) {
1085
1189
  // Whether a scan is running comes from the scanner (useScanner), not from this
1086
1190
  // card: close the card mid-scan and open it again and the scan is still here,
1087
1191
  // and every other card greys its buttons out until it ends.
1088
- function CreateClusterCard({ selected, degree2 }) {
1192
+ //
1193
+ // `resume` is where Resume carries on (useResume, asked once for the card),
1194
+ // null when there is nothing to resume, undefined until it's known. A read that
1195
+ // stopped partway is the first thing the box says: SCAN IN PROGRESS, in gold,
1196
+ // with Resume as the main button (Blake, 2026-10-03: "if someone was scanned but
1197
+ // not finished it needs to show that in their profile card ... showing resume
1198
+ // scanner in that actual card"). It used to sit under CLUSTER ACTIVE, which
1199
+ // reads as done.
1200
+ function CreateClusterCard({ selected, degree2, resume }) {
1089
1201
  const scan = useScanner();
1090
1202
  const [checking, setChecking] = useState(false);
1091
1203
  const [problem, setProblem] = useState(null); // why a scan didn't start: { text, offline }
1092
- const [resume, setResume] = useState(undefined); // where Resume carries on; null: nothing to resume
1093
1204
  const clusterCount = degree2?.filter(d => d.source_connection_id === selected.id).length || 0;
1094
1205
  const hasCluster = clusterCount > 0;
1095
1206
 
@@ -1097,14 +1208,6 @@ function CreateClusterCard({ selected, degree2 }) {
1097
1208
  const busy = scan.running && !mine ? busyReason(scan) : null;
1098
1209
  const last = scan.finished.find((j) => scansCircleOf(j, selected));
1099
1210
 
1100
- // Every read moves where Resume would carry on, so ask again whenever one ends.
1101
- const lastEnded = scan.finished[0]?.startedAt ?? null;
1102
- useEffect(() => {
1103
- let live = true;
1104
- resumePoint(selected.id).then((r) => { if (live) setResume(r); }, () => { if (live) setResume(null); });
1105
- return () => { live = false; };
1106
- }, [selected.id, lastEnded]);
1107
-
1108
1211
  async function start(action) {
1109
1212
  setProblem(null);
1110
1213
  setChecking(true);
@@ -1122,27 +1225,32 @@ function CreateClusterCard({ selected, degree2 }) {
1122
1225
  }
1123
1226
 
1124
1227
  const read = hasCluster || Boolean(resume);
1125
- const accent = hasCluster ? '#00ff88' : '#9B59B6';
1228
+ // Gold while a read is stopped partway, green once a circle is in, purple before.
1229
+ const accent = resume ? '#FFD700' : hasCluster ? '#00ff88' : '#9B59B6';
1230
+ // Gold words vanish on a light look; the theme's own gold reads on either.
1231
+ const ink = resume ? 'var(--sd-gold, #FFD700)' : accent;
1126
1232
  const off = Boolean(busy) || checking;
1127
1233
  const button = (primary) => ({
1128
1234
  flex: 1, padding: '11px 8px', borderRadius: 8, fontWeight: 700, fontSize: primary ? 13 : 12,
1129
1235
  cursor: off ? 'not-allowed' : 'pointer', opacity: off ? 0.45 : 1,
1130
1236
  ...(primary
1131
1237
  ? { border: 'none', background: 'linear-gradient(135deg, #9B59B6, #3498DB)', color: '#fff' }
1132
- : { border: `1px solid ${accent}55`, background: `${accent}14`, color: accent }),
1238
+ : { border: `1px solid ${accent}55`, background: `${accent}14`, color: ink }),
1133
1239
  });
1134
1240
 
1135
1241
  return (
1136
1242
  <div id="scan-box" style={{
1137
- background: hasCluster ? 'rgba(0,255,136,0.06)' : 'rgba(155,89,182,0.08)',
1138
- border: `1px solid ${hasCluster ? 'rgba(0,255,136,0.2)' : 'rgba(155,89,182,0.3)'}`,
1243
+ background: resume ? 'rgba(255,215,0,0.06)' : hasCluster ? 'rgba(0,255,136,0.06)' : 'rgba(155,89,182,0.08)',
1244
+ border: `1px solid ${resume ? 'rgba(255,215,0,0.32)' : hasCluster ? 'rgba(0,255,136,0.2)' : 'rgba(155,89,182,0.3)'}`,
1139
1245
  borderRadius: 10, padding: 16, marginTop: 16,
1140
1246
  }}>
1141
- <div style={{ fontSize: 10, color: accent, fontWeight: 700, letterSpacing: 1, marginBottom: 4, textAlign: 'center' }}>
1142
- {hasCluster ? 'CLUSTER ACTIVE' : 'CREATE CLUSTER'}
1247
+ <div style={{ fontSize: 10, color: ink, fontWeight: 700, letterSpacing: 1, marginBottom: 4, textAlign: 'center' }}>
1248
+ {scanBoxTitle({ resume, hasCluster })}
1143
1249
  </div>
1144
1250
  <div style={{ fontSize: 12, color: 'var(--sd-fg-3, #aaa)', marginBottom: 12, textAlign: 'center' }}>
1145
- {hasCluster
1251
+ {resume
1252
+ ? (hasCluster ? `${clusterCount} connections mapped so far` : 'Their list is only partly read')
1253
+ : hasCluster
1146
1254
  ? `${clusterCount} connections mapped in Six Degrees`
1147
1255
  : <>Scan {selected.name}&apos;s connections to map their network</>}
1148
1256
  </div>
@@ -1169,7 +1277,8 @@ function CreateClusterCard({ selected, degree2 }) {
1169
1277
  {last && <LastRead job={last} />}
1170
1278
  <div style={{ display: 'flex', gap: 8 }}>
1171
1279
  {resume && (
1172
- <button onClick={() => start('resume')} disabled={off} style={button(true)}>
1280
+ // data-resume: the line by their name presses this one (StoppedPartway).
1281
+ <button data-resume onClick={() => start('resume')} disabled={off} style={button(true)}>
1173
1282
  Resume from page {resume.nextPage}
1174
1283
  </button>
1175
1284
  )}
@@ -0,0 +1,275 @@
1
+ 'use client';
2
+
3
+ // Insights → Kingmakers, Gatekeepers, Companies and Industries: each board in
4
+ // full, a hundred rows at a time. The numbers are lib/insights-board.js's,
5
+ // with where they came from under each.
6
+
7
+ import { useState } from 'react';
8
+ import Avatar from '../Avatar';
9
+ import { TIER_COLORS } from '../../../lib/themes';
10
+ import { shortName } from '../../../lib/separation';
11
+ import {
12
+ fmt, one, pc, Hero, PersonCell, Src, NeedsCircles, OpenCircle, Bars, Card, LINE, SOFT_LINE, CARD_BG, ELLIPSIS,
13
+ } from './parts';
14
+ import { Logo } from './Rail';
15
+ import { PAGE } from './PeopleView';
16
+
17
+ /** A board's table: the header row on a computer, the rows, and Show more. */
18
+ function Table({ cols, head, right = [], rows, render, isMobile, source }) {
19
+ const step = isMobile ? 25 : PAGE;
20
+ const [pages, setPages] = useState(1);
21
+ const shown = pages * step;
22
+ const list = rows.slice(0, shown);
23
+ const left = rows.length - list.length;
24
+ return (
25
+ <div style={{ border: `1px solid ${LINE}`, borderRadius: 14, background: CARD_BG, overflow: 'hidden' }}>
26
+ {!isMobile && (
27
+ <div style={{ display: 'grid', gridTemplateColumns: cols, gap: 12, padding: '0 16px', alignItems: 'center', height: 36, borderBottom: `1px solid ${LINE}`, background: 'rgba(var(--sd-ink, 255, 255, 255), 0.02)', fontSize: 10.5, color: 'var(--sd-fg-4, #6f7a88)', textTransform: 'uppercase', letterSpacing: 0.6, fontWeight: 700 }}>
28
+ {head.map((h, i) => <span key={i} style={{ textAlign: right.includes(i) ? 'right' : undefined }}>{h}</span>)}
29
+ </div>
30
+ )}
31
+ {list.map((r, i) => (
32
+ <div key={r.key ?? i} style={isMobile
33
+ ? { padding: '10px 12px', borderBottom: `1px solid ${SOFT_LINE}` }
34
+ : { display: 'grid', gridTemplateColumns: cols, gap: 12, padding: '0 16px', alignItems: 'center', minHeight: 54, borderBottom: `1px solid ${SOFT_LINE}` }}>
35
+ {render(r, i)}
36
+ </div>
37
+ ))}
38
+ {left > 0 && (
39
+ <button type="button" onClick={() => setPages((n) => n + 1)} style={{ display: 'block', width: '100%', textAlign: 'left', padding: '12px 16px', border: 'none', borderBottom: `1px solid ${LINE}`, background: 'transparent', cursor: 'pointer', fontSize: 12.5, fontWeight: 600, color: 'var(--sd-blue, #3498DB)' }}>
40
+ Show {fmt(Math.min(step, left))} more → <span style={{ color: 'var(--sd-fg-4, #6f7a88)', fontWeight: 400, marginLeft: 6 }}>{fmt(left)} to go</span>
41
+ </button>
42
+ )}
43
+ <Src style={{ padding: '10px 16px 14px', marginTop: 0 }}>{source}</Src>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ const Rank = ({ n, isMobile }) => <span style={{ fontSize: isMobile ? 11.5 : 13, fontWeight: 700, color: n <= 3 ? 'var(--sd-gold, #FFD700)' : 'var(--sd-fg-3, #8b9a9a)', fontVariantNumeric: 'tabular-nums' }}>#{fmt(n)}</span>;
49
+ const Num = ({ children, color, big }) => <b style={{ fontSize: big ? 16 : 13.5, fontWeight: big ? 800 : 700, color: color || 'var(--sd-fg-1, #fff)', fontVariantNumeric: 'tabular-nums', display: 'block', textAlign: 'right' }}>{children}</b>;
50
+ const Mute = ({ children, right }) => <span style={{ fontSize: 12, color: 'var(--sd-fg-3, #8b9a9a)', fontVariantNumeric: 'tabular-nums', ...(right ? { display: 'block', textAlign: 'right' } : null) }}>{children}</span>;
51
+
52
+ /** S and A behind someone, as one bar with the letters on it. */
53
+ function SABar({ S, A, max }) {
54
+ return <Bars max={max} parts={[
55
+ { value: S, color: TIER_COLORS.S, label: 'S', ink: TIER_COLORS.S, title: `${fmt(S)} S` },
56
+ { value: A, color: TIER_COLORS.A, label: 'A', ink: TIER_COLORS.A, title: `${fmt(A)} A` },
57
+ ]} />;
58
+ }
59
+
60
+ // ── Kingmakers ──────────────────────────────────────────────────────────────
61
+
62
+ export function KingmakersBoard({ data, source, isMobile }) {
63
+ const { kings } = data;
64
+ const cols = '52px minmax(0,1.5fr) 80px 56px 56px minmax(0,1.3fr) 70px 100px';
65
+ const max = kings.list[0]?.SA || 1;
66
+ return (
67
+ <>
68
+ <Hero title="Kingmakers" isMobile={isMobile}>
69
+ Your connections by the S and A people in their circle: the doors with the most power behind them. A title says what someone holds; a circle says what they can open.
70
+ </Hero>
71
+ {kings.scanned === 0 ? <Card><NeedsCircles source={source} /></Card> : (
72
+ <Table isMobile={isMobile} cols={cols} rows={kings.list.map((k) => ({ ...k, key: k.row.id }))} source={kings.source}
73
+ head={['Rank', 'Connection', 'Circle', 'S', 'A', 'Behind them', 'S+A', '']} right={[2, 3, 4, 6]}
74
+ render={(k, i) => (isMobile ? (
75
+ <div style={{ display: 'grid', gridTemplateColumns: '36px minmax(0,1fr) auto', gap: 8, alignItems: 'center' }}>
76
+ <Rank n={i + 1} isMobile />
77
+ <PersonCell row={k.row} size={30} />
78
+ <Num big>{fmt(k.SA)}</Num>
79
+ <span />
80
+ <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}><SABar S={k.S} A={k.A} max={max} /><Mute>of {fmt(k.size)}</Mute></span>
81
+ <OpenCircle id={k.row.id} compact />
82
+ </div>
83
+ ) : (
84
+ <>
85
+ <Rank n={i + 1} />
86
+ <PersonCell row={k.row} />
87
+ <Mute right>{fmt(k.size)}</Mute>
88
+ <Num color={TIER_COLORS.S}>{fmt(k.S)}</Num>
89
+ <Num color={TIER_COLORS.A}>{fmt(k.A)}</Num>
90
+ <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}><SABar S={k.S} A={k.A} max={max} /><Mute>{pc(100 * k.share)}</Mute></span>
91
+ <Num big>{fmt(k.SA)}</Num>
92
+ <span style={{ justifySelf: 'end' }}><OpenCircle id={k.row.id} /></span>
93
+ </>
94
+ ))} />
95
+ )}
96
+ </>
97
+ );
98
+ }
99
+
100
+ // ── Gatekeepers ─────────────────────────────────────────────────────────────
101
+
102
+ export function GatekeepersBoard({ data, source, isMobile }) {
103
+ const { gates, kings } = data;
104
+ const cols = '52px minmax(0,1.6fr) 70px 110px 76px minmax(0,1fr) 100px';
105
+ const others = Math.max(0, 100 - gates.top3Share);
106
+ return (
107
+ <>
108
+ <Hero title="Gatekeepers" isMobile={isMobile}>
109
+ {kings.scanned === 0 ? 'Who your S and A reach hangs on: the people who reach you through one connection only.'
110
+ : <><b style={{ color: 'var(--sd-fg-1, #fff)' }}>{pc(gates.onlyShare)}</b> of the S and A people you can reach have one way in. Three connections carry <b style={{ color: 'var(--sd-fg-1, #fff)' }}>{pc(gates.top3Share)}</b> of your S and A reach.</>}
111
+ </Hero>
112
+ {kings.scanned === 0 ? <Card><NeedsCircles source={source} /></Card> : (
113
+ <>
114
+ <div style={{ marginBottom: 14 }}>
115
+ <div style={{ display: 'flex', gap: 2, height: 30, borderRadius: 8, overflow: 'hidden' }}>
116
+ {gates.top3.map((g, i) => (
117
+ <span key={g.row.id} title={`${g.row.name}: ${pc(g.share)} of your S and A reach`} style={{ width: `${g.share}%`, background: ['#00ff88', '#00c46a', '#00984f'][i], display: 'flex', alignItems: 'center', paddingLeft: 10, overflow: 'hidden' }}>
118
+ <em style={{ fontStyle: 'normal', fontSize: 12, fontWeight: 750, color: '#00170b', whiteSpace: 'nowrap' }}>{shortName(g.row.name)} {pc(g.share)}</em>
119
+ </span>
120
+ ))}
121
+ <span style={{ width: `${others}%`, background: 'rgba(var(--sd-ink, 255, 255, 255), 0.1)', display: 'flex', alignItems: 'center', paddingLeft: 10, overflow: 'hidden' }}>
122
+ <em style={{ fontStyle: 'normal', fontSize: 12, fontWeight: 600, color: 'var(--sd-fg-2, #b8c4c4)', whiteSpace: 'nowrap' }}>{fmt(Math.max(0, gates.list.length - 3))} others {pc(others)}</em>
123
+ </span>
124
+ </div>
125
+ </div>
126
+ <Table isMobile={isMobile} cols={cols} rows={gates.list.map((g) => ({ ...g, key: g.row.id }))} source={gates.source}
127
+ head={['Rank', 'Connection', 'Circle', 'Only via them', 'Of them S', 'Share of your S and A reach', '']} right={[2, 3, 4]}
128
+ render={(g, i) => (isMobile ? (
129
+ <div style={{ display: 'grid', gridTemplateColumns: '36px minmax(0,1fr) auto', gap: 8, alignItems: 'center' }}>
130
+ <Rank n={i + 1} isMobile />
131
+ <PersonCell row={g.row} size={30} />
132
+ <span style={{ textAlign: 'right' }}><Num big color="var(--sd-green, #00ff88)">{fmt(g.onlySA)}</Num><div style={{ fontSize: 10, color: 'var(--sd-fg-4, #6f7a88)' }}>only via them</div></span>
133
+ <span />
134
+ <Mute>{fmt(g.onlyS)} S · circle of {fmt(g.size)} · {pc((100 * g.credit) / Math.max(1, gates.totalSA))} of your reach</Mute>
135
+ <OpenCircle id={g.row.id} compact />
136
+ </div>
137
+ ) : (
138
+ <>
139
+ <Rank n={i + 1} />
140
+ <PersonCell row={g.row} />
141
+ <Mute right>{fmt(g.size)}</Mute>
142
+ <Num big color="var(--sd-green, #00ff88)">{fmt(g.onlySA)}</Num>
143
+ <Num color={TIER_COLORS.S}>{fmt(g.onlyS)}</Num>
144
+ <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
145
+ <Bars max={gates.top3[0]?.credit || 1} parts={[{ value: g.credit, color: 'var(--sd-green, #00ff88)' }]} height={8} />
146
+ <Mute>{pc((100 * g.credit) / Math.max(1, gates.totalSA))}</Mute>
147
+ </span>
148
+ <span style={{ justifySelf: 'end' }}><OpenCircle id={g.row.id} /></span>
149
+ </>
150
+ ))} />
151
+ </>
152
+ )}
153
+ </>
154
+ );
155
+ }
156
+
157
+ // ── Companies ───────────────────────────────────────────────────────────────
158
+
159
+ const SCORE_FROM = { yours: 'your score', known: 'curated list', data: 'public dataset', estimate: 'estimated' };
160
+
161
+ export function CompaniesBoard({ heavy, isMobile }) {
162
+ const cols = '52px minmax(0,1.6fr) 96px 56px 56px 96px minmax(0,1.2fr) 92px';
163
+ if (!heavy) return <><Hero title="Company power" isMobile={isMobile}>Every company within two steps of you, with everyone’s power added up.</Hero><Card><div style={{ fontSize: 13, color: 'var(--sd-fg-3, #8b9a9a)' }}>Counting…</div></Card></>;
164
+ const { companies } = heavy;
165
+ return (
166
+ <>
167
+ <Hero title="Company power" isMobile={isMobile}>
168
+ <b style={{ color: 'var(--sd-fg-1, #fff)' }}>{fmt(companies.count)}</b> companies someone within two steps of you works at now, with everyone’s power added up: where the power you can reach sits.
169
+ </Hero>
170
+ <Table isMobile={isMobile} cols={cols} rows={companies.list.map((c) => ({ ...c, key: c.name }))} source={`${companies.source} ${companies.bigSource}`}
171
+ head={['Rank', 'Company', 'People', 'S', 'A', 'Total power', 'Ways in', 'Score']} right={[2, 3, 4, 5, 7]}
172
+ render={(c, i) => {
173
+ const ways = (
174
+ <span style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, overflow: 'hidden' }} title={c.bridges.map((b) => `${b.bridge.name}: ${fmt(b.people)} there`).join('\n')}>
175
+ <Mute>{fmt(c.waysIn)}</Mute>
176
+ {c.bridges.slice(0, 2).map((b) => (
177
+ <span key={b.bridge.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 11.5, color: 'var(--sd-fg-2, #b8c4c4)', whiteSpace: 'nowrap' }}>
178
+ <Avatar person={b.bridge} size={16} tierColors={TIER_COLORS} />{shortName(b.bridge.name)}
179
+ </span>
180
+ ))}
181
+ {c.d1 > 0 && <span style={{ fontSize: 11, color: 'var(--sd-green, #7dffc0)', whiteSpace: 'nowrap' }}>{fmt(c.d1)} yours</span>}
182
+ </span>
183
+ );
184
+ const name = (
185
+ <span style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
186
+ <Logo name={c.name} size={30} />
187
+ <span style={{ display: 'flex', flexDirection: 'column', minWidth: 0 }}>
188
+ <b style={{ fontSize: 13.5, fontWeight: 650, ...ELLIPSIS }}>{c.name}</b>
189
+ <span style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 11.5, color: 'var(--sd-fg-3, #8b9a9a)', minWidth: 0 }}>
190
+ <i className="sd-dot-html" style={{ width: 7, height: 7, borderRadius: '50%', background: c.industry.color, flexShrink: 0 }} />
191
+ <span style={ELLIPSIS}>{c.industry.label}</span>
192
+ </span>
193
+ </span>
194
+ </span>
195
+ );
196
+ return isMobile ? (
197
+ <div style={{ display: 'grid', gridTemplateColumns: '36px minmax(0,1fr) auto', gap: 8, alignItems: 'center' }}>
198
+ <Rank n={i + 1} isMobile />
199
+ {name}
200
+ <span style={{ textAlign: 'right' }}><Num big>{fmt(Math.round(c.total))}</Num><div style={{ fontSize: 10, color: 'var(--sd-fg-4, #6f7a88)' }}>total power</div></span>
201
+ <span />
202
+ <Mute><span style={{ color: TIER_COLORS.S }}>{fmt(c.S)} S</span> · <span style={{ color: TIER_COLORS.A }}>{fmt(c.A)} A</span> · {fmt(c.people)} people · {fmt(c.waysIn)} ways in</Mute>
203
+ <Mute>{one(c.score).replace(/\.0$/, '')}/10</Mute>
204
+ </div>
205
+ ) : (
206
+ <>
207
+ <Rank n={i + 1} />
208
+ {name}
209
+ <Mute right>{fmt(c.people)}</Mute>
210
+ <Num color={TIER_COLORS.S}>{fmt(c.S)}</Num>
211
+ <Num color={TIER_COLORS.A}>{fmt(c.A)}</Num>
212
+ <Num big>{fmt(Math.round(c.total))}</Num>
213
+ {ways}
214
+ <span style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
215
+ <Num>{one(c.score).replace(/\.0$/, '')}/10</Num>
216
+ <span style={{ fontSize: 10, color: 'var(--sd-fg-4, #6f7a88)', whiteSpace: 'nowrap' }}>{SCORE_FROM[c.scoreSource]}</span>
217
+ </span>
218
+ </>
219
+ );
220
+ }} />
221
+ </>
222
+ );
223
+ }
224
+
225
+ // ── Industries ──────────────────────────────────────────────────────────────
226
+
227
+ export function IndustriesBoard({ heavy, isMobile }) {
228
+ const cols = 'minmax(0,1.5fr) 90px 90px 64px 64px minmax(0,1.4fr) 100px';
229
+ if (!heavy) return <><Hero title="Industries" isMobile={isMobile}>Where the power you can reach sits, by industry.</Hero><Card><div style={{ fontSize: 13, color: 'var(--sd-fg-3, #8b9a9a)' }}>Counting…</div></Card></>;
230
+ const { industries } = heavy;
231
+ const top = industries.list[0];
232
+ const max = Math.max(1, ...industries.list.map((e) => e.share));
233
+ return (
234
+ <>
235
+ <Hero title="Industries" isMobile={isMobile}>
236
+ Where the power you can reach sits.{top ? <> <b style={{ color: 'var(--sd-fg-1, #fff)' }}>{top.label}</b> holds {pc(top.share)} of your S and A.</> : null}
237
+ </Hero>
238
+ <Table isMobile={isMobile} cols={cols} rows={industries.list} source={industries.source}
239
+ head={['Industry', 'Companies', 'People', 'S', 'A', 'Share of your S and A', 'Total power']} right={[1, 2, 3, 4, 6]}
240
+ render={(e) => {
241
+ const label = (
242
+ <span style={{ display: 'flex', alignItems: 'center', gap: 9, minWidth: 0 }}>
243
+ <i className="sd-dot-html" style={{ width: 11, height: 11, borderRadius: 3, background: e.color, flexShrink: 0 }} />
244
+ <b style={{ fontSize: 13.5, fontWeight: 650, ...ELLIPSIS }}>{e.label}</b>
245
+ </span>
246
+ );
247
+ const bar = (
248
+ <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
249
+ <Bars max={max} parts={[{ value: e.share, color: e.color, ink: e.color }]} height={10} />
250
+ <Mute>{pc(e.share)}</Mute>
251
+ </span>
252
+ );
253
+ return isMobile ? (
254
+ <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) auto', gap: 6, alignItems: 'center' }}>
255
+ {label}
256
+ <Num big>{pc(e.share)}</Num>
257
+ <Mute>{fmt(e.companies)} companies · {fmt(e.people)} people · <span style={{ color: TIER_COLORS.S }}>{fmt(e.S)} S</span> · <span style={{ color: TIER_COLORS.A }}>{fmt(e.A)} A</span></Mute>
258
+ <span />
259
+ </div>
260
+ ) : (
261
+ <>
262
+ {label}
263
+ <Mute right>{fmt(e.companies)}</Mute>
264
+ <Mute right>{fmt(e.people)}</Mute>
265
+ <Num color={TIER_COLORS.S}>{fmt(e.S)}</Num>
266
+ <Num color={TIER_COLORS.A}>{fmt(e.A)}</Num>
267
+ {bar}
268
+ <Num big>{fmt(e.total)}</Num>
269
+ </>
270
+ );
271
+ }} />
272
+ </>
273
+ );
274
+ }
275
+