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)}]);
package/CHANGELOG.md CHANGED
@@ -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
package/README.md CHANGED
@@ -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
 
@@ -869,6 +869,7 @@ export function restartAdvice({ kind, code, dataDir = null }) {
869
869
  return { canRestart: true, how: 'Click Restart now to finish it: Six Degrees restarts, which takes a few seconds, and finishes the import as it starts.' };
870
870
  }
871
871
  if (kind === 'mac-app') return { canRestart: false, how: `Quit Six Degrees (⌘Q) and open it again. ${finish}` };
872
+ if (kind === 'windows-app' || kind === 'linux-app') return { canRestart: false, how: `Quit Six Degrees (File → Exit) and open it again. ${finish}` };
872
873
  if (kind === 'npm') {
873
874
  const flag = dataDir ? ` --data-dir '${String(dataDir).replace(/'/g, `'\\''`)}'` : '';
874
875
  return {
@@ -4,6 +4,7 @@
4
4
  // like any other download.
5
5
 
6
6
  import { clockNow, setClock, play, pause } from './galaxy-lab.js';
7
+ import { MAP_LOOK } from './themes.js';
7
8
 
8
9
  let current = null; // { svg, stamp: () => text or null, legend: () => [[label, colour]] }
9
10
 
@@ -41,7 +42,7 @@ function svgImage(svg) {
41
42
  // One frame: the Galaxy, its date while a replay runs, the legend, and a small credit.
42
43
  async function drawFrame(ctx, w, h) {
43
44
  const img = await svgImage(current.svg);
44
- ctx.fillStyle = '#0a0a1a';
45
+ ctx.fillStyle = MAP_LOOK.bg; // the theme's colour (lib/themes.js)
45
46
  ctx.fillRect(0, 0, w, h);
46
47
  ctx.drawImage(img, 0, 0, w, h);
47
48
  const scale = w / current.svg.clientWidth;
package/lib/galaxy-lab.js CHANGED
@@ -23,7 +23,8 @@ export const LAB_DEFAULTS = Object.freeze({
23
23
  distance: 1, // how far out those people sit
24
24
  dotSize: 1,
25
25
  lines: 1,
26
- sizeBy: 'power', // 'power' (power score) or 'reach' (how many hang off them)
26
+ sizeBy: 'power', // 'power' (power score), 'links' (how many lines meet at them, as Obsidian sizes a note) or 'reach' (how many hang off them)
27
+ orbit: 0, // the whole map turning round you, in tenths of a radian a second; 0 holds still
27
28
  names: 'key', // 'key' (you, S tier, catalysts) or 'all' (all your connections)
28
29
  branch: false, // hovering a dot lights up its branch and dims the rest: off to start (Blake, 2026-10-02: it glitched)
29
30
  colourBy: 'tier', // 'tier', 'heat' (power as a thermal map), 'degree', 'company' or 'warmth' (from the Social tab)
@@ -34,10 +35,24 @@ export const LAB_DEFAULTS = Object.freeze({
34
35
  });
35
36
 
36
37
  /** The slider settings a saved layout keeps (not whether the lab is on, nor the names switch). */
37
- const LAYOUT_KEYS = ['gravity', 'rings', 'push', 'pull', 'distance', 'dotSize', 'lines', 'sizeBy', 'names', 'branch', 'colourBy', 'speed', 'loop'];
38
+ const LAYOUT_KEYS = ['gravity', 'rings', 'push', 'pull', 'distance', 'orbit', 'dotSize', 'lines', 'sizeBy', 'names', 'branch', 'colourBy', 'speed', 'loop'];
38
39
 
39
- /** Let the layout find its own shape: no rings, each connection's circle bunched round them. */
40
- export const CLUSTERS = Object.freeze({ gravity: 0.25, rings: 0, push: 25, pull: 2.5, distance: 0.35, sizeBy: 'reach' });
40
+ /** The forces a layout sets, and the ones the Galaxy re-settles for when they change. */
41
+ export const FORCE_KEYS = ['gravity', 'rings', 'push', 'pull', 'distance'];
42
+
43
+ /**
44
+ * Clusters, the way Obsidian draws a vault (Blake, 2026-10-03: "like obsidian
45
+ * where it showing the nodes of the clusters all connecting back but retaining
46
+ * a circular look but the nodes need to be a big thing"): no rings, a center
47
+ * force strong enough to keep the whole network a round disc, each connection's
48
+ * circle held round them by its links, everyone sized by how many lines meet at
49
+ * them, so a connection with a scanned circle is a big hub. Someone in several
50
+ * circles is linked to each, so they sit between those hubs and tie them together.
51
+ */
52
+ export const CLUSTERS = Object.freeze({ gravity: 0.8, rings: 0, push: 6, pull: 6, distance: 0.15, sizeBy: 'links' });
53
+
54
+ /** What picking a layout does to the look, without being part of what makes it that layout. */
55
+ export const LAYOUT_LOOKS = Object.freeze({ rings: { lines: 1 }, orbit: { lines: 1 }, clusters: { lines: 3 } });
41
56
 
42
57
  /**
43
58
  * Orbit, the old view, as a layout of the Galaxy (Blake, 2026-10-02: "add that as a
@@ -50,7 +65,7 @@ export const ORBIT = Object.freeze({ gravity: 0, rings: 3, push: 6, pull: 4, dis
50
65
  /** Which layout the sliders are set to: 'rings', 'clusters', 'orbit', or null for your own. */
51
66
  export function layoutOf(lab) {
52
67
  const is = (preset) => Object.entries(preset).every(([k, v]) => lab[k] === v);
53
- if (['gravity', 'rings', 'push', 'pull', 'distance'].every((k) => lab[k] === LAB_DEFAULTS[k])) return 'rings';
68
+ if (FORCE_KEYS.every((k) => lab[k] === LAB_DEFAULTS[k])) return 'rings';
54
69
  if (is(CLUSTERS)) return 'clusters';
55
70
  if (is(ORBIT)) return 'orbit';
56
71
  return null;
@@ -123,10 +138,11 @@ export function forgetLayout(name) { writeLayouts(layoutsNow().filter((l) => l.n
123
138
  // `max` are the first and last dates in the network on screen, set by the Galaxy
124
139
  // (both null when it has no dates, as in the sample network).
125
140
  // The same store holds the lab's other passing state: what Find is looking
126
- // for (and a count that asks the Galaxy to fly to the first match), and the
127
- // Social tab's data once it's loaded.
141
+ // for (and a count that asks the Galaxy to fly to the first match), a count
142
+ // that asks it to fit everyone on screen once it settles, and the Social tab's
143
+ // data once it's loaded.
128
144
  const clockListeners = new Set();
129
- let clock = { at: null, playing: false, min: null, max: null, of: 0, find: '', fly: 0, found: 0, social: undefined };
145
+ let clock = { at: null, playing: false, min: null, max: null, of: 0, find: '', fly: 0, found: 0, fit: 0, social: undefined };
130
146
  let raf = null;
131
147
 
132
148
  export const clockNow = () => clock;
package/lib/level.js ADDED
@@ -0,0 +1,51 @@
1
+ // Your network's level: the number in the round button at the top right, and
2
+ // the Profile page's Network Power. One formula for both: the header used to
3
+ // leave out C and D tiers and catalysts, so it showed a lower level than the
4
+ // Profile page it opens.
5
+
6
+ /** Network Power, as the Profile page explains it. */
7
+ export function networkPower(degree1 = [], degree2 = []) {
8
+ const tiers = { S: 0, A: 0, B: 0, C: 0, D: 0 };
9
+ for (const c of degree1) tiers[c.tier] = (tiers[c.tier] || 0) + 1;
10
+ let d2S = 0;
11
+ for (const c of degree2) if (c.tier === 'S') d2S += 1;
12
+ const clusters = new Set(degree2.map((c) => c.source_connection_id).filter(Boolean)).size;
13
+ const catalysts = degree1.filter((c) => c.is_catalyst).length;
14
+ return tiers.S * 100 + tiers.A * 40 + tiers.B * 15 + tiers.C * 5 + tiers.D * 1
15
+ + clusters * 200 + catalysts * 150 + d2S * 50 + degree1.length;
16
+ }
17
+
18
+ /** The level a Network Power reaches: level n takes 10·n² power. */
19
+ export const levelOf = (power) => Math.floor(Math.sqrt(Math.max(0, power) / 10));
20
+
21
+ export const networkLevel = (degree1, degree2) => levelOf(networkPower(degree1, degree2));
22
+
23
+ // Remembered for this tab, so a page that doesn't load the network (Scan, say)
24
+ // can show the same number without loading it. One per network: your own, the
25
+ // sample, or a CSV. A small store, for useSyncExternalStore.
26
+ const KEY = 'six-degrees-level';
27
+ const known = {};
28
+ const listeners = new Set();
29
+
30
+ export function watchLevel(f) {
31
+ listeners.add(f);
32
+ return () => listeners.delete(f);
33
+ }
34
+
35
+ export function levelNow(source = 'own') {
36
+ if (!(source in known)) {
37
+ try {
38
+ const raw = sessionStorage.getItem(`${KEY}:${source}`);
39
+ const n = raw == null ? NaN : Number(raw);
40
+ known[source] = Number.isFinite(n) ? n : null;
41
+ } catch { known[source] = null; }
42
+ }
43
+ return known[source];
44
+ }
45
+
46
+ export function rememberLevel(level, source = 'own') {
47
+ if (levelNow(source) === level) return;
48
+ known[source] = level;
49
+ try { sessionStorage.setItem(`${KEY}:${source}`, String(level)); } catch { /* private window: it's loaded again instead */ }
50
+ listeners.forEach((f) => f());
51
+ }
package/lib/release.js CHANGED
@@ -26,20 +26,26 @@ export function compareVersions(a, b) {
26
26
  return 0;
27
27
  }
28
28
 
29
+ /** The desktop apps that update by running the new installer, not by replacing themselves (DESKTOP.md D3). */
30
+ export const INSTALLER_KINDS = Object.freeze(['windows-app', 'linux-app']);
31
+
29
32
  /**
30
- * 'mac-app', 'source' (a git checkout run with `npm run start:packaged`) or
31
- * 'npm'. The launchers say which (SIX_DEGREES_INSTALL); an older Mac app that
32
- * predates that is recognised by where it is running from.
33
+ * 'mac-app', 'windows-app', 'linux-app', 'source' (a git checkout run with
34
+ * `npm run start:packaged`) or 'npm'. The launchers say which
35
+ * (SIX_DEGREES_INSTALL); an older Mac app that predates that is recognised by
36
+ * where it is running from, on a Mac only.
33
37
  */
34
- export function installKind(env = process.env, root = '') {
35
- if (['mac-app', 'source', 'npm'].includes(env.SIX_DEGREES_INSTALL)) {
38
+ export function installKind(env = process.env, root = '', platform = process.platform) {
39
+ if (['mac-app', 'source', 'npm', ...INSTALLER_KINDS].includes(env.SIX_DEGREES_INSTALL)) {
36
40
  return env.SIX_DEGREES_INSTALL;
37
41
  }
38
- return /\.app\/Contents\/Resources/.test(root || '') ? 'mac-app' : 'npm';
42
+ return platform === 'darwin' && /\.app\/Contents\/Resources/.test(root || '') ? 'mac-app' : 'npm';
39
43
  }
40
44
 
41
- /** What to run to update, for each way of installing. */
45
+ /** What to run to update, for each way of installing (null: download the installer instead). */
42
46
  export function updateCommand(kind, slug, { dataDir } = {}) {
47
+ // Windows and Linux: no Terminal line; the release page's installer replaces the app.
48
+ if (INSTALLER_KINDS.includes(kind)) return null;
43
49
  if (kind === 'mac-app') {
44
50
  return `curl -fsSL https://raw.githubusercontent.com/${slug}/main/install.sh | bash`;
45
51
  }
@@ -38,7 +38,7 @@ import { SETUP_WORK_PREFIX } from './data-folder.js';
38
38
  * download instead of 104 (on a Mac, stripping saves 0.2 MB).
39
39
  *
40
40
  * To move to a newer one: take the new release's SHA256SUMS lines for these
41
- * four names, and each file's size, and change all of them together.
41
+ * five names, and each file's size, and change all of them together.
42
42
  */
43
43
  export const STANDALONE_PYTHON = Object.freeze({
44
44
  release: '20260814',
@@ -64,6 +64,13 @@ export const STANDALONE_PYTHON = Object.freeze({
64
64
  sha256: '2d8e17dfd732102cfeb18e0e1fa6769b24caa034e159981129590fe409c7157a',
65
65
  size: 29217771,
66
66
  }),
67
+ // Windows (DESKTOP.md D3): the desktop app bundles it; its layout differs
68
+ // (python.exe at the top, no bin/), see standalonePython().
69
+ 'win32-x64': Object.freeze({
70
+ file: 'cpython-3.12.14+20260814-x86_64-pc-windows-msvc-install_only_stripped.tar.gz',
71
+ sha256: '89f18f6932917163b74339ebcec2645c8e47ae7f1c5f2ac37f2b4f4cf3beb647',
72
+ size: 21976599,
73
+ }),
67
74
  }),
68
75
  });
69
76
 
@@ -77,7 +84,7 @@ export function standaloneUrl(build, pin = STANDALONE_PYTHON) {
77
84
 
78
85
  /**
79
86
  * This computer, as a key of STANDALONE_PYTHON.builds, or null when there is
80
- * no build for it (Windows, 32-bit, other chips).
87
+ * no download for it here (Windows, 32-bit, other chips).
81
88
  *
82
89
  * On a Mac the chip is the hardware's (`hw.optional.arm64`, as install.sh and
83
90
  * the updater ask), not this process's: Node running under Rosetta calls itself
@@ -86,6 +93,8 @@ export function standaloneUrl(build, pin = STANDALONE_PYTHON) {
86
93
  export function hostKey({ platform = process.platform, arch = process.arch, sysctlArm64 = '' } = {}) {
87
94
  if (platform === 'darwin') return `darwin-${chipOf({ sysctlArm64, processArch: arch })}`;
88
95
  if (platform === 'linux' && (arch === 'x64' || arch === 'arm64')) return `linux-${arch}`;
96
+ // Windows has a pinned build (the desktop app bundles it, scripts/build-desktop.mjs)
97
+ // but no Set up the scanner yet: that path hasn't been tried there (DESKTOP.md D3).
89
98
  return null;
90
99
  }
91
100
 
@@ -126,6 +135,16 @@ export const SYSTEM_CANDIDATES = Object.freeze([
126
135
  'python3.14', 'python3.13', 'python3.12', 'python3.11', 'python3.10',
127
136
  ]);
128
137
 
138
+ /**
139
+ * On Windows: the `py` launcher first, which finds a real install; `python` and
140
+ * `python3` there are often the Microsoft Store's stand-ins, which open the Store
141
+ * instead of running anything.
142
+ */
143
+ export const WINDOWS_CANDIDATES = Object.freeze(['py', 'python', 'python3']);
144
+
145
+ /** The names to try on this operating system. */
146
+ export const systemCandidatesFor = (platform = process.platform) => (platform === 'win32' ? WINDOWS_CANDIDATES : SYSTEM_CANDIDATES);
147
+
129
148
  /** Is "3.12.4" a Python the pinned packages install into? */
130
149
  export function systemPythonFits(version, range = SYSTEM_PYTHON) {
131
150
  const m = String(version || '').match(/^(\d+)\.(\d+)/);
@@ -187,10 +206,14 @@ export function parseProbe(stdout) {
187
206
  * bundle, which breaks its signature
188
207
  * -u unbuffered, which PYTHONUNBUFFERED said before -E: the Scan page shows
189
208
  * the scanner's lines as they come
209
+ * -X utf8 UTF-8 for its output whatever the system's code page: on Windows a
210
+ * piped stdout is cp1252, and scrape.py's "→" would end the scan with a
211
+ * UnicodeEncodeError. PYTHONUTF8 can't say it: -E ignores it. Harmless
212
+ * on a Mac or Linux, already UTF-8 there.
190
213
  * Not -I: that also takes scrape.py's own folder off the import path, and with
191
214
  * it `import image_store`.
192
215
  */
193
- export const OWN_PYTHON_FLAGS = Object.freeze(['-E', '-s', '-B', '-u']);
216
+ export const OWN_PYTHON_FLAGS = Object.freeze(['-E', '-s', '-B', '-u', '-X', 'utf8']);
194
217
 
195
218
  /**
196
219
  * The environment the app's own Python runs with, beside OWN_PYTHON_FLAGS. The
@@ -205,6 +228,7 @@ export function ownPythonEnv(env = process.env) {
205
228
  }
206
229
  out.PYTHONNOUSERSITE = '1';
207
230
  out.PYTHONDONTWRITEBYTECODE = '1';
231
+ out.PYTHONUTF8 = '1'; // for what it starts (-X utf8 is the interpreter's own)
208
232
  return out;
209
233
  }
210
234
 
@@ -216,7 +240,8 @@ export function ownPythonEnv(env = process.env) {
216
240
  * app that folder is inside the signed app, whichever Python runs the scan.
217
241
  */
218
242
  export function noBytecodeEnv(env = process.env) {
219
- return { ...env, PYTHONDONTWRITEBYTECODE: '1' };
243
+ // And UTF-8 output: on Windows a piped stdout is cp1252 otherwise (OWN_PYTHON_FLAGS).
244
+ return { ...env, PYTHONDONTWRITEBYTECODE: '1', PYTHONUTF8: '1' };
220
245
  }
221
246
 
222
247
  /** Is this Python the app's own (choosePython's 'bundled' or 'custom'), run isolated? */
@@ -321,8 +346,13 @@ export function venvPython(dataDir, platform = process.platform) {
321
346
 
322
347
  /** Where "Set up the scanner" puts the standalone Python, and its interpreter. */
323
348
  export const DOWNLOADED_DIR = 'python';
324
- export function downloadedPython(dataDir) {
325
- return path.join(dataDir, DOWNLOADED_DIR, 'bin', 'python3');
349
+ export function downloadedPython(dataDir, platform = process.platform) {
350
+ return standalonePython(path.join(dataDir, DOWNLOADED_DIR), platform);
351
+ }
352
+
353
+ /** The interpreter inside an unpacked python-build-standalone folder: python.exe at its top on Windows, bin/python3 elsewhere. */
354
+ export function standalonePython(dir, platform = process.platform) {
355
+ return platform === 'win32' ? path.join(dir, 'python.exe') : path.join(dir, 'bin', 'python3');
326
356
  }
327
357
 
328
358
  // Private working folders for a setup, inside the data folder (lib/data-folder.js
@@ -342,7 +372,7 @@ export function runProbe(cmd, args, { timeoutMs = 8000, env } = {}) {
342
372
  const finish = (value) => { if (!done) { done = true; resolve(value); } };
343
373
  let child;
344
374
  try {
345
- child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], ...(env ? { env } : {}) });
375
+ child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, ...(env ? { env } : {}) });
346
376
  } catch (err) {
347
377
  finish({ ok: false, stdout, stderr, problem: err.message });
348
378
  return;
@@ -406,7 +436,7 @@ export async function choosePython({
406
436
  probe = runProbe,
407
437
  exists = fs.existsSync,
408
438
  host = null,
409
- systemCandidates = SYSTEM_CANDIDATES,
439
+ systemCandidates = systemCandidatesFor(),
410
440
  timeouts = { own: 20000, other: 8000 },
411
441
  env = process.env,
412
442
  } = {}) {
@@ -121,7 +121,7 @@ const QUIET_MS = 5000;
121
121
  /** Before the first answer, and on the server, where nothing is asked. */
122
122
  export const SCANNER_UNKNOWN = Object.freeze({
123
123
  known: false, running: false, pending: false, action: null, target: null,
124
- startedAt: null, progress: null, log: [], finished: [],
124
+ startedAt: null, progress: null, pages: 0, log: [], finished: [],
125
125
  });
126
126
 
127
127
  const listeners = new Set();
@@ -164,6 +164,7 @@ function take(s) {
164
164
  target: running ? s.target ?? null : starting ? now.target : null,
165
165
  startedAt: running ? s.startedAt ?? null : null,
166
166
  progress: running ? s.progress ?? null : null,
167
+ pages: running ? Number(s.pages) || 0 : 0, // pages read so far (app/api/scraper/route.js)
167
168
  budget: running ? s.budget ?? null : null,
168
169
  log: running && Array.isArray(s.log) ? s.log : [],
169
170
  finished,
package/lib/separation.js CHANGED
@@ -253,8 +253,10 @@ const PILL_W_LAYOUT = 236; // SeparationView's PILL_W: the connection pills' w
253
253
  * @returns {{ width, height, you, people, bridges, links, spokes }}
254
254
  */
255
255
  export function summitLayout(top = [], width = 800, isMobile = false, { cards = false } = {}) {
256
- const gap = cards ? 50 : isMobile ? 36 : 26;
257
- const bridgeGap = cards ? 36 : MIN_BRIDGE_GAP;
256
+ // Cards are 40 px tall and pills 28: rows a few px apart, so ten fit a laptop's
257
+ // window with the slider above them (Blake, 2026-10-03: "too zoomed in" on his MacBook).
258
+ const gap = cards ? 45 : isMobile ? 36 : 26;
259
+ const bridgeGap = cards ? 32 : MIN_BRIDGE_GAP;
258
260
  const youX = 20;
259
261
  const bx = Math.round(width * 0.42);
260
262
  const px = Math.round(width * 0.58);
@@ -0,0 +1,61 @@
1
+ // The theme in use, in this browser (lib/themes.js has what a theme is). Read
2
+ // once when the page loads, before anything draws, so the dots, your dot and
3
+ // the map's lines come out in its colours; changed from Settings → Appearance.
4
+ //
5
+ // Kept in localStorage, like the Galaxy's sliders: a look belongs to the
6
+ // screen it's on. The CSS variables are kept beside it, ready made, for the
7
+ // script in app/layout.js that puts them on the page before it paints, so a
8
+ // theme never flashes the standard colours first.
9
+
10
+ import { cleanTheme, resolveTheme, themeVars, themeAttrs, TIER_COLORS, MAP_LOOK } from './themes.js';
11
+
12
+ const KEY = 'six-degrees-theme';
13
+ export const VARS_KEY = 'six-degrees-theme-vars';
14
+ const DEFAULT = Object.freeze({ base: 'standard', custom: {} });
15
+
16
+ const listeners = new Set();
17
+ let choice = DEFAULT;
18
+
19
+ function read() {
20
+ try { return cleanTheme(JSON.parse(localStorage.getItem(KEY) || 'null') || DEFAULT); } catch { return DEFAULT; }
21
+ }
22
+
23
+ /** The dots and the map's look, and the page's variables, for a choice. */
24
+ function apply(next) {
25
+ const theme = resolveTheme(next);
26
+ Object.assign(TIER_COLORS, theme.tiers);
27
+ Object.assign(MAP_LOOK, { you: theme.you, lines: theme.lines, line: theme.line, bg: theme.bg });
28
+ if (typeof document === 'undefined') return;
29
+ const vars = themeVars(next);
30
+ const root = document.documentElement;
31
+ for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
32
+ Object.assign(root.dataset, themeAttrs(next));
33
+ try { localStorage.setItem(VARS_KEY, JSON.stringify({ vars, attrs: themeAttrs(next) })); } catch { /* it's applied anyway */ }
34
+ }
35
+
36
+ if (typeof window !== 'undefined') {
37
+ choice = read();
38
+ apply(choice);
39
+ // Another window changed it.
40
+ window.addEventListener('storage', (e) => {
41
+ if (e.key !== KEY) return;
42
+ choice = read();
43
+ apply(choice);
44
+ listeners.forEach((f) => f());
45
+ });
46
+ }
47
+
48
+ export const themeNow = () => choice;
49
+ export const serverTheme = () => DEFAULT;
50
+
51
+ export function setTheme(next) {
52
+ choice = cleanTheme(next);
53
+ try { localStorage.setItem(KEY, JSON.stringify(choice)); } catch { /* this visit only */ }
54
+ apply(choice);
55
+ listeners.forEach((f) => f());
56
+ }
57
+
58
+ export function watchTheme(f) {
59
+ listeners.add(f);
60
+ return () => listeners.delete(f);
61
+ }