six-degrees 0.4.10 → 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 (414) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +9 -9
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +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 +1 -1
  29. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  48. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
  50. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/route.js +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
  60. package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
  61. package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
  62. package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
  63. package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
  64. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
  65. package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
  66. package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
  67. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/api/social/route.js +4 -3
  69. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  71. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  74. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  76. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  80. package/.next/standalone/.next/server/app/import.html +1 -1
  81. package/.next/standalone/.next/server/app/import.rsc +15 -15
  82. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
  83. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  84. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
  85. package/.next/standalone/.next/server/app/index.html +1 -1
  86. package/.next/standalone/.next/server/app/index.rsc +15 -15
  87. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  88. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  89. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  90. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/launch.html +2 -2
  93. package/.next/standalone/.next/server/app/launch.rsc +15 -15
  94. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
  95. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
  97. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  101. package/.next/standalone/.next/server/app/paths.html +1 -1
  102. package/.next/standalone/.next/server/app/paths.rsc +15 -15
  103. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
  104. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  105. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
  106. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/app/profile.html +1 -1
  109. package/.next/standalone/.next/server/app/profile.rsc +15 -15
  110. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
  111. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  112. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
  113. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/app/queue.html +1 -1
  116. package/.next/standalone/.next/server/app/queue.rsc +15 -15
  117. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
  118. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  119. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
  120. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  122. package/.next/standalone/.next/server/app/scores.html +1 -1
  123. package/.next/standalone/.next/server/app/scores.rsc +15 -15
  124. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
  125. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  126. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
  127. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  128. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  129. package/.next/standalone/.next/server/app/settings.html +1 -1
  130. package/.next/standalone/.next/server/app/settings.rsc +15 -15
  131. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  132. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  133. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
  134. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  135. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  136. package/.next/standalone/.next/server/app/setup.html +1 -1
  137. package/.next/standalone/.next/server/app/setup.rsc +15 -15
  138. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
  139. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  140. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
  141. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  142. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  143. package/.next/standalone/.next/server/app/social.html +1 -1
  144. package/.next/standalone/.next/server/app/social.rsc +15 -15
  145. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
  146. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  147. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
  148. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  149. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__05_3-22._.js} +2 -2
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__13dx4sd._.js} +7 -2
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
  156. package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  159. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
  162. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
  163. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
  165. package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
  166. package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
  167. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
  168. package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
  169. package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
  170. package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
  172. package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
  173. package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
  174. package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
  175. package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
  176. package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
  177. package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
  178. package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
  180. package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
  181. package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
  182. package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
  183. package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
  184. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
  186. package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
  187. package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
  188. package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
  190. package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
  192. package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
  193. package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
  195. package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
  196. package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
  197. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
  198. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  199. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  200. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
  201. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  202. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
  203. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
  204. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  205. package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
  206. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
  207. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  208. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  209. package/.next/standalone/.next/server/pages/404.html +1 -1
  210. package/.next/standalone/.next/server/pages/500.html +1 -1
  211. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  212. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  213. package/.next/standalone/.next/static/chunks/03bziijw5srol.js +1 -0
  214. package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  215. package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  216. package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  217. package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  218. package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  219. package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  220. package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
  221. package/.next/{static/chunks/1xptlvtcz6654.css → standalone/.next/static/chunks/1x_ysly42-teq.css} +1 -1
  222. package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  223. package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
  224. package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  225. package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  226. package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
  227. package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  228. package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  229. package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  230. package/.next/standalone/CHANGELOG.md +235 -0
  231. package/.next/standalone/app/api/data/export/route.js +2 -1
  232. package/.next/standalone/app/api/ingest/route.js +40 -1
  233. package/.next/standalone/app/api/scraper/route.js +12 -2
  234. package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
  235. package/.next/standalone/app/api/social/crm/route.js +37 -0
  236. package/.next/standalone/app/api/social/messages/route.js +44 -8
  237. package/.next/standalone/app/api/social/route.js +80 -22
  238. package/.next/standalone/app/components/AppTabs.js +59 -0
  239. package/.next/standalone/app/components/ChainView.js +303 -89
  240. package/.next/standalone/app/components/CirclePanel.js +162 -0
  241. package/.next/standalone/app/components/CompanyScores.js +2 -2
  242. package/.next/standalone/app/components/FieldStep.js +4 -4
  243. package/.next/standalone/app/components/FilterPanel.js +121 -183
  244. package/.next/standalone/app/components/ForceGraph.js +41 -3
  245. package/.next/standalone/app/components/GalaxyLab.js +26 -31
  246. package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
  247. package/.next/standalone/app/components/MapControls.js +154 -0
  248. package/.next/standalone/app/components/NoteDetail.js +118 -0
  249. package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
  250. package/.next/standalone/app/components/PeopleMap.js +308 -0
  251. package/.next/standalone/app/components/ScanRadar.js +129 -0
  252. package/.next/standalone/app/components/ScanStatusBar.js +72 -27
  253. package/.next/standalone/app/components/SeparationView.js +626 -118
  254. package/.next/standalone/app/components/Sidebar.js +28 -2
  255. package/.next/standalone/app/components/settings/DataSection.js +12 -3
  256. package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
  257. package/.next/standalone/app/components/settings/SectorSection.js +2 -2
  258. package/.next/standalone/app/components/settings/TierSection.js +1 -1
  259. package/.next/standalone/app/components/settings/TitleSection.js +149 -0
  260. package/.next/standalone/app/components/views.js +15 -14
  261. package/.next/standalone/app/globals.css +11 -0
  262. package/.next/standalone/app/page.js +89 -156
  263. package/.next/standalone/app/paths/page.js +39 -27
  264. package/.next/standalone/app/profile/page.js +33 -5
  265. package/.next/standalone/app/queue/page.js +56 -34
  266. package/.next/standalone/app/scores/page.js +16 -66
  267. package/.next/standalone/app/settings/page.js +27 -13
  268. package/.next/standalone/app/setup/page.js +298 -205
  269. package/.next/standalone/app/social/Crm.js +683 -0
  270. package/.next/standalone/app/social/SocialHub.js +374 -0
  271. package/.next/standalone/app/social/Thread.js +90 -0
  272. package/.next/standalone/app/social/page.js +10 -351
  273. package/.next/standalone/lib/chain-layout.js +105 -35
  274. package/.next/standalone/lib/data-export.js +10 -4
  275. package/.next/standalone/lib/data-folder.js +9 -0
  276. package/.next/standalone/lib/data-import.js +13 -3
  277. package/.next/standalone/lib/degrees.js +0 -6
  278. package/.next/standalone/lib/dot-rings.js +25 -0
  279. package/.next/standalone/lib/galaxy-lab.js +65 -6
  280. package/.next/standalone/lib/ingest.js +2 -0
  281. package/.next/standalone/lib/island.js +40 -0
  282. package/.next/standalone/lib/linkedin-export.js +267 -46
  283. package/.next/standalone/lib/linkedin-limits.js +6 -2
  284. package/.next/standalone/lib/map-heat.js +20 -0
  285. package/.next/standalone/lib/network-health.js +1 -1
  286. package/.next/standalone/lib/notifications.js +121 -0
  287. package/.next/standalone/lib/people-map.js +82 -0
  288. package/.next/standalone/lib/rarity.js +45 -3
  289. package/.next/standalone/lib/reach.js +7 -6
  290. package/.next/standalone/lib/rpc.js +15 -4
  291. package/.next/standalone/lib/scan-pace.js +46 -0
  292. package/.next/standalone/lib/scoring.js +39 -8
  293. package/.next/standalone/lib/scraper-client.js +19 -6
  294. package/.next/standalone/lib/sector-focus.js +4 -4
  295. package/.next/standalone/lib/separation.js +137 -10
  296. package/.next/standalone/lib/settings-effects.js +20 -11
  297. package/.next/standalone/lib/settings.js +3 -0
  298. package/.next/standalone/lib/social-crm.js +415 -0
  299. package/.next/standalone/lib/social-store.js +13 -6
  300. package/.next/standalone/lib/tier-grid.js +99 -0
  301. package/.next/standalone/lib/title-ranking.js +83 -0
  302. package/.next/standalone/package-lock.json +2 -2
  303. package/.next/standalone/package.json +1 -1
  304. package/.next/standalone/scripts/scrape.py +177 -35
  305. package/.next/static/chunks/03bziijw5srol.js +1 -0
  306. package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
  307. package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
  308. package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
  309. package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
  310. package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
  311. package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
  312. package/.next/static/chunks/1i51yylxdzht1.js +1 -0
  313. package/.next/{standalone/.next/static/chunks/1xptlvtcz6654.css → static/chunks/1x_ysly42-teq.css} +1 -1
  314. package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
  315. package/.next/static/chunks/2rqtpccruavf2.js +1 -0
  316. package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
  317. package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
  318. package/.next/static/chunks/355ryio1b7sm1.js +1 -0
  319. package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
  320. package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
  321. package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
  322. package/CHANGELOG.md +235 -0
  323. package/lib/chain-layout.js +105 -35
  324. package/lib/data-export.js +10 -4
  325. package/lib/data-folder.js +9 -0
  326. package/lib/data-import.js +13 -3
  327. package/lib/degrees.js +0 -6
  328. package/lib/dot-rings.js +25 -0
  329. package/lib/galaxy-lab.js +65 -6
  330. package/lib/ingest.js +2 -0
  331. package/lib/island.js +40 -0
  332. package/lib/linkedin-export.js +267 -46
  333. package/lib/linkedin-limits.js +6 -2
  334. package/lib/map-heat.js +20 -0
  335. package/lib/network-health.js +1 -1
  336. package/lib/notifications.js +121 -0
  337. package/lib/people-map.js +82 -0
  338. package/lib/rarity.js +45 -3
  339. package/lib/reach.js +7 -6
  340. package/lib/rpc.js +15 -4
  341. package/lib/scan-pace.js +46 -0
  342. package/lib/scoring.js +39 -8
  343. package/lib/scraper-client.js +19 -6
  344. package/lib/sector-focus.js +4 -4
  345. package/lib/separation.js +137 -10
  346. package/lib/settings-effects.js +20 -11
  347. package/lib/settings.js +3 -0
  348. package/lib/social-crm.js +415 -0
  349. package/lib/social-store.js +13 -6
  350. package/lib/tier-grid.js +99 -0
  351. package/lib/title-ranking.js +83 -0
  352. package/package.json +1 -1
  353. package/scripts/scrape.py +177 -35
  354. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
  355. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
  356. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
  357. package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
  358. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
  359. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
  360. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
  361. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
  362. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
  363. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
  364. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
  365. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
  366. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
  367. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
  368. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
  369. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
  370. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
  371. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
  372. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
  373. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
  374. package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
  375. package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
  376. package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
  377. package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
  378. package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
  379. package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
  380. package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
  381. package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
  382. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
  383. package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +0 -1
  384. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  385. package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
  386. package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
  387. package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  388. package/.next/standalone/.next/static/chunks/206rm6j42gl_a.js +0 -1
  389. package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
  390. package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
  391. package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  392. package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  393. package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  394. package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  395. package/.next/standalone/app/components/OrbitGraph.js +0 -737
  396. package/.next/standalone/app/social/Conversations.js +0 -246
  397. package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
  398. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  399. package/.next/static/chunks/160wjb73ddwqd.js +0 -6
  400. package/.next/static/chunks/1c_n0973t07l0.js +0 -2
  401. package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
  402. package/.next/static/chunks/206rm6j42gl_a.js +0 -1
  403. package/.next/static/chunks/236f0jtp68j_h.js +0 -1
  404. package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
  405. package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
  406. package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  407. package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
  408. package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
  409. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  410. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  411. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
  412. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
  413. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
  414. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,241 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-10-02
10
+
11
+ ### Added
12
+ - **Rank titles your way** (Settings → Scores → Titles). Set how much each kind of title counts, out
13
+ of 10, or start from a preset: Founders first, Investors, or A role I'm looking for. Add the roles
14
+ you're after ("account executive", "head of growth", "recruiter") with their own points, and
15
+ everyone whose title has those words rises, whatever their level: for outbound to one role, or
16
+ hiring for it. Saving rescores everyone and says how many people changed tier, and a card's score
17
+ says "your ranking" where it moved a title. Nothing set scores exactly as before.
18
+
19
+ ### Changed
20
+ - **The Scan page is a launch, not a manual.** It opens with what the scanner is and what it never does
21
+ (never posts or messages anyone, never sees your password, nothing leaves this Mac, stops when you
22
+ say), then the four steps on a rail: Get ready, Sign in, Who you know, Who they know. The one
23
+ you're on pulses, with one big button that does it and how long it takes; any other opens with a
24
+ tap. While it runs, "Your scanner is working" and Stop sit right under the step. How many people
25
+ this round and who first are beside the radar; the daily budget, how much of each list, the hidden
26
+ window, retrying hidden lists and paused lists are in Fine-tune, closed until you want them. The
27
+ log is one tap away, and opens by itself when a run stops badly. Scan has the same header as every
28
+ page now.
29
+ - **Paths reads like Network Circle.** Its notch is now Map and Companies: companies count for more
30
+ than industries, and an industry opens from a company's card (its industry is a link there). Whether
31
+ the map's bubbles are companies or your connections (what was the People tab) is a switch at the
32
+ top of the Filters panel. Filters use the same tiers × degrees grid as Network Circle. Colour is by
33
+ sector or by **Heat**: for companies, how strong your people there are (the average power of the
34
+ five strongest); for people, how strong their cluster is, person for person; size still says how
35
+ many. And **Physics**: switch it to Live to drag bubbles (the ones joined to them follow) and change
36
+ Spread, Pull to their group and Pull along lines. Still, as before, is the default. Old links to
37
+ the People or Industries tab still land on the map.
38
+ - **Heat: power as a thermal map.** A new Colour by in Network Circle's Physics: every dot coloured
39
+ from cold violet through red and orange to white hot by where their power score ranks among the
40
+ people showing, with a soft glow behind the hotter ones that pools where powerful people cluster,
41
+ like a thermal camera. Works with every layout, the replay and the filter grid.
42
+ - **Separation all the way right counts every mutual, scanned or not.** It aims at whoever you share
43
+ the most mutual connections with, even when most of them are in circles you haven't scanned: an
44
+ unscanned mutual is still a likely introduction, so it no longer holds someone back. The ones the
45
+ app can draw are lines as before; the rest are grey dots, each with a faint line in, with "+37 more
46
+ mutuals, in circles not scanned yet". The person glows less the less of it is scanned. The Next
47
+ names are in the same order and say their mutual counts. Narrow the tiers (S only, say) first to
48
+ aim at the strongest.
49
+ - **The right panel follows the circle you're in.** Open someone's circle in Bridge Chains and the
50
+ panel is about them: how many people are in their circle, tier chips to see just one tier, a
51
+ search inside it, the people you added from it (with Scan… for the ones ready), then everyone
52
+ else in it, strongest first. Click anyone for their card.
53
+ - **Orbit is a layout of the Galaxy now**, in Physics next to Rings and Clusters, not a separate
54
+ view: every tier held on its own orbit, S nearest you, and each connection's circle tucked in
55
+ behind them. It's still the Galaxy, so you can drag dots and move every slider on it.
56
+ - **Light up a branch on hover starts off.** It dimmed and relit thousands of dots on every hover,
57
+ which flickered; it's still in Physics if you want it, and moving from one dot to the next no
58
+ longer flashes the whole Galaxy in between.
59
+ - **Social is part of Outlink.** Outlink has the same header as every page, and its views sit in
60
+ the notch: Circles, To add, Pending, and Messages & follow-ups, which is the whole Social tab (your
61
+ LinkedIn export, the live messages sync, the CRM with its inbox, follow-ups, pipeline, sent and
62
+ received). Adding people and keeping track of them are now one place. The Social tab is gone from
63
+ the header, and old links to it land on Messages & follow-ups.
64
+ - **Notifications open.** Click one and it takes over the right panel with the detail: who it is
65
+ about, their tier and score, who you met them through, and, for an S or A, that they're a
66
+ valuable person to know; with buttons to open their card, their circle in Bridge Chains, or
67
+ them in Separation. A notification naming several people lists them all.
68
+ - **New notifications:** someone you met through a bridge added you back ("Ana added you back,
69
+ through Yuki"); a circle scan found people at top companies (a company score of 8 or more), named
70
+ in one notification; and a scan finished ("Scan done: Yuki's circle", "Check for new: done").
71
+ Accepted requests and high-value new connections now say who they are about, and older ones are
72
+ matched by the name in their title.
73
+ - **Paths has the same header as the map.** The app's tabs stay at the top, with Paths lit, and Map,
74
+ People, Industries and Companies sit in the notch under them, as the map page's views do; a tab
75
+ for Network Circle, Degrees or Separation takes you straight to it. Paths' filters moved into a
76
+ Filters panel on the left, like the map page's, with new ways to read the map: what a bubble's
77
+ size means (your people there, the S and A among them, or the ones you already know; on People,
78
+ cluster value, size or S tier) and which names show (the biggest, all, or none).
79
+ - **All the way to easy, Separation aims at whoever the most of your connections lead to**, with
80
+ every one of those lines drawn, the strongest first among equals; the "Next" names are the
81
+ runners-up by the same measure. It used to aim at the top of the list, which could be someone
82
+ with many mutual connections by LinkedIn's count but only one the app could draw.
83
+ - **The Scan page has a radar, and a speed: Slow, Medium or Fast.** Mapping the 2nd degree starts
84
+ from a round Scan button in the middle of a radar: its sweep turns while a scan runs, and the
85
+ ring round it fills with today's searches against your budget. Beside it, three speeds. Fast is
86
+ how the scanner has always run, and nothing is faster; Medium waits 45 seconds before each page
87
+ of results and 3 minutes after every 10, Slow 90 seconds and 5 minutes, and both keep profiles
88
+ further apart (90 and 120 seconds). Each says about how many searches an hour it makes and how
89
+ long the rest of today's budget would take. The daily budget stays the cap at every speed:
90
+ slower spreads it out, it doesn't shrink it. The speed is saved with the budget and the scanner
91
+ says which it is using at the start of every run; the Scan page's time estimates follow it.
92
+ - **Scores moved into Settings.** Your field, how tiers are graded and every company's score are a
93
+ Scores section in Settings (the gear), and the Scores tab is gone from the header. Old links to
94
+ the Scores tab land in the same place in Settings.
95
+ - **Profile has Insights.** An Insights button beside Profile shows network health: how much of
96
+ your 2nd degree you reach two or more ways, the effective reach of your connections, and the
97
+ connections who reach the most people no one else does. It was at the top of the Scores tab.
98
+ - **Paths → People.** A new view beside the company map, drawn the same way but with your
99
+ connections as the bubbles: each is sized by the value of the cluster behind them (S tier counts
100
+ 3, A tier 2, everyone else 1; or by cluster size, or by S tier inside), coloured by their sector
101
+ and grouped like the companies. A white centre is the share of their cluster you can only reach
102
+ through them, a gold ring means S-tier people inside, and a line joins two connections whose
103
+ clusters share people. Point at someone for their numbers and how their cluster compares to the
104
+ rest you've scanned; click to open it in Bridge Chains. Shows the connections you've scanned,
105
+ or all of them, with the ones not scanned yet as small grey dots.
106
+ - **Separation's map draws connections as pills and people as cards** on a computer (a phone keeps
107
+ the dots). Each connection shows their initials, tier and what they are to the people on the
108
+ map: the only door to someone, the best way in for some, or only another way in; the ones with
109
+ a solid line are lit. Each person is a card with rank, role, score and how many ways in. With
110
+ the slider all the way to easy, the one person becomes a large card with LinkedIn's mutual
111
+ count and how many of those are drawn. Everything still glides as the slider moves.
112
+ - **Separation's list is a ledger.** A title bar says how many are shown and that the order
113
+ follows the slider. The way in shows the connection's face with "only way in" or how many more
114
+ ways there are under it, a new column shows how rare the way in is as a bar (full and cyan for
115
+ one door, short and orange for the warmest), and the power score sits beside its bar.
116
+ - **Separation is a tab of its own**, beside Degrees in the header, instead of a view inside
117
+ Degrees. It uses the same Filters grid as Degrees. Degrees keeps Bridge Chains, Pyramid and List.
118
+ - **Separation has a slider.** One slider reorders everyone as you drag it: the rarest ways in
119
+ at one end (strong people only one of your connections knows), the easiest at the other
120
+ (strong people you share the most mutual connections with), and each person's own score alone
121
+ in the middle, as the list has always been. A line under it says in words what it is
122
+ surfacing. It only reorders: scores, tiers and ranks stay as they are, and it claims no odds.
123
+ Double-click the slider, or tap 50, to go back to the middle. It replaces the Power / Ways in
124
+ switch. The tier chips and the rarity chips ("Only show") sit in the same card.
125
+ - **Separation's map follows the slider, and glides.** From the rare end to the middle it fans out
126
+ to ten people, each with the one or two doors that lead to them. Towards the easy end it
127
+ closes in on fewer people, and at the end on one: the person you're aiming at, with rings
128
+ round them and every connection of yours who leads to them drawn in, the top-scored one solid.
129
+ As the slider moves, people, connections and lines slide to their new places and newcomers
130
+ fade in (still, with Reduce Motion on). Pick anyone in the list, or one of the "Next" names
131
+ under the map, to aim at them instead. Beside each name it says how many mutual connections
132
+ lead to them: "only way in", "3 ways in", or, where LinkedIn's own count is higher than the
133
+ connections the app can draw, "37 mutuals · 6 mapped".
134
+ - **Path, in Separation.** A Path button beside the tier chips narrows everyone to one sector or
135
+ one company. The picker opens under the button with the sectors (inferred, as in Paths), the
136
+ biggest companies and a box to type in, and goes away once you've picked; the button then
137
+ shows your pick with a × to clear it.
138
+ - **Separation's heading no longer sits under the notch.**
139
+ - **One filter for tiers and degrees, the same in Network Circle and in Degrees.** The Filters
140
+ panel shows the five tiers as bigger rows, each with six dots, one for each degree. Tap a tier
141
+ to hide or show it, as before. Tap a dot to show or hide that degree for that tier alone:
142
+ S tier at 2nd degree without everyone else's, say. Tap a number at the top to switch a degree
143
+ for every tier at once. A dot with nobody behind it is faint and can't be tapped, each row
144
+ says how many people it is showing, and the last people on screen can't be switched off.
145
+ Network Circle starts with your own connections; Degrees starts with everything. In Degrees
146
+ the rows used to pick one bridge tier at a time; now 1st degree is your bridges and 2nd is
147
+ the people in their circles, each by their own tier.
148
+ - **The Galaxy's physics is the main thing in Network Circle's Filters panel.** The sliders,
149
+ layouts, colours, find and replay sit straight in the panel under the tiers, always on, with
150
+ no switch to turn on first and no "experimental" box. With no slider moved the Galaxy is laid
151
+ out as before; pointing at a dot now lights up its branch unless you turn that off.
152
+ - **Pyramid and List left Network Circle.** Its views are the Galaxy and Orbit. Pyramid and List
153
+ stay in Degrees.
154
+ - **An opened circle shows the chains that lead on from it.** Anyone you added from that circle
155
+ and then scanned keeps their dot in the circle, and the cluster their scan formed sits outside
156
+ it, joined to that dot by a line, so you can see where each path came from. Inside a cluster,
157
+ anyone you added and scanned in turn has their own dot marked and a line on to their cluster,
158
+ for as far as your scans reach. Every dot in a cluster is joined to its middle by a faint line.
159
+ Each cluster says whose circle it is, how many are in it and which degree that is. Point at
160
+ one and the way back to the middle lights up; click and it opens with the whole trail. The
161
+ people you added sit together on the side the chains lead off to, their names taking turns
162
+ above and below so they don't overlap, and the view zooms to fit when it opens.
163
+ - **Bridge Chains shows the bridges you connected with yourself.** Someone you met through a
164
+ bridge's circle is a link in that bridge's chain, not a bridge of their own: they're inside its
165
+ circle.
166
+ - **A bridge's ring now counts the people you added through it.** The ring sits tight on the
167
+ dot, with one bar for each person: orange once they've been scanned and have a cluster of
168
+ their own, green while they're ready for a scan. A small green number at the top right says
169
+ how many are ready, in place of the "ready" label. How much of a bridge's own list is scanned
170
+ is in its tooltip.
171
+ - **A crowded Bridge Chains is easier to read.** When your bridges need more than one ring, the
172
+ rings sit further apart, each with its own guide line, and the counts under each name wait
173
+ until you point at the bridge.
174
+ - **Orbit moved to Network Circle.** It shows the size of every circle at once, which is a
175
+ picture of the network, not a way to follow a chain. The corner switch in Degrees is gone.
176
+ - **Views are in the notch, under the tabs.** Galaxy, Orbit, Pyramid and List in Network Circle,
177
+ and Bridge Chains, Separation, Pyramid and List in Degrees, switch from the notch that hangs
178
+ under the top tabs, instead of a list at the top of the Filters panel. A scan's progress
179
+ shows in the same notch, beside them. The Filters panel now only filters.
180
+
181
+ ### Fixed
182
+ - The scanner's log, shown on the Scan page, says "scanning" throughout; a few lines still said the
183
+ old word.
184
+ - The "Scanning…" dot in the right panel pulses again; the animation it named was never defined.
185
+ - **Separation's map lines now meet every box.** Each connection sits level with the people it
186
+ leads to, so a connection with one person is on that person's row and the line between them runs
187
+ straight; lines start and end exactly on the pill and the card, with a small dot where they plug in.
188
+ - **"Hide the Chrome window while scanning" did nothing for scans started on the Scan page.**
189
+ Its own buttons (Scan, Check for new, Auto scan and the rest) sent their request without the
190
+ setting, so Chrome opened anyway; only scans started from a person's card or Resume were hidden.
191
+ Every scan start now goes through one place that adds it.
192
+ - **"Only way in" could show beside someone with two ways in drawn.** Rarity trusted the mutual
193
+ count a scan saved even when the app had since found them through more of your connections
194
+ than that. It now takes whichever is larger.
195
+
196
+ ## [0.4.11] - 2026-09-29
197
+
198
+ ### Added
199
+ - **The Social tab is a personal CRM.** Everyone you've been in touch with on LinkedIn is one contact,
200
+ connection or not: a list on the left with views and a search box, the person on the right (stacked
201
+ on a phone). The views:
202
+ - **Inbox:** they wrote last, or there's something unread. Unread first.
203
+ - **Awaiting reply:** you wrote last and nothing came back for 7 days or more (you can change the 7).
204
+ - **Follow-ups due:** a follow-up date of today or before.
205
+ - **Pipeline:** a board of the people you've given a stage.
206
+ - **Sent:** connection requests you sent (from your export's Invitations.csv, with the date, and the
207
+ note you wrote if Keep my messages is on), requests the app tracked (*Mark sent*), each accepted
208
+ once they're a connection and pending until then, and messages awaiting a reply.
209
+ - **Received:** requests to you from people who aren't connections yet.
210
+ - **All:** everyone and every group conversation, newest first.
211
+
212
+ Each person's panel shows their tier, company and how you're connected (and who can introduce you,
213
+ for 2nd degree), their conversations (the messages themselves if you keep them), requests both ways,
214
+ and **your own stage** (New, Contacted, Replied, Meeting, Won/Partner, Not now; it suggests one),
215
+ **tags**, **notes** and **next follow-up date**. Those are yours, so they're always kept, in a file of
216
+ their own per profile in the app's data folder, and never sent anywhere. Search covers names,
217
+ companies, tags and notes, and what was said while the messages are kept; in All it finds anyone in
218
+ your network, so you can add a note before you've written to them. **Export CSV** saves the table
219
+ (name, company, profile, connection, stage, tags, last contact, who wrote last, next follow-up,
220
+ notes) as a spreadsheet file. The list shows 100 at a time, so thousands of conversations stay quick.
221
+ - **Every conversation in your export, not just your connections'.** People who aren't connections
222
+ are listed by the name the export gives them, marked *Not a connection*, with their profile link when
223
+ it has one. Past (archived), current and ones only you wrote in are all there, from every folder;
224
+ group conversations are listed and marked; Sponsored Messages and InMails are marked, and adverts are
225
+ kept out of the Inbox. Unsent drafts are left out and said so.
226
+ - **Read my whole history**, beside *Sync messages now*: the live messages sync keeps scrolling your
227
+ messages list until no new conversations load (at most 60 scrolls, about 1,000 conversations), at the
228
+ same slow, fixed pace, with no random waits. The daily sync still reads just the top. The sync now
229
+ also brings in people who aren't your connections (with their name) and group conversations (without
230
+ any words), and each sync adds to what earlier ones found instead of replacing it.
231
+ - **Delete my CRM notes**, and *Forget it* asks, separately, whether to delete them too, so notes
232
+ aren't lost by accident.
233
+
234
+ ### Changed
235
+ - **"Save a copy of my network" carries the Social tab too.** A new box, ticked by default,
236
+ adds its findings, your CRM stages, tags, notes and follow-ups, and any messages you keep,
237
+ so moving to a new computer brings everything. Importing the copy puts them in place and
238
+ keeps this computer's beside the old network. Untick it to leave them behind.
239
+ - **Waiting on you has become the CRM's Inbox**, and Conversations is part of the CRM.
240
+ - **The Social tab keeps the names of people who aren't your connections**, from your export and the
241
+ sync, since nothing else in the app says who they are. A connection's name still isn't copied: the
242
+ app has it already.
243
+
9
244
  ## [0.4.10] - 2026-09-29
10
245
 
11
246
  ### Changed
@@ -17,19 +17,19 @@ function capacity(radius, spacing, sweep) {
17
17
  return Math.max(1, sweep >= TAU - 1e-9 ? Math.floor(arc / spacing) : Math.floor(arc / spacing) + 1);
18
18
  }
19
19
 
20
- /** The rings from `inner` outwards, `spacing` apart, until `n` dots fit. */
21
- function ringsFor(n, inner, spacing, sweep) {
20
+ /** The rings from `inner` outwards, `gap` apart (at least `spacing`), until `n` dots fit. */
21
+ function ringsFor(n, inner, spacing, sweep, gap = spacing) {
22
22
  const radii = [];
23
23
  let room = 0;
24
- for (let r = inner; room < n; r += spacing) {
24
+ for (let r = inner; room < n; r += Math.max(spacing, gap)) {
25
25
  radii.push(r);
26
26
  room += capacity(r, spacing, sweep);
27
27
  }
28
28
  return radii;
29
29
  }
30
30
 
31
- const outerOf = (n, inner, spacing, sweep) => {
32
- const radii = ringsFor(n, inner, spacing, sweep);
31
+ const outerOf = (n, inner, spacing, sweep, gap) => {
32
+ const radii = ringsFor(n, inner, spacing, sweep, gap);
33
33
  return radii[radii.length - 1];
34
34
  };
35
35
 
@@ -46,43 +46,44 @@ const outerOf = (n, inner, spacing, sweep) => {
46
46
  * neighbouring rings don't line up in spokes.
47
47
  *
48
48
  * `start` and `sweep` fill an arc instead of the whole circle, each ring from
49
- * its middle outwards.
49
+ * its middle outwards. `ringGap` keeps the rings at least that far apart, for
50
+ * dots that carry a name under them and need the room.
50
51
  *
51
52
  * @returns {{ spacing: number, inner: number, rings: { radius: number, count: number }[],
52
53
  * points: { x: number, y: number, angle: number, ring: number }[] }}
53
54
  */
54
55
  export function ringLayout(n, {
55
56
  inner, outer, spacing, minSpacing = spacing / 3, innerMin = inner,
56
- start = -Math.PI / 2, sweep = TAU,
57
+ start = -Math.PI / 2, sweep = TAU, ringGap = 0,
57
58
  } = {}) {
58
59
  const count = Math.max(0, Math.floor(n) || 0);
59
60
  if (!count) return { spacing, inner, rings: [], points: [] };
60
61
 
61
62
  let s = spacing;
62
63
  let r0 = inner;
63
- if (outerOf(count, r0, s, sweep) > outer) {
64
+ if (outerOf(count, r0, s, sweep, ringGap) > outer) {
64
65
  // Start further in, as far as innerMin, before closing anything up.
65
- if (outerOf(count, innerMin, s, sweep) <= outer) {
66
+ if (outerOf(count, innerMin, s, sweep, ringGap) <= outer) {
66
67
  let lo = innerMin, hi = inner;
67
68
  for (let i = 0; i < 24; i++) {
68
69
  const mid = (lo + hi) / 2;
69
- if (outerOf(count, mid, s, sweep) <= outer) lo = mid; else hi = mid;
70
+ if (outerOf(count, mid, s, sweep, ringGap) <= outer) lo = mid; else hi = mid;
70
71
  }
71
72
  r0 = lo;
72
73
  } else {
73
74
  r0 = innerMin;
74
75
  let lo = minSpacing, hi = spacing;
75
- if (outerOf(count, r0, lo, sweep) <= outer) {
76
+ if (outerOf(count, r0, lo, sweep, ringGap) <= outer) {
76
77
  for (let i = 0; i < 24; i++) {
77
78
  const mid = (lo + hi) / 2;
78
- if (outerOf(count, r0, mid, sweep) <= outer) lo = mid; else hi = mid;
79
+ if (outerOf(count, r0, mid, sweep, ringGap) <= outer) lo = mid; else hi = mid;
79
80
  }
80
81
  }
81
82
  s = lo;
82
83
  }
83
84
  }
84
85
 
85
- const radii = ringsFor(count, r0, s, sweep);
86
+ const radii = ringsFor(count, r0, s, sweep, ringGap);
86
87
  const caps = radii.map((r) => capacity(r, s, sweep));
87
88
  const room = caps.reduce((a, b) => a + b, 0);
88
89
 
@@ -162,8 +163,9 @@ const TIER_BANDS = ['S', 'A', 'B', 'C', 'D'];
162
163
  *
163
164
  * @returns {{ spacing: number, edge: number, bands: { tier: string, inner: number, outer: number }[],
164
165
  * points: { x: number, y: number, angle: number, slot: number }[] }}
166
+ * `start` is the angle each ring's first dot sits at (the top, unless told).
165
167
  */
166
- export function tierBandLayout(rows, { inner, outer, spacing, minSpacing = spacing / 3, gap = spacing }) {
168
+ export function tierBandLayout(rows, { inner, outer, spacing, minSpacing = spacing / 3, gap = spacing, start = -Math.PI / 2 }) {
167
169
  const groups = TIER_BANDS.map((tier) => ({ tier, idx: [] }));
168
170
  rows.forEach((row, i) => {
169
171
  const t = TIER_BANDS.indexOf(row?.tier);
@@ -175,7 +177,7 @@ export function tierBandLayout(rows, { inner, outer, spacing, minSpacing = spaci
175
177
  const bands = [];
176
178
  let r = inner;
177
179
  for (const g of used) {
178
- const l = ringLayout(g.idx.length, { inner: r, outer: Infinity, spacing: s, minSpacing: s });
180
+ const l = ringLayout(g.idx.length, { inner: r, outer: Infinity, spacing: s, minSpacing: s, start });
179
181
  g.idx.forEach((rowIndex, j) => {
180
182
  const p = l.points[j];
181
183
  points[rowIndex] = { x: p.x, y: p.y, angle: p.angle, slot: TAU / l.rings[p.ring].count };
@@ -201,28 +203,96 @@ export function tierBandLayout(rows, { inner, outer, spacing, minSpacing = spaci
201
203
  }
202
204
 
203
205
  /**
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.
206
+ * The chains that lead on from an opened circle (Blake, 2026-10-02: "once they
207
+ * are clicked on and can view the cluster i want any scanned d2,3,4 or whatever
208
+ * to be show inside with lines out of the d1 leading to the d2s").
209
209
  *
210
- * @param {{ angle: number, slot: number, count: number }[]} anchors
211
- * @returns {({ points: { x: number, y: number }[], more: number } | null)[]}
210
+ * `parents[i]` is the index of the link that introduced link i, or -1 when the
211
+ * person in the middle did. The links one step on sit on a ring at `from`, each
212
+ * as near its own angle in `angles` as it can get (where that person's dot is
213
+ * in the circle), or fanned about `toward` without one; each later step sits `hop` further out,
214
+ * as near the angle of the link it came from as it can get without touching a
215
+ * neighbour on its own ring (`spacing` apart). So a chain reads outwards from
216
+ * the middle: the bridge, someone met through them, someone met through *them*.
217
+ *
218
+ * @param {number[]} parents
219
+ * @returns {{ rings: { radius: number, count: number, depth: number }[],
220
+ * points: { x: number, y: number, angle: number, radius: number, depth: number, parent: number }[] }}
212
221
  */
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,
222
+ export function chainTree(parents = [], { from = 0, hop = 60, spacing = 46, toward = 0, angles = [] } = {}) {
223
+ const n = parents.length;
224
+ const parent = parents.map((p, i) => (Number.isInteger(p) && p >= 0 && p < n && p !== i ? p : -1));
225
+
226
+ // How many steps out each one is. A loop (it shouldn't happen) is cut where
227
+ // it closes, so everyone still gets a place.
228
+ const depth = new Array(n).fill(0);
229
+ for (let i = 0; i < n; i++) {
230
+ const trail = [];
231
+ let k = i;
232
+ while (k >= 0 && !depth[k] && !trail.includes(k)) { trail.push(k); k = parent[k]; }
233
+ if (k >= 0 && !depth[k]) { parent[trail[trail.length - 1]] = -1; k = -1; }
234
+ let d = k >= 0 ? depth[k] + 1 : 1;
235
+ for (let j = trail.length - 1; j >= 0; j--) depth[trail[j]] = d++;
236
+ }
237
+
238
+ const points = new Array(n);
239
+ const rings = [];
240
+ for (let d = 1; d <= Math.max(0, ...depth); d++) {
241
+ const here = [];
242
+ for (let i = 0; i < n; i++) if (depth[i] === d) here.push(i);
243
+ const radius = from + (d - 1) * hop;
244
+ const placed = spread(here.map((i) => (parent[i] < 0 ? (Number.isFinite(angles[i]) ? angles[i] : toward) : points[parent[i]].angle)),
245
+ Math.min(spacing / Math.max(1, radius), TAU / here.length));
246
+ here.forEach((i, j) => {
247
+ const angle = placed[j];
248
+ points[i] = { x: radius * Math.cos(angle), y: radius * Math.sin(angle), angle, radius, depth: d, parent: parent[i] };
225
249
  });
226
- return { points: l.points.map(({ x, y }) => ({ x, y })), more: a.count - n };
250
+ rings.push({ radius, count: here.length, depth: d });
251
+ }
252
+ return { rings, points };
253
+ }
254
+
255
+ /**
256
+ * Angles as near the ones asked for as they can be while staying `sep` apart:
257
+ * neighbours that would touch move apart by the same amount each way, so two
258
+ * people met through the same person sit either side of them. Comes back in
259
+ * the order given.
260
+ */
261
+ function spread(wanted, sep) {
262
+ const m = wanted.length;
263
+ if (m < 2) return wanted.slice();
264
+ const turn = (a) => ((a % TAU) + TAU) % TAU;
265
+ const order = wanted.map((a, i) => i).sort((i, j) => turn(wanted[i]) - turn(wanted[j]) || i - j);
266
+ // Start counting just after the widest empty stretch, so no group straddles the join.
267
+ let cut = 0;
268
+ let widest = -1;
269
+ order.forEach((i, k) => {
270
+ const next = order[(k + 1) % m];
271
+ const gap = turn(turn(wanted[next]) - turn(wanted[i])) || (m > 1 && k === m - 1 ? TAU : 0);
272
+ if (gap > widest) { widest = gap; cut = (k + 1) % m; }
227
273
  });
274
+ const seq = order.slice(cut).concat(order.slice(0, cut));
275
+ const first = turn(wanted[seq[0]]);
276
+ const at = seq.map((i) => first + turn(turn(wanted[i]) - first));
277
+
278
+ // Least movement that keeps them `sep` apart: take the spacing out, pool any
279
+ // neighbours left out of order to their average, put the spacing back.
280
+ const blocks = [];
281
+ at.forEach((a, k) => {
282
+ blocks.push({ sum: a - k * sep, count: 1 });
283
+ while (blocks.length > 1 && blocks[blocks.length - 2].sum / blocks[blocks.length - 2].count > blocks[blocks.length - 1].sum / blocks[blocks.length - 1].count) {
284
+ const last = blocks.pop();
285
+ blocks[blocks.length - 1].sum += last.sum;
286
+ blocks[blocks.length - 1].count += last.count;
287
+ }
288
+ });
289
+ let placed = blocks.flatMap((b) => new Array(b.count).fill(b.sum / b.count)).map((q, k) => q + k * sep);
290
+ // Round the join too: if the last has come back round onto the first, share the ring evenly.
291
+ if (placed[m - 1] - placed[0] > TAU - sep) {
292
+ const mid = (placed[0] + placed[m - 1]) / 2;
293
+ placed = placed.map((_, k) => mid + (k - (m - 1) / 2) * (TAU / m));
294
+ }
295
+ const out = new Array(m);
296
+ seq.forEach((i, k) => { out[i] = placed[k]; });
297
+ return out;
228
298
  }
@@ -14,7 +14,7 @@ import { DatabaseSync } from 'node:sqlite';
14
14
  import { readFileSync, lstatSync, statSync } from 'node:fs';
15
15
  import { createHash } from 'node:crypto';
16
16
  import path from 'node:path';
17
- import { AVATAR_FILE, TRAVELLING_FILES, namesInFolder } from './data-folder.js';
17
+ import { AVATAR_FILE, TRAVELLING_FILES, SOCIAL_FILE, namesInFolder } from './data-folder.js';
18
18
 
19
19
  /** Bump when the file's layout changes in a way an older import can't read. */
20
20
  export const EXPORT_FORMAT = 1;
@@ -72,7 +72,7 @@ export function referencedPhotos(db) {
72
72
  * plain files are taken, never a link, and avatars/ is read only if it is a
73
73
  * real folder, so nothing outside the data folder can be pulled in through one.
74
74
  */
75
- export function travellingFiles(dir, { includePhotos = true, photos = null } = {}) {
75
+ export function travellingFiles(dir, { includePhotos = true, photos = null, includeSocial = true } = {}) {
76
76
  const plain = (p) => {
77
77
  try { return lstatSync(p).isFile(); } catch { return false; }
78
78
  };
@@ -81,6 +81,12 @@ export function travellingFiles(dir, { includePhotos = true, photos = null } = {
81
81
  const abs = path.join(dir, name);
82
82
  if (plain(abs)) out.push({ rel: name, abs });
83
83
  }
84
+ if (includeSocial) {
85
+ for (const name of namesInFolder(dir).sort()) {
86
+ const abs = path.join(dir, name);
87
+ if (SOCIAL_FILE.test(name) && plain(abs)) out.push({ rel: name, abs });
88
+ }
89
+ }
84
90
  if (includePhotos) {
85
91
  for (const name of namesInFolder(path.join(dir, 'avatars')).sort()) {
86
92
  if (!AVATAR_FILE.test(name) || (photos && !photos.has(name))) continue;
@@ -96,7 +102,7 @@ export function travellingFiles(dir, { includePhotos = true, photos = null } = {
96
102
  * `outFile`, which must not exist yet.
97
103
  * @returns {{ bytes, people, photos, files, counts }}
98
104
  */
99
- export function buildExport(db, { dir, outFile, includePhotos = true, appVersion, schemaSql, now = new Date() }) {
105
+ export function buildExport(db, { dir, outFile, includePhotos = true, includeSocial = true, appVersion, schemaSql, now = new Date() }) {
100
106
  db.exec(`VACUUM INTO ${sqlString(outFile)}`);
101
107
 
102
108
  const out = new DatabaseSync(outFile);
@@ -127,7 +133,7 @@ export function buildExport(db, { dir, outFile, includePhotos = true, appVersion
127
133
  // photo of someone whose circle was deleted since; the copy has no row
128
134
  // for them, so their face has no reason to travel.
129
135
  const inNetwork = includePhotos ? referencedPhotos(out) : null;
130
- for (const f of travellingFiles(dir, { includePhotos, photos: inNetwork })) {
136
+ for (const f of travellingFiles(dir, { includePhotos, photos: inNetwork, includeSocial })) {
131
137
  let bytes;
132
138
  try { bytes = readFileSync(f.abs); } catch { continue; } // replaced since the listing
133
139
  put.run(f.rel, bytes, sha256(bytes));
@@ -44,6 +44,15 @@ export const BUDGET_FILES = [
44
44
  'linkedin-cooldown.json',
45
45
  ];
46
46
 
47
+ /**
48
+ * The Social tab's files, one set per profile: its findings, the messages kept
49
+ * with "Keep my messages on this computer", and the CRM's stages, tags and
50
+ * notes (lib/social-store.js). A copy carries them when asked (Blake,
51
+ * 2026-09-29: everything goes with the network), and an import puts them back
52
+ * in place of this computer's, which are kept beside the old network.
53
+ */
54
+ export const SOCIAL_FILE = /^(?:social|social-messages|crm)-[\w-]+\.json$/;
55
+
47
56
  /** Everything of the scanner's that an export carries. */
48
57
  export const TRAVELLING_FILES = [...NETWORK_FILES, ...BUDGET_FILES];
49
58
 
@@ -31,7 +31,7 @@ import { runningNow } from './scan-state.js';
31
31
  import { durable } from './durable.js';
32
32
  import { budgetFileProblem, mergeBudgetFiles } from './linkedin-limits.js';
33
33
  import {
34
- AVATAR_FILE, TRAVELLING_FILES, NETWORK_FILES, BUDGET_FILES, PENDING_DIR, IMPORT_BACKUP_PREFIX,
34
+ AVATAR_FILE, TRAVELLING_FILES, NETWORK_FILES, BUDGET_FILES, SOCIAL_FILE, PENDING_DIR, IMPORT_BACKUP_PREFIX,
35
35
  sweepLeftovers, namesInFolder,
36
36
  } from './data-folder.js';
37
37
  import {
@@ -158,10 +158,11 @@ export async function receiveUpload(body, file, { declared }) {
158
158
  return received;
159
159
  }
160
160
 
161
- /** 'photo', 'file' (one of the scanner's travelling files), or null for anything an export never carries. */
161
+ /** 'photo', 'file' (one of the scanner's travelling files), 'social', or null for anything an export never carries. */
162
162
  export function exportedFileKind(rel) {
163
163
  if (typeof rel !== 'string') return null;
164
164
  if (TRAVELLING_FILES.includes(rel)) return 'file';
165
+ if (SOCIAL_FILE.test(rel)) return 'social';
165
166
  // The same pattern the /avatars route serves: no folders, no dots but the
166
167
  // extension, so no path can climb out of avatars/.
167
168
  if (rel.startsWith('avatars/') && AVATAR_FILE.test(rel.slice('avatars/'.length))) return 'photo';
@@ -421,7 +422,7 @@ export function stageImport(upload, {
421
422
  throw new ImportError(`“${shortName(rel)}” inside this file is damaged, so nothing was imported.`);
422
423
  }
423
424
  writeFileSync(path.join(filesDir, rel), row.bytes, { flag: 'wx' });
424
- if (kind === 'file') scannerFiles.push(path.join(filesDir, rel));
425
+ if (kind === 'file' || kind === 'social') scannerFiles.push(path.join(filesDir, rel));
425
426
  }
426
427
  } finally {
427
428
  db.close();
@@ -731,6 +732,14 @@ export function applyPendingImport({ dir, dbFile, now = new Date(), copyDatabase
731
732
  keptFolder();
732
733
  move(from, path.join(keptFiles, name));
733
734
  }
735
+ // This computer's Social files step aside only when the copy brings its
736
+ // own; a copy made without them leaves them where they are.
737
+ if (namesInFolder(path.join(pending, 'files')).some((n) => SOCIAL_FILE.test(n))) {
738
+ for (const name of namesInFolder(dir).filter((n) => SOCIAL_FILE.test(n))) {
739
+ keptFolder();
740
+ move(path.join(dir, name), path.join(keptFiles, name));
741
+ }
742
+ }
734
743
  for (const name of BUDGET_FILES) {
735
744
  const from = path.join(dir, name);
736
745
  if (!exists(from) || !lstatSync(from).isFile()) continue;
@@ -786,6 +795,7 @@ export function applyPendingImport({ dir, dbFile, now = new Date(), copyDatabase
786
795
  const from = path.join(files, name);
787
796
  if (exists(from)) move(from, path.join(dir, name));
788
797
  }
798
+ for (const name of namesInFolder(files).filter((n) => SOCIAL_FILE.test(n))) move(path.join(files, name), path.join(dir, name));
789
799
  mergeBudgetFiles({ dir, from: files });
790
800
  durable.syncFolder(dir);
791
801
  step('done');
package/lib/degrees.js CHANGED
@@ -22,9 +22,3 @@ export function peopleByDegree(degree1 = [], degree2 = [], degree3 = []) {
22
22
  };
23
23
  return { 1: take(degree1, 1), 2: take(degree2, 2), 3: take(degree3, 3) };
24
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/dot-rings.js CHANGED
@@ -18,4 +18,29 @@ export function ringSegments(r) {
18
18
  });
19
19
  }
20
20
 
21
+ /**
22
+ * The ring round a bridge in Bridge Chains (Blake, 2026-10-02: "the bars on the
23
+ * ring should signify how many dots in the clusters are ready to be scanned
24
+ * and new clusters formed"). One bar for each person you added through that
25
+ * circle: the ones scanned since, with a cluster of their own (`formed`),
26
+ * then the ones waiting for a scan (`ready`). Past `max` bars the two kinds
27
+ * share them in proportion, each keeping at least one.
28
+ *
29
+ * @returns {{ d: string, kind: 'formed' | 'ready' }[]}
30
+ */
31
+ export function reachSegments(r, { formed = 0, ready = 0 } = {}, max = 12) {
32
+ const total = Math.max(0, formed) + Math.max(0, ready);
33
+ if (!total) return [];
34
+ const n = Math.min(total, max);
35
+ let nFormed = formed > 0 ? Math.max(1, Math.round((n * formed) / total)) : 0;
36
+ if (ready > 0 && nFormed >= n) nFormed = n - 1;
37
+ const gap = n === 1 ? 0.5 : Math.min(GAP, ((2 * Math.PI) / n) * 0.35);
38
+ const span = (2 * Math.PI) / n - gap;
39
+ const p = (a) => `${(r * Math.cos(a)).toFixed(2)} ${(r * Math.sin(a)).toFixed(2)}`;
40
+ return Array.from({ length: n }, (_, i) => {
41
+ const a0 = -Math.PI / 2 + gap / 2 + i * (span + gap);
42
+ return { kind: i < nFormed ? 'formed' : 'ready', d: `M${p(a0)} A${r} ${r} 0 ${span > Math.PI ? 1 : 0} 1 ${p(a0 + span)}` };
43
+ });
44
+ }
45
+
21
46
  export const RING = { filled: '#1abc9c', empty: 'rgba(255,255,255,0.16)', badge: '#7F77DD' };