six-degrees 0.7.0 → 0.8.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 (349) 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 +32 -4
  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 +16 -16
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  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.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/backup/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/csv/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/restore/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
  47. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/scraper/route.js +4 -4
  51. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +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.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/route.js +1 -1
  60. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  62. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  65. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  67. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/import.html +1 -1
  72. package/.next/standalone/.next/server/app/import.rsc +18 -18
  73. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +18 -18
  74. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  75. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +16 -16
  76. package/.next/standalone/.next/server/app/index.html +1 -1
  77. package/.next/standalone/.next/server/app/index.rsc +19 -19
  78. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +16 -16
  79. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +19 -19
  80. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  81. package/.next/standalone/.next/server/app/insights/page/app-paths-manifest.json +3 -0
  82. package/.next/standalone/.next/server/app/insights/page/build-manifest.json +20 -0
  83. package/.next/standalone/.next/server/app/insights/page/next-font-manifest.json +11 -0
  84. package/.next/standalone/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
  85. package/.next/standalone/.next/server/app/insights/page/server-reference-manifest.json +4 -0
  86. package/.next/standalone/.next/server/app/insights/page.js +13 -0
  87. package/.next/standalone/.next/server/app/insights/page.js.map +5 -0
  88. package/.next/standalone/.next/server/app/insights/page.js.nft.json +1 -0
  89. package/.next/standalone/.next/server/app/insights/page_client-reference-manifest.js +3 -0
  90. package/.next/standalone/.next/server/app/insights.html +1 -0
  91. package/.next/standalone/.next/server/app/insights.meta +26 -0
  92. package/.next/standalone/.next/server/app/insights.rsc +35 -0
  93. package/.next/standalone/.next/server/app/insights.segments/_full.segment.rsc +35 -0
  94. package/.next/standalone/.next/server/app/insights.segments/_tree.segment.rsc +4 -0
  95. package/.next/standalone/.next/server/app/insights.segments/insights/__PAGE__.segment.rsc +33 -0
  96. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  97. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  98. package/.next/standalone/.next/server/app/launch.html +2 -2
  99. package/.next/standalone/.next/server/app/launch.rsc +18 -18
  100. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +18 -18
  101. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  102. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +16 -16
  103. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  104. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  105. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  106. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  107. package/.next/standalone/.next/server/app/paths.html +1 -1
  108. package/.next/standalone/.next/server/app/paths.rsc +19 -19
  109. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +19 -19
  110. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  111. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +16 -16
  112. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  113. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  114. package/.next/standalone/.next/server/app/profile.html +1 -1
  115. package/.next/standalone/.next/server/app/profile.rsc +19 -19
  116. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +19 -19
  117. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  118. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +16 -16
  119. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  120. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  121. package/.next/standalone/.next/server/app/queue.html +1 -1
  122. package/.next/standalone/.next/server/app/queue.rsc +19 -19
  123. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +19 -19
  124. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  125. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +21 -20
  126. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  127. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  128. package/.next/standalone/.next/server/app/scores.html +1 -1
  129. package/.next/standalone/.next/server/app/scores.rsc +18 -18
  130. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +18 -18
  131. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  132. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +16 -16
  133. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  134. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  135. package/.next/standalone/.next/server/app/settings.html +1 -1
  136. package/.next/standalone/.next/server/app/settings.rsc +19 -19
  137. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +19 -19
  138. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  139. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +16 -16
  140. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  141. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  142. package/.next/standalone/.next/server/app/setup.html +1 -1
  143. package/.next/standalone/.next/server/app/setup.rsc +19 -19
  144. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +19 -19
  145. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  146. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +16 -16
  147. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  148. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  149. package/.next/standalone/.next/server/app/social.html +1 -1
  150. package/.next/standalone/.next/server/app/social.rsc +18 -18
  151. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +18 -18
  152. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  153. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +16 -16
  154. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  155. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__063s0x7._.js → [root-of-the-server]__0r-0t-9._.js} +1 -1
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ra6xyg._.js +15 -0
  157. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20rwfdw._.js → [root-of-the-server]__0rcpjad._.js} +5 -2
  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/lib_02iz_9q._.js +2 -2
  161. package/.next/standalone/.next/server/chunks/lib_05agvk3._.js +2 -2
  162. package/.next/standalone/.next/server/chunks/lib_0v84u66._.js +2 -2
  163. package/.next/standalone/.next/server/chunks/lib_db-client_0_dch35.js +9 -9
  164. package/.next/standalone/.next/server/chunks/{lib_rpc_0jnfp2e.js → lib_rpc_00nndai.js} +1 -1
  165. package/.next/standalone/.next/server/chunks/{lib_rpc_0b17r6v.js → lib_rpc_03ibb-x.js} +1 -1
  166. package/.next/standalone/.next/server/chunks/{lib_rpc_0doq0t-.js → lib_rpc_03ri3ji.js} +1 -1
  167. package/.next/standalone/.next/server/chunks/{lib_rpc_0fd2rlw.js → lib_rpc_078ce48.js} +1 -1
  168. package/.next/standalone/.next/server/chunks/lib_rpc_0a69mgr.js +3 -0
  169. package/.next/standalone/.next/server/chunks/{lib_rpc_005abye.js → lib_rpc_0cej3t8.js} +1 -1
  170. package/.next/standalone/.next/server/chunks/lib_rpc_0cmh4g6.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_0nx2_pc.js +3 -0
  172. package/.next/standalone/.next/server/chunks/lib_rpc_0r79-w1.js +3 -0
  173. package/.next/standalone/.next/server/chunks/{lib_rpc_0is4qoc.js → lib_rpc_0t552z6.js} +1 -1
  174. package/.next/standalone/.next/server/chunks/lib_rpc_0yx8ynd.js +3 -0
  175. package/.next/standalone/.next/server/chunks/lib_rpc_12p-o9-.js +3 -0
  176. package/.next/standalone/.next/server/chunks/lib_rpc_1466dnb.js +3 -0
  177. package/.next/standalone/.next/server/chunks/lib_rpc_1sryxv6.js +3 -0
  178. package/.next/standalone/.next/server/chunks/lib_rpc_1trn-x5.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_1ygu_mv.js +3 -0
  180. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egamyq._.js +3 -0
  181. package/.next/standalone/.next/server/chunks/ssr/_04am_h8._.js +3 -0
  182. package/.next/standalone/.next/server/chunks/ssr/_07zb7lu._.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/_0_79idq._.js +1 -1
  184. package/.next/standalone/.next/server/chunks/ssr/_0gds903._.js +3 -0
  185. package/.next/standalone/.next/server/chunks/ssr/_0klqt9z._.js +3 -0
  186. package/.next/standalone/.next/server/chunks/ssr/{_1k9qfsk._.js → _14jak5s._.js} +6 -2
  187. package/.next/standalone/.next/server/chunks/ssr/_14nxd3q._.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/_1cj2yog._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/_1fm1ih_._.js +1 -1
  190. package/.next/standalone/.next/server/chunks/ssr/_1hdqw0-._.js +7 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_1k2nmbw._.js +1 -1
  192. package/.next/standalone/.next/server/chunks/ssr/{_0gy11ar._.js → _1r62six._.js} +6 -2
  193. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_insights_page_actions_0-0tf75.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/app_components_0uuff77._.js +2 -2
  195. package/.next/standalone/.next/server/chunks/ssr/app_components_1j3q-h5._.js +2 -2
  196. package/.next/standalone/.next/server/chunks/ssr/{_01o3i5y._.js → app_components_settings_SectorPicker_1c-29qw.js} +2 -6
  197. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  198. package/.next/standalone/.next/server/chunks/ssr/app_insights_page_1d_chja.js +3 -0
  199. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +14 -14
  200. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +3 -3
  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 +3 -3
  203. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  204. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  205. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  206. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  207. package/.next/standalone/.next/server/pages/404.html +1 -1
  208. package/.next/standalone/.next/server/pages/500.html +1 -1
  209. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  210. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  211. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  212. package/.next/standalone/.next/static/chunks/02wnf8dkxegxg.js +34 -0
  213. package/.next/standalone/.next/static/chunks/0838_s6w-i_ik.js +1 -0
  214. package/.next/{static/chunks/2eo9pqq9gay3r.js → standalone/.next/static/chunks/0ka4prpk3w9wn.js} +4 -4
  215. package/.next/standalone/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
  216. package/.next/standalone/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
  217. package/.next/standalone/.next/static/chunks/112mv-com-mqa.js +21 -0
  218. package/.next/{static/chunks/44yn0tcd9dfk3.js → standalone/.next/static/chunks/1bkt_hhjzvbh8.js} +1 -1
  219. package/.next/standalone/.next/static/chunks/1cmphb3kh2ers.css +3 -0
  220. package/.next/{static/chunks/1qbjermbf68xj.js → standalone/.next/static/chunks/1d8yv3k_atka7.js} +1 -1
  221. package/.next/standalone/.next/static/chunks/23hc51oa4vkhc.js +5 -0
  222. package/.next/standalone/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
  223. package/.next/standalone/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
  224. package/.next/{static/chunks/2zf4jmz9l3li2.js → standalone/.next/static/chunks/2w-y83mjlpnse.js} +1 -1
  225. package/.next/standalone/.next/static/chunks/2ws1xf293j-tm.js +5 -0
  226. package/.next/standalone/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
  227. package/.next/standalone/.next/static/chunks/3opgg1efrx8q6.js +1 -0
  228. package/.next/standalone/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
  229. package/.next/{static/chunks/0an_b6zi1r_qq.js → standalone/.next/static/chunks/3y40z6of3r9py.js} +1 -5
  230. package/.next/standalone/.next/static/chunks/403dpflln8ah5.js +1 -0
  231. package/.next/standalone/CHANGELOG.md +60 -0
  232. package/.next/standalone/README.md +25 -1
  233. package/.next/standalone/SECURITY.md +8 -0
  234. package/.next/standalone/app/api/outreach/route.js +13 -73
  235. package/.next/standalone/app/api/scraper/route.js +97 -4
  236. package/.next/standalone/app/components/AppHeader.js +22 -6
  237. package/.next/standalone/app/components/AppTabs.js +4 -0
  238. package/.next/standalone/app/components/AutoConnect.js +286 -0
  239. package/.next/standalone/app/components/ChainView.js +75 -2
  240. package/.next/standalone/app/components/FilterPanel.js +8 -1
  241. package/.next/standalone/app/components/LinkedInLimits.js +38 -13
  242. package/.next/standalone/app/components/OutlinkQuest.js +32 -11
  243. package/.next/standalone/app/components/ScanStatusBar.js +18 -3
  244. package/.next/standalone/app/components/ScanTrail.js +13 -3
  245. package/.next/standalone/app/components/SeparationView.js +6 -22
  246. package/.next/standalone/app/components/Sidebar.js +147 -38
  247. package/.next/standalone/app/components/insights/Boards.js +275 -0
  248. package/.next/standalone/app/components/insights/PeopleView.js +266 -0
  249. package/.next/standalone/app/components/insights/Rail.js +156 -0
  250. package/.next/standalone/app/components/insights/Report.js +382 -0
  251. package/.next/standalone/app/components/insights/parts.js +239 -0
  252. package/.next/standalone/app/components/settings/UsageSection.js +50 -3
  253. package/.next/standalone/app/globals.css +2 -3
  254. package/.next/standalone/app/insights/page.js +220 -0
  255. package/.next/standalone/app/page.js +1 -17
  256. package/.next/standalone/app/queue/page.js +30 -18
  257. package/.next/standalone/app/setup/page.js +16 -9
  258. package/.next/standalone/lib/auto-connect.js +145 -0
  259. package/.next/standalone/lib/companies.js +1 -1
  260. package/.next/standalone/lib/in-progress.js +46 -0
  261. package/.next/standalone/lib/insights-board.js +613 -0
  262. package/.next/standalone/lib/linkedin-limits.js +36 -11
  263. package/.next/standalone/lib/requests-client.js +10 -0
  264. package/.next/standalone/lib/requests.js +124 -0
  265. package/.next/standalone/lib/scan-state.js +1 -0
  266. package/.next/standalone/lib/scraper-client.js +18 -5
  267. package/.next/standalone/lib/settings.js +3 -0
  268. package/.next/standalone/lib/usage.js +5 -0
  269. package/.next/standalone/package-lock.json +2 -2
  270. package/.next/standalone/package.json +1 -1
  271. package/.next/standalone/scripts/scrape.py +569 -9
  272. package/.next/static/chunks/02wnf8dkxegxg.js +34 -0
  273. package/.next/static/chunks/0838_s6w-i_ik.js +1 -0
  274. package/.next/{standalone/.next/static/chunks/2eo9pqq9gay3r.js → static/chunks/0ka4prpk3w9wn.js} +4 -4
  275. package/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
  276. package/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
  277. package/.next/static/chunks/112mv-com-mqa.js +21 -0
  278. package/.next/{standalone/.next/static/chunks/44yn0tcd9dfk3.js → static/chunks/1bkt_hhjzvbh8.js} +1 -1
  279. package/.next/static/chunks/1cmphb3kh2ers.css +3 -0
  280. package/.next/{standalone/.next/static/chunks/1qbjermbf68xj.js → static/chunks/1d8yv3k_atka7.js} +1 -1
  281. package/.next/static/chunks/23hc51oa4vkhc.js +5 -0
  282. package/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
  283. package/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
  284. package/.next/{standalone/.next/static/chunks/2zf4jmz9l3li2.js → static/chunks/2w-y83mjlpnse.js} +1 -1
  285. package/.next/static/chunks/2ws1xf293j-tm.js +5 -0
  286. package/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
  287. package/.next/static/chunks/3opgg1efrx8q6.js +1 -0
  288. package/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
  289. package/.next/{standalone/.next/static/chunks/0an_b6zi1r_qq.js → static/chunks/3y40z6of3r9py.js} +1 -5
  290. package/.next/static/chunks/403dpflln8ah5.js +1 -0
  291. package/CHANGELOG.md +60 -0
  292. package/README.md +25 -1
  293. package/SECURITY.md +8 -0
  294. package/lib/auto-connect.js +145 -0
  295. package/lib/companies.js +1 -1
  296. package/lib/in-progress.js +46 -0
  297. package/lib/insights-board.js +613 -0
  298. package/lib/linkedin-limits.js +36 -11
  299. package/lib/requests-client.js +10 -0
  300. package/lib/requests.js +124 -0
  301. package/lib/scan-state.js +1 -0
  302. package/lib/scraper-client.js +18 -5
  303. package/lib/settings.js +3 -0
  304. package/lib/usage.js +5 -0
  305. package/package.json +1 -1
  306. package/scripts/scrape.py +569 -9
  307. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_4y8z7._.js +0 -8
  308. package/.next/standalone/.next/server/chunks/_02twlca._.js +0 -10
  309. package/.next/standalone/.next/server/chunks/lib_rpc_0nef_n7.js +0 -3
  310. package/.next/standalone/.next/server/chunks/lib_rpc_0plrzra.js +0 -3
  311. package/.next/standalone/.next/server/chunks/lib_rpc_13a4m0c.js +0 -3
  312. package/.next/standalone/.next/server/chunks/lib_rpc_1if17c-.js +0 -3
  313. package/.next/standalone/.next/server/chunks/lib_rpc_1j-zw7v.js +0 -3
  314. package/.next/standalone/.next/server/chunks/lib_rpc_1sngt-q.js +0 -3
  315. package/.next/standalone/.next/server/chunks/lib_rpc_1v2sgme.js +0 -3
  316. package/.next/standalone/.next/server/chunks/lib_rpc_1vh-tgx.js +0 -3
  317. package/.next/standalone/.next/server/chunks/lib_rpc_1zaubz7.js +0 -3
  318. package/.next/standalone/.next/server/chunks/ssr/_02sband._.js +0 -7
  319. package/.next/standalone/.next/server/chunks/ssr/_189g8lg._.js +0 -3
  320. package/.next/standalone/.next/server/chunks/ssr/_1f_n00p._.js +0 -3
  321. package/.next/standalone/.next/server/chunks/ssr/app_components_0t908t-._.js +0 -3
  322. package/.next/standalone/.next/static/chunks/0e3g0sil79cus.js +0 -21
  323. package/.next/standalone/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
  324. package/.next/standalone/.next/static/chunks/163b-20vwa06i.js +0 -34
  325. package/.next/standalone/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
  326. package/.next/standalone/.next/static/chunks/23_1z47al92nz.js +0 -57
  327. package/.next/standalone/.next/static/chunks/260bomtr214m8.js +0 -34
  328. package/.next/standalone/.next/static/chunks/2xk2ra4piiuod.js +0 -34
  329. package/.next/standalone/.next/static/chunks/3b0-14cxqsprt.js +0 -1
  330. package/.next/standalone/.next/static/chunks/3jarus1_38s2z.js +0 -1
  331. package/.next/standalone/.next/static/chunks/3ssu341hav_sq.css +0 -3
  332. package/.next/standalone/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
  333. package/.next/static/chunks/0e3g0sil79cus.js +0 -21
  334. package/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
  335. package/.next/static/chunks/163b-20vwa06i.js +0 -34
  336. package/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
  337. package/.next/static/chunks/23_1z47al92nz.js +0 -57
  338. package/.next/static/chunks/260bomtr214m8.js +0 -34
  339. package/.next/static/chunks/2xk2ra4piiuod.js +0 -34
  340. package/.next/static/chunks/3b0-14cxqsprt.js +0 -1
  341. package/.next/static/chunks/3jarus1_38s2z.js +0 -1
  342. package/.next/static/chunks/3ssu341hav_sq.css +0 -3
  343. package/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
  344. /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_buildManifest.js +0 -0
  345. /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_clientMiddlewareManifest.js +0 -0
  346. /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_ssgManifest.js +0 -0
  347. /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_buildManifest.js +0 -0
  348. /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_clientMiddlewareManifest.js +0 -0
  349. /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_ssgManifest.js +0 -0
@@ -0,0 +1 @@
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,94657,e=>{"use strict";var t=e.i(43476),n=e.i(71645),r=e.i(7837);let o=e=>`${e.name} ${e.was} → ${e.estimated?"estimated":e.now}`,l=e=>({padding:"6px 12px",borderRadius:7,cursor:"pointer",fontSize:12,fontWeight:700,border:e?"none":"1px solid rgba(var(--sd-ink, 255, 255, 255), 0.15)",background:e?"linear-gradient(135deg, #00ff88, #3498DB)":"transparent",color:e?"#000":"var(--sd-fg-2, #cfd8d8)"});function s({onAnswered:e}){var a;let i,c,[d,u]=(0,n.useState)(null),[f,p]=(0,n.useState)(!1),[m,g]=(0,n.useState)(()=>new Set),[h,y]=(0,n.useState)(!1),[b,x]=(0,n.useState)(null);if((0,n.useEffect)(()=>{let e=!1;return(0,r.csvNetworkSource)().then(t=>t||e?null:fetch("/api/company-scores/legacy").then(e=>e.json())).then(t=>{!e&&t?.offer?.companies?.length&&(u(t.offer),g(new Set(t.offer.companies.map(e=>e.name))))}).catch(()=>{}),()=>{e=!0}},[]),!d)return null;let v=async t=>{y(!0),x(null);try{let n=await fetch("/api/company-scores/legacy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({keep:t})}),r=await n.json();if(!n.ok&&!r.kept)throw Error(r.error||"Could not save your answer.");u(null),e?.({kept:r.kept||[],scored:r.scored||0,error:n.ok?null:r.error})}catch(e){x(e.message)}finally{y(!1)}};return(0,t.jsxs)("section",{"aria-label":"Built-in scores changed",style:{marginBottom:14,padding:14,borderRadius:10,border:"1px solid rgba(255,215,0,0.35)",background:"rgba(255,215,0,0.06)",fontSize:12.5,color:"var(--sd-fg-2, #cfd8d8)",lineHeight:1.6},children:[(0,t.jsx)("b",{style:{color:"var(--sd-fg-1, #fff)"},children:"Built-in scores changed in this version:"})," ",(i=(a=d.companies).slice(0,3).map(o).join(", "),(c=a.length-3)>0?`${i} and ${c} more`:i),"."," ","Keep any of the old ones as your own?",(0,t.jsx)("div",{style:{marginTop:4,color:"var(--sd-fg-3, #8b9a9a)"},children:"The built-in list now holds only companies most professionals would recognise, scored the same for everyone; anything else is estimated from your network. A score you keep is yours, like one you set below, and Auto hands it back."}),f&&(0,t.jsx)("ul",{style:{listStyle:"none",padding:0,margin:"10px 0 0",display:"grid",gap:4},children:d.companies.map(e=>(0,t.jsx)("li",{children:(0,t.jsxs)("label",{style:{display:"flex",gap:8,alignItems:"baseline",cursor:"pointer"},children:[(0,t.jsx)("input",{type:"checkbox",checked:m.has(e.name),disabled:h,onChange:()=>{let t;return t=e.name,g(e=>{let n=new Set(e);return n.has(t)?n.delete(t):n.add(t),n})}}),(0,t.jsxs)("span",{children:[(0,t.jsx)("b",{style:{color:"var(--sd-fg-1, #fff)"},children:e.name})," ",e.was," → ",e.estimated?`estimated (${e.now})`:e.now,(0,t.jsxs)("span",{style:{color:"var(--sd-fg-4, #778)"},children:[` \xb7 ${e.people} ${1===e.people?"person":"people"} there now`,e.names.length>1&&` \xb7 for ${e.names.join(", ")}`]})]})]})},e.name))}),(0,t.jsxs)("div",{style:{display:"flex",gap:8,marginTop:10,flexWrap:"wrap"},children:[f?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("button",{disabled:h||!m.size,onClick:()=>v([...m]),style:{...l(!0),opacity:h||!m.size?.5:1},children:["Keep ",m.size," ",1===m.size?"score":"scores"]}),(0,t.jsx)("button",{disabled:h,onClick:()=>p(!1),style:l(!1),children:"Back"})]}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("button",{disabled:h,onClick:()=>v(d.companies.map(e=>e.name)),style:l(!0),children:"Keep all"}),(0,t.jsx)("button",{disabled:h,onClick:()=>p(!0),style:l(!1),children:"Choose…"})]}),(0,t.jsx)("button",{disabled:h,onClick:()=>v([]),style:l(!1),children:"No thanks"})]}),b&&(0,t.jsx)("div",{role:"alert",style:{marginTop:8,color:"#e74c3c"},children:b})]})}let a="1px solid rgba(var(--sd-ink, 255, 255, 255), 0.1)",i={yours:{label:"your score",color:"var(--sd-green, #00ff88)"},known:{label:"known list",color:"var(--sd-blue, #3498DB)"},data:{label:"public data",color:"#1abc9c"},network:{label:"estimated: many of your people",color:"var(--sd-orange, #FF6B35)"},default:{label:"unknown company",color:"var(--sd-fg-4, #778)"}};async function c(){let e=await fetch("/api/company-scores"),t=await e.json();if(!e.ok)throw Error(t.error||"Could not load company scores");return t.companies}function d(){let[e,t]=(0,n.useState)(null),[r,o]=(0,n.useState)(null);return(0,n.useEffect)(()=>{let e=!1;return c().then(n=>{e||t(n)}).catch(t=>{e||o(t.message)}),()=>{e=!0}},[]),{companies:e,error:r,setScore:(0,n.useCallback)(async(e,n)=>{let r=await fetch("/api/company-scores",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,score:n})}),o=await r.json();if(!r.ok)throw Error(o.error||"Could not save");return t(await c()),o},[]),reload:(0,n.useCallback)(async()=>t(await c()),[])}}function u({company:e,onSet:r,compact:o}){let[l,s]=(0,n.useState)(!1),i=async t=>{s(!0);try{await r(e.name,"auto"===t?null:Number(t))}finally{s(!1)}};return(0,t.jsxs)("select",{disabled:l,value:"yours"===e.source?String(e.score):"auto",onChange:e=>i(e.target.value),title:"Set this company's score. Auto uses the known list or an estimate.",style:{padding:o?"2px 4px":"4px 6px",borderRadius:6,border:a,background:"var(--sd-card, #11131c)",color:"var(--sd-fg-1, #dfe6e9)",fontSize:12},children:[(0,t.jsxs)("option",{value:"auto",children:["Auto (","yours"===e.source?e.suggested??"est.":e.score,")"]}),[10,9,8,7,6,5,4,3,2,1].map(e=>(0,t.jsx)("option",{value:e,children:e},e))]})}e.s(["ScorePicker",0,u,"default",0,function({onRescored:e,version:r=0}){let{companies:o,error:l,setScore:c,reload:f}=d();(0,n.useEffect)(()=>{r&&f().catch(()=>{})},[r,f]);let[p,m]=(0,n.useState)(""),[g,h]=(0,n.useState)("all"),[y,b]=(0,n.useState)(150),[x,v]=(0,n.useState)(null),w=e=>{v(e),setTimeout(()=>v(null),3500)},S=async(t,n)=>{let r=await c(t,n);w(`${t}: ${null==n?"back to auto":`set to ${n}`} \xb7 rescored ${r.scored} people`),e?.()},k=async({kept:t,scored:n,error:r})=>{if(!t.length)return w("Keeping the new built-in scores.");w(r||`Kept ${t.length} old ${1===t.length?"score":"scores"} as your own \xb7 rescored ${n} people`),await f().catch(()=>{}),e?.()},j=(0,n.useMemo)(()=>{if(!o)return[];let e=p.trim().toLowerCase();return o.filter(t=>(!e||t.name.toLowerCase().includes(e))&&("all"===g||t.source===g))},[o,p,g]);if(l)return(0,t.jsx)("div",{style:{padding:24,color:"#e74c3c"},children:l});if(!o)return(0,t.jsx)("div",{style:{padding:24,color:"var(--sd-fg-4, #778)"},children:"Loading companies…"});let C=e=>o.filter(t=>t.source===e).length;return(0,t.jsxs)("div",{style:{flex:1,overflow:"auto",padding:16},children:[(0,t.jsxs)("div",{style:{maxWidth:980,margin:"0 auto"},children:[(0,t.jsx)(s,{onAnswered:k}),(0,t.jsxs)("div",{style:{padding:14,borderRadius:10,border:a,background:"rgba(var(--sd-ink, 255, 255, 255), 0.03)",fontSize:12.5,color:"var(--sd-fg-2, #cfd8d8)",lineHeight:1.6},children:[(0,t.jsx)("b",{style:{color:"var(--sd-fg-1, #fff)"},children:"How a power score works."})," power = ",(0,t.jsx)("b",{children:"title"})," × ",(0,t.jsx)("b",{children:"company weight"})," + bonus. The title (student 1 … founder or C-suite 10) comes from someone’s roles, with former roles at 70%; an audience of their own counts like a title (1M+ followers like a director). The company weight runs from 0.725 for a company we don’t know (or none found), the middle of the scale, to 1.0 (a company scored 10), so seniority counts for more at a bigger company. The bonus is at most +1.5, for investor, YC, an audience in the millions or the top award of any field, and counts half at an unknown company. A 1st-degree person whose circle is unusually strong gets up to +2 more: up to +1 for a high share of it at A or S, or +1 for every 25 people there. Tiers are graded on your network’s curve unless you choose the fixed scale (",(0,t.jsx)("a",{href:"#tiers",style:{color:"var(--sd-blue, #3498DB)"},children:"Tiers"}),", above): your top 3% are S, the next 12% A, the next 25% B (it only lifts, and never lifts anyone under 4 into S or A). On the fixed scale, S ≥ 7.5 · A ≥ 5.5 · B ≥ 4 · C ≥ 2.5.",(0,t.jsxs)("div",{style:{marginTop:6,color:"var(--sd-fg-3, #8b9a9a)"},children:["So a VP at a 10 scores 9.0, a founder at an unknown company 7.3, a director at an unknown company 5.4, and an intern at a 10 scores 2.0. Set a company’s score below and everyone there is rescored. Companies in the sectors you pick (",(0,t.jsx)("a",{href:"#sector",style:{color:"var(--sd-blue, #3498DB)"},children:"Your sector"}),", above) get +1 or +2; a score you set is never changed."]})]}),(0,t.jsxs)("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap",margin:"14px 0 10px"},children:[(0,t.jsx)("input",{value:p,onChange:e=>m(e.target.value),placeholder:"Find a company",style:{padding:"6px 10px",borderRadius:7,border:a,background:"rgba(var(--sd-ink, 255, 255, 255), 0.05)",color:"var(--sd-fg-1, #fff)",fontSize:13,minWidth:180}}),[["all",`All ${o.length}`],["known",`Known list ${C("known")}`],["data",`Public data ${C("data")}`],["yours",`Yours ${C("yours")}`],["network",`Estimated ${C("network")}`],["default",`Unknown ${C("default")}`]].map(([e,n])=>(0,t.jsx)("button",{onClick:()=>h(e),style:{padding:"5px 10px",borderRadius:7,border:a,cursor:"pointer",fontSize:11.5,fontWeight:600,background:g===e?"rgba(52,152,219,0.25)":"transparent",color:g===e?"var(--sd-fg-1, #cfe6f7)":"var(--sd-fg-3, #8b9a9a)"},children:n},e))]}),(0,t.jsxs)("div",{style:{borderRadius:10,border:a,overflow:"hidden"},children:[(0,t.jsxs)("div",{style:{display:"grid",gridTemplateColumns:"1fr 70px 70px 70px 60px 180px 110px",gap:8,padding:"8px 12px",fontSize:10.5,letterSpacing:.6,color:"var(--sd-fg-3, #8b9a9a)",background:"rgba(var(--sd-ink, 255, 255, 255), 0.04)"},children:[(0,t.jsx)("span",{children:"COMPANY"}),(0,t.jsx)("span",{style:{textAlign:"right"},children:"PEOPLE"}),(0,t.jsx)("span",{style:{textAlign:"right"},children:"YOU KNOW"}),(0,t.jsx)("span",{style:{textAlign:"right"},children:"DIRECTOR+"}),(0,t.jsx)("span",{style:{textAlign:"right"},children:"SCORE"}),(0,t.jsx)("span",{children:"SOURCE"}),(0,t.jsx)("span",{children:"SET"})]}),j.slice(0,y).map(e=>(0,t.jsxs)("div",{style:{display:"grid",gridTemplateColumns:"1fr 70px 70px 70px 60px 180px 110px",gap:8,padding:"7px 12px",alignItems:"center",borderTop:"1px solid rgba(var(--sd-ink, 255, 255, 255), 0.05)",fontSize:12.5},children:[(0,t.jsxs)("span",{style:{display:"flex",alignItems:"center",gap:8,minWidth:0},children:[(0,t.jsx)("span",{title:`${e.industry.label} (inferred)`,style:{width:8,height:8,borderRadius:"50%",background:e.industry.color,flexShrink:0}}),(0,t.jsx)("span",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.name})]}),(0,t.jsx)("span",{style:{textAlign:"right"},children:e.people}),(0,t.jsx)("span",{style:{textAlign:"right",color:e.d1?"var(--sd-green, #00ff88)":"var(--sd-fg-5, #556)"},children:e.d1}),(0,t.jsx)("span",{style:{textAlign:"right",color:e.senior?"var(--sd-gold, #FFD700)":"var(--sd-fg-5, #556)"},children:e.senior}),(0,t.jsx)("b",{style:{textAlign:"right",fontSize:15},children:e.score}),(0,t.jsxs)("span",{style:{fontSize:11,color:i[e.source]?.color},children:[i[e.source]?.label,e.sectorBonus>0&&(0,t.jsxs)("span",{style:{color:"var(--sd-green, #00ff88)"},children:[" + ",e.sectorBonus," your sector"]})]}),(0,t.jsx)(u,{company:e,onSet:S,compact:!0})]},e.name))]}),j.length>y&&(0,t.jsxs)("button",{onClick:()=>b(e=>e+300),style:{marginTop:10,padding:"7px 14px",borderRadius:8,border:a,background:"rgba(var(--sd-ink, 255, 255, 255), 0.05)",color:"var(--sd-fg-1, #dfe6e9)",cursor:"pointer",fontSize:12},children:["Show more (",j.length-y," left)"]})]}),x&&(0,t.jsx)("div",{role:"status",style:{position:"fixed",bottom:24,left:"50%",transform:"translateX(-50%)",padding:"10px 18px",borderRadius:24,background:"linear-gradient(135deg, #00ff88, #3498DB)",color:"#000",fontWeight:800,fontSize:13,zIndex:50},children:x})]})},"useCompanyScores",0,d],94657)},39160,84278,e=>{"use strict";var t=e.i(7214),n=e.i(7837);function r(e){let t=String(e||"").trim();if(!t)return null;let n=t.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\s*UTC)?$/);if(n)return Date.UTC(+n[1],n[2]-1,+n[3],+n[4],+n[5],+(n[6]||0));if(n=t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4}),?\s+(\d{1,2}):(\d{2})\s*(AM|PM)?$/i)){let e=100>+n[3]?2e3+ +n[3]:+n[3],t=n[4]%12;return"PM"===(n[6]||"").toUpperCase()&&(t+=12),Date.UTC(e,n[1]-1,+n[2],t,+n[5])}if(n=t.match(/^([A-Za-z]{3,9})\s+(\d{4})$/)){let e=Date.parse(`1 ${n[1]} ${n[2]} UTC`);return Number.isNaN(e)?null:e}let r=Date.parse(/\d{4}$/.test(t)?`${t} UTC`:t);return Number.isNaN(r)?null:r}let o=e=>(0,t.keyFor)({profile_url:e});function l(e=[]){let t=new Map;for(let n of e){let e=o(n["sender profile url"]),r=n["conversation id"];r&&e&&(t.has(e)||t.set(e,new Set),t.get(e).add(r))}let n=null;for(let[e,r]of t)(!n||r.size>t.get(n).size)&&(n=e);return n}let s=e=>{let t=f(e);return t?`name:${t}`:null},a=/sponsored/i,i=/in\s?mail/i,c=/linkedin\.com\/(company|showcase|school)\//i,d=e=>/^(yes|true|1)$/i.test(String(e||"").trim());function u(e,t){if(!e||!e.total)return"never";let n=t-e.last;return n<=2592e6||e.recent>=3?"warm":n<=31536e6?"cool":"dormant"}let f=e=>String(e||"").split(",")[0].normalize("NFD").replace(/[̀-ͯ]/g,"").toLowerCase().replace(/[^a-z0-9 ]+/g," ").replace(/\s+/g," ").trim();e.s(["buildConversations",0,function(e=[],{keepText:t=!1}={}){let n=l(e),u=new Map;for(let t of e)n&&o(t["sender profile url"])===n&&t.from&&u.set(t.from,(u.get(t.from)||0)+1);let p=f([...u].sort((e,t)=>t[1]-e[1])[0]?.[0]||""),m=e=>!!e&&(e===n||!!p&&e===`name:${p}`),g={},h=new Map,y=0,b=0;for(let n of e){let e=n["conversation id"],l=r(n.date);if(!e||null==l){b+=1;continue}if(d(n["is message draft"])){y+=1;continue}let u=o(n["sender profile url"])||s(n.from);u&&n.from&&!g[u]&&(g[u]=String(n.from).trim());let f=String(n["recipient profile urls"]||"").split(/[,;\s]+/).filter(Boolean).map(o),p=String(n.to||"").split(",").map(e=>e.trim()).filter(Boolean),x=f.length?f:p.map(s).filter(Boolean);f.length&&f.length===p.length&&f.forEach((e,t)=>{g[e]||(g[e]=p[t])}),f.length||p.forEach(e=>{let t=s(e);t&&!g[t]&&(g[t]=e)}),h.has(e)||h.set(e,{id:e,title:"",everyone:new Set,messages:[],kind:null,folder:"",newest:-1/0});let v=h.get(e);u&&v.everyone.add(u),x.forEach(e=>v.everyone.add(e)),v.kind=v.kind||function(e){let t=String(e.folder||""),n=String(e["conversation title"]||"");return a.test(t)||a.test(n)||c.test(String(e["sender profile url"]||""))?"sponsored":i.test(t)||i.test(n)?"inmail":null}(n),l>=v.newest&&(v.newest=l,v.folder=String(n.folder||"").trim().toLowerCase().slice(0,30));let w={t:l,fromMe:m(u)};t&&(w.text=[n.subject,n.content].map(e=>String(e||"").trim()).filter(Boolean).join("\n\n"),!v.title&&n["conversation title"]&&(v.title=String(n["conversation title"]).trim())),v.messages.push(w)}let x=[];for(let e of h.values()){let n=[...e.everyone].filter(e=>e&&!m(e)).sort();e.messages.sort((e,t)=>e.t-t.t);let r={id:e.id,people:n,group:n.length>1,kind:e.kind,folder:e.folder,messages:e.messages};t&&(r.title=e.title),x.push(r)}for(let e of Object.keys(g))m(e)&&delete g[e];return{self:n,conversations:x,names:g,drafts:y,skipped:b}},"buildInvitations",0,function(e=[],{keepText:t=!1}={}){let n=[];for(let l of e){let e=String(l.direction||"").toUpperCase();if("INCOMING"!==e&&"OUTGOING"!==e)continue;let a="INCOMING"===e?"in":"out",i=String(("in"===a?l.from:l.to)||"").trim(),c=o("in"===a?l.inviterprofileurl:l.inviteeprofileurl),d=(c&&/\/in\//.test(c)?c:null)||s(i);if(!d)continue;let u=r(l["sent at"]),f={id:`${a}|${d}|${u??""}`,dir:a,key:d,name:i.slice(0,120),t:u};t&&String(l.message||"").trim()&&(f.note=String(l.message).trim().slice(0,2e3)),n.push(f)}return n.sort((e,t)=>(t.t||0)-(e.t||0))},"careerChapters",0,function(e=[],t=[],n=Date.now()){let l=e.map(e=>({company:e["company name"]||"",title:e.title||"",from:r(e["started on"]),to:r(e["finished on"])})).filter(e=>e.company&&null!=e.from).sort((e,t)=>e.from-t.from),s=t.map(e=>({url:o(e.url||e.profile_url),t:r(e["connected on"]||e.connected_on)})).filter(e=>null!=e.t);return l.map(e=>{let t=e.to??n,r=s.filter(n=>n.t>=e.from&&n.t<t);return{...e,to:e.to,made:r.length,people:r.map(e=>e.url)}})},"conversationSummary",0,function(e){let t=e.messages[e.messages.length-1];return{id:e.id,people:e.people,group:e.group,kind:e.kind||null,folder:e.folder||"",last:t?t.t:null,lastFromThem:t?!t.fromMe:null,count:e.messages.length,mine:e.messages.filter(e=>e.fromMe).length}},"invitationSplit",0,function(e=[]){let t=0,n=0;for(let r of e){let e=String(r.direction||"").toUpperCase();"INCOMING"===e?t+=1:"OUTGOING"===e&&(n+=1)}return{incoming:t,outgoing:n,total:t+n}},"messageStats",0,function(e=[]){let t=l(e),n=new Map;for(let t of e){let e=o(t["sender profile url"]),l=t["conversation id"];if(!l||!e)continue;let s=String(t["recipient profile urls"]||"").split(/[,;\s]+/).filter(Boolean).map(o),a=r(t.date);if(null==a)continue;n.has(l)||n.set(l,{people:new Set,msgs:[]});let i=n.get(l);i.people.add(e),s.forEach(e=>i.people.add(e)),i.msgs.push({from:e,t:a})}let s=0,a=new Map;for(let e of n.values()){let n=[...e.people].filter(e=>e!==t);if(1!==n.length||!e.people.has(t))continue;let r=n[0],o=a.get(r)||{last:0,lastFromThem:!1,total:0,times:[]};for(let n of e.msgs)o.total+=1,o.times.push(n.t),n.t>=o.last&&(o.last=n.t,o.lastFromThem=n.from!==t),n.t>s&&(s=n.t);a.set(r,o)}for(let e of a.values())e.recent=e.times.filter(e=>e>s-7776e6).length,delete e.times;return{self:t,asOf:s||null,people:a}},"nameKey",0,f,"postingEffect",0,function(e=[],t=[]){let n=e.map(e=>({t:r(e.date),link:e.sharelink||e["share link"]||""})).filter(e=>null!=e.t).sort((e,t)=>e.t-t.t),o=t.map(e=>r(e["connected on"]||e.connected_on)).filter(e=>null!=e).sort((e,t)=>e-t);if(!o.length)return{posts:[],usualWeek:0,afterPosts:0};let l=Math.max(6048e5,o[o.length-1]-o[0]),s=o.length/l*6048e5,a=n.map(e=>({...e,week:o.filter(t=>t>=e.t&&t<e.t+6048e5).length})),i=a.length?a.reduce((e,t)=>e+t.week,0)/a.length:0;return{posts:a,usualWeek:s,afterPosts:i}},"readTable",0,function(e,t){let r=(0,n.parseCsv)(String(e||"")),o=r.findIndex(e=>e.some(e=>e.trim().toLowerCase()===t.toLowerCase()));if(o<0)return[];let l=r[o].map(e=>e.trim().toLowerCase());return r.slice(o+1).filter(e=>e.some(e=>e.trim())).map(e=>Object.fromEntries(l.map((t,n)=>[t,(e[n]??"").trim()])))},"warmthOf",0,u],84278);let p=["gravity","rings","push","pull","distance","orbit","dotSize","lines","sizeBy","names","branch","colourBy","speed","loop"],m=e=>"boolean"==typeof e||"number"==typeof e&&Number.isFinite(e)&&1e3>=Math.abs(e)||"string"==typeof e&&/^[a-z]{1,20}$/i.test(e);function g(e){if(!Array.isArray(e))return[];let t=new Map;for(let n of e){let e="string"==typeof n?.name?n.name.trim().slice(0,40):"";if(!e||!n.settings||"object"!=typeof n.settings||Array.isArray(n.settings))continue;let r={};for(let e of p)Object.hasOwn(n.settings,e)&&m(n.settings[e])&&(r[e]=n.settings[e]);t.set(e,{name:e,settings:r})}return[...t.values()].slice(0,50)}var h=e.i(56899),y=e.i(40827);let b="six-degrees-galaxy-lab",x="six-degrees-galaxy-layouts",v=Object.freeze({on:!0,gravity:0,rings:1,push:15,pull:1,distance:1,dotSize:1,lines:1,sizeBy:"power",orbit:0,names:"key",branch:!1,colourBy:"tier",speed:15,loop:!1,labels:!0,v:2}),w=["gravity","rings","push","pull","distance"],S=Object.freeze({gravity:.8,rings:0,push:6,pull:6,distance:.15,sizeBy:"links"}),k=Object.freeze({rings:{lines:1},orbit:{lines:1},clusters:{lines:3}}),j=Object.freeze({gravity:0,rings:3,push:6,pull:4,distance:.22,sizeBy:"power"}),C=new Set,O=null;function T(){return O||(O=function(){try{let e=JSON.parse(localStorage.getItem(b)||"null");if(!e||"object"!=typeof e)return v;return{...v,...e,...e.v===v.v?{}:{branch:!1},v:v.v,on:!0}}catch{return v}}()),O}function F(e){O=null===e?{...v,labels:T().labels}:{...T(),...e,on:!0};try{localStorage.setItem(b,JSON.stringify(O))}catch{}C.forEach(e=>e())}let A=null,N=new Set;function M(){if(!A){try{A=JSON.parse(localStorage.getItem(x)||"[]")}catch{A=[]}Array.isArray(A)||(A=[])}return A}function $(e,{save:t=!0}={}){A=e;try{localStorage.setItem(x,JSON.stringify(e))}catch{}N.forEach(e=>e()),t&&(0,h.saveSettings)({galaxyLayouts:e})}let B=new Set,E={at:null,playing:!1,min:null,max:null,of:0,find:"",fly:0,found:0,fit:0,social:void 0},z=null;function L(e){let t={...E,...e};Object.keys(e).every(e=>Object.is(t[e],E[e]))||(E=t,B.forEach(e=>e()))}function D(){cancelAnimationFrame(z),z=null,L({playing:!1})}let I=null;function R(e){let n=new Map;for(let[r,o]of Object.entries(e?.people||{}))n.set((0,t.keyFor)({profile_url:r}),{...o});for(let[r,o]of Object.entries(e?.live||{})){let e=(0,t.keyFor)({profile_url:r}),l=n.get(e)||{total:0,recent:0};o.last&&(!l.last||o.last>l.last)&&(l.last=o.last,"boolean"==typeof o.lastFromThem&&(l.lastFromThem=o.lastFromThem)),l.unread=o.unread,l.total=Math.max(l.total||0,1),n.set(e,l)}let r=Math.max(e?.asOf||0,e?.liveAt&&Date.parse(e.liveAt)||0);for(let e of n.values())(e.last||0)>r&&(r=e.last);return{merged:n,asOf:r,chapters:e?.chapters||[],posts:e?.posts?.posts||[]}}let U={1:"#4FC3F7",2:"#BA68C8",3:"#FFB74D"},P=["#FF6B6B","#4ECDC4","#FFD93D","#A29BFE","#00B894","#FD79A8","#74B9FF","#E17055"],K={warm:"#FF7043",cool:"#4FC3F7",dormant:"#8D6E63",never:"#546E7A"},W="#4a5568",_=["#2b1b6b","#6a1b9a","#c2185b","#f4511e","#ffb300","#fff3b0"];function J(e){let t=Math.max(0,Math.min(1,Number(e)||0))*(_.length-1),n=Math.min(_.length-2,Math.floor(t)),r=t-n,o=_[n],l=_[n+1];return`#${[1,3,5].map(e=>Math.round(parseInt(o.slice(e,e+2),16)*(1-r)+parseInt(l.slice(e,e+2),16)*r).toString(16).padStart(2,"0")).join("")}`}function Y(e=[],t=()=>0){let n=[...new Set(e.map(t))].sort((e,t)=>e-t),r=new Map(n.map((e,t)=>[e,n.length>1?t/(n.length-1):1]));return e=>r.get(t(e))??0}let G=e=>String(e||"").trim().toLowerCase();e.s(["CLUSTERS",0,S,"FORCE_KEYS",0,w,"LAB_DEFAULTS",0,v,"LAYOUT_LOOKS",0,k,"ORBIT",0,j,"adoptSavedLayouts",0,function(e){let t=g(M()),{use:n,save:r,changed:o}=(0,y.reconcileSaved)({saved:null==e?null:g(e),cached:t,isDefault:e=>0===e.length,same:(e,t)=>JSON.stringify(e)===JSON.stringify(t)});r&&(0,h.saveSettings)({galaxyLayouts:t}),o&&$(n,{save:!1})},"applyLayout",0,function(e){let t=M().find(t=>t.name===e);t&&F(Object.fromEntries(p.filter(e=>e in t.settings).map(e=>[e,t.settings[e]])))},"bornTimes",0,function(e,t){let n=new Map(e.map(e=>[e.id,e])),r=new Map,o=(e,l=0)=>{if(r.has(e.id))return r.get(e.id);let s=-1/0,a=t.get(e.id);if(1===e.degree||null==a||l>6){let t=e.connected_date?Date.parse(e.connected_date):NaN;s=Number.isNaN(t)?-1/0:t}return null!=a&&n.has(a)&&l<=6&&(s=Math.max(s,o(n.get(a),l+1))),r.set(e.id,s),s};return e.map(e=>o(e))},"chapterAt",0,function(e,t){let n=null;for(let r of e?.chapters||[])Number.isFinite(r.from)&&r.from<=t&&(!n||r.from>n.from)&&(n=r);return n&&(!Number.isFinite(n.to)||n.to>t)?n:null},"clockNow",0,()=>E,"colourScheme",0,function(e,n,r,o){if("degree"===e)return{of:e=>U[Math.min(e.degree||1,3)],legend:[["1st",U[1]],["2nd",U[2]],["3rd+",U[3]]]};if("company"===e){let e=new Map;for(let t of n){let n=G(t.company);if(!n)continue;let r=e.get(n)||{name:String(t.company).trim(),n:0};r.n+=1,e.set(n,r)}let t=[...e.entries()].filter(([,e])=>e.n>1).sort((e,t)=>t[1].n-e[1].n).slice(0,P.length),r=new Map(t.map(([e],t)=>[e,P[t]]));return{of:e=>r.get(G(e.company))||W,legend:[...t.map(([,e],t)=>[e.name,P[t]]),["Other",W]]}}if("heat"===e){let e=function(e=[]){return Y(e,t.score)}(n);return{of:t=>J(e(t)),heat:e,legend:[["Cold",J(.1)],["Warm",J(.45)],["Hot",J(.75)],["Hottest",J(1)]]}}return"warmth"===e&&o?{of:e=>1===(e.degree||1)?K[u(o.merged.get((0,t.keyFor)(e)),o.asOf)]:"#2d3748",legend:[["Warm",K.warm],["Cool",K.cool],["Dormant",K.dormant],["Never messaged",K.never]]}:{of:e=>r[e.tier]||"#666",legend:Object.entries(r)}},"effectiveLab",0,e=>e.on?e:e.labels===v.labels?v:{...v,labels:e.labels},"findMatches",0,function(e,t){let n=String(t||"").trim().toLowerCase();if(n.length<2)return null;let r=new Set;for(let t of e)[t.name,t.company,t.role,t.headline].some(e=>e&&String(e).toLowerCase().includes(n))&&r.add(t.id);return r},"forgetLayout",0,function(e){$(M().filter(t=>t.name!==e))},"heatBy",0,Y,"heatColour",0,J,"labNow",0,T,"layoutOf",0,function(e){let t=t=>Object.entries(t).every(([t,n])=>e[t]===n);return w.every(t=>e[t]===v[t])?"rings":t(S)?"clusters":t(j)?"orbit":null},"layoutsNow",0,M,"loadSocial",0,function(){return I||(I=fetch("/api/social").then(e=>e.ok?e.json():{}).then(e=>{L({social:e.social?R(e.social):null})}).catch(()=>L({social:null}))),I},"mergeSocial",0,R,"milestones",0,function(e){let t=[];for(let n of e?.chapters||[])Number.isFinite(n.from)&&t.push({t:n.from,kind:"job",label:n.company});for(let n of e?.posts||[])Number.isFinite(n.t)&&t.push({t:n.t,kind:"post",label:"You posted"});return t.sort((e,t)=>e.t-t.t)},"pause",0,D,"play",0,function e(){if(null==E.min||null==E.max||E.max<=E.min)return;let t=E.max-E.min,n=1e3*Math.max(1,Number(T().speed)||15),r=null==E.at||E.at>=E.max?E.min:E.at,o=performance.now()-(r-E.min)/t*n;L({at:r,playing:!0});let l=r=>{let s=Math.min(E.max,E.min+(r-o)/n*t);if(L({at:s}),s>=E.max){z=null,L({playing:!1}),T().loop&&setTimeout(()=>{z||E.at!==E.max||(L({at:E.min}),e())},1200);return}z=requestAnimationFrame(l)};cancelAnimationFrame(z),z=requestAnimationFrame(l)},"reachCounts",0,function(e,t){let n=new Map(e.map(e=>[e.id,0]));for(let r of e){let e=t.get(r.id);for(let r=0;null!=e&&r<7;r++)n.set(e,(n.get(e)||0)+1),e=t.get(e)}return n},"saveLayout",0,function(e){let t=String(e||"").trim().slice(0,40);if(!t)return;let n=T(),r=Object.fromEntries(p.map(e=>[e,n[e]]));$([...M().filter(e=>e.name!==t),{name:t,settings:r}])},"setClock",0,L,"setLab",0,F,"stopReplay",0,function(){D(),L({at:null})},"watchClock",0,function(e){return B.add(e),()=>B.delete(e)},"watchLab",0,function(e){C.add(e);let t=t=>{t.key===b&&(O=null,e())};return window.addEventListener("storage",t),()=>{C.delete(e),window.removeEventListener("storage",t)}},"watchLayouts",0,function(e){return N.add(e),()=>N.delete(e)}],39160)}]);
@@ -6,6 +6,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.0] - 2026-10-03
10
+
11
+ ### Added
12
+ - **Insights: your network ranked by power.** A new tab between Paths and Outlink. The Power
13
+ Index ranks everyone within two steps of you, each person once, with shared ranks for ties and
14
+ a line saying why a big tie shares its score; filter by degree, tier and rarity, search by name,
15
+ company or who knows them, and from any row open their circle, scan it, or ask (their LinkedIn
16
+ profile opens and the request is marked sent, as on their card). Beside it: Untapped (S and A
17
+ in your 2nd degree with no request out), Kingmakers (the connections with the most S and A
18
+ behind them), Gatekeepers (who your S and A reach hangs on), Hidden giants (S-tier with one way
19
+ in), Company power, How sure (how much of the ranking rests on a guess), and Richest?, which
20
+ says plainly that the app never sees money. Kingmakers, Gatekeepers, Companies, Industries and
21
+ a scrolling Report are views of their own in the notch. Every number says where it came from;
22
+ a CSV import gets the boards that need no circles, and says so for the rest. On your own network every 2nd-degree row offers Auto beside Ask.
23
+ - **Auto: one press sends someone a connection request for you.** Next to every Connect (a person's
24
+ card, and Outlink's lists and next best moves) there's now an Auto button, the bright one. Press it
25
+ and the scanner's Chrome opens their profile, presses Connect and sends the request without a note,
26
+ then shows "Request sent" and marks it pending everywhere in the app. Connect still opens their
27
+ profile for you to do it yourself. One press is one person: never a batch, never on a timer, and it
28
+ waits while a scan runs. It asks once before the first one. When LinkedIn wants their email address
29
+ first, Auto closes that and sends nothing ("Use Connect to add them yourself"); when LinkedIn offers
30
+ a personal note (Premium), it sends without one. It only calls a request sent once their profile
31
+ shows it pending, and says so plainly when it can't tell. It stops at 15 requests in any 24 hours and
32
+ 80 in any 7 days (LinkedIn doesn't publish its limit; about 100 a week is commonly reported), opens
33
+ their profile once like a scan does, holds during a pause after LinkedIn pushed back, and shows in
34
+ Settings → LinkedIn usage as Connection requests (Auto).
35
+
36
+ ### Changed
37
+ - **A scan that stopped partway says so, on the person's card and on the Scan page.** Open someone
38
+ whose circle scan didn't finish and a line by their name says which page it stopped after, with
39
+ Resume right there; their scan box now reads Scan in progress, with Resume from page N as its main
40
+ button, instead of Cluster active. The card's Insights offer Resume for them too, not a scan that
41
+ reads the list again from page 1, and an empty circle in Bridge Chains whose scan stopped partway
42
+ says so, with Resume from page N, instead of saying their list has been read. On the Scan page, everyone stopped partway is in a new In progress
43
+ card right after step 4, with Resume for each person and Resume all, and each name opens their circle
44
+ on the map. It used to be folded away under Fine-tune the scanner.
45
+ - **The tabs sit on the header's line, and the notch hangs from them.** On a computer the row of
46
+ tabs rests on the line under the header, so a page's own buttons in the notch below join onto
47
+ it. The header is the same height on every tab: the Pending count that added a row on Degrees
48
+ and Separation is gone (sent requests are still under Outlink → Pending).
49
+ - **Separation starts with its slider.** The line counting people two steps away and the tier
50
+ strip under it are gone; the search box stays at the top.
51
+ - **The notch comes with the page.** Switching tabs, it waits for the next page to appear instead
52
+ of hanging over a blank screen while it loads.
53
+
54
+ ### Fixed
55
+ - **A company called "Foods" or "Beverages" is in Retail, Consumer & Hospitality.** The word list
56
+ had "food" and "beverage" but not the plurals, so a company like Northwind Foods took its
57
+ industry from its people's headlines instead (tech, on a test network). Scores are worked out
58
+ again once after the update, since a company's industry can change its estimate and your sector.
59
+ - **Scan dots are whole on Paths.** The dots along the header's line while a scan runs were cut in
60
+ half above Paths' left panel; they also stop short of the tabs now that the tabs sit on the line.
61
+ - **The 3rd-degree dot in Degrees and Separation says why it can't be tapped.** It said "No one at
62
+ 3rd degree yet" even with people found by company scans; it now says those are drawn on Network
63
+ Circle.
64
+ - **The ring round a scanned connection's dot shows how much of their list was read.** It showed 2
65
+ of 5 bars, "partly read", for everyone scanned, even when their whole list was in, and the card's
66
+ Insights offered to finish lists that were finished. The app now takes in how far each list was
67
+ read, which the scanner had been noting all along.
68
+
9
69
  ## [0.7.0] - 2026-10-03
10
70
 
11
71
  ### Added
@@ -193,6 +193,16 @@ in, and its most powerful people.
193
193
 
194
194
  <img src="docs/img/paths.png" alt="Paths → Map with the company analyzer open: companies grouped by industry, and one company's people by level and ways in" width="100%">
195
195
 
196
+ **Insights.** Everyone within two steps of you in one table, ranked by power, each person
197
+ once: the **Power Index**, with filters for degree, tier and rarity, and Open circle, Scan
198
+ circle or Ask on every row. Beside it, and each a board of its own in the notch:
199
+ **Kingmakers** (your connections with the most S and A people behind them), **Gatekeepers**
200
+ (who your S and A reach hangs on), **Hidden giants** (S-tier with one way in), **Untapped**
201
+ (S and A with no request out), **Company power** and **Industries**. **Report** is the same
202
+ numbers as a page you scroll. Every number says where it came from, and nothing is about
203
+ money: *Richest?* says plainly that the app never sees it. Power ranks reachability, not
204
+ people.
205
+
196
206
  **Scores.** How a power score is worked out, and the three things you can change about
197
207
  it, in one place: your field (the sectors you work in), how tiers are graded (on your
198
208
  network's curve or the fixed scale), and every scanned company's score.
@@ -202,6 +212,15 @@ circle offers its best five people at a time; mark invites as sent to fill the r
202
212
  unlock the next five. It also shows the next best moves, levels and points (from
203
213
  invites sent and people who accepted, never from browsing).
204
214
 
215
+ **Auto** *(needs a scan)*. Next to every Connect (a person's card, Outlink), Auto sends
216
+ that one person a connection request for you, from the scanner's Chrome, without a note.
217
+ One press, one person: never a batch, never on a timer. It asks once before the first one.
218
+ If LinkedIn wants their email address first, Auto closes that and sends nothing (use
219
+ Connect to add them yourself); if it offers a personal note (Premium), Auto sends without
220
+ one. It calls a request sent only once their profile shows it pending. It stops at 15 in
221
+ any 24 hours and 80 in any 7 days, opens their profile once (a profile view), and counts in
222
+ Settings → LinkedIn usage.
223
+
205
224
  **Scan.** The guided scanner. It shows progress, the budget that's left, the cooldown
206
225
  lock, and a **Paused** list you can carry on from.
207
226
 
@@ -287,6 +306,10 @@ Something out of date, or unfair to another tool? [Open an issue](https://github
287
306
  >
288
307
  > The app asks for an explicit "I understand" once, before the first scan.
289
308
  >
309
+ > **The scanner reads; the one thing it ever sends is a connection request, and only when
310
+ > you press Auto** on a person (one each press, without a note, at most 15 a day and 80 a
311
+ > week). Auto asks once before its first request.
312
+ >
290
313
  > Six Degrees is not affiliated with or endorsed by LinkedIn.
291
314
 
292
315
  ## First run
@@ -395,7 +418,8 @@ It estimates **network position**, not what anyone is worth as a person.
395
418
  data folder as `csv-network.json` (names, positions, companies, profile links and when
396
419
  you connected; never email addresses) until you click × beside *Your CSV*.
397
420
  - The app contacts only these, and only when you act:
398
- - **LinkedIn**, while you scan. Each profile photo is saved on your computer as the
421
+ - **LinkedIn**, while you scan, and when you press **Auto** (one connection request,
422
+ sent from the scanner's Chrome). Each profile photo is saved on your computer as the
399
423
  scan reads it, and the app shows photos only from there, so looking at your network
400
424
  never contacts LinkedIn. (Photos an older version kept as links to LinkedIn show
401
425
  initials until the next scan, or *Save photos* on the Scan page, saves them.)
@@ -129,6 +129,14 @@ Everything stays local:
129
129
  If you run the scanner, `chrome-profile/` in the data directory holds a live
130
130
  logged-in LinkedIn session. Never copy, sync, or commit it.
131
131
 
132
+ The scanner reads LinkedIn. The one thing it ever sends there is a connection request,
133
+ and only when you press **Auto** on a person: one request per press, without a note, at
134
+ most 15 in any 24 hours and 80 in any 7 days. The app looks the person up by the id the
135
+ page sends and passes the scanner their profile URL from the database, never a URL or name
136
+ from the request, and refuses before anything starts during a cooldown, past those caps,
137
+ or before Auto's one-time question has been answered (`app/api/scraper/route.js`,
138
+ `lib/auto-connect.js`).
139
+
132
140
  ### Moving your data (Settings → Your data)
133
141
 
134
142
  - **An export** is one `.sixdegrees` file the user saves and carries. It is
@@ -1,6 +1,7 @@
1
- import { db as supabase } from '../../../lib/db';
2
1
  import { getDb, decodeRow } from '../../../lib/db-client';
3
- import { resolveProfile } from '../../../lib/profile';
2
+ import {
3
+ XP_SEND, XP_ACCEPT, awardXP, whose, copiesOf, inList, bestTier, markSent,
4
+ } from '../../../lib/requests';
4
5
 
5
6
  // Requests you've sent, kept per PERSON.
6
7
  //
@@ -13,56 +14,8 @@ import { resolveProfile } from '../../../lib/profile';
13
14
  // asked through is kept (so once they accept, their card and circle say who
14
15
  // introduced you: lib/promote.js keeps it), XP comes once per person, and Undo
15
16
  // takes back all of it. lib/requests-client.js is the one way the pages call this.
16
-
17
- // XP rewards by tier
18
- const XP_SEND = { S: 25, A: 15, B: 10, C: 5, D: 2 };
19
- const XP_ACCEPT = { S: 100, A: 60, B: 35, C: 15, D: 5 };
20
- const TIER_ORDER = ['S', 'A', 'B', 'C', 'D'];
21
-
22
- async function awardXP(userId, amount) {
23
- if (!userId || !amount) return;
24
- // Upsert: create if not exists, change if exists. Never below zero: an Undo
25
- // takes back what a send gave, including sends from before sends gave any.
26
- const { data: existing } = await supabase.from('user_stats').select('xp').eq('id', userId).single();
27
- if (existing) {
28
- await supabase.from('user_stats').update({ xp: Math.max(0, (existing.xp || 0) + amount) }).eq('id', userId);
29
- } else if (amount > 0) {
30
- await supabase.from('user_stats').insert([{ id: userId, xp: amount, level: 1 }]).catch(() => {});
31
- }
32
- }
33
-
34
- /** The profile a request belongs to: the one sent, else this machine's. */
35
- function whose(userId) {
36
- if (userId) return String(userId);
37
- try { return resolveProfile({ create: false })?.id || null; } catch { return null; }
38
- }
39
-
40
- /** Every copy of one person: by the clicked row's profile URL, or the URL given. */
41
- function copiesOf(db, { connectionId, profileUrl, userId }) {
42
- let url = profileUrl ? String(profileUrl) : null;
43
- let user = userId ? String(userId) : null;
44
- let clicked = null;
45
- if (connectionId != null) {
46
- clicked = db.prepare('SELECT id, profile_url, user_id FROM linkedin_connections WHERE id = ?').get(String(connectionId)) || null;
47
- if (clicked) {
48
- url = clicked.profile_url || url;
49
- user = user || clicked.user_id || null;
50
- }
51
- }
52
- const cols = 'id, degree, tier, outreach_status, unlock_status, unlocked_from_bridge_id';
53
- let rows = [];
54
- if (url) {
55
- rows = user
56
- ? db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE profile_url = ? AND user_id = ?`).all(url, user)
57
- : db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE profile_url = ?`).all(url);
58
- } else if (clicked) {
59
- rows = db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE id = ?`).all(clicked.id);
60
- }
61
- return { url, user, rows };
62
- }
63
-
64
- const inList = (ids) => ids.map(() => '?').join(', ');
65
- const bestTier = (rows) => TIER_ORDER.find((t) => rows.some((r) => r.tier === t)) || 'D';
17
+ // The marking itself is lib/requests.js markSent, which Auto's sends use too
18
+ // (app/api/scraper/route.js).
66
19
 
67
20
  export async function POST(request) {
68
21
  try {
@@ -72,33 +25,20 @@ export async function POST(request) {
72
25
  return Response.json({ error: 'Unknown action' }, { status: 400 });
73
26
  }
74
27
  const db = getDb();
28
+
29
+ if (action === 'mark-sent') {
30
+ const { saved, xp, already } = await markSent(db, { connectionId, profileUrl, userId, bridgeId });
31
+ // Nobody on file (the sample network lives in the browser): nothing to keep.
32
+ if (!saved) return Response.json({ success: true, saved: 0, xp: 0 });
33
+ return Response.json({ success: true, saved, xp, already });
34
+ }
35
+
75
36
  const { user, rows } = copiesOf(db, { connectionId, profileUrl, userId });
76
37
  // Nobody on file (the sample network lives in the browser): nothing to keep.
77
38
  if (!rows.length) return Response.json({ success: true, saved: 0, xp: 0 });
78
39
  const ids = rows.map((r) => r.id);
79
40
  const now = new Date().toISOString();
80
41
 
81
- if (action === 'mark-sent') {
82
- const already = rows.some((r) => r.outreach_status === 'sent' || r.outreach_status === 'accepted');
83
- db.prepare(`UPDATE linkedin_connections SET outreach_status = 'sent', updated_at = ?
84
- WHERE id IN (${inList(ids)}) AND (outreach_status IS NULL OR outreach_status NOT IN ('sent', 'accepted'))`).run(now, ...ids);
85
- db.prepare(`UPDATE linkedin_connections SET unlock_status = 'pending', updated_at = ?
86
- WHERE id IN (${inList(ids)}) AND (unlock_status IS NULL OR unlock_status = 'locked')`).run(now, ...ids);
87
- // Who you asked through: one of your own connections, or nothing.
88
- if (bridgeId != null) {
89
- const bridge = db.prepare(
90
- `SELECT id, name FROM linkedin_connections WHERE id = ? AND degree = 1${user ? ' AND user_id = ?' : ''}`,
91
- ).get(...[String(bridgeId), ...(user ? [user] : [])]);
92
- if (bridge) {
93
- db.prepare(`UPDATE linkedin_connections SET unlocked_from_bridge_id = ?, unlocked_from_name = ?, updated_at = ?
94
- WHERE id IN (${inList(ids)}) AND degree = 2 AND unlocked_from_bridge_id IS NULL`).run(bridge.id, bridge.name, now, ...ids);
95
- }
96
- }
97
- const xp = already ? 0 : XP_SEND[bestTier(rows)] || 2;
98
- await awardXP(whose(user), xp);
99
- return Response.json({ success: true, saved: ids.length, xp, already });
100
- }
101
-
102
42
  if (action === 'mark-accepted') {
103
43
  db.prepare(`UPDATE linkedin_connections SET outreach_status = 'accepted', updated_at = ? WHERE id IN (${inList(ids)})`).run(now, ...ids);
104
44
  return Response.json({ success: true, saved: ids.length, xp: 0, accepts: XP_ACCEPT[bestTier(rows)] || 5 });
@@ -9,13 +9,15 @@ import { pausedList, readProgress, readUnclear } from '../../../lib/paused';
9
9
  import { getDb, backUpDailyIfDue } from '../../../lib/db-client';
10
10
  import { db as notesDb } from '../../../lib/db';
11
11
  import { scanDoneNotification } from '../../../lib/notifications';
12
- import { registerScanState } from '../../../lib/scan-state';
12
+ import { registerScanState, runningNow } from '../../../lib/scan-state';
13
13
  import { pendingImport } from '../../../lib/data-import';
14
14
  import { waitingPhotoCount } from '../../../lib/photos';
15
15
  import { readSettings } from '../../../lib/settings';
16
16
  import { riskAccepted, touchesLinkedIn, RISK_REFUSAL } from '../../../lib/scan-risk';
17
17
  import { reachIndex, circleState } from '../../../lib/reach';
18
18
  import { chromeInstalled, signedInFrom, SIGNED_IN_FILE, CHROME_REFUSAL } from '../../../lib/scanner-setup';
19
+ import { AUTO_REFUSAL, connectResultIn, connectMarks, inviteRefusal, personRefusal } from '../../../lib/auto-connect';
20
+ import { markSentRows, awardXP, whose, requestStanding } from '../../../lib/requests';
19
21
  import {
20
22
  pythonLooker, thisHostKey, scannerCommand, installSteps, downloadedPython, downloadVerified,
21
23
  placeDownloadedPython, sweepSetupLeftovers, megabytes, SETUP_WORK_PREFIX, ScannerSetupError,
@@ -51,6 +53,8 @@ const state = registerScanState({
51
53
  failure: null, // the last lines of stderr from a run that failed — its reason
52
54
  pages: 0, // pages the running scan has read: the dots along the header's line (app/components/ScanTrail.js)
53
55
  found: [], // how many people each of those pages found: the Scan page's cluster (app/components/ScanRadar.js)
56
+ invitee: null, // Auto's person: { id, name, profileUrl, userId, bridgeId }, looked up here (connectTarget)
57
+ outcome: null, // how Auto's request went: the scanner's "Connect result: …" line (lib/auto-connect.js)
54
58
  });
55
59
 
56
60
  // A page read, as scripts/scrape.py prints it: " Page 3... " as a circle or a
@@ -122,6 +126,9 @@ function push(line) {
122
126
  }
123
127
  const got = t.match(PAGE_FOUND);
124
128
  if (got && state.found.length) state.found[state.found.length - 1] = Number(got[1]);
129
+ // Auto's one line saying how the request went (scrape.py main, --connect).
130
+ const result = state.action === 'connect' && connectResultIn(t);
131
+ if (result) state.outcome = result;
125
132
  }
126
133
  if (state.log.length > MAX_LOG) state.log.splice(0, state.log.length - MAX_LOG);
127
134
  }
@@ -529,8 +536,48 @@ const ACTIONS = {
529
536
  // 1,000 conversations at most), for the Social tab's "Read my whole history".
530
537
  'messages-full': { flag: '--messages --full-history', label: 'Reading your whole messages history' },
531
538
  photos: { flag: '--save-photos', label: 'Saving profile photos to this computer' },
539
+ // Auto (Blake, 2026-10-03): one connection request, without a note, to one
540
+ // person pressed on a card. By id, looked up here (connectTarget); the URL
541
+ // and name that reach the command line come from the database, never the page.
542
+ connect: { flag: '--connect', label: 'Sending a connection request to' },
532
543
  };
533
544
 
545
+ /**
546
+ * Auto's person, by id: { person: { id, name, profileUrl, userId, bridgeId } },
547
+ * or why not: { error, status }. Anyone in your network on this computer with a
548
+ * LinkedIn profile who isn't already one of your connections (no copy of them
549
+ * at degree 1) and has no request out. The URL is checked like every other
550
+ * that reaches a command line.
551
+ */
552
+ function connectTarget(id) {
553
+ try {
554
+ const me = resolveProfile({ create: false });
555
+ const db = getDb();
556
+ const row = me && db.prepare(
557
+ 'SELECT id, name, degree, profile_url, source_connection_id FROM linkedin_connections WHERE id = ? AND user_id = ?',
558
+ ).get(String(id ?? ''), me.id);
559
+ if (!row) return personRefusal(null);
560
+ const why = personRefusal(row, requestStanding(db, { profileUrl: row.profile_url, userId: me.id }));
561
+ if (why) return why;
562
+ const profileUrl = cleanProfileUrl(row.profile_url);
563
+ if (!profileUrl) return { status: 400, error: `${row.name || 'Their'} LinkedIn profile address on file can’t be used.` };
564
+ return {
565
+ person: {
566
+ id: row.id, name: cleanName(row.name) || null, profileUrl, userId: me.id,
567
+ // Who you'd be asking through: the connection whose circle they're in.
568
+ bridgeId: row.degree === 2 ? row.source_connection_id ?? null : null,
569
+ },
570
+ };
571
+ } catch {
572
+ return personRefusal(null);
573
+ }
574
+ }
575
+
576
+ /** Auto's one-time yes (lib/auto-connect.js), given on the page that first offers it. */
577
+ function autoAccepted() {
578
+ try { return Boolean(readSettings(getDb()).autoConnectAccepted); } catch { return false; }
579
+ }
580
+
534
581
  /** Only a plain linkedin.com/in/ profile URL becomes an argument. */
535
582
  function cleanProfileUrl(raw) {
536
583
  const url = String(raw ?? '').trim();
@@ -651,6 +698,15 @@ export async function POST(request) {
651
698
  if (!Object.hasOwn(ACTIONS, action)) {
652
699
  return Response.json({ error: `Unknown action '${action}'` }, { status: 400 });
653
700
  }
701
+ // Auto: who, by id, and whether a request can go to them at all, before
702
+ // anything else is asked. Then its own one-time yes (the page asks it).
703
+ let invitee = null;
704
+ if (action === 'connect') {
705
+ const found = connectTarget(body.id);
706
+ if (found.error) return Response.json({ error: found.error }, { status: found.status });
707
+ invitee = found.person;
708
+ if (!autoAccepted()) return Response.json({ error: AUTO_REFUSAL, needsAutoAcceptance: true }, { status: 409 });
709
+ }
654
710
  // Nothing opens LinkedIn before the one-time "I understand" on the Scan page,
655
711
  // whichever button asked (lib/scan-risk.js).
656
712
  if (touchesLinkedIn(action) && !scanRisk()) {
@@ -707,12 +763,14 @@ export async function POST(request) {
707
763
  // never reaches the command line (a name or the URL looked up for it does),
708
764
  // and anything not shaped like one of our ids is dropped.
709
765
  const hintId = typeof body.id === 'string' && /^[\w-]{1,64}$/.test(body.id) ? body.id : null;
710
- const target = name || profileUrl ? { id: hintId, name: name || person?.name || null } : null;
766
+ const target = invitee ? { id: invitee.id, name: invitee.name }
767
+ : name || profileUrl ? { id: hintId, name: name || person?.name || null } : null;
711
768
  // Nothing that searches LinkedIn starts during a cooldown; the scanner checks
712
769
  // too, this just says so before a process is spawned.
713
770
  // The 1st-degree scans aren't searches, but they open LinkedIn with automation too.
771
+ // Auto's request too: a pause after LinkedIn pushed back holds everything.
714
772
  const searches = spec.searches || action.startsWith('auto-bridge')
715
- || ['bridge', 'rescrape', 'company', 'full', 'refresh'].includes(action);
773
+ || ['bridge', 'rescrape', 'company', 'full', 'refresh', 'connect'].includes(action);
716
774
  // A staged import replaces the network at the next start, so anything scanned
717
775
  // now would land in the copy that is about to be set aside. Setting the
718
776
  // scanner up touches no network data.
@@ -723,7 +781,15 @@ export async function POST(request) {
723
781
  if (searches && cooldown) {
724
782
  return Response.json({ error: `Scanning is paused until ${new Date(cooldown.until).toLocaleString()}: ${cooldown.reason}.`, cooldown }, { status: 409 });
725
783
  }
784
+ // Auto's caps (15 in any 24 hours, 80 in any 7 days) and the profile view it
785
+ // takes, said before a process starts; the scanner checks them again.
786
+ if (invitee) {
787
+ const capped = inviteRefusal(linkedinState(dataDir()));
788
+ if (capped) return Response.json({ error: capped }, { status: 409 });
789
+ }
726
790
  if (state.running) {
791
+ // One thing at a time: Auto waits for a scan like any other job.
792
+ if (invitee) return Response.json({ error: `${runningNow(state.action)}. Press Auto again once it has finished.`, action: state.action }, { status: 409 });
727
793
  return Response.json({ error: 'Something is already running.', action: state.action }, { status: 409 });
728
794
  }
729
795
 
@@ -759,7 +825,8 @@ export async function POST(request) {
759
825
  // "-" can then never be read as a flag of its own. Anyone found by
760
826
  // id is --bridge-url, which carries on where their last read stopped
761
827
  // unless told to start at page 1.
762
- ...(name ? [`${spec.flag}=${name}`] : profileUrl ? [`--bridge-url=${profileUrl}`] : spec.flag.split(' ')),
828
+ ...(invitee ? [`--connect=${invitee.profileUrl}`, ...(invitee.name ? [`--connect-name=${invitee.name}`] : [])]
829
+ : name ? [`${spec.flag}=${name}`] : profileUrl ? [`--bridge-url=${profileUrl}`] : spec.flag.split(' ')),
763
830
  ...(profileUrl && action === 'bridge' && !deeper ? ['--from-start'] : []),
764
831
  ...(maxBridges && (action.startsWith('auto-bridge') || action === 'resume-all') ? [`--max-bridges=${maxBridges}`] : []),
765
832
  ...(tiers.length && action.startsWith('auto-bridge') ? [`--tiers=${tiers.join(',')}`] : []),
@@ -793,6 +860,8 @@ export async function POST(request) {
793
860
  state.found = [];
794
861
  state.stderrTail = [];
795
862
  state.failure = null;
863
+ state.invitee = invitee;
864
+ state.outcome = null;
796
865
  forgetChecks();
797
866
 
798
867
  // Name the profile outright. The scraper would otherwise ask the app which one
@@ -810,7 +879,29 @@ export async function POST(request) {
810
879
  SIX_DEGREES_FROM_APP: '1',
811
880
  };
812
881
 
882
+ /**
883
+ * Auto: a request LinkedIn showed as pending (or one already pending there)
884
+ * is marked as sent through the same store as the Connect button
885
+ * (lib/requests.js), before the job is seen to end, so every view that looks
886
+ * next finds it; its XP follows. Anything else marks nothing (TRAPS §7).
887
+ */
888
+ function markAutoSent() {
889
+ const who = state.invitee;
890
+ if (state.action !== 'connect' || !who || who.marked || !connectMarks(state.outcome)) return;
891
+ who.marked = true;
892
+ try {
893
+ const { saved, xp, user } = markSentRows(getDb(), {
894
+ connectionId: who.id, profileUrl: who.profileUrl, userId: who.userId, bridgeId: who.bridgeId,
895
+ });
896
+ if (saved) Promise.resolve(awardXP(whose(user), xp)).catch(() => {});
897
+ push('Marked as sent in Six Degrees.');
898
+ } catch (err) {
899
+ push(`Sent on LinkedIn, but it couldn’t be marked as sent here: ${err.message}`);
900
+ }
901
+ }
902
+
813
903
  function finish(code) {
904
+ markAutoSent();
814
905
  // A failure must say why. stderr is filtered while running because pip and
815
906
  // Playwright are noisy there, and that filter once swallowed the only line
816
907
  // explaining a failed scan, leaving just "exit 1". On a failure, show the
@@ -834,6 +925,8 @@ export async function POST(request) {
834
925
  state.exitCode = code;
835
926
  state.recent = [{
836
927
  action: state.action, target: state.target, startedAt: state.startedAt, exitCode: code, failure: state.failure,
928
+ // How Auto's request went, for the button that started it (lib/auto-connect.js connectOutcome).
929
+ ...(state.action === 'connect' ? { outcome: state.outcome } : {}),
837
930
  }, ...state.recent].slice(0, 5);
838
931
  // A scan that finished leaves a notification ("Scan done: …"); a stop or a failure doesn't.
839
932
  const done = scanDoneNotification({ action: state.action, target: state.target, exitCode: code, stopped, log: state.log.slice(-12), userId: profileId || null });
@@ -10,6 +10,12 @@
10
10
  // The row is a grid of 1fr | auto | 1fr: the two sides share what's left
11
11
  // equally, so the tabs sit in the middle whatever is beside them. A window too
12
12
  // narrow for that lets the sides shrink to fit before anything overlaps.
13
+ //
14
+ // On a computer the tabs sit on the header's bottom line, so the notch that
15
+ // hangs under the line (ScanStatusBar) hangs from the tabs themselves (Blake,
16
+ // 2026-10-03: "lower the section of buttons right to the line so the notch is
17
+ // touching"). They're moved down, not laid out lower, so the header keeps its
18
+ // height and every page under it stays where it was.
13
19
 
14
20
  import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
15
21
  import Link from 'next/link';
@@ -36,6 +42,11 @@ function useIsMobile() {
36
42
  return m;
37
43
  }
38
44
 
45
+ // The header's padding top and bottom, and the gap under the row: the tabs
46
+ // move down by the last two to reach the line.
47
+ const PAD_Y = 20;
48
+ const ROW_GAP = 8;
49
+
39
50
  const TITLE = { fontWeight: 700, margin: 0, whiteSpace: 'nowrap', background: 'linear-gradient(135deg, #FFD700, #9B59B6, #3498DB)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' };
40
51
 
41
52
  /**
@@ -53,22 +64,27 @@ export default function AppHeader({ active, isMobile: mobileProp, csvMode = fals
53
64
  const mobileSeen = useIsMobile();
54
65
  const isMobile = mobileProp ?? mobileSeen;
55
66
  const own = !IS_DEMO && !csvMode; // scanning and notifications are for your own network
67
+ const tabs = (
68
+ <AppTabs active={active} isMobile={isMobile} csvMode={csvMode} onMode={onMode}
69
+ after={own ? <AutoScanButton isMobile={isMobile} /> : null}>
70
+ {chips}
71
+ </AppTabs>
72
+ );
56
73
  return (
57
74
  <header style={{
58
- position: 'relative', padding: isMobile ? '10px 12px' : '20px 30px', borderBottom: '1px solid rgba(var(--sd-ink, 255, 255, 255), 0.1)', flexShrink: 0,
75
+ position: 'relative', padding: isMobile ? '10px 12px' : `${PAD_Y}px 30px`, borderBottom: '1px solid rgba(var(--sd-ink, 255, 255, 255), 0.1)', flexShrink: 0,
59
76
  // Clear, or frosted glass on a glass theme (lib/themes.js).
60
77
  background: 'var(--sd-header)', backdropFilter: 'var(--sd-header-blur)', WebkitBackdropFilter: 'var(--sd-header-blur)',
61
78
  }}>
62
79
  <div style={isMobile
63
80
  ? { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 6, marginBottom: 4 }
64
- : { display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', gap: 16, marginBottom: 8 }}>
81
+ : { display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', gap: 16, marginBottom: ROW_GAP }}>
65
82
  <Link href="/" style={{ textDecoration: 'none', justifySelf: 'start' }}>
66
83
  <Brand style={{ ...TITLE, fontSize: isMobile ? 16 : 28 }}>Six Degrees</Brand>
67
84
  </Link>
68
- <AppTabs active={active} isMobile={isMobile} csvMode={csvMode} onMode={onMode}
69
- after={own ? <AutoScanButton isMobile={isMobile} /> : null}>
70
- {chips}
71
- </AppTabs>
85
+ {isMobile ? tabs : (
86
+ <div style={{ alignSelf: 'end', position: 'relative', top: ROW_GAP + PAD_Y, zIndex: 2 }}>{tabs}</div>
87
+ )}
72
88
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', justifySelf: 'end', gap: isMobile ? 4 : 8 }}>
73
89
  {/* Settings: also in CSV or sample mode and on phones, so updates stay reachable */}
74
90
  {!IS_DEMO && <Link href="/settings" title="Settings" aria-label="Settings" style={{
@@ -17,6 +17,10 @@ const MODES = [
17
17
  ];
18
18
  const PAGES = [
19
19
  { key: 'paths', href: '/paths', label: 'Paths', color: 'var(--sd-green, #00ff88)', on: 'linear-gradient(135deg, #00ff88, #3498DB)', demo: true },
20
+ // Your network ranked by power, and the boards that read it (Blake, 2026-10-03:
21
+ // "stats and insights … based off their circles"). Purple to gold, its own colour.
22
+ // The sample and a CSV import have something to rank too, so it isn't `own`.
23
+ { key: 'insights', href: '/insights', label: 'Insights', color: 'var(--sd-purple, #c39bd3)', on: 'linear-gradient(135deg, #9B59B6, #FFD700)' },
20
24
  // Social is part of Outlink now (Messages & follow-ups); /social forwards there.
21
25
  { key: 'outlink', href: '/queue', label: 'Outlink', color: 'var(--sd-orange, #FF6B35)', on: 'linear-gradient(135deg, #FF6B35, #FFD700)', own: true },
22
26
  { key: 'scan', href: '/setup', label: 'Scan', color: 'var(--sd-fg-4, #666)', on: 'linear-gradient(135deg, #00ff88, #1abc9c)', own: true },