six-degrees 0.4.0-beta.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (389) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +14 -14
  4. package/.next/standalone/.next/required-server-files.json +6 -1
  5. package/.next/standalone/.next/routes-manifest.json +12 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  9. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.rsc +12 -12
  16. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
  18. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  22. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  24. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  26. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  30. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  47. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  54. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  59. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  61. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
  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 +14 -14
  69. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
  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 +14 -14
  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 +14 -14
  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 +14 -14
  81. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
  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 +14 -14
  90. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
  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 +14 -14
  97. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
  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 +14 -14
  104. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
  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.js.nft.json +1 -1
  108. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  109. package/.next/standalone/.next/server/app/scores.html +1 -1
  110. package/.next/standalone/.next/server/app/scores.rsc +14 -14
  111. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
  112. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  113. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +12 -12
  114. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  115. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  116. package/.next/standalone/.next/server/app/settings.html +1 -1
  117. package/.next/standalone/.next/server/app/settings.rsc +14 -14
  118. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  119. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  120. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
  121. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  122. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  123. package/.next/standalone/.next/server/app/setup.html +1 -1
  124. package/.next/standalone/.next/server/app/setup.rsc +14 -14
  125. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
  126. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  127. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
  128. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0rc27za._.js → [root-of-the-server]__0bat8nx._.js} +2 -2
  132. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__20tmem7._.js} +2 -2
  156. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -6
  157. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  159. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +26 -8
  162. package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
  163. package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
  164. package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
  165. package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
  166. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  167. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
  168. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
  169. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
  170. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
  172. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
  173. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
  174. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
  175. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
  176. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +1 -1
  177. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +1 -1
  178. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +1 -1
  179. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
  180. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +1 -1
  181. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +1 -1
  182. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +1 -1
  184. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +1 -1
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +1 -1
  187. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
  190. package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
  192. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +1 -1
  193. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/{_0cu6-an._.js → _1vpfyd-._.js} +2 -2
  195. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
  196. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
  197. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  198. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
  199. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  200. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  201. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  202. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  203. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +6 -2
  204. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +1 -1
  205. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  206. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  207. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  208. package/.next/standalone/.next/server/pages/404.html +1 -1
  209. package/.next/standalone/.next/server/pages/500.html +1 -1
  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/0ax_9zzboyv6d.js +44 -0
  213. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  214. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
  215. package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  216. package/.next/standalone/.next/static/chunks/10784r7w7t5gw.js +18 -0
  217. package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  218. package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
  219. package/.next/standalone/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
  220. package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  221. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  222. package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  223. package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  224. package/.next/standalone/.next/static/chunks/3ol1xux1-ofm4.js +6 -0
  225. package/.next/standalone/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
  226. package/.next/standalone/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  227. package/.next/standalone/CHANGELOG.md +211 -0
  228. package/.next/standalone/CONTRIBUTING.md +15 -4
  229. package/.next/standalone/README.md +21 -15
  230. package/.next/standalone/SECURITY.md +10 -0
  231. package/.next/standalone/app/api/admin-delete/route.js +3 -0
  232. package/.next/standalone/app/api/ingest/route.js +19 -1
  233. package/.next/standalone/app/api/scraper/route.js +79 -11
  234. package/.next/standalone/app/api/update-images/route.js +41 -7
  235. package/.next/standalone/app/avatars/[file]/route.js +6 -1
  236. package/.next/standalone/app/components/Avatar.js +5 -2
  237. package/.next/standalone/app/components/ChainView.js +713 -469
  238. package/.next/standalone/app/components/EmptyState.js +1 -1
  239. package/.next/standalone/app/components/FieldStep.js +94 -0
  240. package/.next/standalone/app/components/FilterPanel.js +37 -1
  241. package/.next/standalone/app/components/ForceGraph.js +228 -774
  242. package/.next/standalone/app/components/GridView.js +9 -7
  243. package/.next/standalone/app/components/LinkedInLimits.js +40 -8
  244. package/.next/standalone/app/components/ListView.js +6 -4
  245. package/.next/standalone/app/components/MappingProgress.js +5 -6
  246. package/.next/standalone/app/components/OnboardingGate.js +1 -1
  247. package/.next/standalone/app/components/OrbitGraph.js +3 -1
  248. package/.next/standalone/app/components/OutlinkQuest.js +5 -4
  249. package/.next/standalone/app/components/RingsView.js +9 -8
  250. package/.next/standalone/app/components/Sidebar.js +209 -112
  251. package/.next/standalone/app/components/TheirCircle.js +18 -11
  252. package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
  253. package/.next/standalone/app/components/settings/SectorSection.js +17 -191
  254. package/.next/standalone/app/components/settings/TierSection.js +7 -12
  255. package/.next/standalone/app/components/views.js +6 -4
  256. package/.next/standalone/app/favicon.ico +0 -0
  257. package/.next/standalone/app/globals.css +7 -0
  258. package/.next/standalone/app/launch/page.js +2 -2
  259. package/.next/standalone/app/page.js +153 -31
  260. package/.next/standalone/app/paths/page.js +11 -3
  261. package/.next/standalone/app/queue/page.js +18 -10
  262. package/.next/standalone/app/setup/page.js +423 -260
  263. package/.next/standalone/db/schema.js +15 -1
  264. package/.next/standalone/eslint.config.mjs +3 -3
  265. package/.next/standalone/install.sh +3 -3
  266. package/.next/standalone/lib/brokerage.js +94 -0
  267. package/.next/standalone/lib/chain-layout.js +228 -0
  268. package/.next/standalone/lib/circle.js +23 -4
  269. package/.next/standalone/lib/csv.js +4 -6
  270. package/.next/standalone/lib/data-export.js +2 -1
  271. package/.next/standalone/lib/data-folder.js +4 -2
  272. package/.next/standalone/lib/degrees.js +30 -0
  273. package/.next/standalone/lib/galaxy.js +12 -0
  274. package/.next/standalone/lib/ingest.js +18 -11
  275. package/.next/standalone/lib/linkedin-limits.js +35 -8
  276. package/.next/standalone/lib/photos.js +47 -0
  277. package/.next/standalone/lib/quest.js +8 -3
  278. package/.next/standalone/lib/reach.js +113 -0
  279. package/.next/standalone/lib/scan-state.js +1 -0
  280. package/.next/standalone/lib/scanner-setup.js +20 -1
  281. package/.next/standalone/lib/scoring.js +769 -18
  282. package/.next/standalone/lib/scraper-client.js +27 -0
  283. package/.next/standalone/lib/search-risk.js +38 -0
  284. package/.next/standalone/lib/sector-focus.js +13 -0
  285. package/.next/standalone/lib/separation.js +4 -2
  286. package/.next/standalone/lib/settings-client.js +21 -0
  287. package/.next/standalone/lib/settings.js +3 -1
  288. package/.next/standalone/lib/ties.js +23 -0
  289. package/.next/standalone/next.config.mjs +25 -3
  290. package/.next/standalone/package-lock.json +2 -2
  291. package/.next/standalone/package.json +1 -1
  292. package/.next/standalone/scripts/build-app.mjs +11 -2
  293. package/.next/standalone/scripts/check-node.mjs +1 -1
  294. package/.next/standalone/scripts/image_store.py +77 -21
  295. package/.next/standalone/scripts/scrape.py +271 -29
  296. package/.next/standalone/server.js +1 -1
  297. package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  298. package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  299. package/.next/static/chunks/0l1568qff0efj.js +1 -0
  300. package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  301. package/.next/static/chunks/10784r7w7t5gw.js +18 -0
  302. package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  303. package/.next/static/chunks/28bu26ausqzmn.js +1 -0
  304. package/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
  305. package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  306. package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  307. package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  308. package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  309. package/.next/static/chunks/3ol1xux1-ofm4.js +6 -0
  310. package/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
  311. package/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  312. package/CHANGELOG.md +211 -0
  313. package/README.md +21 -15
  314. package/SECURITY.md +10 -0
  315. package/db/schema.js +15 -1
  316. package/lib/brokerage.js +94 -0
  317. package/lib/chain-layout.js +228 -0
  318. package/lib/circle.js +23 -4
  319. package/lib/csv.js +4 -6
  320. package/lib/data-export.js +2 -1
  321. package/lib/data-folder.js +4 -2
  322. package/lib/degrees.js +30 -0
  323. package/lib/galaxy.js +12 -0
  324. package/lib/ingest.js +18 -11
  325. package/lib/linkedin-limits.js +35 -8
  326. package/lib/photos.js +47 -0
  327. package/lib/quest.js +8 -3
  328. package/lib/reach.js +113 -0
  329. package/lib/scan-state.js +1 -0
  330. package/lib/scanner-setup.js +20 -1
  331. package/lib/scoring.js +769 -18
  332. package/lib/scraper-client.js +27 -0
  333. package/lib/search-risk.js +38 -0
  334. package/lib/sector-focus.js +13 -0
  335. package/lib/separation.js +4 -2
  336. package/lib/settings-client.js +21 -0
  337. package/lib/settings.js +3 -1
  338. package/lib/ties.js +23 -0
  339. package/package.json +1 -1
  340. package/scripts/check-node.mjs +1 -1
  341. package/scripts/image_store.py +77 -21
  342. package/scripts/scrape.py +271 -29
  343. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
  344. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
  345. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
  346. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
  347. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
  348. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
  349. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
  350. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
  351. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
  352. package/.next/standalone/.next/server/chunks/ssr/_0223v9s._.js +0 -3
  353. package/.next/standalone/.next/server/chunks/ssr/_0bcf13g._.js +0 -3
  354. package/.next/standalone/.next/server/chunks/ssr/_0wxbq78._.js +0 -3
  355. package/.next/standalone/.next/server/chunks/ssr/_0z88zcl._.js +0 -3
  356. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
  357. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
  358. package/.next/standalone/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  359. package/.next/standalone/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  360. package/.next/standalone/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  361. package/.next/standalone/.next/static/chunks/1d5hl1io798n2.js +0 -1
  362. package/.next/standalone/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  363. package/.next/standalone/.next/static/chunks/1roznk7hkjes1.js +0 -44
  364. package/.next/standalone/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  365. package/.next/standalone/.next/static/chunks/36gbwek52x_w-.js +0 -2
  366. package/.next/standalone/.next/static/chunks/3a55suosthntc.js +0 -1
  367. package/.next/standalone/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  368. package/.next/standalone/.next/static/chunks/3nbnio971zlji.js +0 -2
  369. package/.next/standalone/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  370. package/.next/standalone/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  371. package/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  372. package/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  373. package/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  374. package/.next/static/chunks/1d5hl1io798n2.js +0 -1
  375. package/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  376. package/.next/static/chunks/1roznk7hkjes1.js +0 -44
  377. package/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  378. package/.next/static/chunks/36gbwek52x_w-.js +0 -2
  379. package/.next/static/chunks/3a55suosthntc.js +0 -1
  380. package/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  381. package/.next/static/chunks/3nbnio971zlji.js +0 -2
  382. package/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  383. package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  384. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_buildManifest.js +0 -0
  385. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_clientMiddlewareManifest.js +0 -0
  386. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_ssgManifest.js +0 -0
  387. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_buildManifest.js +0 -0
  388. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_clientMiddlewareManifest.js +0 -0
  389. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_ssgManifest.js +0 -0
@@ -0,0 +1,228 @@
1
+ // Where Bridge Chains puts a circle's dots.
2
+ //
3
+ // Blake, 2026-09-28: "when i scan someone and have a lot of 2nd degree its
4
+ // almost a solid line and needs to expand more". Every dot sat on one ring, so
5
+ // 200 people round one circle were a band and 800 were a solid line. Here a
6
+ // circle fills rings from the inside out instead, as many as its count needs:
7
+ // a small circle is one ring, a big one spreads into more, and neighbours keep
8
+ // their distance. Plain maths, no React; tests/chain-layout.test.mjs.
9
+
10
+ const TAU = Math.PI * 2;
11
+
12
+ /** How many dots fit on a ring of `radius` across `sweep` radians, `spacing` apart. */
13
+ function capacity(radius, spacing, sweep) {
14
+ if (radius <= 0) return 1;
15
+ const arc = sweep * radius;
16
+ // A whole circle closes on itself; an arc has a dot at each end.
17
+ return Math.max(1, sweep >= TAU - 1e-9 ? Math.floor(arc / spacing) : Math.floor(arc / spacing) + 1);
18
+ }
19
+
20
+ /** The rings from `inner` outwards, `spacing` apart, until `n` dots fit. */
21
+ function ringsFor(n, inner, spacing, sweep) {
22
+ const radii = [];
23
+ let room = 0;
24
+ for (let r = inner; room < n; r += spacing) {
25
+ radii.push(r);
26
+ room += capacity(r, spacing, sweep);
27
+ }
28
+ return radii;
29
+ }
30
+
31
+ const outerOf = (n, inner, spacing, sweep) => {
32
+ const radii = ringsFor(n, inner, spacing, sweep);
33
+ return radii[radii.length - 1];
34
+ };
35
+
36
+ /**
37
+ * Places `n` dots in rings round a centre at (0, 0).
38
+ *
39
+ * The first ring sits at `inner`, and each next one `spacing` further out.
40
+ * When that would run past `outer`, the rings first start further in (down to
41
+ * `innerMin`), then close up (down to `minSpacing`); past that they carry on
42
+ * beyond `outer` rather than overlap, and zooming out shows them. The dots on
43
+ * each ring share it evenly, in proportion to its length, so every ring is
44
+ * about as full as the rest; the first `n` of a list sorted best-first land
45
+ * nearest the centre. Every other ring is turned by half a step, so dots on
46
+ * neighbouring rings don't line up in spokes.
47
+ *
48
+ * `start` and `sweep` fill an arc instead of the whole circle, each ring from
49
+ * its middle outwards.
50
+ *
51
+ * @returns {{ spacing: number, inner: number, rings: { radius: number, count: number }[],
52
+ * points: { x: number, y: number, angle: number, ring: number }[] }}
53
+ */
54
+ export function ringLayout(n, {
55
+ inner, outer, spacing, minSpacing = spacing / 3, innerMin = inner,
56
+ start = -Math.PI / 2, sweep = TAU,
57
+ } = {}) {
58
+ const count = Math.max(0, Math.floor(n) || 0);
59
+ if (!count) return { spacing, inner, rings: [], points: [] };
60
+
61
+ let s = spacing;
62
+ let r0 = inner;
63
+ if (outerOf(count, r0, s, sweep) > outer) {
64
+ // Start further in, as far as innerMin, before closing anything up.
65
+ if (outerOf(count, innerMin, s, sweep) <= outer) {
66
+ let lo = innerMin, hi = inner;
67
+ for (let i = 0; i < 24; i++) {
68
+ const mid = (lo + hi) / 2;
69
+ if (outerOf(count, mid, s, sweep) <= outer) lo = mid; else hi = mid;
70
+ }
71
+ r0 = lo;
72
+ } else {
73
+ r0 = innerMin;
74
+ let lo = minSpacing, hi = spacing;
75
+ if (outerOf(count, r0, lo, sweep) <= outer) {
76
+ for (let i = 0; i < 24; i++) {
77
+ const mid = (lo + hi) / 2;
78
+ if (outerOf(count, r0, mid, sweep) <= outer) lo = mid; else hi = mid;
79
+ }
80
+ }
81
+ s = lo;
82
+ }
83
+ }
84
+
85
+ const radii = ringsFor(count, r0, s, sweep);
86
+ const caps = radii.map((r) => capacity(r, s, sweep));
87
+ const room = caps.reduce((a, b) => a + b, 0);
88
+
89
+ // Share the dots in proportion to each ring's room, never over it; what the
90
+ // rounding leaves goes to the rings with the most room left.
91
+ const counts = caps.map((c) => Math.min(c, Math.floor((count * c) / room)));
92
+ let left = count - counts.reduce((a, b) => a + b, 0);
93
+ while (left > 0) {
94
+ let best = -1;
95
+ for (let k = 0; k < caps.length; k++) {
96
+ if (counts[k] < caps[k] && (best < 0 || caps[k] - counts[k] > caps[best] - counts[best])) best = k;
97
+ }
98
+ counts[best]++;
99
+ left--;
100
+ }
101
+
102
+ const whole = sweep >= TAU - 1e-9;
103
+ const points = [];
104
+ const rings = [];
105
+ radii.forEach((radius, k) => {
106
+ const m = counts[k];
107
+ if (!m) return;
108
+ rings.push({ radius, count: m });
109
+ // Round a whole circle evenly; across an arc from one edge to the other, a
110
+ // lone dot in the middle of it. An arc fills from its middle outwards, so
111
+ // the first in the list sit nearest whatever the arc fans out from.
112
+ const step = whole ? TAU / m : m > 1 ? sweep / (m - 1) : 0;
113
+ const from = whole ? start + (k % 2 ? step / 2 : 0) : m > 1 ? start : start + sweep / 2;
114
+ const slots = whole ? null : middleOut(m);
115
+ for (let j = 0; j < m; j++) {
116
+ const angle = from + (slots ? slots[j] : j) * step;
117
+ points.push({ x: radius * Math.cos(angle), y: radius * Math.sin(angle), angle, ring: rings.length - 1 });
118
+ }
119
+ });
120
+ return { spacing: s, inner: r0, rings, points };
121
+ }
122
+
123
+ /** 0 … m-1 from the middle outwards: for 5, 2 3 1 4 0. */
124
+ function middleOut(m) {
125
+ let lo = Math.floor((m - 1) / 2);
126
+ let hi = lo + 1;
127
+ const out = [lo--];
128
+ while (out.length < m) {
129
+ if (hi < m) out.push(hi++);
130
+ if (out.length < m && lo >= 0) out.push(lo--);
131
+ }
132
+ return out;
133
+ }
134
+
135
+ /** A dot's radius for a layout's spacing: bigger for higher tiers, never touching a neighbour. */
136
+ export function dotRadius(spacing, tier) {
137
+ const scale = tier === 'S' ? 1 : tier === 'A' ? 0.86 : tier === 'B' ? 0.72 : 0.6;
138
+ return Math.max(1.4, Math.min(7, spacing * 0.42 * scale));
139
+ }
140
+
141
+ /**
142
+ * Where a bridge's circle is previewed on hover: a band beyond every ring of
143
+ * bridges. With many bridges the overview grows extra rings, past where the
144
+ * preview used to start, and it drew over them. `clear` leaves room for a
145
+ * hovered bridge's dot (20) and its name under it.
146
+ */
147
+ export function previewBand(maxR, bridgeRings = [], clear = 34) {
148
+ const edge = Math.max(0, ...bridgeRings.map((r) => r.radius));
149
+ const inner = Math.max(maxR * 0.66, edge ? edge + clear : 0);
150
+ return { inner, outer: Math.max(maxR * 0.8, inner + maxR * 0.14) };
151
+ }
152
+
153
+ const TIER_BANDS = ['S', 'A', 'B', 'C', 'D'];
154
+
155
+ /**
156
+ * An opened circle's people in tier bands round the person: S nearest, then A,
157
+ * B, C and D, with a gap between bands like Network Circle's tier orbits, so a
158
+ * big circle reads as layers instead of one clump. Points come back in the
159
+ * order of `rows`, each with `slot`: the angle its ring gives each dot. The
160
+ * spacing closes up, down to `minSpacing`, until every band fits inside
161
+ * `outer`; past that the bands carry on outwards and zooming out shows them.
162
+ *
163
+ * @returns {{ spacing: number, edge: number, bands: { tier: string, inner: number, outer: number }[],
164
+ * points: { x: number, y: number, angle: number, slot: number }[] }}
165
+ */
166
+ export function tierBandLayout(rows, { inner, outer, spacing, minSpacing = spacing / 3, gap = spacing }) {
167
+ const groups = TIER_BANDS.map((tier) => ({ tier, idx: [] }));
168
+ rows.forEach((row, i) => {
169
+ const t = TIER_BANDS.indexOf(row?.tier);
170
+ groups[t < 0 ? 4 : t].idx.push(i); // no tier: with D
171
+ });
172
+ const used = groups.filter((g) => g.idx.length);
173
+ const place = (s) => {
174
+ const points = new Array(rows.length);
175
+ const bands = [];
176
+ let r = inner;
177
+ for (const g of used) {
178
+ const l = ringLayout(g.idx.length, { inner: r, outer: Infinity, spacing: s, minSpacing: s });
179
+ g.idx.forEach((rowIndex, j) => {
180
+ const p = l.points[j];
181
+ points[rowIndex] = { x: p.x, y: p.y, angle: p.angle, slot: TAU / l.rings[p.ring].count };
182
+ });
183
+ const edge = Math.max(r, ...l.rings.map((x) => x.radius));
184
+ bands.push({ tier: g.tier, inner: r, outer: edge });
185
+ r = edge + gap * (s / spacing);
186
+ }
187
+ return { spacing: s, points, bands, edge: bands.length ? bands[bands.length - 1].outer : inner };
188
+ };
189
+ let best = place(spacing);
190
+ if (best.edge <= outer) return best;
191
+ let lo = minSpacing;
192
+ let hi = spacing;
193
+ best = place(lo);
194
+ if (best.edge > outer) return best;
195
+ for (let i = 0; i < 20; i++) {
196
+ const mid = (lo + hi) / 2;
197
+ const tryMid = place(mid);
198
+ if (tryMid.edge <= outer) { lo = mid; best = tryMid; } else hi = mid;
199
+ }
200
+ return best;
201
+ }
202
+
203
+ /**
204
+ * The people behind each person in an opened circle: their own circle, drawn
205
+ * as a fan of small dots beyond `from`, centred on that person's angle and no
206
+ * wider than their slot (at most a sixth of the circle), so each fan sits
207
+ * behind its own person. At most `cap` dots a fan and about `total` in all, so
208
+ * a big network stays quick; `more` is how many weren't drawn.
209
+ *
210
+ * @param {{ angle: number, slot: number, count: number }[]} anchors
211
+ * @returns {({ points: { x: number, y: number }[], more: number } | null)[]}
212
+ */
213
+ export function outerFans(anchors, { from, spacing = 7, cap = 40, total = 1200 } = {}) {
214
+ const fans = anchors.filter((a) => a?.count > 0).length;
215
+ const each = Math.max(6, Math.min(cap, Math.floor(total / Math.max(1, fans))));
216
+ return anchors.map((a) => {
217
+ if (!(a?.count > 0)) return null;
218
+ const n = Math.min(a.count, each);
219
+ // Only as wide as the dots need at `spacing`, never wider than their slot:
220
+ // more rows once it's full, so a small fan stays a tight cluster.
221
+ const room = Math.min(a.slot || TAU, TAU / 6);
222
+ const width = Math.min(room, Math.max((spacing * 3) / from, (n * spacing) / from));
223
+ const l = ringLayout(n, {
224
+ inner: from, outer: Infinity, spacing, minSpacing: spacing, start: a.angle - width / 2, sweep: width,
225
+ });
226
+ return { points: l.points.map(({ x, y }) => ({ x, y })), more: a.count - n };
227
+ });
228
+ }
package/lib/circle.js CHANGED
@@ -137,17 +137,34 @@ export function circleLayout(rings = [], size = 240) {
137
137
  for (let k = 0; k < R; k++) {
138
138
  const radius = radiusOf(k);
139
139
  const people = rings[k].people;
140
- const dotR = clamp((2 * Math.PI * radius) / Math.max(1, people.length) * 0.32, 0.8, size / 60);
140
+ // More than their slice holds without the dots touching: rows, neighbours
141
+ // alternating between them, so a big circle never draws as a solid line.
142
+ // Each keeps its angle behind who they hang off. Rows go out towards the
143
+ // next ring, or for the outermost, in towards the one before (less than
144
+ // half way either side, so two rings' rows never meet).
145
+ const gap = Math.max(2.4, size / 100);
146
+ const inward = k === R - 1 && R > 1;
147
+ const band = (inward ? radius - radiusOf(k - 1) : (k + 1 < R ? radiusOf(k + 1) : half * 0.97) - radius) * 0.45;
148
+ const maxRows = Math.max(1, Math.floor(band / gap) + 1);
149
+ const rowGap = maxRows > 1 ? (inward ? -1 : 1) * Math.min(gap * 1.2, band / (maxRows - 1)) : 0;
150
+ const rowsFor = (count, arc) => clamp(Math.ceil((count * gap) / Math.max(1e-9, radius * arc)), 1, maxRows);
151
+ let rows = 1;
152
+ let room = Infinity; // the least room any one dot gets along its row
141
153
  const items = [];
142
154
  for (const [parentId, group] of byParent[k]) {
143
155
  const [a0, a1] = k === 0 ? [START, START + 2 * Math.PI] : span.get(parentId) || [START, START + 2 * Math.PI];
144
156
  const total = group.reduce((s, p) => s + weight.get(p), 0);
157
+ const groupRows = rowsFor(group.length, a1 - a0);
158
+ rows = Math.max(rows, groupRows);
159
+ room = Math.min(room, (radius * (a1 - a0) * groupRows) / Math.max(1, group.length));
160
+ let n = 0;
145
161
  let a = a0;
146
162
  for (const p of group) {
147
163
  const w = ((a1 - a0) * weight.get(p)) / total;
148
164
  const angle = a + w / 2;
149
- const x = half + radius * Math.cos(angle);
150
- const y = half + radius * Math.sin(angle);
165
+ const r = radius + (n++ % groupRows) * rowGap;
166
+ const x = half + r * Math.cos(angle);
167
+ const y = half + r * Math.sin(angle);
151
168
  items.push({ ...p, angle, x, y });
152
169
  if (p.state === 'connected') span.set(p.row.id, [a, a + w]);
153
170
  a += w;
@@ -163,7 +180,9 @@ export function circleLayout(rings = [], size = 240) {
163
180
  spokes.push({ x1: it.x, y1: it.y, x2: half + nextR * Math.cos(mid), y2: half + nextR * Math.sin(mid) });
164
181
  }
165
182
  }
166
- out.push({ degree: rings[k].degree, radius, dotR, items });
183
+ // Sized by the most crowded slice, and never wider than the gap between rows.
184
+ const dotR = clamp(Math.min(room * 0.32, rows > 1 ? Math.abs(rowGap) * 0.42 : Infinity), 0.8, size / 60);
185
+ out.push({ degree: rings[k].degree, radius, dotR, rows, items });
167
186
  }
168
187
  return { size, center: { x: half, y: half }, rings: out, spokes };
169
188
  }
package/lib/csv.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { scorePerson, curveCutoffs, curvedTier } from './scoring.js';
2
+ import { toIsoDate } from './ingest.js';
2
3
  // LinkedIn Connections.csv import — parsed ENTIRELY in the browser.
3
4
  //
4
5
  // Nothing is uploaded and nothing is written to the database: an imported
@@ -136,12 +137,9 @@ export function parseConnectionsCsv(text) {
136
137
  if (seen.has(id)) { skipped++; continue; }
137
138
  seen.add(id);
138
139
 
139
- let connected_date = null;
140
- const raw = cell(iConnected);
141
- if (raw) {
142
- const d = new Date(raw);
143
- if (!isNaN(d.getTime())) connected_date = d.toISOString().split('T')[0];
144
- }
140
+ // "28 Sep 2026", read as the calendar date it is. new Date(raw).toISOString()
141
+ // made it local midnight in UTC, a day early anywhere east of London.
142
+ const connected_date = toIsoDate(cell(iConnected));
145
143
 
146
144
  connections.push(connectionFrom({ id, name, role, company, profileUrl, connected_date }));
147
145
  }
@@ -111,7 +111,8 @@ export function buildExport(db, { dir, outFile, includePhotos = true, appVersion
111
111
  // file the other computer doesn't have: some views draw an empty circle
112
112
  // for it. With no photo, they show initials, and the next scan of that
113
113
  // person saves a new one. A photo not saved yet (LinkedIn's own link,
114
- // which expires in a few weeks) is left as it is, as it is here.
114
+ // which expires in a few weeks) is left as it is, as it is here: neither
115
+ // computer shows it, and either saves it once (lib/photos.js).
115
116
  if (!includePhotos) {
116
117
  out.exec("UPDATE linkedin_connections SET profile_image_url = NULL WHERE profile_image_url LIKE '/avatars/%'");
117
118
  }
@@ -11,9 +11,11 @@
11
11
  import { readdirSync, lstatSync, existsSync, rmSync } from 'node:fs';
12
12
  import { spawn } from 'node:child_process';
13
13
  import path from 'node:path';
14
+ import { AVATAR_FILE } from './photos.js';
14
15
 
15
- /** A photo the /avatars route serves: a plain file name, no folders. The route uses this same pattern. */
16
- export const AVATAR_FILE = /^[A-Za-z0-9_-]+\.(webp|jpg|jpeg|png)$/;
16
+ // A photo the /avatars route serves (lib/photos.js). An export carries and an
17
+ // import accepts exactly those names.
18
+ export { AVATAR_FILE };
17
19
 
18
20
  /**
19
21
  * The scanner's notes about the network itself: without them, people whose
package/lib/degrees.js ADDED
@@ -0,0 +1,30 @@
1
+ // Everyone the app knows, by degree, for Network Circle's degree filter.
2
+ //
3
+ // 1st: your connections. 2nd: anyone in a scanned circle, including the people
4
+ // behind someone you added, since you're connected to them now. 3rd: anyone a
5
+ // company scan found who is neither. Each person counts once, at the nearest
6
+ // degree they're found (by profile, lib/separation.js keyFor), whichever
7
+ // circle or scan found them first.
8
+
9
+ import { keyFor } from './separation.js';
10
+
11
+ export function peopleByDegree(degree1 = [], degree2 = [], degree3 = []) {
12
+ const seen = new Set();
13
+ const take = (rows, degree) => {
14
+ const out = [];
15
+ for (const row of rows || []) {
16
+ const key = keyFor(row);
17
+ if (seen.has(key)) continue;
18
+ seen.add(key);
19
+ out.push(row.degree === degree ? row : { ...row, degree });
20
+ }
21
+ return out;
22
+ };
23
+ return { 1: take(degree1, 1), 2: take(degree2, 2), 3: take(degree3, 3) };
24
+ }
25
+
26
+ export function tierCountsOf(rows = []) {
27
+ const counts = {};
28
+ for (const row of rows) counts[row.tier] = (counts[row.tier] || 0) + 1;
29
+ return counts;
30
+ }
package/lib/galaxy.js ADDED
@@ -0,0 +1,12 @@
1
+ // The Galaxy's view, as numbers, kept apart from the drawing so it can be tested
2
+ // (app/components/ForceGraph.js).
3
+
4
+ /**
5
+ * The same view in a box of a new size. It moves by half the change, at the
6
+ * same zoom, so whatever was in the middle of the box is in the middle still.
7
+ * Opening a 320-pixel panel slides the view 160 pixels left, and closing it
8
+ * slides the view back to exactly where it was.
9
+ */
10
+ export function recentre({ x, y, k }, from, to) {
11
+ return { x: x + (to.width - from.width) / 2, y: y + (to.height - from.height) / 2, k };
12
+ }
package/lib/ingest.js CHANGED
@@ -31,34 +31,41 @@ export function uniqueByProfile(records) {
31
31
  */
32
32
  export function splitAlreadyConnected(records, firstDegreeUrls) {
33
33
  const keep = [];
34
- let alreadyConnected = 0;
34
+ const connected = []; // their profile URLs: ties between your connections (lib/ties.js)
35
35
  for (const r of records) {
36
- if (firstDegreeUrls.has(r.profile_url)) alreadyConnected += 1;
36
+ if (firstDegreeUrls.has(r.profile_url)) connected.push(r.profile_url);
37
37
  else keep.push(r);
38
38
  }
39
- return { keep, alreadyConnected };
39
+ return { keep, alreadyConnected: connected.length, connected };
40
40
  }
41
41
 
42
42
  const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july',
43
43
  'august', 'september', 'october', 'november', 'december'];
44
44
 
45
45
  /**
46
- * "September 22, 2026" (as LinkedIn prints "Connected on …") → "2026-09-22".
46
+ * A date as LinkedIn writes it → "2026-09-22": "September 22, 2026" (a profile's
47
+ * "Connected on …", read by the scanner) or "22 Sep 2026" (the "Connected On"
48
+ * column of the Connections.csv export, read by lib/csv.js).
47
49
  *
48
50
  * Parsed by hand rather than with new Date(text).toISOString(): that reads the
49
51
  * text as local midnight and converts to UTC, which moves the date back a day
50
52
  * anywhere east of Greenwich — and throws outright on text it cannot read.
51
- * Returns null for anything it does not recognise.
53
+ * A calendar date has no time zone, so nothing here uses one.
54
+ * Returns null for anything it does not recognise, or a day the month doesn't have.
52
55
  */
53
56
  export function toIsoDate(text) {
54
57
  const t = String(text || '').trim();
55
58
  if (/^\d{4}-\d{2}-\d{2}$/.test(t)) return t;
56
- const m = t.match(/^([A-Za-z]+)\.?\s+(\d{1,2}),?\s+(\d{4})$/);
57
- if (!m) return null;
58
- const month = MONTHS.findIndex((name) => name.startsWith(m[1].toLowerCase()) && m[1].length >= 3);
59
- const day = Number(m[2]);
60
- if (month < 0 || day < 1 || day > 31) return null;
61
- return `${m[3]}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
59
+ let name, day, year;
60
+ let m = t.match(/^([A-Za-z]+)\.?\s+(\d{1,2}),?\s+(\d{4})$/); // September 22, 2026
61
+ if (m) [, name, day, year] = m;
62
+ else if ((m = t.match(/^(\d{1,2})\s+([A-Za-z]+)\.?\s+(\d{4})$/))) [, day, name, year] = m; // 22 Sep 2026
63
+ else return null;
64
+ const month = MONTHS.findIndex((full) => full.startsWith(name.toLowerCase()) && name.length >= 3);
65
+ day = Number(day);
66
+ // Day 0 of the next month is this month's last, counted in UTC so no zone moves it.
67
+ if (month < 0 || day < 1 || day > new Date(Date.UTC(Number(year), month + 1, 0)).getUTCDate()) return null;
68
+ return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
62
69
  }
63
70
 
64
71
  // Tiers that make a new connection worth a notification of its own.
@@ -4,15 +4,21 @@
4
4
  // reads them with the same rules, and writes only the two things a person
5
5
  // changes here — the budget, and lifting a cooldown.
6
6
  //
7
+ // The budget is searches (a day and a month) and profile views (a day). Profile
8
+ // views got an account restricted (TRAPS §16), so their cap has no "no limit",
9
+ // and the scanner also keeps any two a minute apart. Searches did too, at 373 in
10
+ // 24 hours: the page asks before a risky search budget (lib/search-risk.js).
11
+ //
7
12
  // They belong to the LinkedIn account, not to a profile in the app. TRAPS §35.
8
13
 
9
14
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, lstatSync } from 'node:fs';
10
15
  import path from 'node:path';
11
16
  import { durable } from './durable.js';
12
17
 
13
- export const DEFAULT_LIMITS = { daily: 50, monthly: 250 };
18
+ export const DEFAULT_LIMITS = { daily: 50, monthly: 250, profiles: 50 };
14
19
  export const DAILY_CHOICES = [25, 50, 100, 200, 500];
15
20
  export const MONTHLY_CHOICES = [100, 250, 500, 1000, 0]; // 0 = no monthly cap (Premium)
21
+ export const PROFILE_CHOICES = [10, 25, 50, 100]; // profile views a day; never unlimited
16
22
  const DAY = 24 * 3600;
17
23
  const PACIFIC = 'America/Los_Angeles';
18
24
 
@@ -53,18 +59,32 @@ export function nextMonthStartPacific(ms = Date.now()) {
53
59
  return monthStartPacific(start + 40 * DAY * 1000);
54
60
  }
55
61
 
62
+ const profileChoice = (n) => PROFILE_CHOICES.filter((c) => c <= n).pop() ?? PROFILE_CHOICES[0];
63
+
56
64
  export function readLimits(dir) {
57
65
  const raw = readJson(path.join(dir, 'scan-limits.json')) || {};
58
66
  const num = (v, d) => (Number.isInteger(v) && v >= 0 ? v : d);
59
- return { daily: num(raw.daily, DEFAULT_LIMITS.daily), monthly: num(raw.monthly, DEFAULT_LIMITS.monthly) };
67
+ return {
68
+ daily: num(raw.daily, DEFAULT_LIMITS.daily),
69
+ monthly: num(raw.monthly, DEFAULT_LIMITS.monthly),
70
+ // 0 is "no limit" for searches; for profile views the scanner reads it as the
71
+ // default. A number typed in by hand counts as the largest choice under it,
72
+ // as the scanner reads it (scrape.py search_limits).
73
+ profiles: profileChoice(num(raw.profiles, 0) || DEFAULT_LIMITS.profiles),
74
+ };
60
75
  }
61
76
 
62
- /** Only the offered choices are accepted, like everything else that reaches the scanner. */
63
- export function writeLimits(dir, { daily, monthly }) {
77
+ /**
78
+ * Only the offered choices are accepted, like everything else that reaches the
79
+ * scanner. A value not given keeps what is saved: every key is written back,
80
+ * or saving one would drop the others to their defaults.
81
+ */
82
+ export function writeLimits(dir, { daily, monthly, profiles } = {}) {
64
83
  const cur = readLimits(dir);
65
84
  const next = {
66
85
  daily: DAILY_CHOICES.includes(daily) ? daily : cur.daily,
67
86
  monthly: MONTHLY_CHOICES.includes(monthly) ? monthly : cur.monthly,
87
+ profiles: PROFILE_CHOICES.includes(profiles) ? profiles : cur.profiles,
68
88
  };
69
89
  writeJsonAtomic(path.join(dir, 'scan-limits.json'), next);
70
90
  return next;
@@ -72,13 +92,14 @@ export function writeLimits(dir, { daily, monthly }) {
72
92
 
73
93
  /**
74
94
  * Searches in the last 24 hours and since LinkedIn's month began, and profile
75
- * views today. An unreadable record reads as the day used up, as the scanner
76
- * treats it — failing open would say nothing had been searched.
95
+ * views in the last 24 hours. An unreadable record reads as the day used up,
96
+ * searches and profile views both, as the scanner treats it — failing open
97
+ * would say nothing had been searched.
77
98
  */
78
99
  export function usage(dir, now = Date.now()) {
79
100
  const file = path.join(dir, 'linkedin-activity.json');
80
101
  const data = existsSync(file) ? readJson(file) : { searches: [], profiles: [] };
81
- if (!data || typeof data !== 'object') return { searchesToday: Infinity, searchesMonth: 0, profilesToday: 0, unreadable: true };
102
+ if (!data || typeof data !== 'object') return { searchesToday: Infinity, searchesMonth: 0, profilesToday: Infinity, unreadable: true };
82
103
  const s = (Array.isArray(data.searches) ? data.searches : []).map(Number);
83
104
  const p = (Array.isArray(data.profiles) ? data.profiles : []).map(Number);
84
105
  const dayAgo = now / 1000 - DAY;
@@ -113,6 +134,7 @@ export function linkedinState(dir, now = Date.now()) {
113
134
  limits,
114
135
  leftToday: left(limits.daily, use.searchesToday),
115
136
  leftMonth: left(limits.monthly, use.searchesMonth),
137
+ profilesLeftToday: left(limits.profiles, use.profilesToday),
116
138
  monthResets: nextMonthStartPacific(now),
117
139
  cooldown: readCooldown(dir, now),
118
140
  };
@@ -138,7 +160,8 @@ const isTime = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
138
160
  * Why one of these files from another computer can't be used here, in words
139
161
  * for the page, or null when it can. The limits follow writeLimits: only the
140
162
  * menu's own choices. A value the app never offers is refused rather than
141
- * passed to the scanner, which reads a daily limit of 0 as "no limit at all".
163
+ * passed to the scanner, which reads a daily limit of 0 as "no limit at all"
164
+ * (and 1000 profile views a day would be as good as none).
142
165
  */
143
166
  export function budgetFileProblem(name, data) {
144
167
  if (!isObject(data)) return 'it is not a JSON object';
@@ -155,6 +178,9 @@ export function budgetFileProblem(name, data) {
155
178
  if (data.monthly !== undefined && !MONTHLY_CHOICES.includes(data.monthly)) {
156
179
  return `its monthly limit (${String(data.monthly).slice(0, 20)}) is not one Six Degrees offers`;
157
180
  }
181
+ if (data.profiles !== undefined && !PROFILE_CHOICES.includes(data.profiles)) {
182
+ return `its limit on profile views (${String(data.profiles).slice(0, 20)}) is not one Six Degrees offers`;
183
+ }
158
184
  }
159
185
  return null;
160
186
  }
@@ -236,6 +262,7 @@ export function mergeBudgetFiles({ dir, from }) {
236
262
  durable.writeJsonDurably(here(LIMITS), {
237
263
  daily: limits.daily ?? DEFAULT_LIMITS.daily,
238
264
  monthly: limits.monthly ?? DEFAULT_LIMITS.monthly,
265
+ profiles: limits.profiles ?? DEFAULT_LIMITS.profiles,
239
266
  });
240
267
  }
241
268
  }
package/lib/photos.js ADDED
@@ -0,0 +1,47 @@
1
+ // Profile photos are shown from this computer only.
2
+ //
3
+ // The scanner saves each photo into the data folder as it reads it
4
+ // (scripts/image_store.py), and the row points at the saved file,
5
+ // `/avatars/<name>`, which app/avatars/[file] serves. A row can also hold a
6
+ // link: LinkedIn's own, which older versions stored until the scanner replaced
7
+ // it, and which a copy of a network made by one carries too. A page never loads
8
+ // a link: it would contact LinkedIn while someone is only looking at their map,
9
+ // and the app goes online only when you act. That person shows initials until
10
+ // the photo is saved, at the next scan or with Save photos on the Scan page.
11
+ // The Content-Security-Policy in next.config.mjs holds the same line in the
12
+ // browser.
13
+ //
14
+ // No imports, so the views and the server share it: every view and the routes
15
+ // that store photos check with localPhoto, and the /avatars route serves the
16
+ // names AVATAR_FILE allows.
17
+
18
+ /** A photo the /avatars route serves: a plain file name, no folders. The route uses this same pattern. */
19
+ export const AVATAR_FILE = /^[A-Za-z0-9_-]+\.(webp|jpg|jpeg|png)$/;
20
+
21
+ const PREFIX = '/avatars/';
22
+
23
+ /** The photo to show for a row's `profile_image_url`: its saved file's path, or null. */
24
+ export function localPhoto(url) {
25
+ if (typeof url !== 'string' || !url.startsWith(PREFIX)) return null;
26
+ return AVATAR_FILE.test(url.slice(PREFIX.length)) ? url : null;
27
+ }
28
+
29
+ /**
30
+ * The photos still kept as links, one per person and link:
31
+ * [{ profileUrl, imageUrl }]. `db` is an open node:sqlite database. The
32
+ * scanner saves each (`GET /api/update-images`), or forgets the link on a
33
+ * definite no, so a link is tried until there's an answer.
34
+ */
35
+ export function waitingPhotos(db) {
36
+ return db.prepare(`SELECT DISTINCT profile_url AS profileUrl, profile_image_url AS imageUrl
37
+ FROM linkedin_connections
38
+ WHERE profile_image_url IS NOT NULL AND profile_image_url <> ''
39
+ AND substr(profile_image_url, 1, 9) <> '/avatars/'
40
+ ORDER BY profile_url`).all()
41
+ .filter((r) => r.profileUrl && !localPhoto(r.imageUrl));
42
+ }
43
+
44
+ /** How many people have a photo waiting to be saved. */
45
+ export function waitingPhotoCount(db) {
46
+ return new Set(waitingPhotos(db).map((r) => r.profileUrl)).size;
47
+ }
package/lib/quest.js CHANGED
@@ -9,6 +9,8 @@
9
9
  // sent, people who actually joined your network), never from browsing.
10
10
  // Plain functions; tests/quest.test.mjs.
11
11
 
12
+ import { circleState } from './reach.js';
13
+
12
14
  export const STAGE_SIZE = 5;
13
15
  export const XP_SEND = { S: 25, A: 15, B: 10, C: 5, D: 2 };
14
16
  export const XP_ADDED = { S: 100, A: 60, B: 35, C: 15, D: 5 };
@@ -26,8 +28,9 @@ export function levelFor(points) {
26
28
  * @param sentIds ids marked "invite sent"
27
29
  * @param added 1st-degree people who came through a cluster (unlocked_from_bridge_id set)
28
30
  * @param mappedIds ids of 1st-degree people whose own circle is mapped
31
+ * @param reach lib/reach.js reachIndex(), which also knows hidden lists; mappedIds stands in without it
29
32
  */
30
- export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set() }) {
33
+ export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set(), reach = null }) {
31
34
  const byBridge = new Map();
32
35
  const seen = new Set();
33
36
  for (const r of [...recs].sort((a, b) => (b.priority || 0) - (a.priority || 0))) {
@@ -112,8 +115,10 @@ export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedI
112
115
  level: levelFor(points),
113
116
  sentTotal,
114
117
  addedTotal,
115
- // People you've added whose own circle isn't mapped yet: the next degree.
116
- newDoors: added.filter((a) => a.unlocked_from_bridge_id && !mappedIds.has(a.id)),
118
+ // People you've added whose own circle can be scanned: the next degree.
119
+ // Hidden lists aren't doors (lib/reach.js, the rule Bridge Chains uses).
120
+ newDoors: added.filter((a) => a.unlocked_from_bridge_id
121
+ && circleState(a, reach || { scanned: mappedIds, hidden: new Set() }) === 'todo'),
117
122
  clearedStages: clusters.reduce((s, c) => s + (c.stage - 1) + (c.complete ? 1 : 0), 0),
118
123
  };
119
124
  }