six-degrees 0.4.11 → 0.5.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 (375) 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 +9 -9
  4. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +12 -12
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  14. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
  15. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  17. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  36. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
  48. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/settings/route.js +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 +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/route.js +3 -3
  60. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  62. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  65. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  67. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/import.html +1 -1
  72. package/.next/standalone/.next/server/app/import.rsc +14 -14
  73. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
  74. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  75. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
  76. package/.next/standalone/.next/server/app/index.html +1 -1
  77. package/.next/standalone/.next/server/app/index.rsc +14 -14
  78. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  79. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  80. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  81. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/launch.html +2 -2
  84. package/.next/standalone/.next/server/app/launch.rsc +14 -14
  85. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
  86. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  87. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
  88. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  89. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  90. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/paths.html +1 -1
  93. package/.next/standalone/.next/server/app/paths.rsc +14 -14
  94. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
  95. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  96. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
  97. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/profile.html +1 -1
  100. package/.next/standalone/.next/server/app/profile.rsc +14 -14
  101. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
  102. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  103. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
  104. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/queue.html +1 -1
  107. package/.next/standalone/.next/server/app/queue.rsc +14 -14
  108. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
  109. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  110. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
  111. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/scores.html +1 -1
  114. package/.next/standalone/.next/server/app/scores.rsc +14 -14
  115. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
  116. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  117. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
  118. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/settings.html +1 -1
  121. package/.next/standalone/.next/server/app/settings.rsc +14 -14
  122. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  123. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  124. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
  125. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  126. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  127. package/.next/standalone/.next/server/app/setup.html +1 -1
  128. package/.next/standalone/.next/server/app/setup.rsc +14 -14
  129. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
  130. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  131. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
  132. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  133. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  134. package/.next/standalone/.next/server/app/social.html +1 -1
  135. package/.next/standalone/.next/server/app/social.rsc +14 -14
  136. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +14 -14
  137. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
  138. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
  139. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__13dx4sd._.js} +2 -2
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
  143. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  145. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
  148. package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
  149. package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
  150. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
  151. package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
  152. package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
  153. package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
  154. package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
  155. package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
  156. package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
  157. package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
  158. package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
  159. package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
  160. package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
  161. package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
  162. package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
  163. package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
  165. package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
  166. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
  167. package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
  168. package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
  169. package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
  170. package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
  171. package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
  172. package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
  173. package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
  174. package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
  175. package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
  176. package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
  178. package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  180. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
  181. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  182. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
  184. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
  186. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -2
  187. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +1 -1
  190. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  191. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  192. package/.next/standalone/.next/server/pages/404.html +1 -1
  193. package/.next/standalone/.next/server/pages/500.html +1 -1
  194. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  195. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  196. package/.next/standalone/.next/static/chunks/03bziijw5srol.js +1 -0
  197. package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  198. package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  199. package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  200. package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  201. package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  202. package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  203. package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
  204. package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  205. package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
  206. package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  207. package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  208. package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
  209. package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  210. package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  211. package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  212. package/.next/standalone/CHANGELOG.md +187 -0
  213. package/.next/standalone/app/api/ingest/route.js +40 -1
  214. package/.next/standalone/app/api/scraper/route.js +7 -1
  215. package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
  216. package/.next/standalone/app/components/AppTabs.js +59 -0
  217. package/.next/standalone/app/components/ChainView.js +303 -89
  218. package/.next/standalone/app/components/CirclePanel.js +162 -0
  219. package/.next/standalone/app/components/CompanyScores.js +2 -2
  220. package/.next/standalone/app/components/FieldStep.js +4 -4
  221. package/.next/standalone/app/components/FilterPanel.js +121 -183
  222. package/.next/standalone/app/components/ForceGraph.js +41 -3
  223. package/.next/standalone/app/components/GalaxyLab.js +26 -31
  224. package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
  225. package/.next/standalone/app/components/MapControls.js +154 -0
  226. package/.next/standalone/app/components/NoteDetail.js +118 -0
  227. package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
  228. package/.next/standalone/app/components/PeopleMap.js +308 -0
  229. package/.next/standalone/app/components/ScanRadar.js +129 -0
  230. package/.next/standalone/app/components/ScanStatusBar.js +71 -27
  231. package/.next/standalone/app/components/SeparationView.js +626 -118
  232. package/.next/standalone/app/components/Sidebar.js +28 -2
  233. package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
  234. package/.next/standalone/app/components/settings/SectorSection.js +2 -2
  235. package/.next/standalone/app/components/settings/TierSection.js +1 -1
  236. package/.next/standalone/app/components/settings/TitleSection.js +149 -0
  237. package/.next/standalone/app/components/views.js +15 -14
  238. package/.next/standalone/app/page.js +89 -156
  239. package/.next/standalone/app/paths/page.js +39 -27
  240. package/.next/standalone/app/profile/page.js +33 -5
  241. package/.next/standalone/app/queue/page.js +56 -34
  242. package/.next/standalone/app/scores/page.js +16 -66
  243. package/.next/standalone/app/settings/page.js +27 -13
  244. package/.next/standalone/app/setup/page.js +298 -205
  245. package/.next/standalone/app/social/SocialHub.js +374 -0
  246. package/.next/standalone/app/social/page.js +10 -362
  247. package/.next/standalone/lib/chain-layout.js +105 -35
  248. package/.next/standalone/lib/degrees.js +0 -6
  249. package/.next/standalone/lib/dot-rings.js +25 -0
  250. package/.next/standalone/lib/galaxy-lab.js +65 -6
  251. package/.next/standalone/lib/ingest.js +2 -0
  252. package/.next/standalone/lib/island.js +40 -0
  253. package/.next/standalone/lib/linkedin-limits.js +6 -2
  254. package/.next/standalone/lib/map-heat.js +20 -0
  255. package/.next/standalone/lib/network-health.js +1 -1
  256. package/.next/standalone/lib/notifications.js +121 -0
  257. package/.next/standalone/lib/people-map.js +82 -0
  258. package/.next/standalone/lib/rarity.js +45 -3
  259. package/.next/standalone/lib/reach.js +7 -6
  260. package/.next/standalone/lib/rpc.js +15 -4
  261. package/.next/standalone/lib/scan-pace.js +46 -0
  262. package/.next/standalone/lib/scoring.js +39 -8
  263. package/.next/standalone/lib/scraper-client.js +19 -6
  264. package/.next/standalone/lib/sector-focus.js +4 -4
  265. package/.next/standalone/lib/separation.js +137 -10
  266. package/.next/standalone/lib/settings-effects.js +20 -11
  267. package/.next/standalone/lib/settings.js +3 -0
  268. package/.next/standalone/lib/tier-grid.js +99 -0
  269. package/.next/standalone/lib/title-ranking.js +83 -0
  270. package/.next/standalone/package-lock.json +2 -2
  271. package/.next/standalone/package.json +1 -1
  272. package/.next/standalone/scripts/scrape.py +51 -13
  273. package/.next/static/chunks/03bziijw5srol.js +1 -0
  274. package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  275. package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  276. package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  277. package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  278. package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  279. package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  280. package/.next/static/chunks/1i51yylxdzht1.js +1 -0
  281. package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  282. package/.next/static/chunks/2rqtpccruavf2.js +1 -0
  283. package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  284. package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  285. package/.next/static/chunks/355ryio1b7sm1.js +1 -0
  286. package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  287. package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  288. package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  289. package/CHANGELOG.md +187 -0
  290. package/lib/chain-layout.js +105 -35
  291. package/lib/degrees.js +0 -6
  292. package/lib/dot-rings.js +25 -0
  293. package/lib/galaxy-lab.js +65 -6
  294. package/lib/ingest.js +2 -0
  295. package/lib/island.js +40 -0
  296. package/lib/linkedin-limits.js +6 -2
  297. package/lib/map-heat.js +20 -0
  298. package/lib/network-health.js +1 -1
  299. package/lib/notifications.js +121 -0
  300. package/lib/people-map.js +82 -0
  301. package/lib/rarity.js +45 -3
  302. package/lib/reach.js +7 -6
  303. package/lib/rpc.js +15 -4
  304. package/lib/scan-pace.js +46 -0
  305. package/lib/scoring.js +39 -8
  306. package/lib/scraper-client.js +19 -6
  307. package/lib/sector-focus.js +4 -4
  308. package/lib/separation.js +137 -10
  309. package/lib/settings-effects.js +20 -11
  310. package/lib/settings.js +3 -0
  311. package/lib/tier-grid.js +99 -0
  312. package/lib/title-ranking.js +83 -0
  313. package/package.json +1 -1
  314. package/scripts/scrape.py +51 -13
  315. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
  316. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
  317. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
  318. package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +0 -3
  319. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
  320. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
  321. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
  322. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
  323. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
  324. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
  325. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
  326. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
  327. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
  328. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
  329. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
  330. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
  331. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
  332. package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +0 -3
  333. package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +0 -3
  334. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
  335. package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
  336. package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
  337. package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
  338. package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
  339. package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
  340. package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
  341. package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
  342. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
  343. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  344. package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +0 -1
  345. package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
  346. package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +0 -1
  347. package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
  348. package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  349. package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
  350. package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +0 -1
  351. package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
  352. package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  353. package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  354. package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
  355. package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  356. package/.next/standalone/app/components/OrbitGraph.js +0 -737
  357. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  358. package/.next/static/chunks/15sjx07xhpn6p.js +0 -1
  359. package/.next/static/chunks/160wjb73ddwqd.js +0 -6
  360. package/.next/static/chunks/1a74dky22dxd6.js +0 -1
  361. package/.next/static/chunks/1c_n0973t07l0.js +0 -2
  362. package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  363. package/.next/static/chunks/236f0jtp68j_h.js +0 -1
  364. package/.next/static/chunks/297e1z34yi--z.js +0 -1
  365. package/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
  366. package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  367. package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  368. package/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
  369. package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  370. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  371. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  372. /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
  373. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  374. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  375. /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
@@ -19,21 +19,24 @@
19
19
  // circle fills in as the scanner saves (app/page.js, NetworkRefresh). A dot
20
20
  // ready for a scan goes to the Scan page with them picked instead: their
21
21
  // circle is empty until it's scanned.
22
- // - People you reached through a circle are marked by lib/reach.js: a soft
23
- // breathing halo when their own circle is ready for a scan (still, with
24
- // Reduce Motion on), greyed with a lock when their list is hidden. Each
25
- // bridge says how many are ready. The glow this replaced looked for them
26
- // inside the circle they came from, and accepting takes them out of it
27
- // (lib/promote.js), so it never showed. Backlog 2.4.
22
+ // - Chains are drawn inside an opened circle (Blake, 2026-10-02: "once they
23
+ // are clicked on and can view the cluster i want any scanned d2,3,4 or
24
+ // whatever to be show inside with lines out of the d1 leading to the d2s").
25
+ // Anyone you met through this circle whose own circle has been scanned sits
26
+ // outside the tier bands with their cluster round them and a line from the
27
+ // person in the middle; anyone met through *their* circle sits a step beyond,
28
+ // and so on (lib/chain-layout.js chainTree). They aren't bridges on the
29
+ // overview: that shows the people you connected with yourself, and says how
30
+ // many chains lead on from each.
28
31
 
29
32
  import { useState, useRef, useEffect, useMemo, useSyncExternalStore } from 'react';
30
33
  import Link from 'next/link';
31
34
  import { useRouter } from 'next/navigation';
32
- import { circleIndex } from '../../lib/circle';
35
+ import { circleIndex, MAX_DEGREE } from '../../lib/circle';
33
36
  import { localPhoto } from '../../lib/photos';
34
37
  import { reachIndex, reachState, circleState, readyByCircle, circleScanCost, scanBars } from '../../lib/reach';
35
- import { ringSegments, RING } from '../../lib/dot-rings';
36
- import { ringLayout, dotRadius, previewBand, tierBandLayout, outerFans } from '../../lib/chain-layout';
38
+ import { reachSegments, RING } from '../../lib/dot-rings';
39
+ import { ringLayout, chainTree, dotRadius, previewBand, tierBandLayout } from '../../lib/chain-layout';
37
40
  import { redundancy } from '../../lib/brokerage';
38
41
  import { keyFor, score } from '../../lib/separation';
39
42
  import { hasRequest } from '../../lib/requests-client';
@@ -89,6 +92,30 @@ function membersOf(person, index) {
89
92
  return [...take(index.introduced.get(person.id)), ...take(index.circles.get(person.id))];
90
93
  }
91
94
 
95
+ /**
96
+ * The chains that lead on from `person`: everyone you met through their circle
97
+ * whose own circle has been scanned, then the same from each of those, biggest
98
+ * circle first. `parent` is an index into the list, or -1 for `person`; `step`
99
+ * is how many links out. Nobody already on the trail, nobody twice.
100
+ */
101
+ function chainsFrom(person, index, skip = [], maxStep = MAX_DEGREE) {
102
+ const out = [];
103
+ const seen = new Set([person.id, ...skip]);
104
+ const size = (row) => (index.circles.get(row.id)?.length || 0) + (index.introduced.get(row.id)?.length || 0);
105
+ const walk = (from, parent, step) => {
106
+ if (step > maxStep) return;
107
+ const next = (index.introduced.get(from.id) || []).filter((row) => size(row) > 0 && !seen.has(row.id))
108
+ .sort((a, b) => size(b) - size(a) || String(a.name || '').localeCompare(String(b.name || '')));
109
+ next.forEach((row) => seen.add(row.id));
110
+ for (const row of next) {
111
+ out.push({ row, parent, step });
112
+ walk(row, out.length - 1, step + 1);
113
+ }
114
+ };
115
+ walk(person, -1, 1);
116
+ return out;
117
+ }
118
+
92
119
  /** The trail to open for a link: the circle they came from, then them. */
93
120
  function trailTo(id, rows) {
94
121
  if (!id) return [];
@@ -98,10 +125,11 @@ function trailTo(id, rows) {
98
125
  return from != null && rows.some((r) => r.id === from) ? [from, id] : [id];
99
126
  }
100
127
 
101
- export default function ChainView({ connections, degree2 = [], onSelect, userName, fullDegree1, fullDegree2, scanNotes, canScan = true, chainOpen = null, onChainOpened }) {
128
+ export default function ChainView({ connections, degree2 = [], onSelect, userName, fullDegree1, fullDegree2, scanNotes, canScan = true, chainOpen = null, onChainOpened, onCircle }) {
102
129
  const containerRef = useRef(null);
103
130
  const [hovered, setHovered] = useState(null);
104
- const [zoom, setZoom] = useState(1.3);
131
+ // The overview's zoom: null until it's changed, which means "fit" (homeZoom below).
132
+ const [zoomSet, setZoom] = useState(null);
105
133
  const [dims, setDims] = useState({ w: 800, h: 600 });
106
134
  // Every row, whatever the tier filter: a circle opened from a bridge the
107
135
  // filter shows is drawn whole, and so is anyone's circle opened from it.
@@ -162,6 +190,10 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
162
190
  trail.push(row);
163
191
  }
164
192
  const focused = trail.length > 0;
193
+ // Whose circle is open, for the right panel (page.js → Sidebar → CirclePanel).
194
+ const inCircle = trail.length ? trail[trail.length - 1] : null;
195
+ useEffect(() => { onCircle?.(inCircle); }, [inCircle, onCircle]);
196
+ useEffect(() => () => onCircle?.(null), [onCircle]);
165
197
 
166
198
  // Esc goes back one circle, unless it's closing something else (the card's
167
199
  // large map) or leaving a text box.
@@ -191,14 +223,39 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
191
223
  const cy = dims.h / 2;
192
224
  const maxR = Math.min(cx, cy) - 30;
193
225
 
194
- // Bridges round you: one ring while they fit, more once there are many.
195
- const bridgeLayout = ringLayout(bridges.length, {
196
- inner: maxR * 0.55, innerMin: maxR * 0.4, outer: maxR * 0.62, spacing: 46, minSpacing: 20,
226
+ // Someone met through another bridge's circle isn't a bridge of their own
227
+ // here: they're a link in that bridge's chain, shown inside its circle.
228
+ const withCircle = new Set(bridges.map((b) => b.id));
229
+ const chained = new Map();
230
+ for (const b of bridges) {
231
+ if (withCircle.has(b.unlocked_from_bridge_id)) chained.set(b.unlocked_from_bridge_id, (chained.get(b.unlocked_from_bridge_id) || 0) + 1);
232
+ }
233
+ // (A loop in who-introduced-whom shouldn't happen; if one does, they all stay here.)
234
+ const bridgeById = new Map(bridges.map((b) => [b.id, b]));
235
+ const linked = (b) => {
236
+ const seen = new Set([b.id]);
237
+ for (let from = b.unlocked_from_bridge_id; withCircle.has(from); from = bridgeById.get(from).unlocked_from_bridge_id) {
238
+ if (seen.has(from)) return false;
239
+ seen.add(from);
240
+ }
241
+ return withCircle.has(b.unlocked_from_bridge_id);
242
+ };
243
+ const roots = bridges.filter((b) => !linked(b));
244
+ // Bridges round you: one ring while they fit, more once there are many, each
245
+ // ring far enough from the last for the names under it.
246
+ const bridgeLayout = ringLayout(roots.length, {
247
+ inner: maxR * 0.55, innerMin: maxR * 0.4, outer: maxR * 0.62, spacing: 46, minSpacing: 20, ringGap: 62,
197
248
  });
198
- const bridgePos = bridges.map((b, i) => {
249
+ const bridgePos = roots.map((b, i) => {
199
250
  const p = bridgeLayout.points[i];
200
- return { ...b, x: cx + p.x, y: cy + p.y, angle: p.angle, clusterSize: (bridgeMap[b.id] || []).length };
251
+ return { ...b, x: cx + p.x, y: cy + p.y, angle: p.angle, clusterSize: (bridgeMap[b.id] || []).length, chains: chained.get(b.id) || 0 };
201
252
  });
253
+ // More than one ring: the counts under each name wait for a hover.
254
+ const crowded = bridgeLayout.rings.length > 1;
255
+ // Where a hovered circle is previewed, and the zoom at which that still fits the window.
256
+ const band = previewBand(maxR, bridgeLayout.rings);
257
+ const homeZoom = Math.max(0.6, Math.min(1.3, Math.min(cx, cy) / (band.outer + 8)));
258
+ const zoom = zoomSet ?? homeZoom;
202
259
 
203
260
  // Touch rotary dial on the overview: drag a finger round the circle to pick a bridge.
204
261
  const bridgeAnglesRef = useRef([]);
@@ -257,7 +314,7 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
257
314
  key={trail.map((r) => r.id).join('>')}
258
315
  trail={trail} index={index} reach={reach} dims={dims} requests={requests}
259
316
  scanningId={scanningId} still={still} canScan={canScan}
260
- onOpen={(id) => setPath(trail.map((r) => r.id).concat(id))}
317
+ onOpen={(ids) => setPath(trail.map((r) => r.id).concat(ids))}
261
318
  onBack={(depth) => setPath(trail.slice(0, depth).map((r) => r.id))}
262
319
  onSelect={onSelect}
263
320
  />
@@ -275,13 +332,15 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
275
332
  <div ref={containerRef} style={{ flex: 1, background: '#0a0a1a', position: 'relative', overflow: 'hidden' }}>
276
333
  <svg width={dims.w} height={dims.h}>
277
334
  <g transform={`translate(${cx * (1 - zoom)}, ${cy * (1 - zoom)}) scale(${zoom})`}>
278
- {/* Ring guide */}
279
- <circle cx={cx} cy={cy} r={maxR * 0.55} fill="none" stroke={`${DEGREE_COLORS[1]}10`} strokeWidth={1} />
335
+ {/* A guide line for every ring of bridges */}
336
+ {bridgeLayout.rings.map((ring) => (
337
+ <circle key={'ring-' + ring.radius} cx={cx} cy={cy} r={ring.radius} fill="none" stroke={`${DEGREE_COLORS[1]}10`} strokeWidth={1} />
338
+ ))}
280
339
 
281
340
  {/* Their circle, previewed on hover, in a wedge that grows rows as it fills */}
282
341
  {hovBridge && (
283
342
  <CirclePreview bridge={hovBridge} members={membersOf(hovBridge, index)} reach={reach}
284
- cx={cx} cy={cy} maxR={maxR} still={still} band={previewBand(maxR, bridgeLayout.rings)} />
343
+ cx={cx} cy={cy} maxR={maxR} still={still} band={band} />
285
344
  )}
286
345
 
287
346
  {/* Bridge nodes — hover to preview their circle, click to open it */}
@@ -296,24 +355,21 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
296
355
  onMouseEnter={() => setHovered(b.id)}
297
356
  onMouseLeave={() => setHovered(null)}
298
357
  style={{ cursor: 'pointer' }}>
358
+ <title>{bridgeTitle(b, ready, scanBars(b, reach))}</title>
299
359
  {/* Glow */}
300
- <circle cx={b.x} cy={b.y} r={isHov ? 20 : 16}
301
- fill={`${TIER_COLORS[b.tier]}08`}
302
- stroke={`${TIER_COLORS[b.tier]}${isHov ? '60' : '25'}`} strokeWidth={1} />
303
- {/* How much of their circle is scanned: five thin bars (design C) */}
304
- {(() => {
305
- const bars = scanBars(b, reach);
306
- if (bars == null) return null;
307
- return (
308
- <g transform={`translate(${b.x} ${b.y})`} pointerEvents="none">
309
- {ringSegments((isHov ? 20 : 16) + 3.5).map((seg) => (
310
- <path key={seg.i} d={seg.d} fill="none" strokeLinecap="round" strokeWidth={1.6}
311
- stroke={seg.i < bars ? RING.filled : RING.empty} />
312
- ))}
313
- <title>{bars === 5 ? 'Their whole list is scanned' : `About ${bars * 20}% of their list is scanned`}</title>
314
- </g>
315
- );
316
- })()}
360
+ <circle cx={b.x} cy={b.y} r={isHov ? 21 : 18} fill={`${TIER_COLORS[b.tier]}${isHov ? '14' : '08'}`} />
361
+ {/* The ring, tight on the dot: one bar for each person you added through
362
+ this circle. Orange: scanned since, with a cluster of their own.
363
+ Green: ready for a scan. Nobody yet: one faint line. */}
364
+ <g transform={`translate(${b.x} ${b.y})`} pointerEvents="none">
365
+ {b.chains + ready === 0 && (
366
+ <circle r={(isHov ? 13 : 11) + 3.6} fill="none" stroke={RING.empty} strokeWidth={1} />
367
+ )}
368
+ {reachSegments((isHov ? 13 : 11) + 3.6, { formed: b.chains, ready }).map((seg, i) => (
369
+ <path key={i} d={seg.d} fill="none" strokeLinecap="round" strokeWidth={2}
370
+ stroke={seg.kind === 'ready' ? GREEN : DEGREE_COLORS[2]} />
371
+ ))}
372
+ </g>
317
373
  {/* Node */}
318
374
  <circle cx={b.x} cy={b.y} r={isHov ? 13 : 11}
319
375
  fill={localPhoto(b.profile_image_url) ? '#1a1a2e' : TIER_COLORS[b.tier]}
@@ -331,24 +387,22 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
331
387
  <text x={b.x} y={b.y + 4} textAnchor="middle" fill={b.tier === 'S' ? '#000' : '#fff'}
332
388
  fontSize={11} fontWeight={700}>{b.name?.charAt(0)}</text>
333
389
  )}
334
- {/* People you reached through their circle, ready for a scan of their own */}
390
+ {/* How many in their circle are waiting for a scan: a small outlined number, top right */}
335
391
  {ready > 0 && (
336
- <g>
337
- <title>{`${ready} you reached through ${firstName(b)}’s circle ${ready === 1 ? 'is' : 'are'} ready for a scan`}</title>
338
- <rect x={b.x + 7} y={b.y - 21} width={ready > 9 ? 40 : 34} height={11} rx={5.5}
339
- fill="rgba(0,255,136,0.16)" stroke={GREEN} strokeWidth={0.6} />
340
- <text x={b.x + 7 + (ready > 9 ? 20 : 17)} y={b.y - 13} textAnchor="middle" fill={GREEN} fontSize={7} fontWeight={800}>
341
- {ready} ready
342
- </text>
392
+ <g pointerEvents="none">
393
+ <circle cx={b.x + 14} cy={b.y - 14} r={6} fill="#0a0a1a" stroke={GREEN} strokeWidth={1.2} />
394
+ <text x={b.x + 14} y={b.y - 11.4} textAnchor="middle" fill={GREEN} fontSize={ready > 9 ? 6 : 7} fontWeight={800}>{ready > 99 ? '99+' : ready}</text>
343
395
  </g>
344
396
  )}
345
397
  {/* Name + count */}
346
398
  <text x={b.x} y={b.y + (isHov ? 24 : 22)} textAnchor="middle" fill="#fff" fontSize={9} fontWeight={600}>
347
399
  {b.name?.split(' ')[0]}
348
400
  </text>
349
- <text x={b.x} y={b.y + (isHov ? 34 : 32)} textAnchor="middle" fill="#888" fontSize={7}>
350
- {b.clusterSize} · {sCount > 0 ? sCount + 'S ' : ''}{aCount > 0 ? aCount + 'A' : ''}
351
- </text>
401
+ {(!crowded || isHov) && (
402
+ <text x={b.x} y={b.y + (isHov ? 34 : 32)} textAnchor="middle" fill={isHov ? '#bbb' : '#888'} fontSize={7}>
403
+ {b.clusterSize} · {sCount > 0 ? sCount + 'S ' : ''}{aCount > 0 ? aCount + 'A' : ''}
404
+ </text>
405
+ )}
352
406
  </g>
353
407
  );
354
408
  })}
@@ -363,9 +417,9 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
363
417
 
364
418
  {/* Zoom controls, clear of the Galaxy switch below them */}
365
419
  <ZoomButtons
366
- onIn={() => setZoom(z => Math.min(3, z + 0.3))}
367
- onReset={() => setZoom(1.3)}
368
- onOut={() => setZoom(z => Math.max(0.4, z - 0.3))}
420
+ onIn={() => setZoom(z => Math.min(3, (z ?? homeZoom) + 0.3))}
421
+ onReset={() => setZoom(null)}
422
+ onOut={() => setZoom(z => Math.max(0.4, (z ?? homeZoom) - 0.3))}
369
423
  />
370
424
 
371
425
  {/* Depth tracker */}
@@ -394,7 +448,7 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
394
448
  ))}
395
449
  </div>
396
450
  <div style={{ fontSize: 9, color: '#555', marginTop: 8 }}>
397
- {bridges.length} bridges · {d2S} S + {d2A} A at 2nd degree
451
+ {roots.length} bridges{bridges.length > roots.length ? ` + ${bridges.length - roots.length} along their chains` : ''} · {d2S} S + {d2A} A at 2nd degree
398
452
  </div>
399
453
  {twoWays > 0 && (
400
454
  <div style={{ fontSize: 9, color: '#555', marginTop: 4 }}
@@ -408,6 +462,15 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
408
462
  );
409
463
  }
410
464
 
465
+ /** What a bridge's ring and number mean, in words, for its tooltip. */
466
+ function bridgeTitle(b, ready, bars) {
467
+ const parts = [];
468
+ if (ready) parts.push(`${ready} in ${firstName(b)}’s circle ${ready === 1 ? 'is' : 'are'} ready for a scan`);
469
+ if (b.chains) parts.push(`${b.chains} scanned since, with ${b.chains === 1 ? 'a cluster' : 'clusters'} of their own`);
470
+ if (bars != null) parts.push(bars === 5 ? 'their whole list is scanned' : `about ${bars * 20}% of their list is scanned`);
471
+ return parts.length ? parts.join(' · ') : `${firstName(b)}’s circle`;
472
+ }
473
+
411
474
  /** A bridge's circle on hover: the people you reached through it first, then their S, A and B. */
412
475
  function CirclePreview({ bridge, members, reach, cx, cy, maxR, still, band }) {
413
476
  const shown = members.filter((m) => m.degree === 1 || m.tier === 'S' || m.tier === 'A' || m.tier === 'B');
@@ -494,8 +557,17 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
494
557
  const person = trail[trail.length - 1];
495
558
  const depth = trail.length; // the person's degree along the chain: a bridge is 1
496
559
  const members = useMemo(() => membersOf(person, index), [person, index]);
497
- const [hover, setHover] = useState(null); // a member's index, 'center', or null
498
- const [view, setView] = useState({ k: 1, x: 0, y: 0 });
560
+ // The chains that lead on from here. Each link stays a dot in the circle it
561
+ // was met through; the cluster their scan formed sits outside, joined to
562
+ // that dot, so you can see where each path came from.
563
+ const trailKey = trail.map((r) => r.id).join('>');
564
+ const links = useMemo(
565
+ () => chainsFrom(person, index, trailKey.split('>'), Math.max(0, MAX_DEGREE - 1 - depth)),
566
+ [person, index, trailKey, depth],
567
+ );
568
+ const [hover, setHover] = useState(null); // a member's index, 'center', 'L' + a link's index, or null
569
+ // null until it's moved or zoomed: the fit worked out below.
570
+ const [viewSet, setViewSet] = useState(null);
499
571
  const drag = useRef(null);
500
572
  const svgRef = useRef(null);
501
573
  const router = useRouter();
@@ -503,25 +575,74 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
503
575
  const cx = dims.w / 2;
504
576
  const cy = dims.h / 2;
505
577
  const maxR = Math.min(cx, cy) - 30;
506
- // Whose own circle is known, drawn behind them (their people, one degree
507
- // further out): anyone you're connected to whose circle was scanned, or who
508
- // introduced you to someone.
509
- const behind = useMemo(() => members.map((row) => (row.degree === 1 ? membersOf(row, index) : [])), [members, index]);
510
- const hasFans = behind.some((people) => people.length > 0);
511
- // Tier bands round them, S nearest, like Network Circle's orbits; with fans
578
+ // Chains fan out to the side on a wide window, below on a tall one; the people
579
+ // you added sit on that side of the circle, so each line is short.
580
+ const toward = dims.w >= dims.h ? 0 : Math.PI / 2;
581
+ const hasChains = links.length > 0;
582
+ // Tier bands round them, S nearest, like Network Circle's orbits; with chains
512
583
  // to draw, the bands leave the outside of the circle for them.
513
584
  const layout = useMemo(() => tierBandLayout(members, {
514
- inner: Math.max(62, maxR * 0.3), outer: maxR * (hasFans ? 0.72 : 0.94),
585
+ inner: Math.max(62, maxR * 0.3), outer: maxR * (hasChains ? 0.72 : 0.94),
515
586
  spacing: Math.max(14, Math.min(24, maxR * 0.085)), minSpacing: 4, gap: Math.max(10, maxR * 0.04),
516
- }), [members, maxR, hasFans]);
517
- const fans = useMemo(() => {
518
- if (!hasFans) return [];
519
- const placed = outerFans(
520
- members.map((row, i) => ({ angle: layout.points[i].angle, slot: layout.points[i].slot, count: behind[i].length })),
521
- { from: layout.edge + layout.spacing * 1.5 },
522
- );
523
- return placed.map((f, i) => f && { ...f, i, people: behind[i].slice(0, f.points.length) }).filter(Boolean);
524
- }, [members, behind, hasFans, layout]);
587
+ start: hasChains ? toward : -Math.PI / 2,
588
+ }), [members, maxR, hasChains, toward]);
589
+
590
+ // Each link's people (anyone met through their circle first, so the next
591
+ // links of the chain are among the dots drawn) and the disc they need. The
592
+ // same size of dot for all of them, so a bigger circle is a bigger disc.
593
+ const sizes = useMemo(() => links.map(({ row }) => {
594
+ const people = membersOf(row, index);
595
+ const l = ringLayout(Math.min(people.length, LINK_DOTS), CLUSTER);
596
+ return { people, radius: (l.rings[l.rings.length - 1]?.radius || LINK_INNER) + 6 };
597
+ }), [links, index]);
598
+ // Where each cluster sits: straight out from its person's dot in the circle;
599
+ // further along a chain, straight out from the cluster before.
600
+ const tree = useMemo(() => {
601
+ if (!links.length) return null;
602
+ const widest = Math.max(...sizes.map((c) => c.radius));
603
+ const at = new Map(members.map((row, i) => [row.id, i]));
604
+ return chainTree(links.map((l) => l.parent), {
605
+ from: layout.edge + layout.spacing + widest + 22, hop: widest * 2 + 46, spacing: widest * 2 + 26, toward,
606
+ angles: links.map((l) => (l.parent < 0 ? layout.points[at.get(l.row.id)]?.angle : undefined)),
607
+ });
608
+ }, [links, sizes, members, layout, toward]);
609
+ // The dots of each cluster, its first ring starting on the side that faces
610
+ // outwards, where the next link of the chain will be.
611
+ const clusters = useMemo(() => sizes.map((c, j) => ({
612
+ ...c,
613
+ points: ringLayout(Math.min(c.people.length, LINK_DOTS), { ...CLUSTER, start: tree.points[j].angle }).points,
614
+ })), [sizes, tree]);
615
+ // The dot each path comes from: the link's own dot, in the big circle or in
616
+ // the cluster of whoever you met them through. `back` is who that was.
617
+ const origins = useMemo(() => {
618
+ const at = new Map(members.map((row, i) => [row.id, i]));
619
+ return links.map((link) => {
620
+ if (link.parent < 0) {
621
+ const p = layout.points[at.get(link.row.id)];
622
+ return p ? { x: p.x, y: p.y, r: dotRadius(layout.spacing, link.row.tier) * 1.4, back: { x: 0, y: 0, r: 30 } } : { x: 0, y: 0, r: 30, back: null };
623
+ }
624
+ const hub = tree.points[link.parent];
625
+ const i = clusters[link.parent].people.findIndex((row) => row.id === link.row.id);
626
+ const pt = clusters[link.parent].points[i] || { x: 0, y: 0 };
627
+ return { x: hub.x + pt.x, y: hub.y + pt.y, r: KID_DOT, inCluster: true, back: { x: hub.x, y: hub.y, r: LINK_HUB } };
628
+ });
629
+ }, [links, members, layout, tree, clusters]);
630
+ // Fit the bands and every chain in the window to start with.
631
+ const fit = useMemo(() => {
632
+ if (!tree) return { k: 1, x: 0, y: 0 };
633
+ const box = { l: -layout.edge, r: layout.edge, t: -layout.edge, b: layout.edge };
634
+ tree.points.forEach((p, j) => {
635
+ const r = clusters[j].radius;
636
+ box.l = Math.min(box.l, p.x - r - 30); box.r = Math.max(box.r, p.x + r + 30);
637
+ box.t = Math.min(box.t, p.y - r - 8); box.b = Math.max(box.b, p.y + r + 30);
638
+ });
639
+ // Room at the top for the trail, and a little at every other edge.
640
+ const pad = { side: 70, top: 96, bottom: 30 };
641
+ const k = Math.max(0.35, Math.min(1, (dims.w - pad.side * 2) / (box.r - box.l), (dims.h - pad.top - pad.bottom) / (box.b - box.t)));
642
+ return { k, x: (-(box.l + box.r) / 2) * k, y: (-(box.t + box.b) / 2) * k + (pad.top - pad.bottom) / 2 };
643
+ }, [tree, clusters, layout, dims.w, dims.h]);
644
+ const view = viewSet ?? fit;
645
+ const setView = (next) => setViewSet((v) => (typeof next === 'function' ? next(v ?? fit) : next));
525
646
 
526
647
  // What each dot is: someone you reached (ready, hidden, scanned) or a
527
648
  // 2nd-degree row, maybe with a request out; and how big their own circle is.
@@ -543,11 +664,14 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
543
664
  const dots = useMemo(() => {
544
665
  const few = members.length <= 60;
545
666
  const roomy = layout.spacing * k >= 26;
667
+ // The people you added sit side by side, so their names take turns above and below.
668
+ let named = 0;
546
669
  return members.map((row, i) => {
547
670
  const p = layout.points[i];
548
671
  const f = facts[i];
549
672
  const r = dotRadius(layout.spacing, row.tier) * (f.reached ? 1.4 : 1);
550
673
  const color = f.reached === 'hidden' ? HIDDEN : TIER_COLORS[row.tier] || '#555';
674
+ const above = f.reached ? named++ % 2 === 1 : false;
551
675
  return (
552
676
  <g key={row.id}>
553
677
  {(few || f.reached) && (
@@ -567,7 +691,7 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
567
691
  <circle cx={p.x} cy={p.y} r={r * 2} fill="none" stroke={DEGREE_COLORS[3]} strokeWidth={1 / k} strokeDasharray={`${3 / k} ${2 / k}`} />
568
692
  )}
569
693
  {(f.reached || (roomy && row.tier === 'S')) && (
570
- <text x={p.x} y={p.y + r + 10 / k} textAnchor="middle" fontSize={8.5 / k} fontWeight={f.reached ? 700 : 400}
694
+ <text x={p.x} y={above ? p.y - r - 4 / k : p.y + r + 10 / k} textAnchor="middle" fontSize={8.5 / k} fontWeight={f.reached ? 700 : 400}
571
695
  fill={f.reached === 'hidden' ? '#999' : f.reached ? GREEN : '#aaa'} pointerEvents="none"
572
696
  stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">
573
697
  {firstName(row)}
@@ -586,6 +710,10 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
586
710
  const wx = (clientX - rect.left - cx - view.x) / k;
587
711
  const wy = (clientY - rect.top - cy - view.y) / k;
588
712
  if (Math.hypot(wx, wy) <= 30) return 'center';
713
+ if (tree) {
714
+ const j = tree.points.findIndex((p, i) => Math.hypot(p.x - wx, p.y - wy) <= Math.max(16, clusters[i].radius));
715
+ if (j >= 0) return 'L' + j;
716
+ }
589
717
  const near = Math.max(layout.spacing * 0.6, 10 / k);
590
718
  let best = null;
591
719
  let bestD = near;
@@ -620,6 +748,7 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
620
748
  if (!d || d.moved) return;
621
749
  const hit = hitAt(e.clientX, e.clientY);
622
750
  if (hit === 'center') onSelect?.(person);
751
+ else if (typeof hit === 'string') onOpen(chainTo(Number(hit.slice(1))));
623
752
  else if (hit != null && goesToScan(hit)) router.push(scanPageFor(members[hit]));
624
753
  else if (hit != null) onOpen(members[hit].id);
625
754
  };
@@ -633,6 +762,15 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
633
762
  zoomBy(Math.exp(-e.deltaY * 0.0015), { x: e.clientX - rect.left - cx, y: e.clientY - rect.top - cy });
634
763
  };
635
764
 
765
+ // From the person in the middle out to link j: the ids to open, nearest first.
766
+ const chainTo = (j) => {
767
+ const ids = [];
768
+ for (let i = j; i >= 0; i = links[i].parent) ids.unshift(links[i].row.id);
769
+ return ids;
770
+ };
771
+ const hovLink = typeof hover === 'string' && hover[0] === 'L' ? Number(hover.slice(1)) : -1;
772
+ const litLinks = hovLink >= 0 ? new Set(chainTo(hovLink)) : null;
773
+
636
774
  const ready = facts.filter((f) => f.reached === 'ready').length;
637
775
  const hidden = facts.filter((f) => f.reached === 'hidden').length;
638
776
  const added = facts.filter((f) => f.reached).length;
@@ -659,28 +797,70 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
659
797
  stroke={TIER_COLORS[band.tier] || '#555'} strokeOpacity={0.07}
660
798
  strokeWidth={Math.max(1 / k, band.outer - band.inner + layout.spacing)} pointerEvents="none" />
661
799
  ))}
662
- {/* Behind anyone whose circle is known: their people, a faint line to each */}
663
- {fans.map((fan) => {
664
- const a = layout.points[fan.i];
665
- const last = fan.points[fan.points.length - 1];
800
+ {dots}
801
+ {/* The chains that lead on from here. Every cluster is the circle of one
802
+ person you added and then scanned; the line to it starts at that
803
+ person's own dot, so you can see where the path came from. */}
804
+ {tree && links.map((link, j) => {
805
+ const p = tree.points[j];
806
+ const c = clusters[j];
807
+ const row = link.row;
808
+ const lit = litLinks?.has(row.id);
809
+ const color = DEGREE_COLORS[depth + link.step] || DEGREE_COLORS[6];
666
810
  return (
667
- <g key={'fan-' + members[fan.i].id} pointerEvents="none">
668
- {fan.points.map((p, j) => (
669
- <line key={'l' + j} x1={a.x} y1={a.y} x2={p.x} y2={p.y}
670
- stroke={GREEN} strokeOpacity={0.12} strokeWidth={0.5 / k} />
811
+ <g key={'lk-' + row.id} transform={`translate(${p.x} ${p.y})`} pointerEvents="none"
812
+ opacity={litLinks && !lit ? 0.45 : 1}>
813
+ <circle r={c.radius} fill="#0a0a1a" fillOpacity={0.9} stroke={color} strokeOpacity={lit ? 0.7 : 0.25} strokeWidth={1 / k} />
814
+ {/* Their people, each joined to the middle of the cluster */}
815
+ {clusterPaths(c).map(([tier, { spokes }]) => (
816
+ <path key={'s' + tier} d={spokes} fill="none" stroke={TIER_COLORS[tier] || '#555'}
817
+ strokeWidth={0.7} strokeOpacity={lit ? 0.6 : 0.38} />
671
818
  ))}
672
- {fan.points.map((p, j) => (
673
- <circle key={'d' + j} cx={p.x} cy={p.y} r={2.2}
674
- fill={TIER_COLORS[fan.people[j]?.tier] || '#555'} fillOpacity={0.75} />
819
+ {clusterPaths(c).map(([tier, { dots: d }]) => (
820
+ <path key={tier} d={d} fill={TIER_COLORS[tier] || '#555'} fillOpacity={lit ? 1 : 0.9} />
675
821
  ))}
676
- {fan.more > 0 && last && (
677
- <text x={last.x * 1.06} y={last.y * 1.06} textAnchor="middle" fontSize={8 / k} fill="#8b9a9a"
678
- stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">+{fan.more.toLocaleString()}</text>
822
+ <circle r={LINK_HUB} fill={color} stroke={hovLink === j ? '#fff' : '#0a0a1a'} strokeWidth={1.5} />
823
+ {row.id === scanningId && (
824
+ <circle r={c.radius + 4} fill="none" stroke={DEGREE_COLORS[3]} strokeWidth={1 / k} strokeDasharray={`${3 / k} ${2 / k}`} />
825
+ )}
826
+ <text y={c.radius + 12 / k} textAnchor="middle" fill="#fff" fontSize={10 / k} fontWeight={700}
827
+ stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">{firstName(row)}’s circle</text>
828
+ <text y={c.radius + 23 / k} textAnchor="middle" fill={color} fontSize={8 / k}
829
+ stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">
830
+ {c.people.length.toLocaleString('en-US')} · {ordinal(depth + link.step + 1)} degree
831
+ </text>
832
+ </g>
833
+ );
834
+ })}
835
+ {/* The paths, on top of the clusters they cross: from whoever you met
836
+ them through, to their own dot, and on out to the cluster their scan formed. */}
837
+ {tree && links.map((link, j) => {
838
+ const o = origins[j];
839
+ const p = tree.points[j];
840
+ const lit = litLinks?.has(link.row.id);
841
+ const color = DEGREE_COLORS[depth + link.step] || DEGREE_COLORS[6];
842
+ const seg = (a, ra, b, rb, key) => {
843
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
844
+ if (len <= ra + rb) return null;
845
+ const ux = (b.x - a.x) / len;
846
+ const uy = (b.y - a.y) / len;
847
+ return (
848
+ <line key={key} x1={a.x + ux * ra} y1={a.y + uy * ra} x2={b.x - ux * rb} y2={b.y - uy * rb}
849
+ stroke={color} strokeLinecap="round" strokeWidth={(lit ? 2.6 : 1.8) / k}
850
+ strokeOpacity={litLinks && !lit ? 0.3 : lit ? 1 : 0.85} />
851
+ );
852
+ };
853
+ return (
854
+ <g key={'ln-' + link.row.id} pointerEvents="none">
855
+ {o.back && seg(o.back, o.back.r, o, o.r + 1, 'a')}
856
+ {seg(o, o.r + 1, p, LINK_HUB + 1, 'b')}
857
+ {/* Their own dot inside the cluster they were met through */}
858
+ {o.inCluster && (
859
+ <circle cx={o.x} cy={o.y} r={o.r} fill={TIER_COLORS[link.row.tier] || '#555'} stroke={GREEN} strokeWidth={1.4} />
679
860
  )}
680
861
  </g>
681
862
  );
682
863
  })}
683
- {dots}
684
864
  {/* The one under the pointer, on top */}
685
865
  {hp && (
686
866
  <>
@@ -708,14 +888,23 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
708
888
  <Tip x={cx + view.x + hp.x * k} y={cy + view.y + hp.y * k} w={dims.w}
709
889
  lines={tipFor(hovered, facts[hover], scanningId, depth, goesToScan(hover))} accent={facts[hover].reached ? GREEN : TIER_COLORS[hovered.tier]} />
710
890
  )}
891
+ {hovLink >= 0 && (
892
+ <Tip x={cx + view.x + tree.points[hovLink].x * k} y={cy + view.y + (tree.points[hovLink].y - clusters[hovLink].radius) * k} w={dims.w}
893
+ lines={[`${links[hovLink].row.name}’s circle`,
894
+ `${clusters[hovLink].people.length.toLocaleString('en-US')} people · ${ordinal(depth + links[hovLink].step + 1)} degree`,
895
+ `You met ${firstName(links[hovLink].row)} through ${firstName(links[hovLink].parent >= 0 ? links[links[hovLink].parent].row : person)}’s circle, then scanned`,
896
+ 'Click to open it']}
897
+ accent={DEGREE_COLORS[depth + links[hovLink].step] || DEGREE_COLORS[6]} />
898
+ )}
711
899
  {hover === 'center' && (
712
900
  <Tip x={cx + view.x} y={cy + view.y - 28 * k} w={dims.w} lines={[person.name, 'Open their card']} accent={TIER_COLORS[person.tier]} />
713
901
  )}
714
902
  </svg>
715
903
 
716
904
  {/* The way back: one circle at a time, or straight to any on the trail.
717
- Clear of the Filters tab at the left edge. */}
718
- <div style={{ position: 'absolute', top: 14, left: 64, right: 64, pointerEvents: 'none', display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'flex-start' }}>
905
+ Clear of the Filters tab at the left edge, and under the notch that
906
+ holds the view buttons at the top of the map. */}
907
+ <div style={{ position: 'absolute', top: 54, left: 64, right: 64, pointerEvents: 'none', display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'flex-start' }}>
719
908
  <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', pointerEvents: 'auto' }}>
720
909
  <button type="button" onClick={() => onBack(depth - 1)} style={{
721
910
  padding: '8px 16px', borderRadius: 8, border: 'none', cursor: 'pointer',
@@ -778,11 +967,36 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
778
967
  </div>
779
968
  )}
780
969
 
781
- <ZoomButtons onIn={() => zoomBy(1.35)} onReset={() => setView({ k: 1, x: 0, y: 0 })} onOut={() => zoomBy(1 / 1.35)} />
970
+ <ZoomButtons onIn={() => zoomBy(1.35)} onReset={() => setViewSet(null)} onOut={() => zoomBy(1 / 1.35)} />
782
971
  </>
783
972
  );
784
973
  }
785
974
 
975
+ // A link's cluster draws at most this many of their people; the count beside it is the whole circle.
976
+ const LINK_DOTS = 290;
977
+ // The middle of a cluster, where its first ring starts, and a link's own dot inside one.
978
+ const LINK_HUB = 5;
979
+ const LINK_INNER = 15;
980
+ const KID_DOT = 4.6;
981
+ const CLUSTER = { inner: LINK_INNER, outer: Infinity, spacing: 7.5, minSpacing: 7.5 };
982
+
983
+ /**
984
+ * A cluster as two paths per tier, however many people: the dots, and a line
985
+ * from the middle of the cluster out to each of them.
986
+ */
987
+ function clusterPaths(cluster) {
988
+ const paths = {};
989
+ cluster.points.forEach((pt, i) => {
990
+ const tier = cluster.people[i]?.tier || 'D';
991
+ const q = tier === 'S' ? 3.1 : tier === 'A' ? 2.8 : 2.5;
992
+ const len = Math.hypot(pt.x, pt.y) || 1;
993
+ const at = (paths[tier] ||= { dots: '', spokes: '' });
994
+ at.dots += `M${(pt.x - q).toFixed(1)},${pt.y.toFixed(1)}a${q},${q} 0 1,0 ${2 * q},0a${q},${q} 0 1,0 ${-2 * q},0`;
995
+ at.spokes += `M${((pt.x / len) * LINK_HUB).toFixed(1)},${((pt.y / len) * LINK_HUB).toFixed(1)}L${pt.x.toFixed(1)},${pt.y.toFixed(1)}`;
996
+ });
997
+ return Object.entries(paths);
998
+ }
999
+
786
1000
  /** The lines of a dot's tooltip. `toScan`: a click goes to the Scan page with them picked. */
787
1001
  function tipFor(row, f, scanningId, depth, toScan) {
788
1002
  const first = firstName(row);