six-degrees 0.5.0 → 0.6.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 (366) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +14 -14
  4. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.rsc +20 -19
  14. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +20 -19
  15. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +16 -15
  16. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  17. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/update/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/import/page.js +1 -1
  50. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/import.html +1 -1
  53. package/.next/standalone/.next/server/app/import.rsc +24 -23
  54. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +24 -23
  55. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  56. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +18 -17
  57. package/.next/standalone/.next/server/app/index.html +1 -1
  58. package/.next/standalone/.next/server/app/index.rsc +22 -21
  59. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +18 -17
  60. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +22 -21
  61. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  62. package/.next/standalone/.next/server/app/launch/page.js +1 -1
  63. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/launch.html +2 -2
  66. package/.next/standalone/.next/server/app/launch.rsc +24 -23
  67. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +24 -23
  68. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  69. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +18 -17
  70. package/.next/standalone/.next/server/app/page.js +1 -1
  71. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/.next/standalone/.next/server/app/paths/page.js +1 -1
  74. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/paths.html +1 -1
  77. package/.next/standalone/.next/server/app/paths.rsc +24 -23
  78. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +24 -23
  79. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  80. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +18 -17
  81. package/.next/standalone/.next/server/app/profile/page.js +1 -1
  82. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  84. package/.next/standalone/.next/server/app/profile.html +1 -1
  85. package/.next/standalone/.next/server/app/profile.rsc +24 -23
  86. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +24 -23
  87. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  88. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +18 -17
  89. package/.next/standalone/.next/server/app/queue/page.js +1 -1
  90. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/queue.html +1 -1
  93. package/.next/standalone/.next/server/app/queue.rsc +24 -23
  94. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +24 -23
  95. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +18 -17
  97. package/.next/standalone/.next/server/app/scores/page.js +1 -1
  98. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  99. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  100. package/.next/standalone/.next/server/app/scores.html +1 -1
  101. package/.next/standalone/.next/server/app/scores.rsc +24 -23
  102. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -23
  103. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  104. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +18 -17
  105. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  106. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/app/settings.html +1 -1
  109. package/.next/standalone/.next/server/app/settings.rsc +24 -23
  110. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +24 -23
  111. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  112. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +18 -17
  113. package/.next/standalone/.next/server/app/setup/page.js +1 -1
  114. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  115. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  116. package/.next/standalone/.next/server/app/setup.html +1 -1
  117. package/.next/standalone/.next/server/app/setup.rsc +24 -23
  118. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +24 -23
  119. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  120. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +18 -17
  121. package/.next/standalone/.next/server/app/social/page.js +1 -1
  122. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  123. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  124. package/.next/standalone/.next/server/app/social.html +1 -1
  125. package/.next/standalone/.next/server/app/social.rsc +24 -23
  126. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +24 -23
  127. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  128. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +18 -17
  129. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05_3-22._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13dx4sd._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +1 -1
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__18bcnu4._.js +3 -0
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +2 -2
  157. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  158. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +2 -2
  159. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  160. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +2 -2
  161. package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +5 -5
  162. package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +5 -5
  163. package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +5 -5
  164. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +9 -9
  165. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08oxiqh._.js +3 -0
  166. package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +1 -1
  167. package/.next/standalone/.next/server/chunks/ssr/_08g31gm._.js +3 -0
  168. package/.next/standalone/.next/server/chunks/ssr/_0gk_ofx._.js +3 -0
  169. package/.next/standalone/.next/server/chunks/ssr/_0nwsf93._.js +3 -0
  170. package/.next/standalone/.next/server/chunks/ssr/_0vho1lb._.js +3 -0
  171. package/.next/standalone/.next/server/chunks/ssr/{_0jdkqgi._.js → _1-658fq._.js} +2 -2
  172. package/.next/standalone/.next/server/chunks/ssr/_1bc46kz._.js +3 -0
  173. package/.next/standalone/.next/server/chunks/ssr/_1f_n00p._.js +3 -0
  174. package/.next/standalone/.next/server/chunks/ssr/_1fm1ih_._.js +9 -0
  175. package/.next/standalone/.next/server/chunks/ssr/_1k2nmbw._.js +9 -0
  176. package/.next/standalone/.next/server/chunks/ssr/_2105jz8._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +1 -1
  178. package/.next/standalone/.next/server/chunks/ssr/app_components_0t908t-._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/ssr/app_components_0uuff77._.js +13 -0
  180. package/.next/standalone/.next/server/chunks/ssr/app_components_1j3q-h5._.js +13 -0
  181. package/.next/standalone/.next/server/chunks/ssr/{_0gtv9jb._.js → app_components_CompanyScores_1g0mytz.js} +2 -2
  182. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +1 -1
  184. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +14 -12
  185. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
  186. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  187. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -6
  188. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  189. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +4 -4
  190. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
  191. package/.next/standalone/.next/server/chunks/ssr/{lib_0i8v-s3._.js → lib_1c4ieuw._.js} +2 -2
  192. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +1 -1
  193. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  194. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  195. package/.next/standalone/.next/server/pages/404.html +1 -1
  196. package/.next/standalone/.next/server/pages/500.html +1 -1
  197. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  198. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  199. package/.next/standalone/.next/static/chunks/02pq76u7y4v9-.js +22 -0
  200. package/.next/standalone/.next/static/chunks/0cmq0exd4d0h9.js +1 -0
  201. package/.next/{static/chunks/2wgz9ietkqqe6.js → standalone/.next/static/chunks/0i02yxppzds1o.js} +1 -1
  202. package/.next/standalone/.next/static/chunks/0m_p4k-q2qh--.js +1 -0
  203. package/.next/standalone/.next/static/chunks/0oyslj62jkubd.js +1 -0
  204. package/.next/standalone/.next/static/chunks/{1x_ysly42-teq.css → 0vo-l8hxneba6.css} +1 -1
  205. package/.next/{static/chunks/3sowbvbaj1s6n.js → standalone/.next/static/chunks/0ws7zy0w57ufi.js} +1 -1
  206. package/.next/standalone/.next/static/chunks/0yh8z2tbrc5i_.js +1 -0
  207. package/.next/standalone/.next/static/chunks/172e10mi-tpo_.js +34 -0
  208. package/.next/standalone/.next/static/chunks/1ei5vbe2uw6fl.js +1 -0
  209. package/.next/standalone/.next/static/chunks/1f7_pl_wq7lh3.js +11 -0
  210. package/.next/standalone/.next/static/chunks/1jzbe7zdx9vbj.js +2 -0
  211. package/.next/standalone/.next/static/chunks/1m3017gp790pw.js +45 -0
  212. package/.next/standalone/.next/static/chunks/1t20e8u0st7ep.js +1 -0
  213. package/.next/standalone/.next/static/chunks/2-ch9rm27eril.js +11 -0
  214. package/.next/standalone/.next/static/chunks/2ldv-v3s9yaif.js +1 -0
  215. package/.next/standalone/.next/static/chunks/3bp284gjf7dyq.js +1 -0
  216. package/.next/standalone/.next/static/chunks/3k1f6clbn3gj3.js +7 -0
  217. package/.next/standalone/.next/static/chunks/3l4t8je6iguzc.js +1 -0
  218. package/.next/standalone/.next/static/chunks/{0muwt9bykzr-1.js → 3tplzuc9jk-tl.js} +1 -1
  219. package/.next/standalone/.next/static/chunks/42m0bqr8pbovz.js +11 -0
  220. package/.next/standalone/CHANGELOG.md +61 -0
  221. package/.next/standalone/README.md +22 -8
  222. package/.next/standalone/app/api/scraper/route.js +40 -3
  223. package/.next/standalone/app/api/update/route.js +12 -6
  224. package/.next/standalone/app/components/AppHeader.js +257 -0
  225. package/.next/standalone/app/components/Avatar.js +2 -1
  226. package/.next/standalone/app/components/ChainView.js +12 -12
  227. package/.next/standalone/app/components/FilterPanel.js +20 -12
  228. package/.next/standalone/app/components/ForceGraph.js +163 -25
  229. package/.next/standalone/app/components/GalaxyLab.js +43 -12
  230. package/.next/standalone/app/components/GridView.js +4 -4
  231. package/.next/standalone/app/components/LinkedInLimits.js +2 -1
  232. package/.next/standalone/app/components/ListView.js +3 -3
  233. package/.next/standalone/app/components/NoteDetail.js +2 -1
  234. package/.next/standalone/app/components/OnboardingGate.js +2 -2
  235. package/.next/standalone/app/components/OutlinkQuest.js +2 -1
  236. package/.next/standalone/app/components/PathsAnalyzer.js +3 -2
  237. package/.next/standalone/app/components/PeopleMap.js +2 -1
  238. package/.next/standalone/app/components/RingsView.js +3 -3
  239. package/.next/standalone/app/components/ScanTrail.js +85 -0
  240. package/.next/standalone/app/components/SeparationView.js +100 -77
  241. package/.next/standalone/app/components/Sidebar.js +4 -4
  242. package/.next/standalone/app/components/TheirCircle.js +2 -1
  243. package/.next/standalone/app/components/ThemeLoader.js +9 -0
  244. package/.next/standalone/app/components/UpdatePanel.js +20 -2
  245. package/.next/standalone/app/components/settings/AppearanceSection.js +226 -0
  246. package/.next/standalone/app/components/settings/NetworkHealthSection.js +2 -1
  247. package/.next/standalone/app/components/ui.js +2 -1
  248. package/.next/standalone/app/globals.css +70 -2
  249. package/.next/standalone/app/import/page.js +3 -3
  250. package/.next/standalone/app/layout.js +11 -2
  251. package/.next/standalone/app/page.js +69 -215
  252. package/.next/standalone/app/paths/page.js +8 -21
  253. package/.next/standalone/app/profile/page.js +11 -10
  254. package/.next/standalone/app/queue/page.js +8 -21
  255. package/.next/standalone/app/scores/page.js +1 -1
  256. package/.next/standalone/app/settings/page.js +7 -3
  257. package/.next/standalone/app/setup/page.js +83 -88
  258. package/.next/standalone/app/social/Crm.js +2 -1
  259. package/.next/standalone/app/social/SocialHub.js +2 -2
  260. package/.next/standalone/app/social/page.js +1 -1
  261. package/.next/standalone/lib/data-import.js +1 -0
  262. package/.next/standalone/lib/galaxy-export.js +2 -1
  263. package/.next/standalone/lib/galaxy-lab.js +24 -8
  264. package/.next/standalone/lib/level.js +51 -0
  265. package/.next/standalone/lib/release.js +13 -7
  266. package/.next/standalone/lib/scanner-python.js +38 -8
  267. package/.next/standalone/lib/scraper-client.js +2 -1
  268. package/.next/standalone/lib/separation.js +4 -2
  269. package/.next/standalone/lib/theme-store.js +61 -0
  270. package/.next/standalone/lib/themes.js +186 -0
  271. package/.next/standalone/lib/tiers.js +2 -8
  272. package/.next/standalone/package-lock.json +2 -2
  273. package/.next/standalone/package.json +4 -4
  274. package/.next/standalone/scripts/build-desktop.mjs +325 -0
  275. package/.next/standalone/scripts/build-site.mjs +85 -15
  276. package/.next/standalone/scripts/gen-test-network.mjs +93 -0
  277. package/.next/standalone/scripts/next.mjs +20 -0
  278. package/.next/standalone/scripts/readme_buttons.py +3 -0
  279. package/.next/standalone/scripts/requirements.txt +18 -1
  280. package/.next/standalone/scripts/site-news.mjs +54 -9
  281. package/.next/standalone/scripts/smoke-desktop.mjs +177 -0
  282. package/.next/standalone/scripts/windows/six-degrees.iss +100 -0
  283. package/.next/static/chunks/02pq76u7y4v9-.js +22 -0
  284. package/.next/static/chunks/0cmq0exd4d0h9.js +1 -0
  285. package/.next/{standalone/.next/static/chunks/2wgz9ietkqqe6.js → static/chunks/0i02yxppzds1o.js} +1 -1
  286. package/.next/static/chunks/0m_p4k-q2qh--.js +1 -0
  287. package/.next/static/chunks/0oyslj62jkubd.js +1 -0
  288. package/.next/static/chunks/{1x_ysly42-teq.css → 0vo-l8hxneba6.css} +1 -1
  289. package/.next/{standalone/.next/static/chunks/3sowbvbaj1s6n.js → static/chunks/0ws7zy0w57ufi.js} +1 -1
  290. package/.next/static/chunks/0yh8z2tbrc5i_.js +1 -0
  291. package/.next/static/chunks/172e10mi-tpo_.js +34 -0
  292. package/.next/static/chunks/1ei5vbe2uw6fl.js +1 -0
  293. package/.next/static/chunks/1f7_pl_wq7lh3.js +11 -0
  294. package/.next/static/chunks/1jzbe7zdx9vbj.js +2 -0
  295. package/.next/static/chunks/1m3017gp790pw.js +45 -0
  296. package/.next/static/chunks/1t20e8u0st7ep.js +1 -0
  297. package/.next/static/chunks/2-ch9rm27eril.js +11 -0
  298. package/.next/static/chunks/2ldv-v3s9yaif.js +1 -0
  299. package/.next/static/chunks/3bp284gjf7dyq.js +1 -0
  300. package/.next/static/chunks/3k1f6clbn3gj3.js +7 -0
  301. package/.next/static/chunks/3l4t8je6iguzc.js +1 -0
  302. package/.next/static/chunks/{0muwt9bykzr-1.js → 3tplzuc9jk-tl.js} +1 -1
  303. package/.next/static/chunks/42m0bqr8pbovz.js +11 -0
  304. package/CHANGELOG.md +61 -0
  305. package/README.md +22 -8
  306. package/lib/data-import.js +1 -0
  307. package/lib/galaxy-export.js +2 -1
  308. package/lib/galaxy-lab.js +24 -8
  309. package/lib/level.js +51 -0
  310. package/lib/release.js +13 -7
  311. package/lib/scanner-python.js +38 -8
  312. package/lib/scraper-client.js +2 -1
  313. package/lib/separation.js +4 -2
  314. package/lib/theme-store.js +61 -0
  315. package/lib/themes.js +186 -0
  316. package/lib/tiers.js +2 -8
  317. package/package.json +4 -4
  318. package/scripts/requirements.txt +18 -1
  319. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +0 -3
  320. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16hs8kw._.js +0 -3
  321. package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +0 -3
  322. package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +0 -3
  323. package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +0 -3
  324. package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +0 -9
  325. package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +0 -3
  326. package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +0 -3
  327. package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +0 -3
  328. package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +0 -9
  329. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +0 -3
  330. package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +0 -3
  331. package/.next/standalone/.next/static/chunks/03bziijw5srol.js +0 -1
  332. package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +0 -7
  333. package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +0 -1
  334. package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +0 -34
  335. package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +0 -22
  336. package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +0 -2
  337. package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +0 -1
  338. package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +0 -1
  339. package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +0 -43
  340. package/.next/standalone/.next/static/chunks/2dwtoszeq91us.js +0 -1
  341. package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +0 -1
  342. package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +0 -1
  343. package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +0 -1
  344. package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +0 -1
  345. package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +0 -1
  346. package/.next/static/chunks/03bziijw5srol.js +0 -1
  347. package/.next/static/chunks/0bgq8wmopl_ig.js +0 -7
  348. package/.next/static/chunks/0ezus6p2eb-vg.js +0 -1
  349. package/.next/static/chunks/0g_fy2034bp0x.js +0 -34
  350. package/.next/static/chunks/0q5dru4q6gxl5.js +0 -22
  351. package/.next/static/chunks/0w-byiwb2zax8.js +0 -2
  352. package/.next/static/chunks/1g7jcovdejc9-.js +0 -1
  353. package/.next/static/chunks/1i51yylxdzht1.js +0 -1
  354. package/.next/static/chunks/1zf8swaasjf1e.js +0 -43
  355. package/.next/static/chunks/2dwtoszeq91us.js +0 -1
  356. package/.next/static/chunks/2rqtpccruavf2.js +0 -1
  357. package/.next/static/chunks/30x3oq_st7a5r.js +0 -1
  358. package/.next/static/chunks/355ryio1b7sm1.js +0 -1
  359. package/.next/static/chunks/3ap5hgma7n0o7.js +0 -1
  360. package/.next/static/chunks/3fci6qi39-1n5.js +0 -1
  361. /package/.next/standalone/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_buildManifest.js +0 -0
  362. /package/.next/standalone/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_clientMiddlewareManifest.js +0 -0
  363. /package/.next/standalone/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_ssgManifest.js +0 -0
  364. /package/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_buildManifest.js +0 -0
  365. /package/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_clientMiddlewareManifest.js +0 -0
  366. /package/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_ssgManifest.js +0 -0
@@ -0,0 +1,11 @@
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,33821,e=>{"use strict";var t=e.i(43476),n=e.i(71645),r=e.i(22016),o=e.i(18566),i=e.i(26005),l=e.i(76760),a=e.i(30215);let s=["#FFD700","#9B59B6","#3498DB"],c=`
2
+ @keyframes trail-in { from { transform: scale(0); opacity: 0; } }
3
+ @keyframes trail-pulse { 0%, 100% { opacity: 0.45; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.25); } }
4
+ .scan-trail { transition: opacity 1.2s ease; }
5
+ .scan-trail .dot { animation: trail-in 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.3); }
6
+ .scan-trail .dot.newest { box-shadow: 0 0 6px 1px currentColor; }
7
+ .scan-trail .seed { animation: trail-pulse 1.4s ease-in-out infinite; }
8
+ @media (prefers-reduced-motion: reduce) {
9
+ .scan-trail .dot, .scan-trail .seed { animation: none; }
10
+ .scan-trail { transition: none; }
11
+ }`;function u({inset:e}){let o=(0,a.default)(),i=(0,n.useRef)(null),[l,d]=(0,n.useState)(60),[f,p]=(0,n.useState)({on:!1,pages:0,label:""}),g=o.running&&!o.pending,m=o.target?.name?`Scanning ${o.target.name}`:"Scanning";g&&(!f.on||f.pages!==(o.pages||0)||f.label!==m)&&p({on:!0,pages:o.pages||0,label:m}),(0,n.useEffect)(()=>{if(g||!f.on)return;let e=setTimeout(()=>p(e=>({...e,on:!1})),4e3);return()=>clearTimeout(e)},[g,f.on]),(0,n.useEffect)(()=>{let e=i.current;if(!e)return;let t=new ResizeObserver(()=>d(Math.max(8,Math.floor(e.clientWidth/9)-4)));return t.observe(e),()=>t.disconnect()},[]);let{pages:h}=f,b=Math.max(0,h-l),y=[];for(let e=b;e<h;e++)y.push(e);let x=`${f.label}: ${0===h?"starting":`${h.toLocaleString("en-US")} page${1===h?"":"s"} read`}. Open the Scan page`;return(0,t.jsxs)("div",{ref:i,"aria-hidden":!f.on,style:{position:"absolute",left:e,right:e,bottom:-4,height:7,pointerEvents:"none"},children:[(0,t.jsx)("style",{children:c}),(0,t.jsxs)(r.default,{href:"/setup",title:x,tabIndex:f.on?0:-1,className:"scan-trail",style:{position:"absolute",left:0,top:0,height:7,display:"flex",alignItems:"center",gap:4,pointerEvents:f.on?"auto":"none",opacity:+!!f.on,textDecoration:"none"},children:[b>0&&(0,t.jsxs)("span",{style:{fontSize:9,fontWeight:700,color:"#889",marginRight:2,fontVariantNumeric:"tabular-nums"},children:["+",b.toLocaleString("en-US")]}),y.map(e=>{let n=s[e%s.length];return(0,t.jsx)("span",{className:`dot${e===h-1?" newest":""}`,style:{width:5,height:5,borderRadius:"50%",background:n,color:n,flexShrink:0}},e)}),f.on&&(0,t.jsx)("span",{className:"seed",style:{width:5,height:5,borderRadius:"50%",background:"#00ff88",flexShrink:0}})]})]})}var d=e.i(15812),f=e.i(40996),p=e.i(19821),g=e.i(98658),m=e.i(64745);let h={fontWeight:700,margin:0,whiteSpace:"nowrap",background:"linear-gradient(135deg, #FFD700, #9B59B6, #3498DB)",WebkitBackgroundClip:"text",WebkitTextFillColor:"transparent"};function b({isMobile:e,onOpen:r,openId:i}){let{userId:l}=(0,d.useUser)(),a=(0,o.useRouter)(),[s,c]=(0,n.useState)([]),[u,f]=(0,n.useState)(!1),p=(0,n.useRef)(!1),g=(0,n.useRef)(r);(0,n.useEffect)(()=>{g.current=r}),(0,n.useEffect)(()=>{l&&fetch(`/api/notifications?userId=${l}`).then(e=>e.json()).then(e=>{let t=e.notifications||[];c(t);let n=null!=i&&!p.current&&t.find(e=>String(e.id)===String(i));n&&g.current&&(p.current=!0,g.current(n))}).catch(()=>{})},[l,i]);let m=s.filter(e=>!e.seen).length,h=e=>{e.seen||(fetch("/api/notifications",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"mark-seen",id:e.id})}).catch(()=>{}),c(t=>t.map(t=>t.id===e.id?{...t,seen:!0}:t))),f(!1),r?r(e):a.push(`/?note=${encodeURIComponent(e.id)}`)};return(0,t.jsxs)("div",{style:{position:"relative"},children:[(0,t.jsxs)("button",{onClick:()=>f(!u),title:"Notifications","aria-label":m?`Notifications, ${m} new`:"Notifications",style:{width:e?28:32,height:e?28:32,borderRadius:"50%",border:"none",cursor:"pointer",background:"rgba(255,255,255,0.06)",color:"#888",fontSize:e?12:14,display:"flex",alignItems:"center",justifyContent:"center",position:"relative"},children:["🔔",m>0&&(0,t.jsx)("span",{style:{position:"absolute",top:-2,right:-2,width:16,height:16,borderRadius:"50%",background:"#ff5050",color:"#fff",fontSize:9,fontWeight:700,display:"flex",alignItems:"center",justifyContent:"center"},children:m})]}),u&&(0,t.jsxs)("div",{style:{position:"absolute",top:40,right:0,width:320,maxHeight:400,background:"color-mix(in srgb, var(--sd-bg) 97%, transparent)",border:"1px solid rgba(255,255,255,0.1)",borderRadius:12,overflow:"hidden",zIndex:100,backdropFilter:"blur(12px)",WebkitBackdropFilter:"blur(12px)",boxShadow:"0 8px 32px rgba(0,0,0,0.5)"},children:[(0,t.jsxs)("div",{style:{padding:"12px 16px",borderBottom:"1px solid rgba(255,255,255,0.08)",display:"flex",justifyContent:"space-between",alignItems:"center"},children:[(0,t.jsx)("span",{style:{fontWeight:700,fontSize:14},children:"Notifications"}),m>0&&(0,t.jsx)("button",{onClick:()=>{fetch("/api/notifications",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"mark-all-seen"})}),c(e=>e.map(e=>({...e,seen:!0})))},style:{background:"none",border:"none",color:"#3498DB",fontSize:11,cursor:"pointer",fontWeight:600},children:"Mark all read"})]}),(0,t.jsx)("div",{style:{overflowY:"auto",maxHeight:340},children:0===s.length?(0,t.jsx)("div",{style:{padding:20,textAlign:"center",color:"#555",fontSize:12},children:"No notifications yet"}):s.slice(0,20).map(e=>(0,t.jsx)("div",{role:"button",tabIndex:0,onClick:()=>h(e),onKeyDown:t=>{"Enter"===t.key&&h(e)},onMouseEnter:e=>{e.currentTarget.style.background="rgba(255,255,255,0.06)"},onMouseLeave:t=>{t.currentTarget.style.background=e.seen?"transparent":"rgba(255,215,0,0.04)"},style:{padding:"10px 16px",borderBottom:"1px solid rgba(255,255,255,0.04)",cursor:"pointer",background:e.seen?"transparent":"rgba(255,215,0,0.04)"},children:(0,t.jsxs)("div",{style:{display:"flex",gap:8,alignItems:"flex-start"},children:[(0,t.jsx)("span",{style:{fontSize:16},children:e.icon||"📌"}),(0,t.jsxs)("div",{style:{flex:1},children:[(0,t.jsxs)("div",{style:{fontSize:12,fontWeight:600,color:e.seen?"#888":"#fff"},children:[e.title,(0,t.jsx)("span",{style:{color:"#556",marginLeft:6},children:"›"})]}),e.message&&(0,t.jsx)("div",{style:{fontSize:10,color:"#666",marginTop:2},children:e.message}),(0,t.jsx)("div",{style:{fontSize:9,color:"#444",marginTop:3},children:new Date(e.created_at).toLocaleDateString()})]})]})},e.id))})]})]})}function y({isMobile:e}){let n=(0,a.default)(),r=n.running&&"refresh"===n.action,o=(0,f.busyReason)(n);return(0,t.jsxs)("button",{onClick:async()=>{try{let e=(0,f.notReadyMessage)(await (0,f.scraperStatus)());if(e)return void alert(e);if(!window.confirm("Check for new connections?\n\nThis opens a Chrome window on your LinkedIn connections list and reads it from the newest, stopping once it reaches people already saved. It usually takes under a minute. It reads your own list, not a search, so it doesn’t use your search budget, but like any scan it is LinkedIn traffic from your account."))return;await (0,f.beginScrape)("refresh"),alert("Checking for new connections — watch it on the Scan page.")}catch(e){alert(e.message)}},disabled:!!o,title:o?`${o}. One scan at a time.`:"Check for new connections: reads your LinkedIn connections list from the newest","aria-label":"Check for new connections",style:{height:e?28:32,padding:e?"0 10px":"0 14px",borderRadius:16,border:"1px solid rgba(0,255,136,0.3)",cursor:o?"not-allowed":"pointer",opacity:o?.45:1,background:"rgba(0,255,136,0.08)",color:"#bff5d9",fontSize:e?11:12.5,fontWeight:700,display:"flex",alignItems:"center",gap:6,whiteSpace:"nowrap",flexShrink:0},children:[(0,t.jsx)("span",{"aria-hidden":"true",style:{fontSize:e?12:14},children:"↻"}),r?"Checking…":e?"New":"Check for new"]})}let x=null;function w({isMobile:e,csvMode:r,level:o}){let{userId:i}=(0,d.useUser)(),l=m.IS_DEMO?"sample":r?"csv":"own",a=(0,n.useSyncExternalStore)(g.watchLevel,()=>(0,g.levelNow)(l),()=>null);(0,n.useEffect)(()=>{null!=o?(0,g.rememberLevel)(o,l):null!=a||m.IS_DEMO||r||!i||(x||(x=(0,p.loadNetwork)(i).then(e=>(0,g.networkLevel)(e.degree1,e.degree2)).catch(()=>(x=null,null))),x.then(e=>{null!=e&&(0,g.rememberLevel)(e,l)}))},[o,a,r,l,i]);let s=o??a;return(0,t.jsx)("a",{href:m.IS_DEMO?"/launch":r?"/import":"/profile",title:null!=s?`Your profile \xb7 level ${s}`:"Your profile",style:{width:e?30:36,height:e?30:36,borderRadius:"50%",display:"flex",alignItems:"center",justifyContent:"center",background:"linear-gradient(135deg, #FFD700, #FF6B35)",color:"#000",fontWeight:800,fontSize:e?11:14,textDecoration:"none",marginLeft:e?2:4,flexShrink:0},children:s??""})}e.s(["default",0,function({active:e,isMobile:o,csvMode:a=!1,onMode:s,chips:c,level:d,onOpenNote:f,openNoteId:p=null,brand:g="h1",children:x}){let S=function(){let[e,t]=(0,n.useState)(!1);return(0,n.useEffect)(()=>{let e=()=>t(window.innerWidth<768);return e(),window.addEventListener("resize",e),()=>window.removeEventListener("resize",e)},[]),e}(),k=o??S,v=!m.IS_DEMO&&!a;return(0,t.jsxs)("header",{style:{position:"relative",padding:k?"10px 12px":"20px 30px",borderBottom:"1px solid rgba(255,255,255,0.1)",flexShrink:0,background:"var(--sd-header)",backdropFilter:"var(--sd-header-blur)",WebkitBackdropFilter:"var(--sd-header-blur)"},children:[(0,t.jsxs)("div",{style:k?{display:"flex",alignItems:"center",justifyContent:"space-between",flexWrap:"wrap",gap:6,marginBottom:4}:{display:"grid",gridTemplateColumns:"1fr auto 1fr",alignItems:"center",gap:16,marginBottom:8},children:[(0,t.jsx)(r.default,{href:"/",style:{textDecoration:"none",justifySelf:"start"},children:(0,t.jsx)(g,{style:{...h,fontSize:k?16:28},children:"Six Degrees"})}),(0,t.jsx)(i.default,{active:e,isMobile:k,csvMode:a,onMode:s,after:v?(0,t.jsx)(l.default,{isMobile:k}):null,children:c}),(0,t.jsxs)("div",{style:{display:"flex",alignItems:"center",justifyContent:"flex-end",justifySelf:"end",gap:k?4:8},children:[!m.IS_DEMO&&(0,t.jsx)(r.default,{href:"/settings",title:"Settings","aria-label":"Settings",style:{width:k?28:32,height:k?28:32,borderRadius:"50%",flexShrink:0,background:"rgba(255,255,255,0.06)",color:"#888",fontSize:k?14:16,textDecoration:"none",display:"flex",alignItems:"center",justifyContent:"center"},children:"⚙︎"}),v&&(0,t.jsx)(b,{isMobile:k,onOpen:f,openId:p}),v&&(0,t.jsx)(y,{isMobile:k}),(0,t.jsx)(w,{isMobile:k,csvMode:a,level:d})]})]}),x,v&&(0,t.jsx)(u,{inset:k?12:30})]})}],33821)},26005,76760,e=>{"use strict";var t=e.i(43476),n=e.i(22016),r=e.i(64745);let o=[{key:"network",label:"Network Circle",short:"Circle",on:{background:"#fff",color:"#000"},off:{background:"transparent",color:"#888"}},{key:"degrees",label:"Degrees",on:{background:"linear-gradient(135deg, #FFD700, #FF6B35)",color:"#000"},off:{background:"rgba(255,255,255,0.12)",color:"#fff"}},{key:"separation",label:"Separation",on:{background:"linear-gradient(135deg, #00E5FF, #FFD700 55%, #FF7043)",color:"#000"},off:{background:"rgba(255,255,255,0.12)",color:"#fff"}}],i=[{key:"paths",href:"/paths",label:"Paths",color:"#00ff88",on:"linear-gradient(135deg, #00ff88, #3498DB)",demo:!0},{key:"outlink",href:"/queue",label:"Outlink",color:"#FF6B35",on:"linear-gradient(135deg, #FF6B35, #FFD700)",own:!0},{key:"scan",href:"/setup",label:"Scan",color:"#666",on:"linear-gradient(135deg, #00ff88, #1abc9c)",own:!0}];e.s(["default",0,function({active:e,isMobile:l=!1,csvMode:a=!1,onMode:s,children:c,after:u}){let d={padding:l?"6px 10px":"8px 16px",borderRadius:6,border:"none",fontSize:l?11:13,fontWeight:600,textDecoration:"none"};return(0,t.jsxs)("div",{id:"main-tabs",style:{display:"flex",gap:l?2:4,background:"rgba(255,255,255,0.08)",borderRadius:8,padding:l?2:3,flexWrap:l?"wrap":"nowrap"},children:[o.map(r=>{let o={...d,cursor:"pointer",...e===r.key?r.on:r.off},i=l&&r.short?r.short:r.label;return s?(0,t.jsx)("button",{type:"button",onClick:()=>s(r.key),style:o,children:i},r.key):(0,t.jsx)(n.default,{href:`/?mode=${r.key}`,style:{...o,display:"flex",alignItems:"center"},children:i},r.key)}),c,i.filter(e=>!r.IS_DEMO&&(!e.own||!a)).map(r=>{let o=e===r.key;return(0,t.jsxs)(n.default,{href:r.href,title:r.title,"aria-current":o?"page":void 0,style:{...d,display:"flex",alignItems:"center",gap:4,background:o?r.on:"rgba(255,255,255,0.06)",color:o?"#000":r.color},children:[r.label,r.badge&&(0,t.jsx)("span",{style:{fontSize:9,opacity:.7,marginLeft:2},children:r.badge})]},r.key)}),u]})}],26005);var l=e.i(71645),a=e.i(40996),s=e.i(68792);e.s(["default",0,function({isMobile:e=!1}){let r=(0,l.useSyncExternalStore)(s.watchAllDay,s.allDayNow,()=>!1),[o,i]=(0,l.useState)(()=>(0,a.scannerNow)()),[c,u]=(0,l.useState)(!1);if((0,l.useEffect)(()=>(0,a.watchScanner)(()=>{i((0,a.scannerNow)()),u(!1)}),[]),!r)return null;let d=o?.running&&String(o.action||"").startsWith("auto-bridge"),f={padding:e?"8px 12px":"8px 16px",borderRadius:6,fontSize:13,fontWeight:600,textDecoration:"none",display:"flex",alignItems:"center",gap:6,whiteSpace:"nowrap"};if(d)return(0,t.jsxs)(n.default,{href:"/setup",title:"Auto scan is running: see it on the Scan page",style:{...f,background:"rgba(0,255,136,0.15)",color:"#00ff88",border:"1px solid rgba(0,255,136,0.45)"},children:[(0,t.jsx)("span",{style:{width:7,height:7,borderRadius:"50%",background:"#00ff88"}}),"Auto scan"]});let p=o?.running||c;return(0,t.jsxs)("button",{onClick:()=>{u(!0),(0,s.startAutoScan)().catch(()=>u(!1))},disabled:p,title:p?"The scanner is busy: one scan at a time":"Start the all-day Auto-Bridge (experimental)",style:{...f,cursor:p?"default":"pointer",background:"rgba(255,255,255,0.04)",color:p?"#555":"#8b9a9a",border:"1px solid rgba(255,215,0,0.25)"},children:[(0,t.jsx)("span",{style:{width:7,height:7,borderRadius:"50%",background:"#556"}}),"Auto scan"]})}],76760)},28867,86106,e=>{"use strict";var t=e.i(71645),n=e.i(7214);let r=Object.freeze({known:!1,sent:new Set,mine:new Map,version:0}),o=new Set,i=r,l=null,a=new Map;function s(e=i.sent,t=i.known){for(let n of(i=Object.freeze({known:t,sent:e,mine:new Map(a),version:i.version+1}),[...o]))n()}function c(e){return o.add(e),l||i.known||(l=fetch("/api/outreach").then(e=>e.ok?e.json():null).then(e=>{e&&s(new Set((e.pending||[]).map(n.keyFor)),!0)}).catch(()=>{}).finally(()=>{l=null})),()=>o.delete(e)}function u(){return i}async function d(e){let t=await fetch("/api/outreach",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await t.json().catch(()=>({}));if(!t.ok)throw Error(n.error||"Could not save that.");return n}async function f(e,{bridgeId:t}={}){let r=(0,n.keyFor)(e),o=a.has(r)?a.get(r):void 0;a.set(r,!0),s();try{return await d({action:"mark-sent",connectionId:e.id,profileUrl:e.profile_url,bridgeId:t})}catch(e){throw void 0===o?a.delete(r):a.set(r,o),s(),e}}async function p(e){let t=(0,n.keyFor)(e),r=a.has(t)?a.get(t):void 0;a.set(t,!1),s();try{return await d({action:"undo",connectionId:e.id,profileUrl:e.profile_url})}catch(e){throw void 0===r?a.delete(t):a.set(t,r),s(),e}}e.s(["REQUESTS_UNKNOWN",0,r,"hasRequest",0,function(e,t=i){if(!e)return!1;let r=(0,n.keyFor)(e);return t.mine.has(r)?t.mine.get(r):t.known?t.sent.has(r):"pending"===e.unlock_status||"sent"===e.outreach_status},"markRequested",0,f,"requestCount",0,function(e=i){let t=new Set(e.known?e.sent:[]);for(let[n,r]of e.mine)r?t.add(n):t.delete(n);return t.size},"requestsNow",0,u,"undoRequest",0,p,"watchRequests",0,c],86106),e.s(["default",0,function(){return(0,t.useSyncExternalStore)(c,u,()=>r)}],28867)},30215,e=>{"use strict";var t=e.i(71645),n=e.i(40996);e.s(["default",0,function(){return(0,t.useSyncExternalStore)(n.watchScanner,n.scannerNow,()=>n.SCANNER_UNKNOWN)}])},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 i(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 l=e=>{let t=f(e);return t?`name:${t}`:null},a=/sponsored/i,s=/in\s?mail/i,c=/linkedin\.com\/(company|showcase|school)\//i,u=e=>/^(yes|true|1)$/i.test(String(e||"").trim());function d(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=i(e),d=new Map;for(let t of e)n&&o(t["sender profile url"])===n&&t.from&&d.set(t.from,(d.get(t.from)||0)+1);let p=f([...d].sort((e,t)=>t[1]-e[1])[0]?.[0]||""),g=e=>!!e&&(e===n||!!p&&e===`name:${p}`),m={},h=new Map,b=0,y=0;for(let n of e){let e=n["conversation id"],i=r(n.date);if(!e||null==i){y+=1;continue}if(u(n["is message draft"])){b+=1;continue}let d=o(n["sender profile url"])||l(n.from);d&&n.from&&!m[d]&&(m[d]=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(l).filter(Boolean);f.length&&f.length===p.length&&f.forEach((e,t)=>{m[e]||(m[e]=p[t])}),f.length||p.forEach(e=>{let t=l(e);t&&!m[t]&&(m[t]=e)}),h.has(e)||h.set(e,{id:e,title:"",everyone:new Set,messages:[],kind:null,folder:"",newest:-1/0});let w=h.get(e);d&&w.everyone.add(d),x.forEach(e=>w.everyone.add(e)),w.kind=w.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":s.test(t)||s.test(n)?"inmail":null}(n),i>=w.newest&&(w.newest=i,w.folder=String(n.folder||"").trim().toLowerCase().slice(0,30));let S={t:i,fromMe:g(d)};t&&(S.text=[n.subject,n.content].map(e=>String(e||"").trim()).filter(Boolean).join("\n\n"),!w.title&&n["conversation title"]&&(w.title=String(n["conversation title"]).trim())),w.messages.push(S)}let x=[];for(let e of h.values()){let n=[...e.everyone].filter(e=>e&&!g(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(m))g(e)&&delete m[e];return{self:n,conversations:x,names:m,drafts:b,skipped:y}},"buildInvitations",0,function(e=[],{keepText:t=!1}={}){let n=[];for(let i of e){let e=String(i.direction||"").toUpperCase();if("INCOMING"!==e&&"OUTGOING"!==e)continue;let a="INCOMING"===e?"in":"out",s=String(("in"===a?i.from:i.to)||"").trim(),c=o("in"===a?i.inviterprofileurl:i.inviteeprofileurl),u=(c&&/\/in\//.test(c)?c:null)||l(s);if(!u)continue;let d=r(i["sent at"]),f={id:`${a}|${u}|${d??""}`,dir:a,key:u,name:s.slice(0,120),t:d};t&&String(i.message||"").trim()&&(f.note=String(i.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 i=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),l=t.map(e=>({url:o(e.url||e.profile_url),t:r(e["connected on"]||e.connected_on)})).filter(e=>null!=e.t);return i.map(e=>{let t=e.to??n,r=l.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=i(e),n=new Map;for(let t of e){let e=o(t["sender profile url"]),i=t["conversation id"];if(!i||!e)continue;let l=String(t["recipient profile urls"]||"").split(/[,;\s]+/).filter(Boolean).map(o),a=r(t.date);if(null==a)continue;n.has(i)||n.set(i,{people:new Set,msgs:[]});let s=n.get(i);s.people.add(e),l.forEach(e=>s.people.add(e)),s.msgs.push({from:e,t:a})}let l=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>l&&(l=n.t);a.set(r,o)}for(let e of a.values())e.recent=e.times.filter(e=>e>l-7776e6).length,delete e.times;return{self:t,asOf:l||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 i=Math.max(6048e5,o[o.length-1]-o[0]),l=o.length/i*6048e5,a=n.map(e=>({...e,week:o.filter(t=>t>=e.t&&t<e.t+6048e5).length})),s=a.length?a.reduce((e,t)=>e+t.week,0)/a.length:0;return{posts:a,usualWeek:l,afterPosts:s}},"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 i=r[o].map(e=>e.trim().toLowerCase());return r.slice(o+1).filter(e=>e.some(e=>e.trim())).map(e=>Object.fromEntries(i.map((t,n)=>[t,(e[n]??"").trim()])))},"warmthOf",0,d],84278);let p="six-degrees-galaxy-lab",g="six-degrees-galaxy-layouts",m=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}),h=["gravity","rings","push","pull","distance","orbit","dotSize","lines","sizeBy","names","branch","colourBy","speed","loop"],b=["gravity","rings","push","pull","distance"],y=Object.freeze({gravity:.8,rings:0,push:6,pull:6,distance:.15,sizeBy:"links"}),x=Object.freeze({rings:{lines:1},orbit:{lines:1},clusters:{lines:3}}),w=Object.freeze({gravity:0,rings:3,push:6,pull:4,distance:.22,sizeBy:"power"}),S=new Set,k=null;function v(){return k||(k=function(){try{let e=JSON.parse(localStorage.getItem(p)||"null");if(!e||"object"!=typeof e)return m;return{...m,...e,...e.v===m.v?{}:{branch:!1},v:m.v,on:!0}}catch{return m}}()),k}function j(e){k=null===e?{...m,labels:v().labels}:{...v(),...e,on:!0};try{localStorage.setItem(p,JSON.stringify(k))}catch{}S.forEach(e=>e())}let C=null,F=new Set;function N(){if(!C){try{C=JSON.parse(localStorage.getItem(g)||"[]")}catch{C=[]}Array.isArray(C)||(C=[])}return C}function M(e){C=e;try{localStorage.setItem(g,JSON.stringify(e))}catch{}F.forEach(e=>e())}let O=new Set,E={at:null,playing:!1,min:null,max:null,of:0,find:"",fly:0,found:0,fit:0,social:void 0},I=null;function D(e){let t={...E,...e};Object.keys(e).every(e=>Object.is(t[e],E[e]))||(E=t,O.forEach(e=>e()))}function B(){cancelAnimationFrame(I),I=null,D({playing:!1})}let T=null;function z(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}),i=n.get(e)||{total:0,recent:0};o.last&&(!i.last||o.last>i.last)&&(i.last=o.last,"boolean"==typeof o.lastFromThem&&(i.lastFromThem=o.lastFromThem)),i.unread=o.unread,i.total=Math.max(i.total||0,1),n.set(e,i)}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 L={1:"#4FC3F7",2:"#BA68C8",3:"#FFB74D"},R=["#FF6B6B","#4ECDC4","#FFD93D","#A29BFE","#00B894","#FD79A8","#74B9FF","#E17055"],A={warm:"#FF7043",cool:"#4FC3F7",dormant:"#8D6E63",never:"#546E7A"},$="#4a5568",U=["#2b1b6b","#6a1b9a","#c2185b","#f4511e","#ffb300","#fff3b0"];function W(e){let t=Math.max(0,Math.min(1,Number(e)||0))*(U.length-1),n=Math.min(U.length-2,Math.floor(t)),r=t-n,o=U[n],i=U[n+1];return`#${[1,3,5].map(e=>Math.round(parseInt(o.slice(e,e+2),16)*(1-r)+parseInt(i.slice(e,e+2),16)*r).toString(16).padStart(2,"0")).join("")}`}function _(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 q=e=>String(e||"").trim().toLowerCase();e.s(["CLUSTERS",0,y,"FORCE_KEYS",0,b,"LAB_DEFAULTS",0,m,"LAYOUT_LOOKS",0,x,"ORBIT",0,w,"applyLayout",0,function(e){let t=N().find(t=>t.name===e);t&&j(Object.fromEntries(h.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,i=0)=>{if(r.has(e.id))return r.get(e.id);let l=-1/0,a=t.get(e.id);if(1===e.degree||null==a||i>6){let t=e.connected_date?Date.parse(e.connected_date):NaN;l=Number.isNaN(t)?-1/0:t}return null!=a&&n.has(a)&&i<=6&&(l=Math.max(l,o(n.get(a),i+1))),r.set(e.id,l),l};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=>L[Math.min(e.degree||1,3)],legend:[["1st",L[1]],["2nd",L[2]],["3rd+",L[3]]]};if("company"===e){let e=new Map;for(let t of n){let n=q(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,R.length),r=new Map(t.map(([e],t)=>[e,R[t]]));return{of:e=>r.get(q(e.company))||$,legend:[...t.map(([,e],t)=>[e.name,R[t]]),["Other",$]]}}if("heat"===e){let e=function(e=[]){return _(e,t.score)}(n);return{of:t=>W(e(t)),heat:e,legend:[["Cold",W(.1)],["Warm",W(.45)],["Hot",W(.75)],["Hottest",W(1)]]}}return"warmth"===e&&o?{of:e=>1===(e.degree||1)?A[d(o.merged.get((0,t.keyFor)(e)),o.asOf)]:"#2d3748",legend:[["Warm",A.warm],["Cool",A.cool],["Dormant",A.dormant],["Never messaged",A.never]]}:{of:e=>r[e.tier]||"#666",legend:Object.entries(r)}},"effectiveLab",0,e=>e.on?e:e.labels===m.labels?m:{...m,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(N().filter(t=>t.name!==e))},"heatBy",0,_,"heatColour",0,W,"labNow",0,v,"layoutOf",0,function(e){let t=t=>Object.entries(t).every(([t,n])=>e[t]===n);return b.every(t=>e[t]===m[t])?"rings":t(y)?"clusters":t(w)?"orbit":null},"layoutsNow",0,N,"loadSocial",0,function(){return T||(T=fetch("/api/social").then(e=>e.ok?e.json():{}).then(e=>{D({social:e.social?z(e.social):null})}).catch(()=>D({social:null}))),T},"mergeSocial",0,z,"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,B,"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(v().speed)||15),r=null==E.at||E.at>=E.max?E.min:E.at,o=performance.now()-(r-E.min)/t*n;D({at:r,playing:!0});let i=r=>{let l=Math.min(E.max,E.min+(r-o)/n*t);if(D({at:l}),l>=E.max){I=null,D({playing:!1}),v().loop&&setTimeout(()=>{I||E.at!==E.max||(D({at:E.min}),e())},1200);return}I=requestAnimationFrame(i)};cancelAnimationFrame(I),I=requestAnimationFrame(i)},"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=v(),r=Object.fromEntries(h.map(e=>[e,n[e]]));M([...N().filter(e=>e.name!==t),{name:t,settings:r}])},"setClock",0,D,"setLab",0,j,"stopReplay",0,function(){B(),D({at:null})},"watchClock",0,function(e){return O.add(e),()=>O.delete(e)},"watchLab",0,function(e){S.add(e);let t=t=>{t.key===p&&(k=null,e())};return window.addEventListener("storage",t),()=>{S.delete(e),window.removeEventListener("storage",t)}},"watchLayouts",0,function(e){return F.add(e),()=>F.delete(e)}],39160)}]);
@@ -6,6 +6,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.0] - 2026-10-03
10
+
11
+ ### Added
12
+ - **Themes** (Settings → Appearance). Seven looks: Standard, High contrast, Obsidian, Glass,
13
+ Analyst, Space and Synthwave, each with a small preview of the map in its colours. Then make it
14
+ yours: the background and a second colour, a backdrop behind everything (stars, a grid, a glow,
15
+ a sunset horizon), the accent, each tier's dot and your own, the map's lines (in the colour of
16
+ the person they lead to, or one colour), how see-through the side panels are, and the font.
17
+ Everything changes as you go, on every page, and stays in this browser. Copy a theme code to
18
+ share a look; pasting one only ever sets colours and choices.
19
+ - **Clusters, the way Obsidian draws a graph** (Network Circle → Filters → Layout → Clusters). Your
20
+ network as one round disc: each connection with a scanned circle is a big hub with their circle
21
+ around them, and everyone is sized by their lines. Someone in several circles is linked to each
22
+ of them, so they sit between those hubs and tie them together. The forces have Obsidian's names,
23
+ Center force, Repel force, Link force and Link distance, each saying what it does to your
24
+ network. Names fade as you zoom out, all but the hubs'. Picking a layout fits everyone on screen,
25
+ and so does ⤢ Fit.
26
+ - **Orbit** (Filters → Forces): sets the whole map turning round you, every circle with it, so
27
+ nothing loses its shape. Still under Reduce Motion.
28
+ - **Dots along the header's line while a scan runs**: one for each page the scanner reads,
29
+ collecting from the left, on every tab. A click opens the Scan page.
30
+
31
+ ### Changed
32
+ - **The same header on every tab.** The tabs sit in the exact middle of the window, and Settings,
33
+ notifications, ↻ Check for new and your level are on Paths, Outlink and Scan too. A notification
34
+ picked on another tab opens on the map.
35
+ - **Separation fits a laptop's window.** The header is slimmer: the search sits in the title's row,
36
+ the tier line stays without the sentence under it (it's the title's tooltip now), Only show sits
37
+ beside the tiers, the pill saying where the slider is sits over its middle, and the long
38
+ explanations moved into tooltips. The cards are packed a little closer, so the whole map, ten
39
+ people and every way in, shows at once in the app's normal window wherever the slider is.
40
+ - **The Scan page shows every step at once**, down a line: Get ready, Sign in, Who you know, Who
41
+ they know, each with its label, what it does and its one button, marked Done, Now or Next. The
42
+ line turns green as each is done, so it's plain what's finished and what's left; no more tapping
43
+ dots to see a step.
44
+ - **↻ Check for new**, in words, in the map's header (it was a bare ↻): the same name as the Scan
45
+ page's button, for checking your LinkedIn for new connections in one click.
46
+
47
+ ### Fixed
48
+ - **Separation's tiers choose who's ranked, not who leads to them.** Hiding a tier in Filters also
49
+ hid your connections of that tier (the purple A-tier lines), so the people they lead to lost a
50
+ way in. In Separation every connection now stays a way in, whatever its tier.
51
+ - The level in the round button at the top right matches the Profile page's: it left out C and D
52
+ tiers and catalysts, so it read lower.
53
+
54
+ ## [0.5.1-beta.1] - 2026-10-02 (beta: a pre-release, never installed automatically)
55
+
56
+ ### Added
57
+ - **Six Degrees for Windows and Linux (beta).** The same app as on the Mac, with the scanner's
58
+ own Python inside, so nothing needs installing before a scan. **Windows:**
59
+ *Six-Degrees-Windows-Setup.exe* installs it for you with one click (no admin rights), with Start
60
+ Menu and Desktop shortcuts, and opens it; running a newer one over it updates it. It isn't
61
+ signed yet, so SmartScreen may ask the first time (*More info → Run anyway*). **Linux:** a
62
+ `.deb` (`sudo apt install ./Six-Degrees-Linux-x64.deb`) or a `.tar.gz`. Scanning needs Google
63
+ Chrome on both. Your network stays in your home folder's `.six-degrees`, as on the Mac;
64
+ uninstalling never touches it. Settings → Updates points to the new installer there.
65
+
66
+ ### Fixed
67
+ - On Windows the scanner would have stopped after its first batch: its progress lines
68
+ ("→") can't be written in Windows' default text encoding. Its Python now always writes UTF-8.
69
+
9
70
  ## [0.5.0] - 2026-10-02
10
71
 
11
72
  ### Added
@@ -41,22 +41,24 @@ know placed on rings by how much they can open up for you, the handful of people
41
41
  whose own circles reach the furthest, and the shortest chain from you to a stranger
42
42
  worth meeting.
43
43
 
44
- It's a Mac app (or `npx six-degrees` on Linux). It runs on your computer, against your own data, with no account and
44
+ It's a Mac app, and now a Windows and Linux app too (beta). It runs on your computer, against your own data, with no account and
45
45
  no server.
46
46
 
47
47
  ## Download
48
48
 
49
49
  <p align="center">
50
50
  <a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Apple-Silicon.dmg"><img src="docs/img/download-apple-silicon.png" width="346" alt="Download Six Degrees for a Mac with Apple Silicon (M1 or newer)"></a>
51
- <a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg"><img src="docs/img/download-intel.png" width="346" alt="Download Six Degrees for a Mac with an Intel chip"></a>
51
+ <a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg"><img src="docs/img/download-intel.png" width="346" alt="Download Six Degrees for a Mac with an Intel chip"></a><br>
52
+ <a href="https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Windows-Setup.exe"><img src="docs/img/download-windows.png" width="346" alt="Download Six Degrees for Windows 10 or 11 (beta): one-click Setup.exe"></a>
53
+ <a href="https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.deb"><img src="docs/img/download-linux.png" width="346" alt="Download Six Degrees for Linux (beta): .deb for Ubuntu and Debian"></a>
52
54
  </p>
53
55
 
54
56
  | Platform | Download | Needs |
55
57
  |---|---|---|
56
58
  | **macOS**, Apple Silicon (M1 or newer) | [Six-Degrees-Mac-Apple-Silicon.dmg](https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Apple-Silicon.dmg) | macOS 13.5 (Ventura) or later |
57
59
  | **macOS**, Intel | [Six-Degrees-Mac-Intel.dmg](https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg) | macOS 13.5 (Ventura) or later |
58
- | **Linux** | App: *coming soon.* Run it today from the Terminal: `npx six-degrees` ([how](https://sixdegreesapp.com/download/#linux)) | Node 22.13 or later |
59
- | **Windows** | *Coming soon.* [Watch releases](https://github.com/blakeb056/six-degrees) (Watch → Custom → Releases) to hear when it's out | — |
60
+ | **Windows** (beta) | [Six-Degrees-Windows-Setup.exe](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Windows-Setup.exe) | Windows 10 or 11, 64-bit |
61
+ | **Linux** (beta) | [Six-Degrees-Linux-x64.deb](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.deb) or [.tar.gz](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.tar.gz), or from the Terminal: `npx six-degrees` ([how](https://sixdegreesapp.com/download/#linux)) | Ubuntu or Debian, 64-bit (npx: Node 22.13 or later) |
60
62
 
61
63
  <sub>Every release is on [GitHub Releases](https://github.com/blakeb056/six-degrees/releases) with a
62
64
  `SHA256SUMS` file ([latest](https://github.com/blakeb056/six-degrees/releases/latest/download/SHA256SUMS)); the Terminal install below checks the download against it.
@@ -114,7 +116,20 @@ touch it. Each new version also copies your data into its `backups` folder befor
114
116
  first opens it. To take it to another computer, see
115
117
  [Moving to a new computer](#moving-to-a-new-computer).
116
118
 
117
- **Linux:** install **Node 22.13 or later** (from nodejs.org or nvm; Ubuntu's own
119
+ **Windows (beta):** download [Six-Degrees-Windows-Setup.exe](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Windows-Setup.exe)
120
+ and open it. It installs for you alone (no administrator password), adds Start Menu and
121
+ Desktop shortcuts, and opens Six Degrees. It isn't signed with a paid certificate yet, so
122
+ SmartScreen may say it protected your PC: **More info → Run anyway**. A newer Setup.exe
123
+ updates it in place; your network stays in `%USERPROFILE%\.six-degrees`, which
124
+ uninstalling never touches. Scanning needs Google Chrome.
125
+
126
+ **Linux app (beta):** `sudo apt install ./Six-Degrees-Linux-x64.deb` with the
127
+ [.deb](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.deb), then open it from your apps. The
128
+ [.tar.gz](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.tar.gz) runs from its folder; on Ubuntu 24.04 and
129
+ later run `sudo chown root:root chrome-sandbox && sudo chmod 4755 chrome-sandbox` in it
130
+ once (or start it with `--no-sandbox`). Scanning needs Google Chrome (not Chromium).
131
+
132
+ **Linux from the Terminal:** install **Node 22.13 or later** (from nodejs.org or nvm; Ubuntu's own
118
133
  `nodejs` package is too old), then run:
119
134
 
120
135
  ```bash
@@ -131,8 +146,7 @@ up the scanner** button downloads a private copy into your data folder (24–33
131
146
  GitHub, checked against a checksum built into Six Degrees). It's tested on Ubuntu, and
132
147
  works on a Mac too if you'd rather not install the app.
133
148
 
134
- **Windows:** not yet. There's no Windows app, and `npx six-degrees` doesn't run on
135
- Windows yet; npm says so if you try.
149
+ `npx six-degrees` doesn't run on Windows; use the Windows app above.
136
150
 
137
151
  <details>
138
152
  <summary>Run it from source (contributors)</summary>
@@ -248,7 +262,7 @@ two steps away, and 503 of them, 84%, are reached through only one connection.
248
262
  | Any network, any data, research-grade statistics | — (LinkedIn networks only) | — | any data | ✓ what they're built for |
249
263
  | Runs on your computer, with no account | ✓ | — (online, with your account) | varies | ✓ |
250
264
  | Price | free, MIT | free, with paid plans | free to paid | free, open source |
251
- | Windows | coming soon | ✓ | ✓ | ✓ |
265
+ | Windows | ✓ (beta) | ✓ | ✓ | ✓ |
252
266
 
253
267
  Something out of date, or unfair to another tool? [Open an issue](https://github.com/blakeb056/six-degrees/issues).
254
268
 
@@ -46,8 +46,15 @@ const state = registerScanState({
46
46
  stopping: false,
47
47
  stderrTail: [],
48
48
  failure: null, // the last lines of stderr from a run that failed — its reason
49
+ pages: 0, // pages the running scan has read: the dots along the header's line (app/components/ScanTrail.js)
49
50
  });
50
51
 
52
+ // A page read, as scripts/scrape.py prints it: " Page 3... " as a circle or a
53
+ // company scan reads each page, and " 350 / 817 collected" for each fifty
54
+ // people of your own connections list. Counted as the lines arrive, since the
55
+ // log only keeps its last MAX_LOG lines.
56
+ const PAGE_READ = /^\s*Page \d+\.\.\.|\d+\s*\/\s*\d+\s+collected/;
57
+
51
58
  /** Stop the running job without orphaning the browser it opened.
52
59
  *
53
60
  * The child is spawned into its own process group, so the negative pid
@@ -69,6 +76,19 @@ function stopChild() {
69
76
  state.stopping = true;
70
77
  push('Stopping…');
71
78
  const pid = child.pid;
79
+ // Windows has no process groups or SIGTERM. taskkill /T walks the tree the
80
+ // scanner started; without /F it asks Chrome's windows to close, so the
81
+ // browser shuts down cleanly and keeps the LinkedIn session. Forced after 5 s.
82
+ if (process.platform === 'win32') {
83
+ spawn('taskkill', ['/pid', String(pid), '/T'], { stdio: 'ignore', windowsHide: true }).on('error', () => {});
84
+ setTimeout(() => {
85
+ if (state.child && state.child.pid === pid) {
86
+ push('Still running — forcing it.');
87
+ spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }).on('error', () => {});
88
+ }
89
+ }, 5000);
90
+ return;
91
+ }
72
92
  try { process.kill(-pid, 'SIGTERM'); }
73
93
  catch { try { child.kill('SIGTERM'); } catch {} }
74
94
 
@@ -86,6 +106,7 @@ function push(line) {
86
106
  const t = part.replace(/\s+$/, '');
87
107
  if (!t) continue;
88
108
  state.log.push(t);
109
+ if (PAGE_READ.test(t)) state.pages += 1;
89
110
  }
90
111
  if (state.log.length > MAX_LOG) state.log.splice(0, state.log.length - MAX_LOG);
91
112
  }
@@ -234,13 +255,24 @@ async function look() {
234
255
  const chrome =
235
256
  process.platform === 'darwin' ? existsSync('/Applications/Google Chrome.app')
236
257
  : process.platform === 'linux' ? existsSync('/opt/google/chrome/chrome')
237
- : true; // elsewhere Playwright resolves the channel itself
258
+ : process.platform === 'win32' ? windowsChrome()
259
+ : true; // elsewhere Playwright resolves the channel itself
238
260
 
239
- const signedIn = existsSync(path.join(dataDir(), 'chrome-profile', 'Default', 'Cookies'));
261
+ // Windows' Chrome keeps its cookies one folder down, in Default\Network.
262
+ const profile = path.join(dataDir(), 'chrome-profile', 'Default');
263
+ const signedIn = existsSync(path.join(profile, 'Cookies')) || existsSync(path.join(profile, 'Network', 'Cookies'));
240
264
 
241
265
  return { root, python, chrome, signedIn };
242
266
  }
243
267
 
268
+ /** Google Chrome on Windows, where Playwright's "chrome" channel looks: for this user, then for everyone. */
269
+ function windowsChrome() {
270
+ const env = process.env;
271
+ return [env.LOCALAPPDATA, env.PROGRAMFILES, env['PROGRAMFILES(X86)']]
272
+ .filter(Boolean)
273
+ .some((root) => existsSync(path.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe')));
274
+ }
275
+
244
276
  function machineChecks() {
245
277
  if (Date.now() - cached.at < 4000 && cached.value) return Promise.resolve(cached.value);
246
278
  if (looking) return looking;
@@ -342,6 +374,7 @@ function job() {
342
374
  exitCode: state.exitCode,
343
375
  failure: state.running ? null : state.failure,
344
376
  progress: state.running ? scanProgress(state.log, state.action) : null,
377
+ pages: state.running ? state.pages : 0,
345
378
  log: state.log.slice(-120),
346
379
  recent: state.recent,
347
380
  budget: state.running ? budgetNow() : null,
@@ -680,6 +713,7 @@ export async function POST(request) {
680
713
  state.exitCode = null;
681
714
  state.startedAt = Date.now();
682
715
  state.log = [spec.label + (target?.name ? ` ${target.name}…` : '…')];
716
+ state.pages = 0;
683
717
  state.stderrTail = [];
684
718
  state.failure = null;
685
719
  forgetChecks();
@@ -764,7 +798,10 @@ export async function POST(request) {
764
798
  try {
765
799
  // Its own process group, so cancelling reaches the browser as well. A
766
800
  // step's env() makes its own environment from this one.
767
- child = spawn(step.cmd, step.args, { cwd: root, env: step.env ? step.env(childEnv) : childEnv, detached: true });
801
+ // On Windows `detached` means a console of its own, a visible window for
802
+ // Playwright's driver: there taskkill /T reaches the tree instead (stopChild).
803
+ const win = process.platform === 'win32';
804
+ child = spawn(step.cmd, step.args, { cwd: root, env: step.env ? step.env(childEnv) : childEnv, detached: !win, windowsHide: true });
768
805
  } catch (err) {
769
806
  push(`Could not start: ${err.message}`);
770
807
  return finish(-1);
@@ -3,7 +3,7 @@ import { accessSync, constants, existsSync } from 'node:fs';
3
3
  import { projectRoot, isGitCheckout, dataDir } from '../../../lib/paths';
4
4
  import { homedir, tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
- import { repoSlug, compareVersions, installKind, updateCommand } from '../../../lib/release';
6
+ import { repoSlug, compareVersions, installKind, updateCommand, INSTALLER_KINDS } from '../../../lib/release';
7
7
  import {
8
8
  bundleRefusal, dataRefusal, installerTarget, lastUpdateReport, planFromRelease, releaseSource, runningBundle,
9
9
  terminalFallback,
@@ -239,12 +239,18 @@ async function installedPost(action, root) {
239
239
  // Install installs the version this check offered and no other
240
240
  // (lib/updater-job.js rememberCheck); none, when it offered none.
241
241
  if (mac) rememberCheck(newer ? latest.version : null);
242
+ // Windows and Linux: the newer release's page, where its installer is. Only
243
+ // ever this project's own releases on GitHub (the page opens it in the browser).
244
+ const releasePage = `https://github.com/${info.slug}/releases/`;
245
+ const download = newer && INSTALLER_KINDS.includes(info.kind) && latest?.url && String(latest.url).startsWith(releasePage)
246
+ ? { url: latest.url } : null;
242
247
  return Response.json({
243
248
  ...info,
244
249
  latest: latest && { version: latest.version, url: latest.url },
245
250
  newer,
246
251
  install,
247
252
  fallback,
253
+ download,
248
254
  });
249
255
  } catch (e) {
250
256
  return Response.json({ error: `Could not reach GitHub (${e.message}). Try again in a moment.` }, { status: 502 });
@@ -252,11 +258,11 @@ async function installedPost(action, root) {
252
258
  }
253
259
 
254
260
  if (action === 'install-release') {
255
- if (info.kind !== 'mac-app') {
256
- return Response.json(
257
- { error: 'Only the Mac app can install an update itself. Use the line below instead.', command: info.command },
258
- { status: 400 },
259
- );
261
+ if (info.kind !== 'mac-app' || process.platform !== 'darwin') {
262
+ const error = INSTALLER_KINDS.includes(info.kind)
263
+ ? 'On Windows and Linux an update is the new installer: download it from the release page and run it.'
264
+ : 'Only the Mac app can install an update itself. Use the line below instead.';
265
+ return Response.json({ error, command: info.command }, { status: 400 });
260
266
  }
261
267
  if (!info.slug) {
262
268
  return Response.json({ error: 'This copy does not say where it was published.' }, { status: 400 });
@@ -0,0 +1,257 @@
1
+ 'use client';
2
+
3
+ // The header on every page that has the app's tabs: the name on the left, the
4
+ // tabs in the exact middle of the window, and the same buttons on the right
5
+ // everywhere — Settings, notifications, ↻ Check for new and your profile.
6
+ // Blake, 2026-10-03: on Paths, Outlink and Scan "the profile settings and
7
+ // refresh buttons aren't there, every section should have the same buttons",
8
+ // and "align the button bar to the exact middle".
9
+ //
10
+ // The row is a grid of 1fr | auto | 1fr: the two sides share what's left
11
+ // equally, so the tabs sit in the middle whatever is beside them. A window too
12
+ // narrow for that lets the sides shrink to fit before anything overlaps.
13
+
14
+ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
15
+ import Link from 'next/link';
16
+ import { useRouter } from 'next/navigation';
17
+ import AppTabs from './AppTabs';
18
+ import AutoScanButton from './AutoScanButton';
19
+ import ScanTrail from './ScanTrail';
20
+ import useScanner from './useScanner';
21
+ import { useUser } from './UserProvider';
22
+ import { scraperStatus, beginScrape, notReadyMessage, busyReason } from '../../lib/scraper-client';
23
+ import { loadNetwork } from '../../lib/network';
24
+ import { networkLevel, rememberLevel, levelNow, watchLevel } from '../../lib/level';
25
+ import { IS_DEMO } from '../../lib/demo';
26
+
27
+ function useIsMobile() {
28
+ const [m, setM] = useState(false);
29
+ useEffect(() => {
30
+ const check = () => setM(window.innerWidth < 768);
31
+ check();
32
+ window.addEventListener('resize', check);
33
+ return () => window.removeEventListener('resize', check);
34
+ }, []);
35
+ return m;
36
+ }
37
+
38
+ const TITLE = { fontWeight: 700, margin: 0, whiteSpace: 'nowrap', background: 'linear-gradient(135deg, #FFD700, #9B59B6, #3498DB)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' };
39
+
40
+ /**
41
+ * @param {{ active: string, isMobile?: boolean, csvMode?: boolean, onMode?: (key: string) => void,
42
+ * chips?: any, level?: number | null, onOpenNote?: (note: object) => void, openNoteId?: string | null,
43
+ * brand?: string, children?: any }} props
44
+ * `chips` go in the tab row after the map's tabs (the sample or CSV chip);
45
+ * `children` under the row. `level` is your network's level when the page has
46
+ * the network loaded; elsewhere the button finds it. `onOpenNote` opens a
47
+ * notification on this page (the map's side panel); without it, a notification
48
+ * opens there (/?note=…). `brand` is the element the name is drawn in: an
49
+ * <h1> unless the page has its own (the Scan page).
50
+ */
51
+ export default function AppHeader({ active, isMobile: mobileProp, csvMode = false, onMode, chips, level, onOpenNote, openNoteId = null, brand: Brand = 'h1', children }) {
52
+ const mobileSeen = useIsMobile();
53
+ const isMobile = mobileProp ?? mobileSeen;
54
+ const own = !IS_DEMO && !csvMode; // scanning and notifications are for your own network
55
+ return (
56
+ <header style={{
57
+ position: 'relative', padding: isMobile ? '10px 12px' : '20px 30px', borderBottom: '1px solid rgba(255,255,255,0.1)', flexShrink: 0,
58
+ // Clear, or frosted glass on a glass theme (lib/themes.js).
59
+ background: 'var(--sd-header)', backdropFilter: 'var(--sd-header-blur)', WebkitBackdropFilter: 'var(--sd-header-blur)',
60
+ }}>
61
+ <div style={isMobile
62
+ ? { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 6, marginBottom: 4 }
63
+ : { display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', gap: 16, marginBottom: 8 }}>
64
+ <Link href="/" style={{ textDecoration: 'none', justifySelf: 'start' }}>
65
+ <Brand style={{ ...TITLE, fontSize: isMobile ? 16 : 28 }}>Six Degrees</Brand>
66
+ </Link>
67
+ <AppTabs active={active} isMobile={isMobile} csvMode={csvMode} onMode={onMode}
68
+ after={own ? <AutoScanButton isMobile={isMobile} /> : null}>
69
+ {chips}
70
+ </AppTabs>
71
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', justifySelf: 'end', gap: isMobile ? 4 : 8 }}>
72
+ {/* Settings: also in CSV or sample mode and on phones, so updates stay reachable */}
73
+ {!IS_DEMO && <Link href="/settings" title="Settings" aria-label="Settings" style={{
74
+ width: isMobile ? 28 : 32, height: isMobile ? 28 : 32, borderRadius: '50%', flexShrink: 0,
75
+ background: 'rgba(255,255,255,0.06)', color: '#888', fontSize: isMobile ? 14 : 16, textDecoration: 'none',
76
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
77
+ }}>{'⚙︎'}</Link>}
78
+ {own && <NotificationBell isMobile={isMobile} onOpen={onOpenNote} openId={openNoteId} />}
79
+ {own && <RefreshButton isMobile={isMobile} />}
80
+ <ProfileButton isMobile={isMobile} csvMode={csvMode} level={level} />
81
+ </div>
82
+ </div>
83
+ {children}
84
+ {/* While a scan runs: a dot on the line below for each page it reads */}
85
+ {own && <ScanTrail inset={isMobile ? 12 : 30} />}
86
+ </header>
87
+ );
88
+ }
89
+
90
+ /** Notifications: the bell, its count, and the list under it. */
91
+ function NotificationBell({ isMobile, onOpen, openId }) {
92
+ const { userId } = useUser();
93
+ const router = useRouter();
94
+ const [notes, setNotes] = useState([]);
95
+ const [open, setOpen] = useState(false);
96
+ // A notification picked on another page arrives as /?note=<id>: opened once, when the list is in.
97
+ const opened = useRef(false);
98
+ const onOpenRef = useRef(onOpen);
99
+ useEffect(() => { onOpenRef.current = onOpen; });
100
+ useEffect(() => {
101
+ if (!userId) return;
102
+ fetch(`/api/notifications?userId=${userId}`).then((r) => r.json()).then((d) => {
103
+ const list = d.notifications || [];
104
+ setNotes(list);
105
+ const asked = openId != null && !opened.current && list.find((n) => String(n.id) === String(openId));
106
+ if (asked && onOpenRef.current) { opened.current = true; onOpenRef.current(asked); }
107
+ }).catch(() => {});
108
+ }, [userId, openId]);
109
+ const unseen = notes.filter((n) => !n.seen).length;
110
+ const show = (n) => {
111
+ // Open it, and count it as read.
112
+ if (!n.seen) {
113
+ fetch('/api/notifications', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'mark-seen', id: n.id }) }).catch(() => {});
114
+ setNotes((prev) => prev.map((x) => (x.id === n.id ? { ...x, seen: true } : x)));
115
+ }
116
+ setOpen(false);
117
+ if (onOpen) onOpen(n);
118
+ else router.push(`/?note=${encodeURIComponent(n.id)}`);
119
+ };
120
+ return (
121
+ <div style={{ position: 'relative' }}>
122
+ <button onClick={() => setOpen(!open)} title="Notifications" aria-label={unseen ? `Notifications, ${unseen} new` : 'Notifications'} style={{
123
+ width: isMobile ? 28 : 32, height: isMobile ? 28 : 32, borderRadius: '50%', border: 'none', cursor: 'pointer',
124
+ background: 'rgba(255,255,255,0.06)', color: '#888', fontSize: isMobile ? 12 : 14,
125
+ display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative',
126
+ }}>
127
+ 🔔
128
+ {unseen > 0 && (
129
+ <span style={{
130
+ position: 'absolute', top: -2, right: -2, width: 16, height: 16, borderRadius: '50%',
131
+ background: '#ff5050', color: '#fff', fontSize: 9, fontWeight: 700,
132
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
133
+ }}>{unseen}</span>
134
+ )}
135
+ </button>
136
+ {open && (
137
+ <div style={{
138
+ position: 'absolute', top: 40, right: 0, width: 320, maxHeight: 400,
139
+ background: 'color-mix(in srgb, var(--sd-bg) 97%, transparent)', border: '1px solid rgba(255,255,255,0.1)',
140
+ borderRadius: 12, overflow: 'hidden', zIndex: 100,
141
+ backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)',
142
+ boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
143
+ }}>
144
+ <div style={{ padding: '12px 16px', borderBottom: '1px solid rgba(255,255,255,0.08)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
145
+ <span style={{ fontWeight: 700, fontSize: 14 }}>Notifications</span>
146
+ {unseen > 0 && (
147
+ <button onClick={() => {
148
+ fetch('/api/notifications', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'mark-all-seen' }) });
149
+ setNotes((prev) => prev.map((n) => ({ ...n, seen: true })));
150
+ }} style={{ background: 'none', border: 'none', color: '#3498DB', fontSize: 11, cursor: 'pointer', fontWeight: 600 }}>
151
+ Mark all read
152
+ </button>
153
+ )}
154
+ </div>
155
+ <div style={{ overflowY: 'auto', maxHeight: 340 }}>
156
+ {notes.length === 0 ? (
157
+ <div style={{ padding: 20, textAlign: 'center', color: '#555', fontSize: 12 }}>No notifications yet</div>
158
+ ) : notes.slice(0, 20).map((n) => (
159
+ <div key={n.id} role="button" tabIndex={0}
160
+ onClick={() => show(n)}
161
+ onKeyDown={(e) => { if (e.key === 'Enter') show(n); }}
162
+ onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; }}
163
+ onMouseLeave={(e) => { e.currentTarget.style.background = n.seen ? 'transparent' : 'rgba(255,215,0,0.04)'; }}
164
+ style={{
165
+ padding: '10px 16px', borderBottom: '1px solid rgba(255,255,255,0.04)', cursor: 'pointer',
166
+ background: n.seen ? 'transparent' : 'rgba(255,215,0,0.04)',
167
+ }}>
168
+ <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
169
+ <span style={{ fontSize: 16 }}>{n.icon || '📌'}</span>
170
+ <div style={{ flex: 1 }}>
171
+ <div style={{ fontSize: 12, fontWeight: 600, color: n.seen ? '#888' : '#fff' }}>{n.title}<span style={{ color: '#556', marginLeft: 6 }}>›</span></div>
172
+ {n.message && <div style={{ fontSize: 10, color: '#666', marginTop: 2 }}>{n.message}</div>}
173
+ <div style={{ fontSize: 9, color: '#444', marginTop: 3 }}>{new Date(n.created_at).toLocaleDateString()}</div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ ))}
178
+ </div>
179
+ </div>
180
+ )}
181
+ </div>
182
+ );
183
+ }
184
+
185
+ // Its own component so the scanner's answer, which changes every second or two
186
+ // while a scan runs, re-renders this button and not the whole page.
187
+ //
188
+ // It asks first, as a company scan does, and says what it will do and what it
189
+ // costs: one click on a small icon used to open a Chrome window on LinkedIn
190
+ // with no word of what it was about to do.
191
+ function RefreshButton({ isMobile }) {
192
+ const scan = useScanner();
193
+ const checking = scan.running && scan.action === 'refresh';
194
+ const busy = busyReason(scan);
195
+ return (
196
+ <button
197
+ onClick={async () => {
198
+ try {
199
+ const blocked = notReadyMessage(await scraperStatus());
200
+ if (blocked) { alert(blocked); return; }
201
+ const ok = window.confirm(
202
+ 'Check for new connections?\n\nThis opens a Chrome window on your LinkedIn connections ' +
203
+ 'list and reads it from the newest, stopping once it reaches people already saved. It ' +
204
+ 'usually takes under a minute. It reads your own list, not a search, so it doesn’t ' +
205
+ 'use your search budget, but like any scan it is LinkedIn traffic from your account.',
206
+ );
207
+ if (!ok) return;
208
+ await beginScrape('refresh');
209
+ alert('Checking for new connections — watch it on the Scan page.');
210
+ } catch (e) {
211
+ alert(e.message);
212
+ }
213
+ }}
214
+ disabled={Boolean(busy)}
215
+ title={busy ? `${busy}. One scan at a time.` : 'Check for new connections: reads your LinkedIn connections list from the newest'}
216
+ aria-label="Check for new connections"
217
+ style={{
218
+ // Words, not a bare ↻ (Blake, 2026-10-02: "make it easier to check for new
219
+ // connections"), and the same words as the Scan page's button.
220
+ height: isMobile ? 28 : 32, padding: isMobile ? '0 10px' : '0 14px', borderRadius: 16,
221
+ border: '1px solid rgba(0,255,136,0.3)', cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.45 : 1,
222
+ background: 'rgba(0,255,136,0.08)', color: '#bff5d9', fontSize: isMobile ? 11 : 12.5, fontWeight: 700,
223
+ display: 'flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap', flexShrink: 0,
224
+ }}
225
+ >
226
+ <span aria-hidden="true" style={{ fontSize: isMobile ? 12 : 14 }}>↻</span>
227
+ {checking ? 'Checking…' : isMobile ? 'New' : 'Check for new'}
228
+ </button>
229
+ );
230
+ }
231
+
232
+ // Asked once per tab, for the pages that don't load the network themselves.
233
+ let levelLoad = null;
234
+
235
+ /** Your level, opening your Profile (the sample's launch page, or a CSV's import page). */
236
+ function ProfileButton({ isMobile, csvMode, level }) {
237
+ const { userId } = useUser();
238
+ const source = IS_DEMO ? 'sample' : csvMode ? 'csv' : 'own';
239
+ const remembered = useSyncExternalStore(watchLevel, () => levelNow(source), () => null);
240
+ useEffect(() => {
241
+ if (level != null) { rememberLevel(level, source); return; }
242
+ // The sample and a CSV live in the map page's tab: only your own network is loaded here.
243
+ if (remembered != null || IS_DEMO || csvMode || !userId) return;
244
+ if (!levelLoad) levelLoad = loadNetwork(userId).then((n) => networkLevel(n.degree1, n.degree2)).catch(() => { levelLoad = null; return null; });
245
+ levelLoad.then((l) => { if (l != null) rememberLevel(l, source); });
246
+ }, [level, remembered, csvMode, source, userId]);
247
+ const shown = level ?? remembered;
248
+ return (
249
+ <a href={IS_DEMO ? '/launch' : csvMode ? '/import' : '/profile'} title={shown != null ? `Your profile · level ${shown}` : 'Your profile'} style={{
250
+ width: isMobile ? 30 : 36, height: isMobile ? 30 : 36, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
251
+ background: 'linear-gradient(135deg, #FFD700, #FF6B35)', color: '#000', fontWeight: 800, fontSize: isMobile ? 11 : 14,
252
+ textDecoration: 'none', marginLeft: isMobile ? 2 : 4, flexShrink: 0,
253
+ }}>
254
+ {shown ?? ''}
255
+ </a>
256
+ );
257
+ }