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
@@ -1,360 +1,19 @@
1
1
  'use client';
2
2
 
3
- // Social (experimental): what your own LinkedIn data says about your
4
- // relationships (lib/linkedin-export.js). Two sources, both yours:
5
- // - the export LinkedIn emails you (Settings → Data privacy → Get a copy of
6
- // your data), read here in the browser: full history, no traffic to LinkedIn;
7
- // - an experimental live sync (the scanner's --messages): your messages list,
8
- // read once in your own Chrome, by hand or once a day.
9
- // Kept on this computer: dates, who wrote last and counts, and the messages
10
- // themselves only if you turn on Keep my messages (Conversations.js), in a file
11
- // of their own that switching it off deletes.
3
+ // Social is part of Outlink now (Blake, 2026-10-02): the CRM, your LinkedIn
4
+ // export and the messages sit under Outlink → Messages & follow-ups
5
+ // (app/social/SocialHub.js). Old links land there.
12
6
 
13
- import { useEffect, useMemo, useRef, useState } from 'react';
7
+ import { useEffect } from 'react';
14
8
  import Link from 'next/link';
15
- import OnboardingGate from '../components/OnboardingGate';
16
- import { useUser } from '../components/UserProvider';
17
- import { loadNetwork } from '../../lib/network';
18
- import {
19
- readTable, messageStats, careerChapters, postingEffect, invitationSplit, warmthOf, repliesWaiting, buildConversations, conversationSummary,
20
- } from '../../lib/linkedin-export';
21
- import { mergeSocial } from '../../lib/galaxy-lab';
22
- import { keyFor } from '../../lib/separation';
23
- import { runScrape, watchScanner, scannerNow } from '../../lib/scraper-client';
24
- import { Body, LINE, FONT } from '../components/ui';
25
- import { IS_DEMO } from '../../lib/demo';
26
- import Conversations from './Conversations';
27
-
28
- const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
29
- const WARMTH = { warm: ['Warm', '#ff9f43'], cool: ['Cool', '#3498DB'], dormant: ['Dormant', '#8899aa'], never: ['Never messaged', '#556'] };
30
- const FILES = { 'connections.csv': 'first name', 'messages.csv': 'conversation id', 'invitations.csv': 'direction', 'positions.csv': 'company name', 'shares.csv': 'date' };
31
- const day = (t) => (t ? new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) : '');
32
- const slug = (u) => String(u || '').replace(/\/+$/, '').split('/').pop();
33
- // Messages go to the server in parts of about this many characters: a request
34
- // here can carry 10 MB, and a character can take up to 3 bytes.
35
- const PART_CHARS = 2_500_000;
36
-
37
- /** An import's messages, in parts that each fit in one request. */
38
- async function sendMessages(conversations) {
39
- const parts = [];
40
- let cur = {};
41
- let size = 0;
42
- for (const c of conversations) {
43
- for (const m of c.messages) {
44
- const n = JSON.stringify(m).length + 2;
45
- if (size + n > PART_CHARS && size > 0) { parts.push(cur); cur = {}; size = 0; }
46
- if (!cur[c.id]) { cur[c.id] = { title: c.title || '', messages: [] }; size += c.id.length + (c.title || '').length + 40; }
47
- cur[c.id].messages.push(m);
48
- size += n;
49
- }
50
- }
51
- parts.push(cur);
52
- for (let i = 0; i < parts.length; i++) {
53
- const r = await fetch('/api/social/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ part: i, parts: parts.length, threads: parts[i] }) });
54
- if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'The messages couldn’t be saved.');
55
- }
56
- }
57
-
58
- export default function SocialPage() {
59
- return <OnboardingGate><SocialInner /></OnboardingGate>;
60
- }
61
-
62
- function SocialInner() {
63
- const { userId } = useUser();
64
- const [social, setSocial] = useState(undefined);
65
- const [net, setNet] = useState(null);
66
- const [keepEmails, setKeepEmails] = useState(false);
67
- const [note, setNote] = useState(null);
68
- const [busy, setBusy] = useState(false);
69
- const [job, setJob] = useState(() => scannerNow());
70
- const [switching, setSwitching] = useState(false);
71
- const [listToken, setListToken] = useState(0);
72
- // The last export's messages.csv, in this window only, so turning Keep my
73
- // messages on right after an import needn't ask for the folder again.
74
- const lastMessages = useRef(null);
75
- const keep = social?.keepMessages === true;
76
-
77
- // The Conversations list reads again whenever this does (listToken).
78
- const reload = () => fetch('/api/social').then((r) => r.json()).then((d) => setSocial(d.social || null)).catch(() => setSocial(null))
79
- .finally(() => setListToken((n) => n + 1));
80
- useEffect(() => { reload(); }, []);
81
- useEffect(() => watchScanner(() => setJob(scannerNow())), []);
82
- useEffect(() => {
83
- if (!userId) return undefined;
84
- let live = true;
85
- loadNetwork(userId).then((n) => { if (live) setNet(n); }).catch(() => {});
86
- return () => { live = false; };
87
- }, [userId]);
88
-
89
- // Everyone you know, by profile: name and tier for the lists below.
90
- const byKey = useMemo(() => {
91
- const m = new Map();
92
- for (const r of [...(net?.degree1 || []), ...(net?.degree2 || [])]) if (!m.has(keyFor(r))) m.set(keyFor(r), r);
93
- return m;
94
- }, [net]);
95
- const connected = useMemo(() => new Set((net?.degree1 || []).map(keyFor)), [net]);
96
-
97
- async function importFolder(list) {
98
- setBusy(true);
99
- setNote(null);
100
- try {
101
- const found = {};
102
- for (const f of Array.from(list || [])) {
103
- const name = f.name.toLowerCase();
104
- if (FILES[name] && !found[name]) found[name] = readTable(await f.text(), FILES[name]);
105
- }
106
- if (!Object.keys(found).length) throw new Error('That folder doesn’t have LinkedIn’s export files in it (Connections.csv, messages.csv and so on).');
107
- const conns = found['connections.csv'] || [];
108
- const stats = found['messages.csv'] ? messageStats(found['messages.csv']) : { asOf: null, people: new Map() };
109
- // The words are read out of messages.csv only when Keep my messages is on.
110
- const built = found['messages.csv'] ? buildConversations(found['messages.csv'], { keepText: keep }) : null;
111
- lastMessages.current = found['messages.csv'] || null;
112
- const people = {};
113
- for (const [url, v] of stats.people) people[url] = { last: v.last, lastFromThem: v.lastFromThem, total: v.total, recent: v.recent };
114
- if (keepEmails) {
115
- for (const c of conns) {
116
- const e = c['email address'];
117
- if (!e) continue;
118
- const k = keyFor({ profile_url: c.url });
119
- people[k] = { ...(people[k] || {}), email: e };
120
- }
121
- }
122
- const asOf = stats.asOf || Date.now();
123
- const body = {
124
- asOf,
125
- people,
126
- conversations: built ? built.conversations.map(conversationSummary) : [],
127
- chapters: careerChapters(found['positions.csv'] || [], conns, asOf),
128
- posts: found['shares.csv'] ? postingEffect(found['shares.csv'], conns) : null,
129
- invites: found['invitations.csv'] ? invitationSplit(found['invitations.csv']) : null,
130
- emails: keepEmails,
131
- };
132
- const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
133
- if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'It couldn’t be saved.');
134
- if (built && keep) await sendMessages(built.conversations);
135
- setNote({
136
- good: true,
137
- text: `Read ${Object.keys(found).length} of LinkedIn’s files: ${stats.people.size.toLocaleString()} people you’ve messaged one to one`
138
- + (built ? `, ${built.conversations.length.toLocaleString()} conversations${keep ? ', their messages kept on this computer' : ''}.` : '.'),
139
- });
140
- await reload();
141
- } catch (e) {
142
- setNote({ good: false, text: e.message });
143
- } finally {
144
- setBusy(false);
145
- }
146
- }
147
-
148
- async function syncNow() {
149
- setNote(null);
150
- try {
151
- const end = await runScrape('messages');
152
- setNote(end?.exitCode === 0 ? { good: true, text: 'Your messages list is read.' } : { good: false, text: 'The sync stopped before it finished. The Scan page has the log.' });
153
- await reload();
154
- } catch (e) {
155
- setNote({ good: false, text: e.message });
156
- }
157
- }
158
-
159
- async function setAuto(on) {
160
- await fetch('/api/social', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ autoSync: on }) });
161
- reload();
162
- }
163
-
164
- // Keep my messages. On: from now on the export's and the sync's words are
165
- // kept, and if an export was read in this window, its messages are kept now.
166
- // Off, once you've said so: the kept messages are deleted.
167
- async function setKeep(on) {
168
- if (!on && !window.confirm('Delete the messages kept on this computer? The list of conversations stays; the messages themselves are deleted.')) return;
169
- setSwitching(true);
170
- setNote(null);
171
- try {
172
- if (!on) await fetch('/api/social/messages', { method: 'DELETE' });
173
- const r = await fetch('/api/social', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keepMessages: on }) });
174
- if (!r.ok) throw new Error('The switch couldn’t be saved.');
175
- if (on && lastMessages.current) {
176
- await sendMessages(buildConversations(lastMessages.current, { keepText: true }).conversations);
177
- setNote({ good: true, text: 'Your messages are kept on this computer now.' });
178
- } else if (on) {
179
- setNote({ good: true, text: 'On. Choose your export folder again, or sync your messages, to bring the messages in.' });
180
- } else {
181
- setNote({ good: true, text: 'Off. The kept messages are deleted.' });
182
- }
183
- } catch (e) {
184
- setNote({ good: false, text: e.message });
185
- } finally {
186
- setSwitching(false);
187
- reload();
188
- }
189
- }
190
-
191
- async function forget() {
192
- await fetch('/api/social', { method: 'DELETE' });
193
- lastMessages.current = null;
194
- setNote({ good: true, text: 'Forgotten. Nothing from your export or messages is kept here now, the messages themselves included.' });
195
- reload();
196
- }
197
-
198
- // What each person's messages say, the export and the live sync together, and
199
- // "now" for warmth: the newest thing the data knows, so an older export reads as it was.
200
- const { merged, asOf } = useMemo(() => mergeSocial(social), [social]);
201
-
202
- const warmth = useMemo(() => {
203
- const counts = { warm: 0, cool: 0, dormant: 0, never: 0 };
204
- for (const c of net?.degree1 || []) counts[warmthOf(merged.get(keyFor(c)), asOf)] += 1;
205
- return counts;
206
- }, [net, merged, asOf]);
207
-
208
- const waiting = useMemo(() => {
209
- const stats = { asOf, people: new Map([...merged].filter(([, v]) => v.lastFromThem != null)) };
210
- const list = repliesWaiting(stats).map((w) => ({ ...w, person: byKey.get(w.url) }));
211
- for (const [k, v] of merged) {
212
- if (v.unread > 0 && !list.some((w) => w.url === k)) list.push({ url: k, unread: v.unread, person: byKey.get(k) });
213
- }
214
- const rank = (w) => ({ S: 0, A: 1 }[w.person?.tier] ?? 2);
215
- return list.sort((a, b) => rank(a) - rank(b) || (a.days ?? 0) - (b.days ?? 0)).slice(0, 15);
216
- }, [merged, asOf, byKey]);
217
-
218
- if (IS_DEMO) return <div style={{ minHeight: '100vh', background: '#0a0a1a', color: '#aaa', padding: 40, fontFamily: FONT }}>Not part of the demo.</div>;
219
- const running = job?.running;
220
- const box = { padding: '12px 14px', borderRadius: 8, border: LINE, background: 'rgba(255,255,255,0.03)', marginTop: 10 };
221
- const h2 = { fontSize: 16, margin: '24px 0 6px' };
9
+ import { useRouter } from 'next/navigation';
222
10
 
11
+ export default function SocialMoved() {
12
+ const router = useRouter();
13
+ useEffect(() => { router.replace('/queue?view=messages'); }, [router]);
223
14
  return (
224
- <div style={{ minHeight: '100vh', background: '#0a0a1a', color: '#fff', fontFamily: FONT }}>
225
- <header style={{ padding: '16px 24px', borderBottom: LINE, display: 'flex', alignItems: 'center', gap: 16 }}>
226
- <Link href="/" style={{ color: '#888', textDecoration: 'none', fontSize: 13, fontWeight: 600, padding: '6px 14px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', border: LINE }}>← Back to Map</Link>
227
- <h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, color: '#FFD700' }}>Social <span style={{ fontSize: 12, color: '#8b9a9a', fontWeight: 600 }}>experimental</span></h1>
228
- </header>
229
- <main style={{ maxWidth: 980, margin: '0 auto', padding: '8px 24px 64px' }}>
230
- <Body style={{ marginTop: 16 }}>
231
- Who you&rsquo;re warm with, who&rsquo;s waiting on a reply, and how each chapter of your career built your network,
232
- from your own LinkedIn data. Only dates, who wrote last and counts are kept on this computer, and what
233
- messages say only if you turn on <i>Keep my messages</i> under Conversations.
234
- </Body>
235
-
236
- <h2 style={h2}>Your LinkedIn export</h2>
237
- <div style={box}>
238
- <Body style={{ margin: 0 }}>
239
- On LinkedIn: <i>Settings → Data privacy → Get a copy of your data</i>, pick the larger archive, and unzip the
240
- file it emails you. Then choose that folder here. It&rsquo;s read in this window; nothing goes to LinkedIn.
241
- </Body>
242
- <label style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12.5, color: '#b8c4c4', marginTop: 10 }}>
243
- <input type="checkbox" checked={keepEmails} onChange={(e) => setKeepEmails(e.target.checked)} style={{ marginTop: 3 }} />
244
- <span>Also keep email addresses from it, on this computer only, for one-click email.</span>
245
- </label>
246
- <div style={{ marginTop: 10, display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
247
- <label style={{ padding: '7px 14px', borderRadius: 8, background: 'rgba(52,152,219,0.18)', color: '#cfe6f7', cursor: busy ? 'default' : 'pointer', fontSize: 13 }}>
248
- {busy ? 'Reading…' : social ? 'Choose a newer export' : 'Choose the export folder'}
249
- <input type="file" webkitdirectory="" directory="" multiple disabled={busy} style={{ display: 'none' }} onChange={(e) => importFolder(e.target.files)} />
250
- </label>
251
- {social && <span style={{ fontSize: 12, color: '#8b9a9a' }}>From an export up to {day(social.asOf)}{social.liveAt ? ` · messages synced ${day(Date.parse(social.liveAt))}` : ''}</span>}
252
- {social && <button onClick={forget} style={{ background: 'none', border: 'none', color: '#ff9b9b', cursor: 'pointer', fontSize: 12 }}>Forget it</button>}
253
- </div>
254
- </div>
255
-
256
- <h2 style={h2}>Live messages sync <span style={{ fontSize: 11, color: '#FFD700' }}>experimental</span></h2>
257
- <div style={box}>
258
- <Body style={{ margin: 0 }}>
259
- Reads your messages list once, in your own Chrome: who each one-to-one conversation is with, when it was last
260
- active, whether it&rsquo;s unread and who wrote last. It uses no search budget and opens no conversation. The
261
- newest message&rsquo;s words are kept only if <i>Keep my messages</i> is on.
262
- </Body>
263
- <div style={{ marginTop: 10, display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
264
- <button onClick={syncNow} disabled={running} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', background: running ? '#333' : 'rgba(0,255,136,0.15)', color: running ? '#777' : '#00ff88', cursor: running ? 'default' : 'pointer', fontSize: 13 }}>
265
- {running ? 'The scanner is busy' : 'Sync messages now'}
266
- </button>
267
- <label style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 12.5, color: '#b8c4c4' }}>
268
- <input type="checkbox" checked={social?.autoSync === true} onChange={(e) => setAuto(e.target.checked)} />
269
- Once a day while the app is open (daytime only)
270
- </label>
271
- </div>
272
- </div>
273
- {note && <div style={{ marginTop: 10, fontSize: 13, color: note.good ? '#00ff88' : '#ff9b9b' }}>{note.text}</div>}
274
-
275
- <h2 style={h2}>Conversations</h2>
276
- <Conversations byKey={byKey} connected={connected} ready={!!net} keep={keep} onKeep={setKeep} switching={switching || busy} token={listToken} />
277
-
278
- {social && (
279
- <>
280
- <h2 style={h2}>Warmth</h2>
281
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 10 }}>
282
- {Object.entries(WARMTH).map(([k, [label, color]]) => (
283
- <div key={k} style={{ ...box, marginTop: 0 }}>
284
- <div style={{ fontSize: 22, fontWeight: 700, color }}>{(warmth[k] || 0).toLocaleString()}</div>
285
- <div style={{ fontSize: 12.5, color: '#b8c4c4' }}>{label}</div>
286
- </div>
287
- ))}
288
- </div>
289
- <div style={{ fontSize: 11.5, color: '#778', marginTop: 6 }}>
290
- Of your connections. Warm: messaged in the last 30 days, or 3+ messages in 90. Cool: within a year. Dormant: longer ago.
291
- </div>
292
-
293
- <h2 style={h2}>Waiting on you</h2>
294
- <div style={{ ...box, padding: 0 }}>
295
- {waiting.length === 0 && <div style={{ padding: 12, fontSize: 13, color: '#8b9a9a' }}>No one is waiting on a reply.</div>}
296
- {waiting.map((w) => (
297
- <div key={w.url} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', borderBottom: '1px solid rgba(255,255,255,0.04)', fontSize: 13 }}>
298
- <span style={{ width: 8, height: 8, borderRadius: '50%', background: TIER[w.person?.tier] || '#556', flexShrink: 0 }} />
299
- <span style={{ flex: 1, minWidth: 0 }}>
300
- <b>{w.person?.name || slug(w.url)}</b>
301
- {w.person?.company ? <span style={{ color: '#8b9a9a' }}> · {w.person.company}</span> : null}
302
- </span>
303
- <span style={{ color: '#b8c4c4' }}>{w.unread ? `${w.unread} unread` : `wrote ${w.days} days before your export`}</span>
304
- <a href={w.url} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none' }}>Open ↗</a>
305
- </div>
306
- ))}
307
- </div>
308
-
309
- {social.chapters?.length > 0 && (
310
- <>
311
- <h2 style={h2}>Career chapters</h2>
312
- <div style={{ ...box, padding: 0 }}>
313
- {social.chapters.map((c) => {
314
- const warm = (c.people || []).filter((u) => ['warm', 'cool'].includes(warmthOf(merged.get(keyFor({ profile_url: u })), asOf))).length;
315
- return (
316
- <div key={`${c.company}-${c.from}`} style={{ display: 'flex', gap: 10, padding: '8px 14px', borderBottom: '1px solid rgba(255,255,255,0.04)', fontSize: 13, alignItems: 'baseline' }}>
317
- <span style={{ flex: 1 }}><b>{c.company}</b> <span style={{ color: '#8b9a9a' }}>{c.title} · {new Date(c.from).getFullYear()}–{c.to ? new Date(c.to).getFullYear() : 'now'}</span></span>
318
- <span style={{ color: '#b8c4c4' }}>{c.made.toLocaleString()} connections made</span>
319
- <span style={{ color: '#ff9f43', minWidth: 90, textAlign: 'right' }}>{c.made ? `${Math.round((100 * warm) / c.made)}% still in touch` : ''}</span>
320
- </div>
321
- );
322
- })}
323
- </div>
324
- </>
325
- )}
326
-
327
- {social.posts?.posts?.length > 0 && (
328
- <>
329
- <h2 style={h2}>Does posting work?</h2>
330
- <div style={box}>
331
- <div style={{ fontSize: 13 }}>
332
- In the week after a post you made <b>{social.posts.afterPosts.toFixed(1)}</b> new connections on average; a usual week, <b>{social.posts.usualWeek.toFixed(1)}</b>.
333
- </div>
334
- <div style={{ fontSize: 11.5, color: '#778', marginTop: 4 }}>What came after a post, not proof it came from it.</div>
335
- {[...social.posts.posts].sort((a, b) => b.week - a.week).slice(0, 3).map((p) => (
336
- <div key={p.t} style={{ fontSize: 12.5, color: '#b8c4c4', marginTop: 6 }}>
337
- {day(p.t)}: {p.week} new connections that week {p.link ? <a href={p.link} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none' }}>Post ↗</a> : null}
338
- </div>
339
- ))}
340
- </div>
341
- </>
342
- )}
343
-
344
- {social.invites?.total > 0 && (
345
- <>
346
- <h2 style={h2}>Who reaches out</h2>
347
- <div style={box}>
348
- <span style={{ fontSize: 13 }}>
349
- {social.invites.incoming.toLocaleString()} requests came to you and you sent {social.invites.outgoing.toLocaleString()}
350
- {' '}({Math.round((100 * social.invites.incoming) / social.invites.total)}% came to you), as far as your export shows.
351
- </span>
352
- </div>
353
- </>
354
- )}
355
- </>
356
- )}
357
- </main>
15
+ <div style={{ minHeight: '100vh', background: '#0a0a1a', color: '#aab', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13 }}>
16
+ Social is part of <Link href="/queue?view=messages" style={{ color: '#FF6B35', marginLeft: 4 }}>Outlink</Link> now.
358
17
  </div>
359
18
  );
360
19
  }
@@ -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