six-degrees 0.4.3 → 0.4.5

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 (279) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +3 -1
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +41 -13
  5. package/.next/standalone/.next/routes-manifest.json +12 -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 +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.rsc +18 -17
  16. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +18 -17
  17. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -13
  18. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +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.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/social/route/app-paths-manifest.json +3 -0
  44. package/.next/standalone/.next/server/app/api/social/route/build-manifest.json +12 -0
  45. package/.next/standalone/.next/server/app/api/social/route/server-reference-manifest.json +4 -0
  46. package/.next/standalone/.next/server/app/api/social/route.js +8 -0
  47. package/.next/standalone/.next/server/app/api/social/route.js.map +5 -0
  48. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -0
  49. package/.next/standalone/.next/server/app/api/social/route_client-reference-manifest.js +3 -0
  50. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/import/page.js +1 -1
  56. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/import.html +1 -1
  59. package/.next/standalone/.next/server/app/import.rsc +22 -21
  60. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +22 -21
  61. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  62. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +16 -15
  63. package/.next/standalone/.next/server/app/index.html +1 -1
  64. package/.next/standalone/.next/server/app/index.rsc +20 -19
  65. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +16 -15
  66. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +20 -19
  67. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  68. package/.next/standalone/.next/server/app/launch/page.js +1 -1
  69. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/launch.html +2 -2
  72. package/.next/standalone/.next/server/app/launch.rsc +22 -21
  73. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +22 -21
  74. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  75. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +16 -15
  76. package/.next/standalone/.next/server/app/page.js +1 -1
  77. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/paths/page.js +1 -1
  80. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  81. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  82. package/.next/standalone/.next/server/app/paths.html +1 -1
  83. package/.next/standalone/.next/server/app/paths.rsc +22 -21
  84. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +22 -21
  85. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  86. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +16 -15
  87. package/.next/standalone/.next/server/app/profile/page.js +1 -1
  88. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  89. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  90. package/.next/standalone/.next/server/app/profile.html +1 -1
  91. package/.next/standalone/.next/server/app/profile.rsc +22 -21
  92. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +22 -21
  93. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  94. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +16 -15
  95. package/.next/standalone/.next/server/app/queue/page.js +1 -1
  96. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  97. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  98. package/.next/standalone/.next/server/app/queue.html +1 -1
  99. package/.next/standalone/.next/server/app/queue.rsc +22 -21
  100. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +22 -21
  101. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  102. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +16 -15
  103. package/.next/standalone/.next/server/app/scores/page.js +1 -1
  104. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/scores.html +1 -1
  107. package/.next/standalone/.next/server/app/scores.rsc +22 -21
  108. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +22 -21
  109. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  110. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +16 -15
  111. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  112. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  113. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  114. package/.next/standalone/.next/server/app/settings.html +1 -1
  115. package/.next/standalone/.next/server/app/settings.rsc +22 -21
  116. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +22 -21
  117. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  118. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +16 -15
  119. package/.next/standalone/.next/server/app/setup/page.js +1 -1
  120. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  122. package/.next/standalone/.next/server/app/setup.html +1 -1
  123. package/.next/standalone/.next/server/app/setup.rsc +22 -21
  124. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +22 -21
  125. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  126. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +16 -15
  127. package/.next/standalone/.next/server/app/social/page/app-paths-manifest.json +3 -0
  128. package/.next/standalone/.next/server/app/social/page/build-manifest.json +20 -0
  129. package/.next/standalone/.next/server/app/social/page/next-font-manifest.json +11 -0
  130. package/.next/standalone/.next/server/app/social/page/react-loadable-manifest.json +1 -0
  131. package/.next/standalone/.next/server/app/social/page/server-reference-manifest.json +4 -0
  132. package/.next/standalone/.next/server/app/social/page.js +13 -0
  133. package/.next/standalone/.next/server/app/social/page.js.map +5 -0
  134. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -0
  135. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +3 -0
  136. package/.next/standalone/.next/server/app/social.html +1 -0
  137. package/.next/standalone/.next/server/app/social.meta +26 -0
  138. package/.next/standalone/.next/server/app/social.rsc +26 -0
  139. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +26 -0
  140. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +4 -0
  141. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +30 -0
  142. package/.next/standalone/.next/server/app-paths-manifest.json +3 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17kn1mk._.js +8 -0
  145. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0uag-k5._.js → [root-of-the-server]__1dmc7-t._.js} +1 -1
  146. package/.next/standalone/.next/server/chunks/_1_fynia._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +3 -0
  149. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_route_actions_1_3c6qn.js +3 -0
  150. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
  151. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16hs8kw._.js +3 -0
  152. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1feicqq._.js +3 -0
  153. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jeztd-._.js +3 -0
  154. package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +3 -0
  155. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +3 -0
  156. package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +3 -0
  157. package/.next/standalone/.next/server/chunks/ssr/{_0hw_qnj._.js → _0wxbq78._.js} +1 -1
  158. package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +3 -0
  159. package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +3 -0
  160. package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +3 -0
  161. package/.next/standalone/.next/server/chunks/ssr/{_1cyxocj._.js → _1ap1tu_._.js} +2 -2
  162. package/.next/standalone/.next/server/chunks/ssr/_1mj739k._.js +3 -0
  163. package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +3 -0
  164. package/.next/standalone/.next/server/chunks/ssr/_217nddo._.js +3 -0
  165. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_social_page_actions_176i1kl.js +3 -0
  166. package/.next/standalone/.next/server/chunks/ssr/app_0i3fw_m._.js +3 -0
  167. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +17 -4
  168. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  169. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  170. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
  171. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +3 -0
  172. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  173. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  174. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  175. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  176. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  177. package/.next/standalone/.next/server/pages/404.html +1 -1
  178. package/.next/standalone/.next/server/pages/500.html +1 -1
  179. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  180. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  181. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  182. package/.next/standalone/.next/static/chunks/04a3zockahetx.js +1 -0
  183. package/.next/standalone/.next/static/chunks/{2wwmljnf7yn_5.js → 0a5iadnmt9u9k.js} +1 -1
  184. package/.next/standalone/.next/static/chunks/0g_p4p7oci18u.js +1 -0
  185. package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +1 -0
  186. package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +1 -0
  187. package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +1 -0
  188. package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +1 -0
  189. package/.next/standalone/.next/static/chunks/{1zwjy1mmpkdrg.js → 24m745q_85u34.js} +2 -2
  190. package/.next/standalone/.next/static/chunks/2_zu7rmb8flzl.js +2 -0
  191. package/.next/standalone/.next/static/chunks/38wwtni5e1ec1.js +31 -0
  192. package/.next/standalone/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
  193. package/.next/{static/chunks/0_3ubivay59bq.js → standalone/.next/static/chunks/3zw8bv7-mc4n6.js} +1 -1
  194. package/.next/standalone/CHANGELOG.md +58 -0
  195. package/.next/standalone/app/api/network/route.js +11 -0
  196. package/.next/standalone/app/api/scraper/route.js +2 -0
  197. package/.next/standalone/app/api/social/route.js +96 -0
  198. package/.next/standalone/app/components/AutoScanButton.js +41 -0
  199. package/.next/standalone/app/components/ChainView.js +7 -0
  200. package/.next/standalone/app/components/FilterPanel.js +6 -0
  201. package/.next/standalone/app/components/ForceGraph.js +257 -39
  202. package/.next/standalone/app/components/GalaxyLab.js +275 -0
  203. package/.next/standalone/app/components/ScanStatusBar.js +22 -8
  204. package/.next/standalone/app/components/SeparationView.js +9 -1
  205. package/.next/standalone/app/components/Sidebar.js +88 -26
  206. package/.next/standalone/app/components/SocialAutoSync.js +34 -0
  207. package/.next/standalone/app/components/settings/NetworkHealthSection.js +85 -0
  208. package/.next/standalone/app/layout.js +3 -0
  209. package/.next/standalone/app/page.js +29 -0
  210. package/.next/standalone/app/scores/page.js +3 -0
  211. package/.next/standalone/app/setup/page.js +1 -1
  212. package/.next/standalone/app/social/page.js +281 -0
  213. package/.next/standalone/lib/experimental-client.js +38 -0
  214. package/.next/standalone/lib/galaxy-export.js +152 -0
  215. package/.next/standalone/lib/galaxy-lab.js +296 -0
  216. package/.next/standalone/lib/insights.js +54 -0
  217. package/.next/standalone/lib/linkedin-export.js +156 -0
  218. package/.next/standalone/lib/network-health.js +42 -0
  219. package/.next/standalone/lib/network.js +1 -0
  220. package/.next/standalone/package-lock.json +2 -2
  221. package/.next/standalone/package.json +1 -1
  222. package/.next/standalone/scripts/scrape.py +118 -2
  223. package/.next/static/chunks/04a3zockahetx.js +1 -0
  224. package/.next/static/chunks/{2wwmljnf7yn_5.js → 0a5iadnmt9u9k.js} +1 -1
  225. package/.next/static/chunks/0g_p4p7oci18u.js +1 -0
  226. package/.next/static/chunks/0y1yms5kez2f3.js +1 -0
  227. package/.next/static/chunks/14m1zu-8warte.js +1 -0
  228. package/.next/static/chunks/1e1n0kkz-psdb.js +1 -0
  229. package/.next/static/chunks/236f0jtp68j_h.js +1 -0
  230. package/.next/static/chunks/{1zwjy1mmpkdrg.js → 24m745q_85u34.js} +2 -2
  231. package/.next/static/chunks/2_zu7rmb8flzl.js +2 -0
  232. package/.next/static/chunks/38wwtni5e1ec1.js +31 -0
  233. package/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
  234. package/.next/{standalone/.next/static/chunks/0_3ubivay59bq.js → static/chunks/3zw8bv7-mc4n6.js} +1 -1
  235. package/CHANGELOG.md +58 -0
  236. package/lib/experimental-client.js +38 -0
  237. package/lib/galaxy-export.js +152 -0
  238. package/lib/galaxy-lab.js +296 -0
  239. package/lib/insights.js +54 -0
  240. package/lib/linkedin-export.js +156 -0
  241. package/lib/network-health.js +42 -0
  242. package/lib/network.js +1 -0
  243. package/package.json +1 -1
  244. package/scripts/scrape.py +118 -2
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d3vjsq._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m4vk0b._.js +0 -3
  247. package/.next/standalone/.next/server/chunks/ssr/_012mop3._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +0 -3
  249. package/.next/standalone/.next/server/chunks/ssr/_1163x49._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/_1av11ws._.js +0 -3
  251. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +0 -3
  252. package/.next/standalone/.next/server/chunks/ssr/_1ig7em7._.js +0 -3
  253. package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +0 -3
  254. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +0 -3
  255. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +0 -3
  256. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +0 -1
  257. package/.next/standalone/.next/static/chunks/0f4ugd6v3ph0-.js +0 -1
  258. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +0 -1
  259. package/.next/standalone/.next/static/chunks/10ogurbcuf3nq.js +0 -1
  260. package/.next/standalone/.next/static/chunks/1489t7jrjojk_.js +0 -1
  261. package/.next/standalone/.next/static/chunks/1zunq4tk-u3ng.js +0 -1
  262. package/.next/standalone/.next/static/chunks/2p5yt1o9tnxjn.js +0 -2
  263. package/.next/standalone/.next/static/chunks/2pyo4j84prgp6.js +0 -18
  264. package/.next/standalone/.next/static/chunks/3kllnkb3hof1s.js +0 -1
  265. package/.next/static/chunks/0cuuo2jwc9h94.js +0 -1
  266. package/.next/static/chunks/0f4ugd6v3ph0-.js +0 -1
  267. package/.next/static/chunks/0l1568qff0efj.js +0 -1
  268. package/.next/static/chunks/10ogurbcuf3nq.js +0 -1
  269. package/.next/static/chunks/1489t7jrjojk_.js +0 -1
  270. package/.next/static/chunks/1zunq4tk-u3ng.js +0 -1
  271. package/.next/static/chunks/2p5yt1o9tnxjn.js +0 -2
  272. package/.next/static/chunks/2pyo4j84prgp6.js +0 -18
  273. package/.next/static/chunks/3kllnkb3hof1s.js +0 -1
  274. /package/.next/standalone/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_buildManifest.js +0 -0
  275. /package/.next/standalone/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_clientMiddlewareManifest.js +0 -0
  276. /package/.next/standalone/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_ssgManifest.js +0 -0
  277. /package/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_buildManifest.js +0 -0
  278. /package/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_clientMiddlewareManifest.js +0 -0
  279. /package/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_ssgManifest.js +0 -0
@@ -0,0 +1,275 @@
1
+ 'use client';
2
+
3
+ // The Galaxy's physics lab (experimental), in the Filter panel: Obsidian-style
4
+ // sliders for the forces that lay the Galaxy out, and a replay of your network
5
+ // growing by the date you connected. Everything moves the Galaxy in place;
6
+ // lib/galaxy-lab.js holds the settings and the clock.
7
+
8
+ import { useState, useSyncExternalStore } from 'react';
9
+ import {
10
+ LAB_DEFAULTS, CLUSTERS, labNow, setLab, watchLab,
11
+ clockNow, setClock, watchClock, play, pause, stopReplay,
12
+ layoutsNow, watchLayouts, saveLayout, applyLayout, forgetLayout, milestones,
13
+ } from '../../lib/galaxy-lab';
14
+ import { savePicture, recordReplay, canRecord } from '../../lib/galaxy-export';
15
+
16
+ const noLayouts = [];
17
+
18
+ const DAY = 86400000;
19
+
20
+ const FORCES = [
21
+ { key: 'gravity', label: 'Gravity', min: 0, max: 3, step: 0.01, fmt: (v) => v.toFixed(2), hint: 'Pulls every dot in towards you.' },
22
+ { key: 'rings', label: 'Rings', min: 0, max: 5, step: 0.05, fmt: (v) => `${v.toFixed(2)}×`, hint: 'How hard each tier holds its ring. At 0 the Galaxy finds its own shape.' },
23
+ { key: 'push', label: 'Push', min: 0, max: 300, step: 1, fmt: (v) => String(v), hint: 'How hard dots push each other apart.' },
24
+ { key: 'pull', label: 'Pull', min: 0, max: 15, step: 0.05, fmt: (v) => `${v.toFixed(2)}×`, hint: 'How hard each person pulls the people who came through them.' },
25
+ { key: 'distance', label: 'Distance', min: 0.05, max: 6, step: 0.05, fmt: (v) => `${v.toFixed(2)}×`, hint: 'How far out those people sit.' },
26
+ ];
27
+ const DISPLAY = [
28
+ { key: 'dotSize', label: 'Dot size', min: 0.2, max: 8, step: 0.05, fmt: (v) => `${v.toFixed(2)}×` },
29
+ { key: 'lines', label: 'Lines', min: 0, max: 20, step: 0.1, fmt: (v) => `${v.toFixed(1)}×` },
30
+ ];
31
+
32
+ const heading = { fontSize: 9, fontWeight: 700, color: '#555', letterSpacing: 1, margin: '12px 0 6px', textTransform: 'uppercase' };
33
+ const small = { fontSize: 10, color: '#667', lineHeight: 1.4 };
34
+
35
+ function Slider({ spec, value }) {
36
+ return (
37
+ <label title={spec.hint} style={{ display: 'block', marginBottom: 7 }}>
38
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: '#aab' }}>
39
+ <span>{spec.label}</span>
40
+ <span style={{ color: value === LAB_DEFAULTS[spec.key] ? '#556' : '#cfe6f7', fontVariantNumeric: 'tabular-nums' }}>{spec.fmt(value)}</span>
41
+ </div>
42
+ <input
43
+ type="range" min={spec.min} max={spec.max} step={spec.step} value={value}
44
+ onChange={(e) => setLab({ [spec.key]: Number(e.target.value) })}
45
+ style={{ width: '100%', accentColor: '#3498DB' }}
46
+ />
47
+ </label>
48
+ );
49
+ }
50
+
51
+ function Choice({ options, value, onPick }) {
52
+ return (
53
+ <div style={{ display: 'flex', gap: 4, marginBottom: 8 }}>
54
+ {options.map(([v, label]) => (
55
+ <button key={v} onClick={() => onPick(v)} aria-pressed={value === v} style={{
56
+ flex: 1, padding: '5px 4px', borderRadius: 6, fontSize: 10.5, fontWeight: 600, cursor: 'pointer',
57
+ border: value === v ? '1px solid rgba(52,152,219,0.6)' : '1px solid rgba(255,255,255,0.08)',
58
+ background: value === v ? 'rgba(52,152,219,0.18)' : 'rgba(255,255,255,0.03)',
59
+ color: value === v ? '#cfe6f7' : '#888',
60
+ }}>{label}</button>
61
+ ))}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ /** Filter → Names: every name on the Galaxy on or off, with or without the lab. */
67
+ export function NamesSwitch() {
68
+ const lab = useSyncExternalStore(watchLab, labNow, () => LAB_DEFAULTS);
69
+ return (
70
+ <div style={{ marginBottom: 18 }}>
71
+ <div style={{ fontSize: 9, fontWeight: 700, color: '#555', letterSpacing: 1, marginBottom: 8, textTransform: 'uppercase' }}>Names</div>
72
+ <Choice value={lab.labels} options={[[true, 'On'], [false, 'Off']]} onPick={(v) => setLab({ labels: v })} />
73
+ </div>
74
+ );
75
+ }
76
+
77
+ const month = (t) => new Date(t).toLocaleDateString(undefined, { month: 'short', year: 'numeric' });
78
+
79
+ export default function GalaxyLab() {
80
+ const lab = useSyncExternalStore(watchLab, labNow, () => LAB_DEFAULTS);
81
+ const clock = useSyncExternalStore(watchClock, clockNow, clockNow);
82
+ const layouts = useSyncExternalStore(watchLayouts, layoutsNow, () => noLayouts);
83
+ const [name, setName] = useState('');
84
+ const [busy, setBusy] = useState(null); // 'picture' | 'video' while saving
85
+ const [note, setNote] = useState(null);
86
+ const recordable = useSyncExternalStore(() => () => {}, canRecord, () => false);
87
+ const marks = clock.min != null && clock.max > clock.min
88
+ ? milestones(clock.social).filter((m) => m.t >= clock.min && m.t <= clock.max) : [];
89
+ const run = async (what, job) => {
90
+ setBusy(what);
91
+ setNote(null);
92
+ try { await job(); } catch (e) { setNote(e.message || 'That didn’t work.'); } finally { setBusy(null); }
93
+ };
94
+ const canReplay = clock.min != null && clock.max != null && clock.max > clock.min;
95
+ const preset = FORCES.every((f) => lab[f.key] === LAB_DEFAULTS[f.key]) ? 'rings'
96
+ : Object.entries(CLUSTERS).every(([k, v]) => lab[k] === v) ? 'clusters' : null;
97
+
98
+ return (
99
+ <div style={{ marginBottom: 18, padding: '10px 10px 8px', borderRadius: 8, border: '1px solid rgba(212,175,55,0.25)', background: 'rgba(212,175,55,0.04)' }}>
100
+ <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>
101
+ <input
102
+ type="checkbox" checked={lab.on}
103
+ onChange={(e) => { if (!e.target.checked) { stopReplay(); setClock({ find: '' }); } setLab({ on: e.target.checked }); }}
104
+ style={{ accentColor: '#D4AF37' }}
105
+ />
106
+ <span style={{ fontSize: 11.5, fontWeight: 700, color: '#e6d7a0' }}>Physics lab</span>
107
+ <span style={{ fontSize: 9, fontWeight: 700, color: '#D4AF37', border: '1px solid rgba(212,175,55,0.4)', borderRadius: 4, padding: '0 4px' }}>EXPERIMENTAL</span>
108
+ </label>
109
+ {!lab.on && (
110
+ <div style={{ ...small, marginTop: 6 }}>
111
+ Sliders for the forces that lay the Galaxy out, colours by company or warmth, find, a branch that lights
112
+ up on hover, and a replay of your network growing that you can save as a video.
113
+ </div>
114
+ )}
115
+
116
+ {lab.on && (
117
+ <>
118
+ <div style={heading}>Find</div>
119
+ <input
120
+ type="search" value={clock.find} placeholder="A name, company or role"
121
+ onChange={(e) => setClock({ find: e.target.value })}
122
+ onKeyDown={(e) => { if (e.key === 'Enter') setClock({ fly: clock.fly + 1 }); }}
123
+ style={{
124
+ width: '100%', boxSizing: 'border-box', padding: '6px 9px', borderRadius: 6, fontSize: 11.5,
125
+ border: '1px solid rgba(255,255,255,0.12)', background: 'rgba(0,0,0,0.25)', color: '#dde',
126
+ }}
127
+ />
128
+ {clock.find.trim().length >= 2 && (
129
+ <div style={{ ...small, marginTop: 4 }}>
130
+ {clock.found ? `${clock.found.toLocaleString()} found · Enter flies to the best one` : 'No one on screen matches.'}
131
+ </div>
132
+ )}
133
+
134
+ <div style={heading}>Layout</div>
135
+ <Choice
136
+ value={preset}
137
+ options={[['rings', 'Rings'], ['clusters', 'Clusters']]}
138
+ onPick={(v) => setLab(v === 'rings' ? { ...Object.fromEntries(FORCES.map((f) => [f.key, LAB_DEFAULTS[f.key]])), sizeBy: 'power' } : CLUSTERS)}
139
+ />
140
+ <div style={{ ...small, marginBottom: 6 }}>
141
+ {preset === 'clusters'
142
+ ? 'No rings: each connection pulls their circle round them, and a dot grows with everyone behind it.'
143
+ : 'Drag a dot to see what it pulls with it. Try Rings at 0 and Pull up.'}
144
+ </div>
145
+
146
+ <div style={heading}>Forces</div>
147
+ {FORCES.map((f) => <Slider key={f.key} spec={f} value={lab[f.key]} />)}
148
+
149
+ <div style={heading}>Display</div>
150
+ {DISPLAY.map((f) => <Slider key={f.key} spec={f} value={lab[f.key]} />)}
151
+ <div style={{ fontSize: 11, color: '#aab', marginBottom: 4 }}>Size dots by</div>
152
+ <Choice value={lab.sizeBy} options={[['power', 'Power score'], ['reach', 'Who hangs off them']]} onPick={(v) => setLab({ sizeBy: v })} />
153
+ <div style={{ fontSize: 11, color: '#aab', marginBottom: 4 }}>Colour by</div>
154
+ <Choice
155
+ value={lab.colourBy}
156
+ options={[['tier', 'Tier'], ['degree', 'Degree'], ['company', 'Company'], ['warmth', 'Warmth']]}
157
+ onPick={(v) => setLab({ colourBy: v })}
158
+ />
159
+ {lab.colourBy === 'warmth' && clock.social === null && (
160
+ <div style={{ ...small, marginTop: -4, marginBottom: 8 }}>
161
+ Warmth comes from the Social tab: import your LinkedIn export there first. Until then it&rsquo;s by tier.
162
+ </div>
163
+ )}
164
+ <div style={{ fontSize: 11, color: '#aab', marginBottom: 4 }}>Which names{lab.labels ? '' : ' (Names is off)'}</div>
165
+ <Choice value={lab.names === 'all' ? 'all' : 'key'} options={[['key', 'S + catalysts'], ['all', 'All 1st']]} onPick={(v) => setLab({ names: v })} />
166
+ <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#aab', cursor: 'pointer' }}>
167
+ <input type="checkbox" checked={lab.branch} onChange={(e) => setLab({ branch: e.target.checked })} style={{ accentColor: '#3498DB' }} />
168
+ Light up a branch on hover
169
+ </label>
170
+
171
+ <div style={heading}>Replay</div>
172
+ {canReplay ? (
173
+ <>
174
+ <div style={{ display: 'flex', gap: 6, alignItems: 'center', marginBottom: 6 }}>
175
+ <button onClick={() => (clock.playing ? pause() : play())} style={{
176
+ padding: '5px 12px', borderRadius: 6, fontSize: 11, fontWeight: 700, cursor: 'pointer',
177
+ border: '1px solid rgba(52,152,219,0.6)', background: 'rgba(52,152,219,0.18)', color: '#cfe6f7',
178
+ }}>{clock.playing ? '❚❚ Pause' : '▶ Replay'}</button>
179
+ {clock.at != null && (
180
+ <button onClick={stopReplay} style={{
181
+ padding: '5px 10px', borderRadius: 6, fontSize: 11, cursor: 'pointer',
182
+ border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.04)', color: '#aaa',
183
+ }}>Show all</button>
184
+ )}
185
+ <span style={{ marginLeft: 'auto', fontSize: 10.5, color: '#aab', fontVariantNumeric: 'tabular-nums' }}>
186
+ {month(clock.at ?? clock.max)}
187
+ </span>
188
+ </div>
189
+ <div style={{ position: 'relative' }}>
190
+ <input
191
+ type="range" min={clock.min} max={clock.max} step={DAY} value={clock.at ?? clock.max}
192
+ onChange={(e) => { pause(); setClock({ at: Number(e.target.value) }); }}
193
+ style={{ width: '100%', accentColor: '#3498DB' }}
194
+ aria-label="Time"
195
+ />
196
+ {/* Milestones from the Social tab: a job start (gold) or a post (blue). */}
197
+ <div style={{ position: 'absolute', left: 7, right: 7, top: -3, height: 6, pointerEvents: 'none' }}>
198
+ {marks.map((m) => (
199
+ <span key={`${m.kind}-${m.t}`} title={`${m.label} · ${month(m.t)}`} style={{
200
+ position: 'absolute', left: `${((m.t - clock.min) / (clock.max - clock.min)) * 100}%`,
201
+ width: m.kind === 'job' ? 3 : 2, height: m.kind === 'job' ? 8 : 5, borderRadius: 1, transform: 'translateX(-50%)',
202
+ background: m.kind === 'job' ? '#D4AF37' : '#74B9FF', opacity: m.kind === 'job' ? 0.95 : 0.7,
203
+ }} />
204
+ ))}
205
+ </div>
206
+ </div>
207
+ <div style={{ display: 'flex', justifyContent: 'space-between', ...small }}>
208
+ <span>{month(clock.min)}</span><span>{month(clock.max)}</span>
209
+ </div>
210
+ <div style={{ fontSize: 11, color: '#aab', margin: '8px 0 4px' }}>Length</div>
211
+ <Choice value={lab.speed} options={[[5, '5 s'], [15, '15 s'], [30, '30 s'], [60, '1 min']]} onPick={(v) => setLab({ speed: v })} />
212
+ <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#aab', cursor: 'pointer' }}>
213
+ <input type="checkbox" checked={lab.loop} onChange={(e) => setLab({ loop: e.target.checked })} style={{ accentColor: '#3498DB' }} />
214
+ Loop
215
+ </label>
216
+ <div style={{ ...small, marginTop: 4 }}>
217
+ Your connections appear on the day you connected; their circles come with them.
218
+ {marks.length > 0 ? ' Gold marks are your job starts and blue ones your posts, from the Social tab.' : ''}
219
+ </div>
220
+ {recordable && (
221
+ <button disabled={!!busy} onClick={() => run('video', recordReplay)} style={{
222
+ marginTop: 8, width: '100%', padding: '6px 10px', borderRadius: 6, fontSize: 11, fontWeight: 600,
223
+ cursor: busy ? 'default' : 'pointer', border: '1px solid rgba(231,76,60,0.5)',
224
+ background: 'rgba(231,76,60,0.12)', color: '#f5b7b1',
225
+ }}>{busy === 'video' ? '● Recording…' : '● Record the replay as a video'}</button>
226
+ )}
227
+ </>
228
+ ) : (
229
+ <div style={small}>
230
+ Nothing to replay: the connections on screen have no &ldquo;connected on&rdquo; dates. A LinkedIn CSV or a
231
+ network scan has them; the sample network doesn&rsquo;t.
232
+ </div>
233
+ )}
234
+
235
+ <div style={heading}>Saved layouts</div>
236
+ {layouts.length > 0 && (
237
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 6 }}>
238
+ {layouts.map((l) => (
239
+ <span key={l.name} style={{ display: 'inline-flex', alignItems: 'center', borderRadius: 12, border: '1px solid rgba(52,152,219,0.4)', background: 'rgba(52,152,219,0.1)' }}>
240
+ <button onClick={() => applyLayout(l.name)} style={{ padding: '3px 4px 3px 9px', border: 'none', background: 'none', color: '#cfe6f7', fontSize: 10.5, cursor: 'pointer' }}>{l.name}</button>
241
+ <button onClick={() => forgetLayout(l.name)} aria-label={`Forget ${l.name}`} title="Forget this layout" style={{ padding: '3px 8px 3px 3px', border: 'none', background: 'none', color: '#667', fontSize: 11, cursor: 'pointer' }}>×</button>
242
+ </span>
243
+ ))}
244
+ </div>
245
+ )}
246
+ <form onSubmit={(e) => { e.preventDefault(); saveLayout(name); setName(''); }} style={{ display: 'flex', gap: 4 }}>
247
+ <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name this layout" maxLength={40} style={{
248
+ flex: 1, minWidth: 0, padding: '5px 8px', borderRadius: 6, fontSize: 11,
249
+ border: '1px solid rgba(255,255,255,0.12)', background: 'rgba(0,0,0,0.25)', color: '#dde',
250
+ }} />
251
+ <button type="submit" disabled={!name.trim()} style={{
252
+ padding: '5px 10px', borderRadius: 6, fontSize: 11, fontWeight: 600, cursor: name.trim() ? 'pointer' : 'default',
253
+ border: '1px solid rgba(52,152,219,0.6)', background: 'rgba(52,152,219,0.18)', color: name.trim() ? '#cfe6f7' : '#667',
254
+ }}>Save</button>
255
+ </form>
256
+
257
+ <div style={heading}>Share</div>
258
+ <button disabled={!!busy} onClick={() => run('picture', savePicture)} style={{
259
+ width: '100%', padding: '6px 10px', borderRadius: 6, fontSize: 11, fontWeight: 600, cursor: busy ? 'default' : 'pointer',
260
+ border: '1px solid rgba(52,152,219,0.6)', background: 'rgba(52,152,219,0.18)', color: '#cfe6f7',
261
+ }}>{busy === 'picture' ? 'Saving…' : 'Save a picture of the Galaxy'}</button>
262
+ <div style={{ ...small, marginTop: 4 }}>
263
+ A PNG of what&rsquo;s on screen, at twice the size. It shows real names, so check it before you post it.
264
+ </div>
265
+ {note && <div style={{ ...small, color: '#e67e73', marginTop: 4 }}>{note}</div>}
266
+
267
+ <button onClick={() => { stopReplay(); setClock({ find: '' }); setLab(null); }} style={{
268
+ marginTop: 10, width: '100%', padding: '6px 10px', borderRadius: 6, fontSize: 11, cursor: 'pointer',
269
+ border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.04)', color: '#aaa',
270
+ }}>Reset to today&rsquo;s layout</button>
271
+ </>
272
+ )}
273
+ </div>
274
+ );
275
+ }
@@ -7,9 +7,10 @@
7
7
  // page, the Scan page too (Blake, 2026-09-29). It only reports: the
8
8
  // pacing and the caps live in the scanner (scripts/scrape.py, lib/linkedin-limits.js).
9
9
 
10
- import { useEffect, useState } from 'react';
10
+ import { useEffect, useState, useSyncExternalStore } from 'react';
11
11
  import Link from 'next/link';
12
12
  import { watchScanner, scannerNow, stopScrape } from '../../lib/scraper-client';
13
+ import { watchAllDay, allDayNow } from '../../lib/experimental-client';
13
14
 
14
15
  const WHAT = {
15
16
  full: 'Scanning your network',
@@ -24,6 +25,7 @@ const WHAT = {
24
25
  setup: 'Setting up the scanner',
25
26
  company: 'Scanning a company',
26
27
  photos: 'Saving photos',
28
+ messages: 'Reading your messages list',
27
29
  };
28
30
 
29
31
  // Green while there's plenty left, amber past 60%, red past 90%.
@@ -32,12 +34,29 @@ const tone = (used, cap) => (!cap ? '#8b9a9a' : used / cap >= 0.9 ? '#ff6b6b' :
32
34
  export default function ScanStatusBar() {
33
35
  const [job, setJob] = useState(() => scannerNow());
34
36
  const [stopping, setStopping] = useState(false);
37
+ // The Scan page's experimental all-day Auto-Bridge switch: with it on, the bar
38
+ // stays under the tabs even while nothing runs, so you can see the mode is on.
39
+ const allDay = useSyncExternalStore(watchAllDay, allDayNow, () => false);
35
40
  useEffect(() => watchScanner(() => {
36
41
  const now = scannerNow();
37
42
  setJob(now);
38
43
  if (!now.running) setStopping(false);
39
44
  }), []);
40
- if (!job?.running) return null;
45
+ const place = {
46
+ position: 'fixed', left: '50%', top: 'var(--scan-bar-top, 94px)', transform: 'translateX(-50%)', zIndex: 60,
47
+ maxWidth: 'calc(100vw - 32px)', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'nowrap', whiteSpace: 'nowrap',
48
+ padding: '8px 12px', borderRadius: 12, fontSize: 12, color: '#cfd8d8', background: 'rgba(14,16,32,0.94)',
49
+ };
50
+ if (!job?.running) {
51
+ if (!allDay) return null;
52
+ return (
53
+ <div role="status" style={{ ...place, opacity: 0.55, border: '1px solid rgba(255,255,255,0.1)' }}>
54
+ <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#556', flexShrink: 0 }} />
55
+ <b style={{ color: '#b8c4c4' }}>Auto scan</b>
56
+ <span style={{ color: '#8b9a9a' }}>ready · press Auto scan beside Scan to start</span>
57
+ </div>
58
+ );
59
+ }
41
60
 
42
61
  const p = job.progress;
43
62
  const step = p?.kind === 'batch' && p.total ? `${p.current || p.done || 0} of ${p.total}`
@@ -47,12 +66,7 @@ export default function ScanStatusBar() {
47
66
  const last = [...(job.log || [])].reverse().find((l) => l && l.trim())?.trim();
48
67
 
49
68
  return (
50
- <div role="status" aria-live="polite" style={{
51
- position: 'fixed', left: '50%', top: 'var(--scan-bar-top, 94px)', transform: 'translateX(-50%)', zIndex: 60,
52
- maxWidth: 'calc(100vw - 32px)', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'nowrap', whiteSpace: 'nowrap',
53
- padding: '8px 12px', borderRadius: 12, fontSize: 12, color: '#cfd8d8',
54
- background: 'rgba(14,16,32,0.94)', border: '1px solid rgba(0,255,136,0.25)',
55
- }}>
69
+ <div role="status" aria-live="polite" style={{ ...place, border: '1px solid rgba(0,255,136,0.25)' }}>
56
70
  <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#00ff88', flexShrink: 0 }} />
57
71
  <b style={{ color: '#fff' }}>{WHAT[job.action] || 'Scanning'}</b>
58
72
  {job.target?.name && <span>{job.target.name}</span>}
@@ -66,7 +66,7 @@ function subline(person) {
66
66
  return person.headline || person.role || person.company || '';
67
67
  }
68
68
 
69
- export default function SeparationView({ connections = [], degree2 = [], fullDegree1 = connections, fullDegree2 = degree2, onSelect, userName, selectedId, tierColors = CLASSIC }) {
69
+ export default function SeparationView({ connections = [], degree2 = [], fullDegree1 = connections, fullDegree2 = degree2, onSelect, userName, selectedId, tierColors = CLASSIC, preset = null }) {
70
70
  const isMobile = useIsMobile();
71
71
  const ROW_H = isMobile ? 68 : 56;
72
72
  const K = isMobile ? 5 : 10;
@@ -78,6 +78,14 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
78
78
  const [tier, setTier] = useState('all');
79
79
  // Rarity beside the tier (lib/rarity.js): any bands switched on; none is everyone.
80
80
  const [rarities, setRarities] = useState(() => new Set());
81
+ // Filters a card's Insights asked for ("Show them", "S only"): applied once each.
82
+ const [presetFor, setPresetFor] = useState(null);
83
+ if (preset && preset.id !== presetFor) {
84
+ setPresetFor(preset.id);
85
+ setQuery(preset.query || '');
86
+ setTier(preset.tier || 'all');
87
+ setRarities(new Set(preset.rarity ? [preset.rarity] : []));
88
+ }
81
89
  const requests = useRequests();
82
90
  const [scrollY, setScrollY] = useState(0);
83
91
  const [box, setBox] = useState({ w: 0, h: 800 });
@@ -12,10 +12,11 @@ import { routeIndex, routesFor } from '../../lib/separation';
12
12
  import { topCompanies } from '../../lib/scoring';
13
13
  import { reachIndex, reachState, circleState, readyToScan, circleScanCost } from '../../lib/reach';
14
14
  import { exclusiveReach, bridgeOverlap } from '../../lib/brokerage';
15
+ import { profileInsights } from '../../lib/insights';
15
16
  import Avatar from './Avatar';
16
17
  import { localPhoto } from '../../lib/photos';
17
18
 
18
- export default function Sidebar({ selected, stats, tierColors, connections, degree2 = [], mode, collapsed, filter, pending = [], onToggle, onSelect, onSwitchMode, onFocusNode, onMarkSent, onUndoPending, scanNotes, csvSource = null }) {
19
+ export default function Sidebar({ selected, stats, tierColors, connections, degree2 = [], mode, collapsed, filter, pending = [], onToggle, onSelect, onSwitchMode, onFocusNode, onMarkSent, onUndoPending, scanNotes, csvSource = null, onOpenCircle, onShowInSeparation }) {
19
20
  const isDegreesMode = mode === 'degrees';
20
21
  // 'sample' or 'csv' while the sample or a CSV import is open in this window,
21
22
  // null for your own network. Only your own can be scanned: the scanner looks
@@ -43,6 +44,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
43
44
  // Who only each connection reaches (lib/brokerage.js), for their card.
44
45
  const exclusive = useMemo(() => exclusiveReach(degree2, connections), [degree2, connections]);
45
46
  const overlap = useMemo(() => bridgeOverlap(degree2, connections), [degree2, connections]);
47
+ const insightReach = useMemo(() => reachIndex(connections, degree2, scanNotes), [connections, degree2, scanNotes]);
48
+ // The Insights panel on a connection's card: open or closed, kept as you move between cards.
49
+ const [showInsights, setShowInsights] = useState(false);
46
50
  const bridgeById = useMemo(() => new Map(connections.map(c => [c.id, c])), [connections]);
47
51
  // Who you've asked, shared with every view: a request sent here shows in
48
52
  // Separation, the circle and the Outlink queue at once (lib/requests-client.js).
@@ -143,33 +147,91 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
143
147
  />
144
148
  )}
145
149
 
146
- {selected.degree !== 2 && (exclusive.get(selected.id)?.only > 0 || (overlap.get(selected.id)?.share >= 0.15 && overlap.get(selected.id)?.shared >= 3)) && (() => {
147
- const ex = exclusive.get(selected.id);
148
- const ov = overlap.get(selected.id);
149
- // A twin only when it's real: at least 15% of either's people, and 3 of them.
150
- const twin = ov && ov.share >= 0.15 && ov.shared >= 3 && connections.find((c) => c.id === ov.with);
150
+ {selected.degree !== 2 && (() => {
151
+ const i = profileInsights(selected, { degree2, exclusive, overlap, reach: insightReach });
152
+ if (!i) return null;
151
153
  const first = selected.name?.split(' ')[0] || 'them';
154
+ const twin = i.twin && connections.find((c) => c.id === i.twin.id);
155
+ const row = { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10, padding: '7px 0', borderTop: '1px solid rgba(255,255,255,0.05)' };
156
+ const label = { fontSize: 10.5, color: '#8b9a9a', minWidth: 92 };
157
+ const act = { background: 'none', border: 'none', padding: 0, color: '#3498DB', cursor: 'pointer', fontSize: 11.5, whiteSpace: 'nowrap' };
158
+ const scanned = i.bars == null ? 'Not scanned yet' : i.bars === 5 ? 'Their whole list' : `About ${i.bars * 20}% of their list`;
152
159
  return (
153
- <div style={{
154
- background: 'rgba(0,255,136,0.05)', border: '1px solid rgba(0,255,136,0.2)',
155
- borderRadius: 8, padding: 12, marginBottom: 16,
156
- }}>
157
- <div style={{ fontSize: 10, color: '#00ff88', fontWeight: 700, marginBottom: 4, letterSpacing: 1 }}>
158
- {ex?.only > 0 ? `ONLY THROUGH ${first.toUpperCase()}` : `${first.toUpperCase()}’S CIRCLE`}
159
- </div>
160
- {ex?.only > 0 && (
161
- <div style={{ fontSize: 12, color: '#ccc', lineHeight: 1.5 }}>
162
- <strong style={{ color: '#00ff88' }}>{ex.only.toLocaleString()}</strong> of the {ex.total.toLocaleString()} people in {first}&rsquo;s
163
- circle are reached by none of your other connections. {first} is your only way to them.
164
- </div>
165
- )}
166
- {twin && (
167
- <div style={{ fontSize: 12, color: '#aab', lineHeight: 1.5, marginTop: ex?.only > 0 ? 6 : 0 }}>
168
- Opens the same doors as{' '}
169
- <button onClick={() => onSelect?.(twin)} style={{ background: 'none', border: 'none', padding: 0, color: '#fff', fontWeight: 700, cursor: 'pointer', fontSize: 12 }}>
170
- {twin.name}
171
- </button>
172
- : {Math.round(ov.share * 100)}% of the people either reaches, both do ({ov.shared.toLocaleString()}).
160
+ <div style={{ border: '1px solid rgba(52,152,219,0.25)', borderRadius: 8, marginBottom: 16, background: 'rgba(52,152,219,0.04)' }}>
161
+ <button onClick={() => setShowInsights((v) => !v)} aria-expanded={showInsights}
162
+ style={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '9px 12px', background: 'none', border: 'none', cursor: 'pointer', color: '#cfe6f7' }}>
163
+ <span style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1 }}>INSIGHTS</span>
164
+ <span style={{ fontSize: 11, color: '#8b9a9a' }}>
165
+ {!showInsights && i.circle > 0 && `${i.circle.toLocaleString()} in their circle${i.only ? ` · ${i.only.toLocaleString()} only through ${first}` : ''} `}
166
+ {showInsights ? '▲' : '▼'}
167
+ </span>
168
+ </button>
169
+ {showInsights && (
170
+ <div style={{ padding: '0 12px 8px', fontSize: 12, color: '#ccc' }}>
171
+ <div style={row}>
172
+ <span style={label}>Their circle</span>
173
+ <span style={{ flex: 1 }}>{i.circle ? `${i.circle.toLocaleString()} people` : 'None scanned yet'}</span>
174
+ {i.circle > 0 && onOpenCircle && <button style={act} onClick={() => onOpenCircle(selected.id)}>Explore →</button>}
175
+ </div>
176
+ <div style={row}>
177
+ <span style={label}>Only through {first}</span>
178
+ <span style={{ flex: 1 }}>
179
+ {i.circle ? <><b style={{ color: '#00ff88' }}>{i.only.toLocaleString()}</b> ({Math.round(i.onlyShare * 100)}%): none of your other connections reach them</> : '—'}
180
+ </span>
181
+ {i.only > 0 && onShowInSeparation && (
182
+ <button style={act} onClick={() => onShowInSeparation({ query: selected.name, rarity: 'only' })}>Show them →</button>
183
+ )}
184
+ </div>
185
+ <div style={row}>
186
+ <span style={label}>Closest overlap</span>
187
+ <span style={{ flex: 1 }}>
188
+ {twin ? <>Opens the same doors as <b style={{ color: '#fff' }}>{twin.name}</b> ({Math.round(i.twin.share * 100)}%, {i.twin.shared.toLocaleString()} people)</> : 'No one opens the same doors'}
189
+ </span>
190
+ {twin && <button style={act} onClick={() => onSelect?.(twin)}>Open →</button>}
191
+ </div>
192
+ <div style={row}>
193
+ <span style={label}>Tier mix</span>
194
+ <span style={{ flex: 1, display: 'flex', height: 8, borderRadius: 4, overflow: 'hidden', background: 'rgba(255,255,255,0.05)', alignSelf: 'center' }}>
195
+ {i.circle > 0 && ['S', 'A', 'B', 'C', 'D'].map((t) => i.mix[t] > 0 && (
196
+ <span key={t} title={`${t}: ${i.mix[t]}`} style={{ width: `${(100 * i.mix[t]) / i.circle}%`, background: tierColors[t] }} />
197
+ ))}
198
+ </span>
199
+ <span style={{ fontSize: 10.5, color: '#8b9a9a' }}>{i.circle ? `${i.mix.S} S · ${i.mix.A} A` : ''}</span>
200
+ {i.mix.S > 0 && onShowInSeparation && (
201
+ <button style={act} onClick={() => onShowInSeparation({ query: selected.name, tier: 'S' })}>S only →</button>
202
+ )}
203
+ </div>
204
+ {i.companies.length > 0 && (
205
+ <div style={row}>
206
+ <span style={label}>Where they work</span>
207
+ <span style={{ flex: 1 }}>{i.companies.map((c) => `${c.count} at ${c.name}`).join(' · ')}</span>
208
+ </div>
209
+ )}
210
+ <div style={row}>
211
+ <span style={label}>Scanned</span>
212
+ <span style={{ flex: 1 }}>{scanned}</span>
213
+ {i.bars !== 5 && canScan && (
214
+ <a href={`/setup?scan=${encodeURIComponent(selected.id)}`} style={act}>{i.bars == null ? 'Scan →' : 'Finish →'}</a>
215
+ )}
216
+ </div>
217
+ {(() => {
218
+ const since = selected.connected_date ? new Date(selected.connected_date) : null;
219
+ const asked = (bridgeMap[selected.id] || []).filter((p) => hasRequest(p, requests)).length;
220
+ if (!(since && !Number.isNaN(since.getTime())) && !asked) return null;
221
+ return (
222
+ <div style={row}>
223
+ <span style={label}>You and {first}</span>
224
+ <span style={{ flex: 1 }}>
225
+ {since && !Number.isNaN(since.getTime()) ? `Connected ${since.toLocaleDateString(undefined, { month: 'short', year: 'numeric' })}` : ''}
226
+ {asked ? `${since ? ' · ' : ''}${asked} request${asked === 1 ? '' : 's'} out through them` : ''}
227
+ </span>
228
+ </div>
229
+ );
230
+ })()}
231
+ <div style={row}>
232
+ <span style={label}>Rank</span>
233
+ <span style={{ flex: 1 }}>{i.rank ? `#${i.rank} of ${i.of} by who only they reach` : 'Ranked once their circle is scanned'}</span>
234
+ </div>
173
235
  </div>
174
236
  )}
175
237
  </div>
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ // The Social tab's "once a day" live messages sync (experimental): once per
4
+ // app session, if it's switched on, the last sync is over a day old, it's
5
+ // daytime and the scanner is free, read the messages list (scripts/scrape.py
6
+ // --messages). Nothing else starts it.
7
+
8
+ import { useEffect } from 'react';
9
+ import { watchScanner, scannerNow, beginScrape } from '../../lib/scraper-client';
10
+ import { IS_DEMO } from '../../lib/demo';
11
+
12
+ let tried = false;
13
+
14
+ export default function SocialAutoSync() {
15
+ useEffect(() => {
16
+ if (IS_DEMO || tried) return undefined;
17
+ let stop = () => {};
18
+ fetch('/api/social').then((r) => r.json()).then(({ social }) => {
19
+ if (!social?.autoSync || tried) return;
20
+ const last = social.liveAt ? Date.parse(social.liveAt) : 0;
21
+ const hour = new Date().getHours();
22
+ if (Date.now() - last < 24 * 3600 * 1000 || hour < 9 || hour >= 19) return;
23
+ stop = watchScanner(() => {
24
+ const now = scannerNow();
25
+ if (!now.known || tried) return;
26
+ tried = true;
27
+ stop();
28
+ if (!now.running) beginScrape('messages').catch(() => {});
29
+ });
30
+ }).catch(() => {});
31
+ return () => stop();
32
+ }, []);
33
+ return null;
34
+ }
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ // Network health on the Scores tab (lib/network-health.js): how much of your
4
+ // 2nd degree you reach two or more ways, the effective size of your own
5
+ // network from the ties scans have kept, and your top five connections by who
6
+ // only they reach. Only your own numbers, never a percentile against others.
7
+
8
+ import { useEffect, useState } from 'react';
9
+ import Link from 'next/link';
10
+ import { useUser } from '../UserProvider';
11
+ import { loadNetwork } from '../../../lib/network';
12
+ import { networkHealth } from '../../../lib/network-health';
13
+ import { Body, LINE } from '../ui';
14
+
15
+ const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
16
+ const pct = (x) => `${Math.round(x * 100)}%`;
17
+
18
+ export default function NetworkHealthSection() {
19
+ const { userId } = useUser();
20
+ const [health, setHealth] = useState(null);
21
+ const [failed, setFailed] = useState(false);
22
+ useEffect(() => {
23
+ if (!userId) return undefined;
24
+ let live = true;
25
+ loadNetwork(userId)
26
+ .then((net) => { if (live) setHealth(networkHealth(net)); })
27
+ .catch(() => { if (live) setFailed(true); });
28
+ return () => { live = false; };
29
+ }, [userId]);
30
+
31
+ const box = { padding: '12px 14px', borderRadius: 8, border: LINE, background: 'rgba(255,255,255,0.03)' };
32
+ return (
33
+ <section id="health" style={{ marginTop: 20 }}>
34
+ <h2 style={{ fontSize: 16, margin: '0 0 6px' }}>Network health</h2>
35
+ {failed && <Body>Couldn&rsquo;t read your network just now.</Body>}
36
+ {!failed && !health && <Body>Reading your network…</Body>}
37
+ {health && health.circles === 0 && (
38
+ <Body>Scan a few of your connections&rsquo; circles (the Scan page, step 4) and this shows how your network holds together.</Body>
39
+ )}
40
+ {health && health.circles > 0 && (
41
+ <>
42
+ {health.early && (
43
+ <div style={{ fontSize: 12.5, color: '#FFD700', margin: '4px 0 10px' }}>
44
+ An early estimate: {health.circles} {health.circles === 1 ? 'circle' : 'circles'} scanned. These get sharper as you scan more.
45
+ </div>
46
+ )}
47
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 }}>
48
+ <div style={box}>
49
+ <div style={{ fontSize: 22, fontWeight: 700 }}>{pct(health.twoWays)}</div>
50
+ <div style={{ fontSize: 12.5, color: '#b8c4c4' }}>of your 2nd degree you reach two or more ways</div>
51
+ <div style={{ fontSize: 11.5, color: '#778', marginTop: 4 }}>Higher is safer: lose one connection and they&rsquo;re still reachable.</div>
52
+ </div>
53
+ <div style={box}>
54
+ <div style={{ fontSize: 22, fontWeight: 700 }}>{Math.round(health.effective.size).toLocaleString()}</div>
55
+ <div style={{ fontSize: 12.5, color: '#b8c4c4' }}>
56
+ effective reach of your {health.effective.n.toLocaleString()} connections ({pct(health.effective.efficiency)} non-overlapping)
57
+ </div>
58
+ <div style={{ fontSize: 11.5, color: '#778', marginTop: 4 }}>
59
+ From {health.effective.ties.toLocaleString()} {health.effective.ties === 1 ? 'tie' : 'ties'} between your connections seen in circle scans so far,
60
+ so it can only come down as you scan more.
61
+ </div>
62
+ </div>
63
+ </div>
64
+ {health.top.length > 0 && (
65
+ <div style={{ ...box, marginTop: 10, padding: 0 }}>
66
+ <div style={{ padding: '10px 14px', fontSize: 12.5, color: '#b8c4c4', borderBottom: LINE }}>Your connections who reach the most people no one else does</div>
67
+ {health.top.map((b, k) => (
68
+ <div key={b.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', borderBottom: k < health.top.length - 1 ? '1px solid rgba(255,255,255,0.04)' : 'none', fontSize: 13 }}>
69
+ <span style={{ width: 22, color: '#778' }}>#{k + 1}</span>
70
+ <span style={{ width: 8, height: 8, borderRadius: '50%', background: TIER[b.tier] || '#667', flexShrink: 0 }} />
71
+ <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
72
+ <b>{b.name}</b>{b.company ? <span style={{ color: '#8b9a9a' }}> · {b.company}</span> : null}
73
+ </span>
74
+ <span style={{ color: '#b8c4c4', whiteSpace: 'nowrap' }}>{b.circle.toLocaleString()} in circle</span>
75
+ <span style={{ color: '#00ff88', whiteSpace: 'nowrap' }}>{b.only.toLocaleString()} only them ({pct(b.share)})</span>
76
+ <Link href={`/?chain=${encodeURIComponent(b.id)}`} style={{ color: '#3498DB', textDecoration: 'none', whiteSpace: 'nowrap' }}>Explore →</Link>
77
+ </div>
78
+ ))}
79
+ </div>
80
+ )}
81
+ </>
82
+ )}
83
+ </section>
84
+ );
85
+ }