six-degrees 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +35 -7
  5. package/.next/standalone/.next/required-server-files.json +6 -1
  6. package/.next/standalone/.next/routes-manifest.json +18 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  22. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  24. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  26. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  28. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  32. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
  47. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  51. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  61. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  63. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/import.html +1 -1
  68. package/.next/standalone/.next/server/app/import.rsc +13 -13
  69. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  70. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  71. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
  72. package/.next/standalone/.next/server/app/index.html +1 -1
  73. package/.next/standalone/.next/server/app/index.rsc +13 -13
  74. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
  75. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  76. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  77. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/launch.html +2 -2
  80. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  81. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  82. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  83. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
  84. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  86. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  87. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  88. package/.next/standalone/.next/server/app/paths.html +1 -1
  89. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  90. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  91. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  92. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
  93. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  94. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  95. package/.next/standalone/.next/server/app/profile.html +1 -1
  96. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  97. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  98. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  99. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
  100. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  101. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  102. package/.next/standalone/.next/server/app/queue.html +1 -1
  103. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  104. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  105. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  106. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
  107. package/.next/standalone/.next/server/app/scores/page/app-paths-manifest.json +3 -0
  108. package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
  109. package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
  110. package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
  111. package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
  112. package/.next/standalone/.next/server/app/scores/page.js +13 -0
  113. package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
  114. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
  115. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
  116. package/.next/standalone/.next/server/app/scores.html +1 -0
  117. package/.next/standalone/.next/server/app/scores.meta +26 -0
  118. package/.next/standalone/.next/server/app/scores.rsc +24 -0
  119. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
  120. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
  121. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
  122. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  123. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  124. package/.next/standalone/.next/server/app/settings.html +1 -1
  125. package/.next/standalone/.next/server/app/settings.rsc +13 -13
  126. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
  127. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  128. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
  129. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  130. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  131. package/.next/standalone/.next/server/app/setup.html +1 -1
  132. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  133. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  134. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  135. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
  136. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +8 -0
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  159. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  162. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  163. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  164. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  165. package/.next/standalone/.next/server/chunks/_02twlca._.js +10 -0
  166. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  167. package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
  168. package/.next/standalone/.next/server/chunks/_1_fynia._.js +8 -6
  169. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
  170. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  171. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  172. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +13 -8
  173. package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
  174. package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
  175. package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
  176. package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
  177. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  178. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
  180. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
  181. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
  182. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
  183. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
  184. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
  185. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
  186. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
  187. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
  190. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
  192. package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
  193. package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
  195. package/.next/standalone/.next/server/chunks/ssr/_1cyxocj._.js +3 -0
  196. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
  198. package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +3 -0
  199. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
  200. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.js +3 -0
  201. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
  202. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  203. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
  204. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
  205. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  206. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  207. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +3 -0
  208. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  209. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  210. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
  211. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
  212. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
  213. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  214. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  215. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  216. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  217. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  218. package/.next/standalone/.next/server/pages/404.html +1 -1
  219. package/.next/standalone/.next/server/pages/500.html +1 -1
  220. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  221. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  222. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  223. package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  224. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  225. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
  226. package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  227. package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  228. package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
  229. package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  230. package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
  231. package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  232. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  233. package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  234. package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  235. package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  236. package/.next/standalone/CHANGELOG.md +286 -0
  237. package/.next/standalone/CONTRIBUTING.md +15 -4
  238. package/.next/standalone/README.md +50 -29
  239. package/.next/standalone/SECURITY.md +10 -0
  240. package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
  241. package/.next/standalone/app/api/company-scores/route.js +1 -1
  242. package/.next/standalone/app/api/ingest/route.js +23 -2
  243. package/.next/standalone/app/api/outreach/route.js +107 -44
  244. package/.next/standalone/app/api/scraper/route.js +146 -17
  245. package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
  246. package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
  247. package/.next/standalone/app/api/update-images/route.js +41 -7
  248. package/.next/standalone/app/avatars/[file]/route.js +6 -1
  249. package/.next/standalone/app/components/Avatar.js +5 -2
  250. package/.next/standalone/app/components/ChainView.js +659 -469
  251. package/.next/standalone/app/components/CompanyScores.js +16 -10
  252. package/.next/standalone/app/components/EmptyState.js +1 -1
  253. package/.next/standalone/app/components/FieldStep.js +94 -0
  254. package/.next/standalone/app/components/ForceGraph.js +183 -768
  255. package/.next/standalone/app/components/GridView.js +9 -7
  256. package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
  257. package/.next/standalone/app/components/LinkedInLimits.js +22 -7
  258. package/.next/standalone/app/components/ListView.js +6 -4
  259. package/.next/standalone/app/components/MappingProgress.js +5 -6
  260. package/.next/standalone/app/components/OnboardingGate.js +1 -1
  261. package/.next/standalone/app/components/OrbitGraph.js +33 -1
  262. package/.next/standalone/app/components/OutlinkQuest.js +5 -4
  263. package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
  264. package/.next/standalone/app/components/RingsView.js +9 -8
  265. package/.next/standalone/app/components/SeparationView.js +96 -14
  266. package/.next/standalone/app/components/Sidebar.js +366 -287
  267. package/.next/standalone/app/components/TheirCircle.js +300 -0
  268. package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
  269. package/.next/standalone/app/components/settings/SectorSection.js +21 -195
  270. package/.next/standalone/app/components/settings/TierSection.js +99 -0
  271. package/.next/standalone/app/components/useRequests.js +12 -0
  272. package/.next/standalone/app/components/useScanner.js +13 -0
  273. package/.next/standalone/app/components/views.js +3 -5
  274. package/.next/standalone/app/globals.css +7 -0
  275. package/.next/standalone/app/launch/page.js +6 -3
  276. package/.next/standalone/app/page.js +183 -74
  277. package/.next/standalone/app/paths/page.js +20 -14
  278. package/.next/standalone/app/profile/page.js +3 -3
  279. package/.next/standalone/app/queue/page.js +56 -60
  280. package/.next/standalone/app/scores/page.js +72 -0
  281. package/.next/standalone/app/settings/page.js +15 -3
  282. package/.next/standalone/app/setup/page.js +423 -260
  283. package/.next/standalone/db/schema.js +2 -1
  284. package/.next/standalone/eslint.config.mjs +3 -3
  285. package/.next/standalone/install.sh +3 -3
  286. package/.next/standalone/lib/chain-layout.js +139 -0
  287. package/.next/standalone/lib/circle.js +169 -0
  288. package/.next/standalone/lib/companies.js +1 -1
  289. package/.next/standalone/lib/csv.js +21 -10
  290. package/.next/standalone/lib/data-export.js +2 -1
  291. package/.next/standalone/lib/data-folder.js +4 -2
  292. package/.next/standalone/lib/db-client.js +1 -0
  293. package/.next/standalone/lib/galaxy.js +12 -0
  294. package/.next/standalone/lib/ingest.js +27 -8
  295. package/.next/standalone/lib/legacy-offer.js +7 -6
  296. package/.next/standalone/lib/legacy-scores.js +2 -2
  297. package/.next/standalone/lib/linkedin-limits.js +30 -8
  298. package/.next/standalone/lib/photos.js +47 -0
  299. package/.next/standalone/lib/quest.js +8 -3
  300. package/.next/standalone/lib/rarity.js +80 -0
  301. package/.next/standalone/lib/reach.js +113 -0
  302. package/.next/standalone/lib/requests-client.js +113 -0
  303. package/.next/standalone/lib/rpc.js +26 -15
  304. package/.next/standalone/lib/scan-state.js +1 -0
  305. package/.next/standalone/lib/scanner-setup.js +20 -1
  306. package/.next/standalone/lib/score-guess.js +60 -0
  307. package/.next/standalone/lib/scoring.js +1761 -69
  308. package/.next/standalone/lib/scraper-client.js +235 -17
  309. package/.next/standalone/lib/sector-directory.js +1 -1
  310. package/.next/standalone/lib/sector-focus.js +17 -3
  311. package/.next/standalone/lib/separation.js +4 -2
  312. package/.next/standalone/lib/settings-client.js +21 -0
  313. package/.next/standalone/lib/settings-effects.js +19 -1
  314. package/.next/standalone/lib/settings.js +7 -2
  315. package/.next/standalone/next.config.mjs +25 -3
  316. package/.next/standalone/package-lock.json +2 -2
  317. package/.next/standalone/package.json +2 -2
  318. package/.next/standalone/public/demo-data.json +1 -1
  319. package/.next/standalone/scripts/build-app.mjs +11 -2
  320. package/.next/standalone/scripts/check-node.mjs +1 -1
  321. package/.next/standalone/scripts/gen-synthetic.mjs +1 -1
  322. package/.next/standalone/scripts/image_store.py +77 -21
  323. package/.next/standalone/scripts/scrape.py +305 -29
  324. package/.next/standalone/server.js +1 -1
  325. package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  326. package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  327. package/.next/static/chunks/0l1568qff0efj.js +1 -0
  328. package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  329. package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  330. package/.next/static/chunks/135n87ydxjrra.js +2 -0
  331. package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  332. package/.next/static/chunks/28bu26ausqzmn.js +1 -0
  333. package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  334. package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  335. package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  336. package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  337. package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  338. package/CHANGELOG.md +286 -0
  339. package/README.md +50 -29
  340. package/SECURITY.md +10 -0
  341. package/db/schema.js +2 -1
  342. package/lib/chain-layout.js +139 -0
  343. package/lib/circle.js +169 -0
  344. package/lib/companies.js +1 -1
  345. package/lib/csv.js +21 -10
  346. package/lib/data-export.js +2 -1
  347. package/lib/data-folder.js +4 -2
  348. package/lib/db-client.js +1 -0
  349. package/lib/galaxy.js +12 -0
  350. package/lib/ingest.js +27 -8
  351. package/lib/legacy-offer.js +7 -6
  352. package/lib/legacy-scores.js +2 -2
  353. package/lib/linkedin-limits.js +30 -8
  354. package/lib/photos.js +47 -0
  355. package/lib/quest.js +8 -3
  356. package/lib/rarity.js +80 -0
  357. package/lib/reach.js +113 -0
  358. package/lib/requests-client.js +113 -0
  359. package/lib/rpc.js +26 -15
  360. package/lib/scan-state.js +1 -0
  361. package/lib/scanner-setup.js +20 -1
  362. package/lib/score-guess.js +60 -0
  363. package/lib/scoring.js +1761 -69
  364. package/lib/scraper-client.js +235 -17
  365. package/lib/sector-directory.js +1 -1
  366. package/lib/sector-focus.js +17 -3
  367. package/lib/separation.js +4 -2
  368. package/lib/settings-client.js +21 -0
  369. package/lib/settings-effects.js +19 -1
  370. package/lib/settings.js +7 -2
  371. package/package.json +2 -2
  372. package/public/demo-data.json +1 -1
  373. package/scripts/check-node.mjs +1 -1
  374. package/scripts/image_store.py +77 -21
  375. package/scripts/scrape.py +305 -29
  376. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
  377. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
  378. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
  379. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
  380. package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +0 -3
  381. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
  382. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
  383. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
  384. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
  385. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
  386. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
  387. package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +0 -3
  388. package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
  389. package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
  390. package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
  391. package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
  392. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
  393. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
  394. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
  395. package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
  396. package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
  397. package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  398. package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
  399. package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  400. package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
  401. package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
  402. package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +0 -44
  403. package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  404. package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
  405. package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  406. package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
  407. package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  408. package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  409. package/.next/standalone/app/components/BridgeRing.js +0 -753
  410. package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
  411. package/.next/static/chunks/08k1crid-05dt.js +0 -1
  412. package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
  413. package/.next/static/chunks/0q-_khndfxubv.js +0 -2
  414. package/.next/static/chunks/1c79veqedvubt.js +0 -1
  415. package/.next/static/chunks/1chp3nx45meg5.js +0 -44
  416. package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
  417. package/.next/static/chunks/1uadqylrod5_v.js +0 -1
  418. package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
  419. package/.next/static/chunks/289ktj97di6zg.js +0 -1
  420. package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
  421. package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
  422. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  423. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  424. /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
  425. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  426. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  427. /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
@@ -1,80 +1,143 @@
1
1
  import { db as supabase } from '../../../lib/db';
2
+ import { getDb, decodeRow } from '../../../lib/db-client';
3
+ import { resolveProfile } from '../../../lib/profile';
4
+
5
+ // Requests you've sent, kept per PERSON.
6
+ //
7
+ // A person has one row per bridge whose circle holds them (the table's key is
8
+ // profile + bridge + user), and a request used to mark only the copy that was
9
+ // clicked. The other copies still offered "Add", the Pending count counted
10
+ // copies, and every click awarded XP again. And the card's link wrote a second
11
+ // column (unlock_status) through another route, which Undo never took back.
12
+ // Now a request marks every copy of the person, in both columns, the bridge you
13
+ // asked through is kept (so once they accept, their card and circle say who
14
+ // introduced you: lib/promote.js keeps it), XP comes once per person, and Undo
15
+ // takes back all of it. lib/requests-client.js is the one way the pages call this.
2
16
 
3
17
  // XP rewards by tier
4
18
  const XP_SEND = { S: 25, A: 15, B: 10, C: 5, D: 2 };
5
19
  const XP_ACCEPT = { S: 100, A: 60, B: 35, C: 15, D: 5 };
20
+ const TIER_ORDER = ['S', 'A', 'B', 'C', 'D'];
6
21
 
7
22
  async function awardXP(userId, amount) {
8
23
  if (!userId || !amount) return;
9
- // Upsert: create if not exists, increment if exists
24
+ // Upsert: create if not exists, change if exists. Never below zero: an Undo
25
+ // takes back what a send gave, including sends from before sends gave any.
10
26
  const { data: existing } = await supabase.from('user_stats').select('xp').eq('id', userId).single();
11
27
  if (existing) {
12
- await supabase.from('user_stats').update({ xp: (existing.xp || 0) + amount }).eq('id', userId);
13
- } else {
28
+ await supabase.from('user_stats').update({ xp: Math.max(0, (existing.xp || 0) + amount) }).eq('id', userId);
29
+ } else if (amount > 0) {
14
30
  await supabase.from('user_stats').insert([{ id: userId, xp: amount, level: 1 }]).catch(() => {});
15
31
  }
16
32
  }
17
33
 
34
+ /** The profile a request belongs to: the one sent, else this machine's. */
35
+ function whose(userId) {
36
+ if (userId) return String(userId);
37
+ try { return resolveProfile({ create: false })?.id || null; } catch { return null; }
38
+ }
39
+
40
+ /** Every copy of one person: by the clicked row's profile URL, or the URL given. */
41
+ function copiesOf(db, { connectionId, profileUrl, userId }) {
42
+ let url = profileUrl ? String(profileUrl) : null;
43
+ let user = userId ? String(userId) : null;
44
+ let clicked = null;
45
+ if (connectionId != null) {
46
+ clicked = db.prepare('SELECT id, profile_url, user_id FROM linkedin_connections WHERE id = ?').get(String(connectionId)) || null;
47
+ if (clicked) {
48
+ url = clicked.profile_url || url;
49
+ user = user || clicked.user_id || null;
50
+ }
51
+ }
52
+ const cols = 'id, degree, tier, outreach_status, unlock_status, unlocked_from_bridge_id';
53
+ let rows = [];
54
+ if (url) {
55
+ rows = user
56
+ ? db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE profile_url = ? AND user_id = ?`).all(url, user)
57
+ : db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE profile_url = ?`).all(url);
58
+ } else if (clicked) {
59
+ rows = db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE id = ?`).all(clicked.id);
60
+ }
61
+ return { url, user, rows };
62
+ }
63
+
64
+ const inList = (ids) => ids.map(() => '?').join(', ');
65
+ const bestTier = (rows) => TIER_ORDER.find((t) => rows.some((r) => r.tier === t)) || 'D';
66
+
18
67
  export async function POST(request) {
19
68
  try {
20
69
  const body = await request.json();
21
- const { action, connectionId, profileUrl, userId } = body;
70
+ const { action, connectionId, profileUrl, userId, bridgeId } = body;
71
+ if (!['mark-sent', 'mark-accepted', 'undo'].includes(action)) {
72
+ return Response.json({ error: 'Unknown action' }, { status: 400 });
73
+ }
74
+ const db = getDb();
75
+ const { user, rows } = copiesOf(db, { connectionId, profileUrl, userId });
76
+ // Nobody on file (the sample network lives in the browser): nothing to keep.
77
+ if (!rows.length) return Response.json({ success: true, saved: 0, xp: 0 });
78
+ const ids = rows.map((r) => r.id);
79
+ const now = new Date().toISOString();
22
80
 
23
81
  if (action === 'mark-sent') {
24
- // Get the person's tier for XP calculation
25
- let tierQuery = supabase.from('linkedin_connections').select('tier');
26
- tierQuery = connectionId ? tierQuery.eq('id', connectionId) : tierQuery.eq('profile_url', profileUrl);
27
- const { data: person } = await tierQuery.single();
28
- const tier = person?.tier || 'D';
29
-
30
- let query = supabase.from('linkedin_connections').update({ outreach_status: 'sent' });
31
- query = connectionId ? query.eq('id', connectionId) : query.eq('profile_url', profileUrl);
32
- if (userId) query = query.eq('user_id', userId);
33
- const { error } = await query;
34
- if (error) return Response.json({ error: error.message }, { status: 500 });
35
-
36
- // Award XP for sending request
37
- await awardXP(userId, XP_SEND[tier] || 2);
38
-
39
- return Response.json({ success: true, xp: XP_SEND[tier] || 2 });
82
+ const already = rows.some((r) => r.outreach_status === 'sent' || r.outreach_status === 'accepted');
83
+ db.prepare(`UPDATE linkedin_connections SET outreach_status = 'sent', updated_at = ?
84
+ WHERE id IN (${inList(ids)}) AND (outreach_status IS NULL OR outreach_status NOT IN ('sent', 'accepted'))`).run(now, ...ids);
85
+ db.prepare(`UPDATE linkedin_connections SET unlock_status = 'pending', updated_at = ?
86
+ WHERE id IN (${inList(ids)}) AND (unlock_status IS NULL OR unlock_status = 'locked')`).run(now, ...ids);
87
+ // Who you asked through: one of your own connections, or nothing.
88
+ if (bridgeId != null) {
89
+ const bridge = db.prepare(
90
+ `SELECT id, name FROM linkedin_connections WHERE id = ? AND degree = 1${user ? ' AND user_id = ?' : ''}`,
91
+ ).get(...[String(bridgeId), ...(user ? [user] : [])]);
92
+ if (bridge) {
93
+ db.prepare(`UPDATE linkedin_connections SET unlocked_from_bridge_id = ?, unlocked_from_name = ?, updated_at = ?
94
+ WHERE id IN (${inList(ids)}) AND degree = 2 AND unlocked_from_bridge_id IS NULL`).run(bridge.id, bridge.name, now, ...ids);
95
+ }
96
+ }
97
+ const xp = already ? 0 : XP_SEND[bestTier(rows)] || 2;
98
+ await awardXP(whose(user), xp);
99
+ return Response.json({ success: true, saved: ids.length, xp, already });
40
100
  }
41
101
 
42
102
  if (action === 'mark-accepted') {
43
- let query = supabase.from('linkedin_connections').update({ outreach_status: 'accepted' });
44
- query = connectionId ? query.eq('id', connectionId) : query.eq('profile_url', profileUrl);
45
- if (userId) query = query.eq('user_id', userId);
46
- const { error } = await query;
47
- if (error) return Response.json({ error: error.message }, { status: 500 });
48
- return Response.json({ success: true });
103
+ db.prepare(`UPDATE linkedin_connections SET outreach_status = 'accepted', updated_at = ? WHERE id IN (${inList(ids)})`).run(now, ...ids);
104
+ return Response.json({ success: true, saved: ids.length, xp: 0, accepts: XP_ACCEPT[bestTier(rows)] || 5 });
49
105
  }
50
106
 
51
- if (action === 'undo') {
52
- let query = supabase.from('linkedin_connections').update({ outreach_status: null });
53
- query = connectionId ? query.eq('id', connectionId) : query.eq('profile_url', profileUrl);
54
- if (userId) query = query.eq('user_id', userId);
55
- const { error } = await query;
56
- if (error) return Response.json({ error: error.message }, { status: 500 });
57
- return Response.json({ success: true });
58
- }
59
-
60
- return Response.json({ error: 'Unknown action' }, { status: 400 });
107
+ // undo
108
+ const wasSent = rows.some((r) => r.outreach_status === 'sent');
109
+ db.prepare(`UPDATE linkedin_connections SET outreach_status = NULL, updated_at = ?
110
+ WHERE id IN (${inList(ids)}) AND outreach_status = 'sent'`).run(now, ...ids);
111
+ db.prepare(`UPDATE linkedin_connections SET unlock_status = 'locked', updated_at = ?
112
+ WHERE id IN (${inList(ids)}) AND unlock_status = 'pending'`).run(now, ...ids);
113
+ const xp = wasSent ? -(XP_SEND[bestTier(rows)] || 2) : 0;
114
+ await awardXP(whose(user), xp);
115
+ return Response.json({ success: true, saved: ids.length, xp });
61
116
  } catch (err) {
62
117
  console.error('Outreach error:', err);
63
118
  return Response.json({ error: err.message }, { status: 500 });
64
119
  }
65
120
  }
66
121
 
122
+ /** Everyone with a request out, once each (the newest copy), newest first. */
67
123
  export async function GET(request) {
68
124
  try {
69
125
  const { searchParams } = new URL(request.url);
70
- const userId = searchParams.get('userId');
71
-
72
- let query = supabase.from('linkedin_connections').select('*').eq('outreach_status', 'sent').order('created_at', { ascending: false });
73
- if (userId) query = query.eq('user_id', userId);
74
-
75
- const { data, error } = await query;
76
- if (error) return Response.json({ error: error.message }, { status: 500 });
77
- return Response.json({ pending: data || [] });
126
+ const user = whose(searchParams.get('userId'));
127
+ const rows = getDb().prepare(
128
+ `SELECT * FROM linkedin_connections
129
+ WHERE (outreach_status = 'sent' OR unlock_status = 'pending')${user ? ' AND user_id = ?' : ''}
130
+ ORDER BY created_at DESC`,
131
+ ).all(...(user ? [user] : []));
132
+ const seen = new Set();
133
+ const pending = [];
134
+ for (const row of rows) {
135
+ const key = row.profile_url || `id:${row.id}`;
136
+ if (seen.has(key)) continue;
137
+ seen.add(key);
138
+ pending.push(decodeRow('linkedin_connections', row));
139
+ }
140
+ return Response.json({ pending });
78
141
  } catch (err) {
79
142
  return Response.json({ error: err.message }, { status: 500 });
80
143
  }
@@ -9,6 +9,8 @@ import { pausedList, readProgress, readUnclear } from '../../../lib/paused';
9
9
  import { getDb } from '../../../lib/db-client';
10
10
  import { registerScanState } from '../../../lib/scan-state';
11
11
  import { pendingImport } from '../../../lib/data-import';
12
+ import { waitingPhotoCount } from '../../../lib/photos';
13
+ import { reachIndex, circleState } from '../../../lib/reach';
12
14
  import {
13
15
  pythonLooker, thisHostKey, scannerCommand, installSteps, downloadedPython, downloadVerified,
14
16
  placeDownloadedPython, sweepSetupLeftovers, megabytes, SETUP_WORK_PREFIX, ScannerSetupError,
@@ -32,6 +34,8 @@ const MAX_LOG = 500;
32
34
  const state = registerScanState({
33
35
  running: false,
34
36
  action: null,
37
+ target: null, // who a scan is of: { id, name } (either may be null), or null for scans of no one
38
+ recent: [], // the last few finished jobs, newest first: how each one ended
35
39
  log: [],
36
40
  startedAt: null,
37
41
  exitCode: null,
@@ -126,6 +130,53 @@ function bridgeSkips() {
126
130
  }
127
131
  }
128
132
 
133
+ /**
134
+ * What lib/reach.js needs from the scanner's files to tell who is ready for a
135
+ * circle scan: whose list is hidden, and whose list has been read even though
136
+ * nothing in it was saved (everyone on it was already yours). Read on its own,
137
+ * without the machine checks, because the map asks for it when it loads and
138
+ * again after every scan.
139
+ */
140
+ function scanNotes() {
141
+ let read = [];
142
+ try {
143
+ const me = resolveProfile({ create: false });
144
+ if (me) read = Object.keys(readProgress(dataDir(), me.id));
145
+ } catch {}
146
+ return { skips: bridgeSkips(), read };
147
+ }
148
+
149
+ /**
150
+ * One of your connections, for the Scan page's "Scan one circle" box (Bridge
151
+ * Chains and the Degrees panel send people there by id): who they are, and how
152
+ * their circle stands by the same rule the map uses. Null when not found.
153
+ */
154
+ function pickedPerson(id) {
155
+ try {
156
+ const me = resolveProfile({ create: false });
157
+ if (!me) return { person: null };
158
+ const db = getDb();
159
+ const row = db.prepare(
160
+ `SELECT id, name, tier, power_score, degree, profile_url, unlocked_from_bridge_id, unlocked_from_name
161
+ FROM linkedin_connections WHERE id = ? AND user_id = ? AND degree = 1`,
162
+ ).get(String(id ?? ''), me.id);
163
+ if (!row) return { person: null };
164
+ const mapped = db.prepare(
165
+ 'SELECT 1 FROM linkedin_connections WHERE user_id = ? AND degree = 2 AND source_connection_id = ? LIMIT 1',
166
+ ).get(me.id, row.id);
167
+ const reach = reachIndex([], mapped ? [{ source_connection_id: row.id }] : [], scanNotes());
168
+ return {
169
+ person: {
170
+ id: row.id, name: row.name, tier: row.tier, power_score: row.power_score,
171
+ unlocked_from_bridge_id: row.unlocked_from_bridge_id, unlocked_from_name: row.unlocked_from_name,
172
+ },
173
+ circle: circleState(row, reach),
174
+ };
175
+ } catch {
176
+ return { person: null };
177
+ }
178
+ }
179
+
129
180
  // The machine checks (Python, Chrome, signed in) spawn processes, so they are
130
181
  // cached for a few seconds, and callers that arrive while a look is under way
131
182
  // share it rather than each starting their own Pythons. Everything about the
@@ -225,6 +276,10 @@ async function status() {
225
276
  me = resolveProfile({ create: false });
226
277
  if (me) network = networkCounts(me.id);
227
278
  } catch {}
279
+ // People whose photo is still a link, which the app doesn't load: Save
280
+ // photos appears while there are any (lib/photos.js).
281
+ let photosWaiting = 0;
282
+ try { photosWaiting = waitingPhotoCount(getDb()); } catch {}
228
283
 
229
284
  const py = m.python;
230
285
  return {
@@ -251,20 +306,69 @@ async function status() {
251
306
  signedIn: m.signedIn,
252
307
  },
253
308
  network,
309
+ photosWaiting,
310
+ ...job(),
311
+ skips: bridgeSkips(),
312
+ linkedin: linkedinState(dataDir()),
313
+ paused: paused(me?.id),
314
+ };
315
+ }
316
+
317
+ /**
318
+ * The running job alone, from memory: no checks, no database. Every Scan
319
+ * button in the app asks this (lib/scraper-client.js watchScanner), every few
320
+ * seconds, so it must cost nothing.
321
+ *
322
+ * `recent` is how the last few jobs ended. A page following one job can then
323
+ * learn how it ended even when another job started before it looked again;
324
+ * with only the latest job's exit code, that one would never seem to finish.
325
+ */
326
+ function job() {
327
+ return {
254
328
  running: state.running,
255
329
  action: state.action,
330
+ target: state.target,
256
331
  startedAt: state.startedAt,
257
332
  exitCode: state.exitCode,
258
333
  failure: state.running ? null : state.failure,
259
334
  progress: state.running ? scanProgress(state.log, state.action) : null,
260
335
  log: state.log.slice(-120),
261
- skips: bridgeSkips(),
262
- linkedin: linkedinState(dataDir()),
263
- paused: paused(me?.id),
336
+ recent: state.recent,
264
337
  };
265
338
  }
266
339
 
267
- export async function GET() {
340
+ /**
341
+ * Where Resume would carry on with one of your connections, for their profile
342
+ * card: { nextPage, pagesRead, legacy }, or null when there is nothing to carry
343
+ * on with (never mapped, read to the end, hidden). The Paused list's own rules
344
+ * (lib/paused.js), so the card and the Scan page can't disagree.
345
+ */
346
+ function resumePoint(id) {
347
+ try {
348
+ const me = resolveProfile({ create: false });
349
+ if (!me) return null;
350
+ const db = getDb();
351
+ const row = db.prepare(
352
+ `SELECT id, name, tier, profile_url, connected_date, created_at
353
+ FROM linkedin_connections WHERE id = ? AND user_id = ? AND degree = 1`,
354
+ ).get(String(id ?? ''), me.id);
355
+ if (!row) return null;
356
+ const mapped = db.prepare(
357
+ 'SELECT 1 FROM linkedin_connections WHERE user_id = ? AND degree = 2 AND source_connection_id = ? LIMIT 1',
358
+ ).get(me.id, row.id);
359
+ const [p] = pausedList([row], new Set(mapped ? [row.id] : []), readProgress(dataDir(), me.id), readUnclear(dataDir()));
360
+ return p ? { nextPage: p.nextPage, pagesRead: p.pagesRead, legacy: p.legacy } : null;
361
+ } catch {
362
+ return null;
363
+ }
364
+ }
365
+
366
+ export async function GET(request) {
367
+ const q = new URL(request?.url || 'http://127.0.0.1/api/scraper').searchParams;
368
+ if (q.has('job')) return Response.json(job());
369
+ if (q.has('resume')) return Response.json({ resume: resumePoint(q.get('resume')) });
370
+ if (q.has('reach')) return Response.json(scanNotes());
371
+ if (q.has('person')) return Response.json(pickedPerson(q.get('person')));
268
372
  return Response.json(await status());
269
373
  }
270
374
 
@@ -283,7 +387,10 @@ const ACTIONS = {
283
387
  // Hidden profiles are remembered so they are not retried forever; this is
284
388
  // the way back in without a terminal.
285
389
  'auto-bridge-retry': { flag: '--auto-bridge --retry-private', label: 'Mapping every bridge, hidden ones included' },
286
- bridge: { flag: '--bridge', needsName: true, label: 'Mapping the circle behind' },
390
+ // One person's circle, from page 1. By profile URL when the page sends their
391
+ // id, as every Scan button does: two connections can share a name, and by name
392
+ // the scanner reads whichever was saved first. By name for a caller with no id.
393
+ bridge: { flag: '--bridge', needsName: true, byId: true, label: 'Mapping the circle behind' },
287
394
  // Carry on with one person whose read was cut short. By profile URL, not name:
288
395
  // two connections can share a name, and Resume must reach the one clicked.
289
396
  resume: { flag: '--bridge-url', needsId: true, label: 'Carrying on with', searches: true },
@@ -291,6 +398,9 @@ const ACTIONS = {
291
398
  'resume-all': { flag: '--auto-bridge --only-unfinished', label: 'Carrying on with every paused list', searches: true },
292
399
  rescrape: { flag: '--rescrape', needsName: true, label: 'Re-mapping the circle behind' },
293
400
  company: { flag: '--company', needsName: true, label: 'Scanning' },
401
+ // Photos an older version kept as links to LinkedIn: saved here, once each.
402
+ // No browser, no search; every scan does the same at its end.
403
+ photos: { flag: '--save-photos', label: 'Saving profile photos to this computer' },
294
404
  };
295
405
 
296
406
  /** Only a plain linkedin.com/in/ profile URL becomes an argument. */
@@ -402,7 +512,7 @@ export async function POST(request) {
402
512
 
403
513
  // The two settings a person changes here. Neither starts anything.
404
514
  if (action === 'set-limits') {
405
- const limits = writeLimits(dataDir(), { daily: body.daily, monthly: body.monthly });
515
+ const limits = writeLimits(dataDir(), { daily: body.daily, monthly: body.monthly, profiles: body.profiles });
406
516
  return Response.json({ ok: true, limits });
407
517
  }
408
518
  if (action === 'lift-cooldown') {
@@ -431,24 +541,34 @@ export async function POST(request) {
431
541
  const readsCircles = action.startsWith('auto-bridge') || ['bridge', 'rescrape', 'resume', 'resume-all'].includes(action);
432
542
  // Carry on with people already mapped, from the page each one stopped at.
433
543
  const deeper = body.deeper === true && (action.startsWith('auto-bridge') || action === 'bridge');
544
+ // Resume, and a circle scan of someone picked by id, go by the connection's
545
+ // profile URL rather than a name.
546
+ const byId = spec.needsId || (spec.byId && Boolean(body.id));
434
547
  let name = null;
435
- if (spec.needsName) {
548
+ if (spec.needsName && !byId) {
436
549
  name = cleanName(body.name);
437
550
  if (!name) return Response.json({ error: 'A name is required for this action.' }, { status: 400 });
438
551
  }
439
- // Resume sends the connection's id; the URL is looked up here, for this
440
- // profile, so nothing from the request itself reaches the command line.
552
+ // The id comes from the page; the URL is looked up here, for this profile, so
553
+ // nothing from the request itself reaches the command line.
441
554
  let profileUrl = null;
442
- if (spec.needsId) {
555
+ let person = null;
556
+ if (byId) {
443
557
  try {
444
558
  const me = resolveProfile({ create: false });
445
- const row = me && getDb().prepare(
446
- 'SELECT profile_url FROM linkedin_connections WHERE id = ? AND user_id = ? AND degree = 1',
559
+ person = me && getDb().prepare(
560
+ 'SELECT name, profile_url FROM linkedin_connections WHERE id = ? AND user_id = ? AND degree = 1',
447
561
  ).get(String(body.id ?? ''), me.id);
448
- profileUrl = row?.profile_url && cleanProfileUrl(row.profile_url);
562
+ profileUrl = person?.profile_url && cleanProfileUrl(person.profile_url);
449
563
  } catch { profileUrl = null; }
450
564
  if (!profileUrl) return Response.json({ error: 'That connection could not be found.' }, { status: 400 });
451
565
  }
566
+ // Who the job is of, so every Scan button can say whose scan is running and a
567
+ // profile card can show its own person's progress. Only ever shown: the id
568
+ // never reaches the command line (a name or the URL looked up for it does),
569
+ // and anything not shaped like one of our ids is dropped.
570
+ const hintId = typeof body.id === 'string' && /^[\w-]{1,64}$/.test(body.id) ? body.id : null;
571
+ const target = name || profileUrl ? { id: hintId, name: name || person?.name || null } : null;
452
572
  // Nothing that searches LinkedIn starts during a cooldown; the scanner checks
453
573
  // too, this just says so before a process is spawned.
454
574
  // The 1st-degree scans aren't searches, but they open LinkedIn with automation too.
@@ -497,8 +617,11 @@ export async function POST(request) {
497
617
  // app is inside the signed app (lib/scanner-python.js scannerCommand).
498
618
  plan = [scannerCommand(found.run, path.join(root, 'scripts', 'scrape.py'), [
499
619
  // `--flag=value` is one token on purpose: a name beginning with
500
- // "-" can then never be read as a flag of its own.
501
- ...(name ? [`${spec.flag}=${name}`] : profileUrl ? [`${spec.flag}=${profileUrl}`] : spec.flag.split(' ')),
620
+ // "-" can then never be read as a flag of its own. Anyone found by
621
+ // id is --bridge-url, which carries on where their last read stopped
622
+ // unless told to start at page 1.
623
+ ...(name ? [`${spec.flag}=${name}`] : profileUrl ? [`--bridge-url=${profileUrl}`] : spec.flag.split(' ')),
624
+ ...(profileUrl && action === 'bridge' && !deeper ? ['--from-start'] : []),
502
625
  ...(maxBridges && (action.startsWith('auto-bridge') || action === 'resume-all') ? [`--max-bridges=${maxBridges}`] : []),
503
626
  ...(tiers.length && action.startsWith('auto-bridge') ? [`--tiers=${tiers.join(',')}`] : []),
504
627
  ...(action.startsWith('auto-bridge') ? [`--order=${order}`] : []),
@@ -517,9 +640,10 @@ export async function POST(request) {
517
640
  state.running = true;
518
641
  state.stopping = false;
519
642
  state.action = action;
643
+ state.target = target;
520
644
  state.exitCode = null;
521
645
  state.startedAt = Date.now();
522
- state.log = [spec.label + (name ? ` ${name}…` : '…')];
646
+ state.log = [spec.label + (target?.name ? ` ${target.name}…` : '…')];
523
647
  state.stderrTail = [];
524
648
  state.failure = null;
525
649
  forgetChecks();
@@ -560,6 +684,9 @@ export async function POST(request) {
560
684
  state.child = null;
561
685
  state.abort = null;
562
686
  state.exitCode = code;
687
+ state.recent = [{
688
+ action: state.action, target: state.target, startedAt: state.startedAt, exitCode: code, failure: state.failure,
689
+ }, ...state.recent].slice(0, 5);
563
690
  // A setup's private folder (the download and what was unpacked from it).
564
691
  if (cleanup) {
565
692
  try { cleanup(); } catch { /* swept at the next setup (sweepSetupLeftovers) */ }
@@ -625,5 +752,7 @@ export async function POST(request) {
625
752
 
626
753
  runStep(0);
627
754
 
628
- return Response.json({ ok: true, action });
755
+ // Which job this is, so the page that started it can tell its end from the
756
+ // end of whatever runs next (lib/scraper-client.js runScrape).
757
+ return Response.json({ ok: true, action, startedAt: state.startedAt });
629
758
  }
@@ -1,8 +1,8 @@
1
1
  import { getDb } from '../../../../lib/db-client';
2
- import { companyOverrides, readForScoring, scoringRows, sectorFocusOf } from '../../../../lib/rpc';
2
+ import { companyOverrides, readForScoring, scoringRows, sectorFocusOf, tierScaleOf } from '../../../../lib/rpc';
3
3
  import { parseSectorFocus, previewSectorFocus } from '../../../../lib/sector-focus';
4
4
 
5
- // Settings → Your sector, before you save: what a sector focus would change,
5
+ // Scores → Your sector, before you save: what a sector focus would change,
6
6
  // against the one saved now. It reads the network once and scores it twice in
7
7
  // memory, with the inputs rescoreAll() uses (readForScoring: each company's
8
8
  // industry and its sectors from the directory), and writes nothing. A save
@@ -31,6 +31,7 @@ export async function POST(request) {
31
31
  read: readForScoring(rows),
32
32
  from: sectorFocusOf(db),
33
33
  to,
34
+ tierScale: tierScaleOf(db),
34
35
  }));
35
36
  } catch (err) {
36
37
  return Response.json({ error: err.message }, { status: 500 });
@@ -2,7 +2,7 @@ import { getDb } from '../../../../lib/db-client';
2
2
  import { readForScoring, scoringRows } from '../../../../lib/rpc';
3
3
  import { suggestSectors } from '../../../../lib/sector-directory';
4
4
 
5
- // Settings → Your sector: "Suggested from your network". The directory's
5
+ // Scores → Your sector: "Suggested from your network". The directory's
6
6
  // sectors most of your scanned people work in, counted here from the rows
7
7
  // with the same read rescoring uses, so a suggestion's companies are the ones
8
8
  // picking it would lean. Its own read-only GET, not part of GET
@@ -1,4 +1,6 @@
1
1
  import { db as supabase } from '../../../lib/db';
2
+ import { getDb } from '../../../lib/db-client';
3
+ import { localPhoto, waitingPhotos } from '../../../lib/photos';
2
4
 
3
5
  const CORS_HEADERS = {
4
6
  'Access-Control-Allow-Origin': '*',
@@ -10,29 +12,61 @@ export async function OPTIONS() {
10
12
  return new Response(null, { status: 204, headers: CORS_HEADERS });
11
13
  }
12
14
 
15
+ // The photos still kept as links, for the scanner to save: at the end of a
16
+ // scan, and for Save photos on the Scan page (scrape.py save_waiting_photos).
17
+ // A read, so no CORS header: another site's page can't see the answer.
18
+ export async function GET() {
19
+ try {
20
+ return Response.json({ waiting: waitingPhotos(getDb()) });
21
+ } catch (err) {
22
+ return Response.json({ error: err.message }, { status: 500 });
23
+ }
24
+ }
25
+
26
+ // `images`: [{ profileUrl, imageUrl }], each a photo the scanner has saved,
27
+ // attached to every row of that person. Only a saved file's path is stored,
28
+ // never a link (lib/photos.js).
29
+ //
30
+ // `forget`: [{ profileUrl, imageUrl }], links the scanner got a definite no for
31
+ // (expired, not LinkedIn's, not a picture, or the same picture as someone
32
+ // else's), never one it couldn't reach. The link is cleared, and only where it
33
+ // is still that link, so each is tried once and the person shows initials.
13
34
  export async function POST(request) {
14
35
  try {
15
- const { images } = await request.json();
36
+ const { images, forget } = await request.json();
16
37
 
17
- if (!images || !Array.isArray(images)) {
38
+ if ((!images || !Array.isArray(images)) && !Array.isArray(forget)) {
18
39
  return Response.json({ error: 'Missing images array' }, { status: 400, headers: CORS_HEADERS });
19
40
  }
20
41
 
21
42
  let updated = 0;
22
- for (let i = 0; i < images.length; i += 20) {
23
- const batch = images.slice(i, i + 20);
43
+ const saved = Array.isArray(images) ? images : [];
44
+ for (let i = 0; i < saved.length; i += 20) {
45
+ const batch = saved.slice(i, i + 20);
24
46
  const promises = batch.map(({ profileUrl, imageUrl }) => {
25
- if (!profileUrl || !imageUrl) return Promise.resolve();
47
+ const photo = localPhoto(imageUrl);
48
+ if (!profileUrl || !photo) return Promise.resolve();
26
49
  return supabase
27
50
  .from('linkedin_connections')
28
- .update({ profile_image_url: imageUrl })
51
+ .update({ profile_image_url: photo })
29
52
  .eq('profile_url', profileUrl)
30
53
  .then(({ error }) => { if (!error) updated++; });
31
54
  });
32
55
  await Promise.all(promises);
33
56
  }
34
57
 
35
- return Response.json({ success: true, updated }, { headers: CORS_HEADERS });
58
+ let forgotten = 0;
59
+ for (const { profileUrl, imageUrl } of Array.isArray(forget) ? forget : []) {
60
+ if (typeof profileUrl !== 'string' || typeof imageUrl !== 'string' || !imageUrl || localPhoto(imageUrl)) continue;
61
+ const { data, error } = await supabase
62
+ .from('linkedin_connections')
63
+ .update({ profile_image_url: null })
64
+ .eq('profile_url', profileUrl)
65
+ .eq('profile_image_url', imageUrl);
66
+ if (!error && data?.length) forgotten++;
67
+ }
68
+
69
+ return Response.json({ success: true, updated, forgotten }, { headers: CORS_HEADERS });
36
70
  } catch (err) {
37
71
  return Response.json({ error: err.message }, { status: 500, headers: CORS_HEADERS });
38
72
  }
@@ -1,12 +1,14 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { dataDir } from '../../../lib/db-client';
4
- import { AVATAR_FILE } from '../../../lib/data-folder';
4
+ import { AVATAR_FILE } from '../../../lib/photos';
5
5
 
6
6
  // Avatars are captured by the scraper and live in the user's data directory,
7
7
  // never inside the app package — an installed copy must not write into its own
8
8
  // files, and real faces must never end up in the repo. Rows store the path
9
9
  // `/avatars/<name>.webp`, so this route resolves that to ~/.six-degrees/avatars.
10
+ // It is the only place a page gets a photo from (lib/photos.js): reading one
11
+ // never goes online.
10
12
 
11
13
  export async function GET(_request, { params }) {
12
14
  const { file } = await params;
@@ -26,6 +28,9 @@ export async function GET(_request, { params }) {
26
28
  'Content-Type': type,
27
29
  // Content-addressed filenames, so they can be cached indefinitely.
28
30
  'Cache-Control': 'public, max-age=31536000, immutable',
31
+ // A photo that came in with an import is only as good as its name: never
32
+ // let the browser read one as anything but the image type it claims.
33
+ 'X-Content-Type-Options': 'nosniff',
29
34
  },
30
35
  });
31
36
  } catch {
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { initialsFor } from '../../lib/tiers';
4
+ import { localPhoto } from '../../lib/photos';
4
5
 
5
6
  const FALLBACK = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
6
7
 
@@ -9,8 +10,10 @@ const FALLBACK = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#
9
10
  // Most people have no photo, so the initials disc is the common case, not the
10
11
  // fallback — and it sits UNDER the photo, so a photo that fails to load just
11
12
  // reveals it. No state, and no reaching into a sibling node to un-hide it.
13
+ // Only a photo saved on this computer is drawn (lib/photos.js).
12
14
  export default function Avatar({ person, size = 32, tierColors = FALLBACK }) {
13
15
  const c = tierColors[person?.tier] || '#666';
16
+ const photo = localPhoto(person?.profile_image_url);
14
17
  return (
15
18
  <span style={{
16
19
  position: 'relative', display: 'inline-block', width: size, height: size, flexShrink: 0,
@@ -20,9 +23,9 @@ export default function Avatar({ person, size = 32, tierColors = FALLBACK }) {
20
23
  position: 'absolute', inset: 0, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
21
24
  background: `${c}2e`, color: c, fontWeight: 800, fontSize: Math.max(7, Math.round(size * 0.36)), lineHeight: 1,
22
25
  }}>{initialsFor(person?.name)}</span>
23
- {person?.profile_image_url && (
26
+ {photo && (
24
27
  <img
25
- src={person.profile_image_url}
28
+ src={photo}
26
29
  alt=""
27
30
  loading="lazy"
28
31
  decoding="async"