six-degrees 0.2.0 → 0.2.1

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 (180) 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 +11 -11
  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.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  14. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
  15. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/import.html +1 -1
  38. package/.next/standalone/.next/server/app/import.rsc +13 -13
  39. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  40. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  41. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
  42. package/.next/standalone/.next/server/app/index.html +1 -1
  43. package/.next/standalone/.next/server/app/index.rsc +13 -13
  44. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
  45. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  46. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  47. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/launch.html +2 -2
  50. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  51. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  52. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  53. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
  54. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/paths.html +1 -1
  59. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  60. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  61. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  62. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
  63. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/profile.html +1 -1
  66. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  67. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  68. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  69. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
  70. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/queue.html +1 -1
  73. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  74. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  75. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  76. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
  77. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/setup.html +1 -1
  80. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  81. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  82. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  83. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jrmmj-._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qx548g._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1an3leo._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m8zedk._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/_1_fynia._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/_0bf3x4q._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/_1djpk8t._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  100. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  101. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  102. package/.next/standalone/.next/server/pages/404.html +1 -1
  103. package/.next/standalone/.next/server/pages/500.html +1 -1
  104. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  105. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  106. package/.next/standalone/.next/static/chunks/0jk64vo17avsb.js +1 -0
  107. package/.next/standalone/.next/static/chunks/0s65dsrthylo6.js +2 -0
  108. package/.next/standalone/.next/static/chunks/0uyfdhi8dek85.js +16 -0
  109. package/.next/standalone/.next/static/chunks/{1zmpcmngg5c7-.js → 229haorf0f19b.js} +1 -1
  110. package/.next/{static/chunks/3ts-_4i6bfkh2.js → standalone/.next/static/chunks/2g54uafnwuakc.js} +1 -1
  111. package/.next/standalone/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
  112. package/.next/{static/chunks/3pfyuezyqd9ob.js → standalone/.next/static/chunks/2spwqs6b6dx13.js} +1 -1
  113. package/.next/{static/chunks/1odow92u6wxz0.js → standalone/.next/static/chunks/38dlepuubiox0.js} +1 -1
  114. package/.next/standalone/AGENTS.md +10 -0
  115. package/.next/standalone/CHANGELOG.md +41 -0
  116. package/.next/standalone/CONTRIBUTING.md +9 -0
  117. package/.next/standalone/README.md +49 -29
  118. package/.next/standalone/SECURITY.md +12 -9
  119. package/.next/standalone/app/api/scraper/route.js +4 -3
  120. package/.next/standalone/app/api/update/route.js +10 -2
  121. package/.next/standalone/app/components/ForceGraph.js +1 -1
  122. package/.next/standalone/app/components/Sidebar.js +8 -8
  123. package/.next/standalone/app/components/UpdatePanel.js +39 -8
  124. package/.next/standalone/app/import/page.js +3 -3
  125. package/.next/standalone/app/layout.js +2 -2
  126. package/.next/standalone/app/page.js +2 -2
  127. package/.next/standalone/app/paths/page.js +1 -1
  128. package/.next/standalone/app/profile/page.js +0 -178
  129. package/.next/standalone/app/setup/page.js +4 -4
  130. package/.next/standalone/bin/six-degrees.mjs +7 -2
  131. package/.next/standalone/install.sh +1 -1
  132. package/.next/standalone/lib/csv.js +53 -27
  133. package/.next/standalone/lib/release.js +5 -2
  134. package/.next/standalone/lib/scraper-client.js +3 -3
  135. package/.next/standalone/package-lock.json +103 -9
  136. package/.next/standalone/package.json +12 -10
  137. package/.next/standalone/public/demo-data.json +1 -1
  138. package/.next/standalone/scripts/gen-synthetic.mjs +16 -1
  139. package/.next/standalone/scripts/readme_buttons.py +89 -0
  140. package/.next/standalone/scripts/screenshots.py +2 -0
  141. package/.next/standalone/site/app.js +84 -0
  142. package/.next/standalone/site/index.html +221 -0
  143. package/.next/standalone/site/style.css +155 -0
  144. package/.next/standalone/site/video/install-mac-poster.jpg +0 -0
  145. package/.next/standalone/site/video/install-mac.mp4 +0 -0
  146. package/.next/standalone/site/video/linux-poster.jpg +0 -0
  147. package/.next/standalone/site/video/linux.mp4 +0 -0
  148. package/.next/standalone/site/video/scan-poster.jpg +0 -0
  149. package/.next/standalone/site/video/scan.mp4 +0 -0
  150. package/.next/static/chunks/0jk64vo17avsb.js +1 -0
  151. package/.next/static/chunks/0s65dsrthylo6.js +2 -0
  152. package/.next/static/chunks/0uyfdhi8dek85.js +16 -0
  153. package/.next/static/chunks/{1zmpcmngg5c7-.js → 229haorf0f19b.js} +1 -1
  154. package/.next/{standalone/.next/static/chunks/3ts-_4i6bfkh2.js → static/chunks/2g54uafnwuakc.js} +1 -1
  155. package/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
  156. package/.next/{standalone/.next/static/chunks/3pfyuezyqd9ob.js → static/chunks/2spwqs6b6dx13.js} +1 -1
  157. package/.next/{standalone/.next/static/chunks/1odow92u6wxz0.js → static/chunks/38dlepuubiox0.js} +1 -1
  158. package/CHANGELOG.md +41 -0
  159. package/README.md +49 -29
  160. package/SECURITY.md +12 -9
  161. package/bin/six-degrees.mjs +7 -2
  162. package/lib/csv.js +53 -27
  163. package/lib/release.js +5 -2
  164. package/lib/scraper-client.js +3 -3
  165. package/package.json +12 -10
  166. package/public/demo-data.json +1 -1
  167. package/.next/standalone/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
  168. package/.next/standalone/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
  169. package/.next/standalone/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
  170. package/.next/standalone/.next/static/chunks/3hsi90cejq2dg.js +0 -1
  171. package/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
  172. package/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
  173. package/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
  174. package/.next/static/chunks/3hsi90cejq2dg.js +0 -1
  175. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_buildManifest.js +0 -0
  176. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_clientMiddlewareManifest.js +0 -0
  177. /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_ssgManifest.js +0 -0
  178. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_buildManifest.js +0 -0
  179. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_clientMiddlewareManifest.js +0 -0
  180. /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,2776,e=>{"use strict";var t=e.i(43476),r=e.i(15812);e.s(["default",0,function({children:e}){let{userId:n,ready:o}=(0,r.useUser)();return o&&n?e:(0,t.jsx)("div",{style:{minHeight:"100vh",display:"flex",alignItems:"center",justifyContent:"center",background:"#0a0a1a",color:"#fff",padding:24,textAlign:"center",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'},children:o?(0,t.jsxs)("div",{style:{maxWidth:420},children:[(0,t.jsx)("div",{style:{fontSize:18,fontWeight:700,marginBottom:8},children:"Can’t reach 6 Degrees’ local server"}),(0,t.jsx)("div",{style:{color:"#888",fontSize:14,lineHeight:1.6},children:"The page loaded but the app behind it is not answering. If you started it from a terminal, check that window for an error, then reload this page."})]}):(0,t.jsx)("div",{style:{fontSize:18,fontWeight:600},children:"Loading…"})})}])},19821,e=>{"use strict";async function t(e){let t=e?`/api/network?userId=${encodeURIComponent(e)}`:"/api/network",r=await fetch(t);if(!r.ok)throw Error(`Failed to load network (${r.status})`);let n=await r.json();return{degree1:n.degree1||[],degree2:n.degree2||[],degree3:n.degree3||[]}}e.s(["loadNetwork",0,t])},40996,e=>{"use strict";async function t(){let e=await fetch("/api/scraper");if(!e.ok)throw Error("Could not reach the app.");return e.json()}async function r(e,t,n={}){let o=await fetch("/api/scraper",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e,...t?{name:t}:{},...n})}),a=await o.json().catch(()=>({}));if(!o.ok)throw Error(a.error||"Could not start the scraper.");return a}async function n(){await fetch("/api/scraper",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"cancel"})})}e.s(["notReadyMessage",0,function(e){return e?e.checks?.scriptsFound?e.checks?.python?e.checks?.dependencies?e.checks?.chrome?null:"Google Chrome is not installed.":"The scraper is not installed yet — open Scan to set it up.":"Python 3 is not installed on this machine.":"The scraper files are missing from this install.":"Could not reach the app."},"runScrape",0,function(e,{name:n,onLog:o,...a}={}){return new Promise((i,u)=>{r(e,n,a).then(()=>{let e=setInterval(async()=>{try{let r=await t();o?.(r.log||[]),r.running||(clearInterval(e),i(r))}catch{}},1500)}).catch(u)})},"scraperStatus",0,t,"startScrape",0,r,"stopScrape",0,n])},5258,e=>{"use strict";e.s(["TIER_COLORS",0,{S:"#f6c344",A:"#a78bfa",B:"#60a5fa",C:"#94a3b8",D:"#6b7280"},"TIER_COLORS_CLASSIC",0,{S:"#FFD700",A:"#9B59B6",B:"#3498DB",C:"#95A5A6",D:"#BDC3C7"},"TIER_ORDER",0,["S","A","B","C","D"],"TIER_RING_RADIUS",0,{S:150,A:230,B:310,C:390,D:470},"initialsFor",0,function(e){let t=String(e||"").trim().split(/\s+/).filter(Boolean);return 0===t.length?"?":(t[0].charAt(0)+(t.length>1?t[t.length-1].charAt(0):"")).toUpperCase()}])},22016,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={default:function(){return m},useLinkStatus:function(){return S}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=e.r(90809),i=e.r(43476),u=a._(e.r(71645)),s=e.r(95057),l=e.r(8372),c=e.r(18581),f=e.r(18967),p=e.r(5550),d=e.r(88540),h=e.r(91949),y=e.r(73668),g=e.r(9396);function m(t){var r;let n,o,a,[m,S]=(0,u.useOptimistic)(h.IDLE_LINK_STATUS),v=(0,u.useRef)(null),{href:C,as:P,children:E,prefetch:O=null,passHref:_,replace:j,shallow:T,scroll:R,onClick:w,onMouseEnter:A,onTouchStart:I,legacyBehavior:k=!1,onNavigate:x,transitionTypes:N,ref:L,unstable_dynamicOnHover:U,...B}=t;n=E,k&&("string"==typeof n||"number"==typeof n)&&(n=(0,i.jsx)("a",{children:n}));let D=u.default.useContext(l.AppRouterContext),M=!1!==O,$=!1===O?"none":!0===O?"full":"auto",F="none"!==$?"auto"===$?g.FetchStrategy.PPR:g.FetchStrategy.Full:g.FetchStrategy.PPR,z="string"==typeof(r=P||C)?r:(0,s.formatUrl)(r);if(k){if(n?.$$typeof===Symbol.for("react.lazy"))throw Object.defineProperty(Error("`<Link legacyBehavior>` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's `<a>` tag."),"__NEXT_ERROR_CODE",{value:"E863",enumerable:!1,configurable:!0});o=u.default.Children.only(n)}let K=k?o&&"object"==typeof o&&o.ref:L,W,J=u.default.useCallback(e=>(null!==D&&(v.current=(0,h.mountLinkInstance)(e,z,D,F,M,S,W)),()=>{v.current&&((0,h.unmountLinkForCurrentNavigation)(v.current),v.current=null),(0,h.unmountPrefetchableInstance)(e)}),[M,z,D,F,S,W]),G={ref:(0,c.useMergedRef)(J,K),onClick(t){k||"function"!=typeof w||w(t),k&&o.props&&"function"==typeof o.props.onClick&&o.props.onClick(t),!D||t.defaultPrevented||function(t,r,n,o,a,i,s,l="none"){if("u">typeof window){let c,{nodeName:f}=t.currentTarget;if("A"===f.toUpperCase()&&((c=t.currentTarget.getAttribute("target"))&&"_self"!==c||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||t.nativeEvent&&2===t.nativeEvent.which)||t.currentTarget.hasAttribute("download"))return;if(!(0,y.isLocalURL)(r)){o&&(t.preventDefault(),location.replace(r));return}if(t.preventDefault(),i){let e=!1;if(i({preventDefault:()=>{e=!0}}),e)return}let{dispatchNavigateAction:p}=e.r(99781);u.default.startTransition(()=>{p(r,o?"replace":"push",!1===a?d.ScrollBehavior.NoScroll:d.ScrollBehavior.Default,n.current,s,l)})}}(t,z,v,j,R,x,N,$)},onMouseEnter(e){k||"function"!=typeof A||A(e),k&&o.props&&"function"==typeof o.props.onMouseEnter&&o.props.onMouseEnter(e),D&&M&&(0,h.onNavigationIntent)(e.currentTarget,!0===U)},onTouchStart:function(e){k||"function"!=typeof I||I(e),k&&o.props&&"function"==typeof o.props.onTouchStart&&o.props.onTouchStart(e),D&&M&&(0,h.onNavigationIntent)(e.currentTarget,!0===U)}};return(0,f.isAbsoluteUrl)(z)?G.href=z:k&&!_&&("a"!==o.type||"href"in o.props)||(G.href=(0,p.addBasePath)(z)),a=k?u.default.cloneElement(o,G):(0,i.jsx)("a",{...B,...G,children:n}),(0,i.jsx)(b.Provider,{value:m,children:a})}let b=(0,u.createContext)(h.IDLE_LINK_STATUS),S=()=>(0,u.useContext)(b);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},18581,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"useMergedRef",{enumerable:!0,get:function(){return o}});let n=e.r(71645);function o(e,t){let r=(0,n.useRef)(null),o=(0,n.useRef)(null);return(0,n.useCallback)(n=>{if(null===n){let e=r.current;e&&(r.current=null,e());let t=o.current;t&&(o.current=null,t())}else e&&(r.current=a(e,n)),t&&(o.current=a(t,n))},[e,t])}function a(e,t){if("function"!=typeof e)return e.current=t,()=>{e.current=null};{let r=e(t);return"function"==typeof r?r:()=>e(null)}}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},18967,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={DecodeError:function(){return m},MiddlewareNotFoundError:function(){return C},MissingStaticPage:function(){return v},NormalizeError:function(){return b},PageNotFoundError:function(){return S},SP:function(){return y},ST:function(){return g},WEB_VITALS:function(){return a},execOnce:function(){return i},getDisplayName:function(){return f},getLocationOrigin:function(){return l},getURL:function(){return c},isAbsoluteUrl:function(){return s},isResSent:function(){return p},loadGetInitialProps:function(){return h},normalizeRepeatedSlashes:function(){return d},stringifyError:function(){return P}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=["CLS","FCP","FID","INP","LCP","TTFB"];function i(e){let t,r=!1;return(...n)=>(r||(r=!0,t=e(...n)),t)}let u=/^[a-zA-Z][a-zA-Z\d+\-.]*?:/,s=e=>{let t=e.charCodeAt(0);return!!(t>=65&&t<=90||t>=97&&t<=122)&&u.test(e)};function l(){let{protocol:e,hostname:t,port:r}=window.location;return`${e}//${t}${r?":"+r:""}`}function c(){let{href:e}=window.location,t=l();return e.substring(t.length)}function f(e){return"string"==typeof e?e:e.displayName||e.name||"Unknown"}function p(e){return e.finished||e.headersSent}function d(e){let t=e.split("?");return t[0].replace(/\\/g,"/").replace(/\/\/+/g,"/")+(t[1]?`?${t.slice(1).join("?")}`:"")}async function h(e,t){let r=t.res||t.ctx&&t.ctx.res;if(!e.getInitialProps)return t.ctx&&t.Component?{pageProps:await h(t.Component,t.ctx)}:{};let n=await e.getInitialProps(t);if(r&&p(r))return n;if(!n)throw Object.defineProperty(Error(`"${f(e)}.getInitialProps()" should resolve to an object. But found "${n}" instead.`),"__NEXT_ERROR_CODE",{value:"E1025",enumerable:!1,configurable:!0});return n}let y="u">typeof performance,g=y&&["mark","measure","getEntriesByName"].every(e=>"function"==typeof performance[e]);class m extends Error{}class b extends Error{}class S extends Error{constructor(e){super(),this.code="ENOENT",this.name="PageNotFoundError",this.message=`Cannot find module for page: ${e}`}}class v extends Error{constructor(e,t){super(),this.message=`Failed to load static file for page: ${e} ${t}`}}class C extends Error{constructor(){super(),this.code="ENOENT",this.message="Cannot find the middleware module"}}function P(e){return JSON.stringify({message:e.message,stack:e.stack})}},73668,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"isLocalURL",{enumerable:!0,get:function(){return a}});let n=e.r(18967),o=e.r(52817);function a(e){if(!(0,n.isAbsoluteUrl)(e))return!0;try{let t=(0,n.getLocationOrigin)(),r=new URL(e,t);return r.origin===t&&(0,o.hasBasePath)(r.pathname)}catch(e){return!1}}},98183,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={assign:function(){return s},searchParamsToUrlQuery:function(){return a},urlQueryToSearchParams:function(){return u}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});function a(e){let t={};for(let[r,n]of e.entries()){let e=t[r];void 0===e?t[r]=n:Array.isArray(e)?e.push(n):t[r]=[e,n]}return t}function i(e){return"string"==typeof e?e:("number"!=typeof e||isNaN(e))&&"boolean"!=typeof e?"":String(e)}function u(e){let t=new URLSearchParams;for(let[r,n]of Object.entries(e))if(Array.isArray(n))for(let e of n)t.append(r,i(e));else t.set(r,i(n));return t}function s(e,...t){for(let r of t){for(let t of r.keys())e.delete(t);for(let[t,n]of r.entries())e.append(t,n)}return e}},95057,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={formatUrl:function(){return u},formatWithValidation:function(){return l},urlObjectKeys:function(){return s}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=e.r(90809)._(e.r(98183)),i=/https?|ftp|gopher|file/;function u(e){let{auth:t,hostname:r}=e,n=e.protocol||"",o=e.pathname||"",u=e.hash||"",s=e.query||"",l=!1;t=t?encodeURIComponent(t).replace(/%3A/i,":")+"@":"",e.host?l=t+e.host:r&&(l=t+(~r.indexOf(":")?`[${r}]`:r),e.port&&(l+=":"+e.port)),s&&"object"==typeof s&&(s=String(a.urlQueryToSearchParams(s)));let c=e.search||s&&`?${s}`||"";return n&&!n.endsWith(":")&&(n+=":"),e.slashes||(!n||i.test(n))&&!1!==l?(l="//"+(l||""),o&&"/"!==o[0]&&(o="/"+o)):l||(l=""),u&&"#"!==u[0]&&(u="#"+u),c&&"?"!==c[0]&&(c="?"+c),o=o.replace(/[?#]/g,encodeURIComponent),c=c.replace("#","%23"),`${n}${l}${o}${c}${u}`}let s=["auth","hash","host","hostname","href","path","pathname","port","protocol","query","search","slashes"];function l(e){return u(e)}}]);
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,2776,e=>{"use strict";var t=e.i(43476),r=e.i(15812);e.s(["default",0,function({children:e}){let{userId:n,ready:o}=(0,r.useUser)();return o&&n?e:(0,t.jsx)("div",{style:{minHeight:"100vh",display:"flex",alignItems:"center",justifyContent:"center",background:"#0a0a1a",color:"#fff",padding:24,textAlign:"center",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'},children:o?(0,t.jsxs)("div",{style:{maxWidth:420},children:[(0,t.jsx)("div",{style:{fontSize:18,fontWeight:700,marginBottom:8},children:"Can’t reach 6 Degrees’ local server"}),(0,t.jsx)("div",{style:{color:"#888",fontSize:14,lineHeight:1.6},children:"The page loaded but the app behind it is not answering. If you started it from a terminal, check that window for an error, then reload this page."})]}):(0,t.jsx)("div",{style:{fontSize:18,fontWeight:600},children:"Loading…"})})}])},19821,e=>{"use strict";async function t(e){let t=e?`/api/network?userId=${encodeURIComponent(e)}`:"/api/network",r=await fetch(t);if(!r.ok)throw Error(`Failed to load network (${r.status})`);let n=await r.json();return{degree1:n.degree1||[],degree2:n.degree2||[],degree3:n.degree3||[]}}e.s(["loadNetwork",0,t])},40996,e=>{"use strict";async function t(){let e=await fetch("/api/scraper");if(!e.ok)throw Error("Could not reach the app.");return e.json()}async function r(e,t,n={}){let o=await fetch("/api/scraper",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e,...t?{name:t}:{},...n})}),a=await o.json().catch(()=>({}));if(!o.ok)throw Error(a.error||"Could not start the scanner.");return a}async function n(){await fetch("/api/scraper",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"cancel"})})}e.s(["notReadyMessage",0,function(e){return e?e.checks?.scriptsFound?e.checks?.python?e.checks?.dependencies?e.checks?.chrome?null:"Google Chrome is not installed.":"The scanner is not installed yet — open Scan to set it up.":"Python 3 is not installed on this machine.":"The scanner files are missing from this install.":"Could not reach the app."},"runScrape",0,function(e,{name:n,onLog:o,...a}={}){return new Promise((i,u)=>{r(e,n,a).then(()=>{let e=setInterval(async()=>{try{let r=await t();o?.(r.log||[]),r.running||(clearInterval(e),i(r))}catch{}},1500)}).catch(u)})},"scraperStatus",0,t,"startScrape",0,r,"stopScrape",0,n])},5258,e=>{"use strict";e.s(["TIER_COLORS",0,{S:"#f6c344",A:"#a78bfa",B:"#60a5fa",C:"#94a3b8",D:"#6b7280"},"TIER_COLORS_CLASSIC",0,{S:"#FFD700",A:"#9B59B6",B:"#3498DB",C:"#95A5A6",D:"#BDC3C7"},"TIER_ORDER",0,["S","A","B","C","D"],"TIER_RING_RADIUS",0,{S:150,A:230,B:310,C:390,D:470},"initialsFor",0,function(e){let t=String(e||"").trim().split(/\s+/).filter(Boolean);return 0===t.length?"?":(t[0].charAt(0)+(t.length>1?t[t.length-1].charAt(0):"")).toUpperCase()}])},22016,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={default:function(){return m},useLinkStatus:function(){return S}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=e.r(90809),i=e.r(43476),u=a._(e.r(71645)),s=e.r(95057),l=e.r(8372),c=e.r(18581),f=e.r(18967),d=e.r(5550),p=e.r(88540),h=e.r(91949),y=e.r(73668),g=e.r(9396);function m(t){var r;let n,o,a,[m,S]=(0,u.useOptimistic)(h.IDLE_LINK_STATUS),v=(0,u.useRef)(null),{href:C,as:P,children:E,prefetch:O=null,passHref:_,replace:j,shallow:T,scroll:R,onClick:w,onMouseEnter:A,onTouchStart:I,legacyBehavior:k=!1,onNavigate:x,transitionTypes:N,ref:L,unstable_dynamicOnHover:U,...B}=t;n=E,k&&("string"==typeof n||"number"==typeof n)&&(n=(0,i.jsx)("a",{children:n}));let D=u.default.useContext(l.AppRouterContext),M=!1!==O,$=!1===O?"none":!0===O?"full":"auto",F="none"!==$?"auto"===$?g.FetchStrategy.PPR:g.FetchStrategy.Full:g.FetchStrategy.PPR,z="string"==typeof(r=P||C)?r:(0,s.formatUrl)(r);if(k){if(n?.$$typeof===Symbol.for("react.lazy"))throw Object.defineProperty(Error("`<Link legacyBehavior>` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's `<a>` tag."),"__NEXT_ERROR_CODE",{value:"E863",enumerable:!1,configurable:!0});o=u.default.Children.only(n)}let K=k?o&&"object"==typeof o&&o.ref:L,W,J=u.default.useCallback(e=>(null!==D&&(v.current=(0,h.mountLinkInstance)(e,z,D,F,M,S,W)),()=>{v.current&&((0,h.unmountLinkForCurrentNavigation)(v.current),v.current=null),(0,h.unmountPrefetchableInstance)(e)}),[M,z,D,F,S,W]),G={ref:(0,c.useMergedRef)(J,K),onClick(t){k||"function"!=typeof w||w(t),k&&o.props&&"function"==typeof o.props.onClick&&o.props.onClick(t),!D||t.defaultPrevented||function(t,r,n,o,a,i,s,l="none"){if("u">typeof window){let c,{nodeName:f}=t.currentTarget;if("A"===f.toUpperCase()&&((c=t.currentTarget.getAttribute("target"))&&"_self"!==c||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||t.nativeEvent&&2===t.nativeEvent.which)||t.currentTarget.hasAttribute("download"))return;if(!(0,y.isLocalURL)(r)){o&&(t.preventDefault(),location.replace(r));return}if(t.preventDefault(),i){let e=!1;if(i({preventDefault:()=>{e=!0}}),e)return}let{dispatchNavigateAction:d}=e.r(99781);u.default.startTransition(()=>{d(r,o?"replace":"push",!1===a?p.ScrollBehavior.NoScroll:p.ScrollBehavior.Default,n.current,s,l)})}}(t,z,v,j,R,x,N,$)},onMouseEnter(e){k||"function"!=typeof A||A(e),k&&o.props&&"function"==typeof o.props.onMouseEnter&&o.props.onMouseEnter(e),D&&M&&(0,h.onNavigationIntent)(e.currentTarget,!0===U)},onTouchStart:function(e){k||"function"!=typeof I||I(e),k&&o.props&&"function"==typeof o.props.onTouchStart&&o.props.onTouchStart(e),D&&M&&(0,h.onNavigationIntent)(e.currentTarget,!0===U)}};return(0,f.isAbsoluteUrl)(z)?G.href=z:k&&!_&&("a"!==o.type||"href"in o.props)||(G.href=(0,d.addBasePath)(z)),a=k?u.default.cloneElement(o,G):(0,i.jsx)("a",{...B,...G,children:n}),(0,i.jsx)(b.Provider,{value:m,children:a})}let b=(0,u.createContext)(h.IDLE_LINK_STATUS),S=()=>(0,u.useContext)(b);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},18581,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"useMergedRef",{enumerable:!0,get:function(){return o}});let n=e.r(71645);function o(e,t){let r=(0,n.useRef)(null),o=(0,n.useRef)(null);return(0,n.useCallback)(n=>{if(null===n){let e=r.current;e&&(r.current=null,e());let t=o.current;t&&(o.current=null,t())}else e&&(r.current=a(e,n)),t&&(o.current=a(t,n))},[e,t])}function a(e,t){if("function"!=typeof e)return e.current=t,()=>{e.current=null};{let r=e(t);return"function"==typeof r?r:()=>e(null)}}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},18967,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={DecodeError:function(){return m},MiddlewareNotFoundError:function(){return C},MissingStaticPage:function(){return v},NormalizeError:function(){return b},PageNotFoundError:function(){return S},SP:function(){return y},ST:function(){return g},WEB_VITALS:function(){return a},execOnce:function(){return i},getDisplayName:function(){return f},getLocationOrigin:function(){return l},getURL:function(){return c},isAbsoluteUrl:function(){return s},isResSent:function(){return d},loadGetInitialProps:function(){return h},normalizeRepeatedSlashes:function(){return p},stringifyError:function(){return P}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=["CLS","FCP","FID","INP","LCP","TTFB"];function i(e){let t,r=!1;return(...n)=>(r||(r=!0,t=e(...n)),t)}let u=/^[a-zA-Z][a-zA-Z\d+\-.]*?:/,s=e=>{let t=e.charCodeAt(0);return!!(t>=65&&t<=90||t>=97&&t<=122)&&u.test(e)};function l(){let{protocol:e,hostname:t,port:r}=window.location;return`${e}//${t}${r?":"+r:""}`}function c(){let{href:e}=window.location,t=l();return e.substring(t.length)}function f(e){return"string"==typeof e?e:e.displayName||e.name||"Unknown"}function d(e){return e.finished||e.headersSent}function p(e){let t=e.split("?");return t[0].replace(/\\/g,"/").replace(/\/\/+/g,"/")+(t[1]?`?${t.slice(1).join("?")}`:"")}async function h(e,t){let r=t.res||t.ctx&&t.ctx.res;if(!e.getInitialProps)return t.ctx&&t.Component?{pageProps:await h(t.Component,t.ctx)}:{};let n=await e.getInitialProps(t);if(r&&d(r))return n;if(!n)throw Object.defineProperty(Error(`"${f(e)}.getInitialProps()" should resolve to an object. But found "${n}" instead.`),"__NEXT_ERROR_CODE",{value:"E1025",enumerable:!1,configurable:!0});return n}let y="u">typeof performance,g=y&&["mark","measure","getEntriesByName"].every(e=>"function"==typeof performance[e]);class m extends Error{}class b extends Error{}class S extends Error{constructor(e){super(),this.code="ENOENT",this.name="PageNotFoundError",this.message=`Cannot find module for page: ${e}`}}class v extends Error{constructor(e,t){super(),this.message=`Failed to load static file for page: ${e} ${t}`}}class C extends Error{constructor(){super(),this.code="ENOENT",this.message="Cannot find the middleware module"}}function P(e){return JSON.stringify({message:e.message,stack:e.stack})}},73668,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"isLocalURL",{enumerable:!0,get:function(){return a}});let n=e.r(18967),o=e.r(52817);function a(e){if(!(0,n.isAbsoluteUrl)(e))return!0;try{let t=(0,n.getLocationOrigin)(),r=new URL(e,t);return r.origin===t&&(0,o.hasBasePath)(r.pathname)}catch(e){return!1}}},98183,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={assign:function(){return s},searchParamsToUrlQuery:function(){return a},urlQueryToSearchParams:function(){return u}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});function a(e){let t={};for(let[r,n]of e.entries()){let e=t[r];void 0===e?t[r]=n:Array.isArray(e)?e.push(n):t[r]=[e,n]}return t}function i(e){return"string"==typeof e?e:("number"!=typeof e||isNaN(e))&&"boolean"!=typeof e?"":String(e)}function u(e){let t=new URLSearchParams;for(let[r,n]of Object.entries(e))if(Array.isArray(n))for(let e of n)t.append(r,i(e));else t.set(r,i(n));return t}function s(e,...t){for(let r of t){for(let t of r.keys())e.delete(t);for(let[t,n]of r.entries())e.append(t,n)}return e}},95057,(e,t,r)=>{"use strict";e.i(47167),Object.defineProperty(r,"__esModule",{value:!0});var n={formatUrl:function(){return u},formatWithValidation:function(){return l},urlObjectKeys:function(){return s}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let a=e.r(90809)._(e.r(98183)),i=/https?|ftp|gopher|file/;function u(e){let{auth:t,hostname:r}=e,n=e.protocol||"",o=e.pathname||"",u=e.hash||"",s=e.query||"",l=!1;t=t?encodeURIComponent(t).replace(/%3A/i,":")+"@":"",e.host?l=t+e.host:r&&(l=t+(~r.indexOf(":")?`[${r}]`:r),e.port&&(l+=":"+e.port)),s&&"object"==typeof s&&(s=String(a.urlQueryToSearchParams(s)));let c=e.search||s&&`?${s}`||"";return n&&!n.endsWith(":")&&(n+=":"),e.slashes||(!n||i.test(n))&&!1!==l?(l="//"+(l||""),o&&"/"!==o[0]&&(o="/"+o)):l||(l=""),u&&"#"!==u[0]&&(u="#"+u),c&&"?"!==c[0]&&(c="?"+c),o=o.replace(/[?#]/g,encodeURIComponent),c=c.replace("#","%23"),`${n}${l}${o}${c}${u}`}let s=["auth","hash","host","hostname","href","path","pathname","port","protocol","query","search","slashes"];function l(e){return u(e)}}]);
@@ -6,3 +6,13 @@ then a map of every other document.
6
6
  Do not begin editing from this file, the README, or a guess. The README is written
7
7
  for a person deciding whether to run this; the brain is written for whoever has to
8
8
  change it.
9
+
10
+ <!-- BEGIN:nextjs-agent-rules -->
11
+
12
+ # This is NOT the Next.js you know
13
+
14
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
15
+
16
+ This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
17
+
18
+ <!-- END:nextjs-agent-rules -->
@@ -6,6 +6,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] - 2026-09-25
10
+
11
+ ### Added
12
+ - **Tutorial videos on the download website:** installing on a Mac, running it on Linux, and
13
+ how scanning works. They're short and silent, with the steps as captions.
14
+ - **`npx six-degrees` on Linux.** The npm package is published (0.2.0 was the first). The
15
+ README, the website and the installer's message now point Linux users to it, with how to
16
+ update it and where it keeps your data.
17
+
18
+ ### Fixed
19
+ - **Check for updates says what it found.** An up-to-date copy showed only a faint line, so
20
+ the button seemed to do nothing. The answer is now a clear status ("You're up to date: 0.2.1
21
+ is the newest version", with the time). The Mac app's **Check for Updates…** menu item opens
22
+ the Updates panel and runs the check, instead of just opening the top of the Scan page.
23
+ - **The profile page no longer starts scans.** Its "Set Up Account — Full Scan" and "Auto-Bridge
24
+ All Connections" cards are gone. The second mapped every connection in one go, with none of
25
+ the batch size, budget or warnings of the Scan page, where scanning lives.
26
+ - The window title is **Six Degrees** (it was "6 Degrees of Separation — LinkedIn Network
27
+ Research"), and the page description no longer claims to be a research project.
28
+ - The import page's "close the tab" note is true in the Mac app too.
29
+ - **Big LinkedIn exports import.** An export over about 9,000 connections was refused ("too large to hold in this browser tab"). The tab now keeps only what the export says about each person and scores it again on load, so exports up to LinkedIn's 30,000 maximum fit. Checked in a browser: 10,000 connections import and draw in about 4 seconds, 30,000 in about 25. It still never touches the database.
30
+ - **`npx six-degrees` downloads about 22 MB instead of about 235 MB.** The package already
31
+ carries its built server; Next, React and d3 are now build-time only.
32
+ - On Windows, npm now refuses the package with a clear "not supported" message instead of
33
+ installing it and crashing at start.
34
+ - On Linux without a desktop, `npx six-degrees` prints the address to open instead of a
35
+ crash when there's no browser to launch.
36
+ - The Scan page checks for Google Chrome on Linux too (where Playwright looks for it).
37
+ - Updating an npx copy says to stop it first, and keeps `--data-dir` if it was started
38
+ with one, so the new version finds your network.
39
+
40
+ ### Changed
41
+ - The app says **scanner** and **scan** everywhere you read it (the Scan page, the side
42
+ panel, the profile page, the import page); the code names are unchanged.
43
+ - Releases publish to npm through npm's trusted publishing: GitHub vouches for the release
44
+ workflow, so no npm token is stored anywhere. Each release builds the package on Linux,
45
+ installs it, checks it serves the app, and publishes that exact file.
46
+ - The README: macOS 15's second button is **Open Anyway**; the LinkedIn warning's link no
47
+ longer shows a file name; the release notes link the changelog.
48
+ - SECURITY.md lists all six gated routes (it said four).
49
+
9
50
  ## [0.2.0] - 2026-09-24
10
51
 
11
52
  ### Changed
@@ -80,6 +80,15 @@ python3 scripts/screenshots.py # writes docs/img/*.png
80
80
  It runs against the sample network only. If you change the UI in a way the
81
81
  README shows, regenerate rather than cropping a screenshot of your own data.
82
82
 
83
+ The README's download buttons are pictures too (a README can't style a link).
84
+ Redraw them after changing the icon or their wording; no server needed:
85
+
86
+ ```bash
87
+ python3 scripts/readme_buttons.py # writes docs/img/download-*.png
88
+ ```
89
+
90
+ Then look at the two pictures before committing them.
91
+
83
92
  ## Commit messages
84
93
 
85
94
  Explain **why**, not just what. A one-line subject, then a body if the change
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <a href="https://blakeb056.github.io/six-degrees/"><img src="desktop/icon/icon.svg" width="128" height="128" alt="Six Degrees website"></a>
3
+ </p>
4
+
1
5
  <h1 align="center">Six Degrees</h1>
2
6
 
3
7
  <p align="center">
@@ -5,32 +9,16 @@
5
9
  and find the shortest path to someone you haven't met.</em>
6
10
  </p>
7
11
 
8
- <p align="center">
9
- <a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Apple-Silicon.dmg"><img src="https://img.shields.io/badge/Download-Apple%20Silicon%20(M1%20or%20newer)-2ea44f?style=for-the-badge&logo=apple&logoColor=white" alt="Download for a Mac with Apple Silicon (M1 or newer)"></a>
10
- &nbsp;
11
- <a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg"><img src="https://img.shields.io/badge/Download-Intel%20Mac-555555?style=for-the-badge&logo=apple&logoColor=white" alt="Download for a Mac with an Intel processor"></a>
12
- </p>
12
+ ## Install it
13
13
 
14
- <p align="center"><sub>Free Mac app · macOS 13.5 or later · <a href="#install-it">how to install</a> · <a href="https://github.com/blakeb056/six-degrees/releases/latest">what's new</a></sub></p>
14
+ <p align="center"><strong>Click the button for your Mac to download the app.</strong></p>
15
15
 
16
16
  <p align="center">
17
- <img src="docs/img/app-window.png" alt="Six Degrees on a Mac: your network drawn as circles around you, the most powerful people closest" width="100%">
17
+ <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>
18
+ <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>
18
19
  </p>
19
20
 
20
- <p align="center"><sub>Every person shown in this README is invented — see <a href="scripts/gen-synthetic.mjs"><code>gen-synthetic.mjs</code></a>.</sub></p>
21
-
22
- ---
23
-
24
- Your network has a shape, and you can't see it. Six Degrees draws it: everyone you
25
- know placed on rings by how much they can open up for you, the handful of people
26
- whose own circles reach the furthest, and the shortest chain from you to a stranger
27
- worth meeting.
28
-
29
- It's a Mac app. It runs on your computer, against your own data, with no account and
30
- no server. (Inside the app the logo reads *6 Degrees*; in Applications it's **Six
31
- Degrees**.)
32
-
33
- ## Install it
21
+ <p align="center"><sub>Free Mac app · macOS 13.5 or later · <a href="https://blakeb056.github.io/six-degrees/">website</a> · <a href="https://github.com/blakeb056/six-degrees/releases/latest">what's new</a></sub></p>
34
22
 
35
23
  **Which download?** Apple menu → **About This Mac**. If it says "Chip: Apple M…", take
36
24
  **Apple Silicon**. If it says "Processor: …Intel…", take **Intel**. It needs **macOS 13.5
@@ -45,8 +33,8 @@ paid Apple certificate yet:
45
33
  malware. Close that message with **Done** (on macOS 13 or 14 it may say **OK** or
46
34
  **Cancel**). Anything except *Move to Trash*.
47
35
  3. Open **System Settings → Privacy & Security**, scroll down to *"Six Degrees" was
48
- blocked*, and click **Open Anyway**. Confirm with **Open** (and your password if
49
- asked).
36
+ blocked*, and click **Open Anyway**. Confirm with **Open Anyway** again (on macOS
37
+ 13 or 14 the button says **Open**), and enter your password if asked.
50
38
 
51
39
  That's once per download; after that it opens like any app.
52
40
 
@@ -72,11 +60,26 @@ Your network isn't stored in the app. It's in a hidden folder in your home folde
72
60
  touch it. Each new version also copies your data into its `backups` folder before it
73
61
  first opens it.
74
62
 
75
- **Linux:** run it from source (below); it's tested on Ubuntu.
76
- **Windows:** not yet. A Windows app is planned; nothing is released.
63
+ **Linux:** install **Node 22.13 or later** (from nodejs.org or nvm; Ubuntu's own
64
+ `nodejs` package is too old), then run:
65
+
66
+ ```bash
67
+ npx six-degrees
68
+ ```
69
+
70
+ It opens http://127.0.0.1:6363 in your browser (`--no-open` on a machine without a
71
+ desktop), keeps your data in `~/.six-degrees` (it prints the folder when it starts;
72
+ `--data-dir` puts it elsewhere), and stops with Ctrl-C. To update, stop it and run
73
+ `npx six-degrees@latest`; the Updates panel at the bottom of the Scan page gives the same
74
+ line. Scanning LinkedIn also needs Google Chrome (not Chromium) and Python 3.9+ with venv
75
+ (on Ubuntu: `sudo apt install python3-venv`). It's tested on Ubuntu, and works on a Mac
76
+ too if you'd rather not install the app.
77
+
78
+ **Windows:** not yet. There's no Windows app, and `npx six-degrees` doesn't run on
79
+ Windows yet; npm says so if you try.
77
80
 
78
81
  <details>
79
- <summary>Run it from source (Linux, or a Mac without the app)</summary>
82
+ <summary>Run it from source (contributors)</summary>
80
83
 
81
84
  Needs **Node 22.13 or later** and git.
82
85
 
@@ -93,6 +96,23 @@ To update later: stop it (Ctrl-C), then
93
96
  Scanning LinkedIn also needs Python 3.9+ and Google Chrome (see below).
94
97
  </details>
95
98
 
99
+ ## What it is
100
+
101
+ <p align="center">
102
+ <img src="docs/img/app-window.png" alt="Six Degrees on a Mac: your network drawn as circles around you, the most powerful people closest" width="100%">
103
+ </p>
104
+
105
+ <p align="center"><sub>Every person shown in this README is invented — see <a href="scripts/gen-synthetic.mjs"><code>gen-synthetic.mjs</code></a>.</sub></p>
106
+
107
+ Your network has a shape, and you can't see it. Six Degrees draws it: everyone you
108
+ know placed on rings by how much they can open up for you, the handful of people
109
+ whose own circles reach the furthest, and the shortest chain from you to a stranger
110
+ worth meeting.
111
+
112
+ It's a Mac app (or `npx six-degrees` on Linux). It runs on your computer, against your own data, with no account and
113
+ no server. (Inside the app the logo reads *6 Degrees*; in Applications it's **Six
114
+ Degrees**.)
115
+
96
116
  ## Before you scan LinkedIn
97
117
 
98
118
  > [!WARNING]
@@ -108,7 +128,7 @@ Scanning LinkedIn also needs Python 3.9+ and Google Chrome (see below).
108
128
  > stops at the first sign of push-back. The risk is still yours.
109
129
  >
110
130
  > **The CSV import and the sample network carry no LinkedIn risk at all.** Details:
111
- > [`docs/SCRAPING.md`](docs/SCRAPING.md).
131
+ > [how scanning works and what it risks](docs/SCRAPING.md).
112
132
 
113
133
  ## First run
114
134
 
@@ -116,7 +136,7 @@ The app opens on a welcome screen with three ways in, and asks nothing about you
116
136
 
117
137
  - **Scan my LinkedIn**: a guided page that ticks each step off as it goes. It sets up
118
138
  the scanner in one click, you sign into LinkedIn yourself in a Chrome window, and then
119
- it scans. Needs Google Chrome (in Applications) and Python 3.9+; the page checks that
139
+ it scans. Needs Google Chrome and Python 3.9+; the page checks that
120
140
  both are installed and says what's missing. The app marks this *Recommended* because
121
141
  it's the only way to Degrees and Outlink. Read the warning above first.
122
142
  - **Import my LinkedIn CSV**: LinkedIn's official export, read on your machine. On
@@ -251,7 +271,7 @@ Nothing to configure. Two optional environment variables exist:
251
271
 
252
272
  | Variable | Purpose |
253
273
  |---|---|
254
- | `SIX_DEGREES_HOME` | Where your data lives. Defaults to `~/.six-degrees`. It's read at launch, so it applies when you run from source, not when the Mac app is opened from the Dock or Finder. |
274
+ | `SIX_DEGREES_HOME` | Where your data lives. Defaults to `~/.six-degrees`. It's read at launch, so it applies to `npx six-degrees` and source runs (npx also takes `--data-dir`; see `npx six-degrees --help`), not when the Mac app is opened from the Dock or Finder. |
255
275
  | `ADMIN_TOKEN` | Not needed on your own computer. The app listens only on 127.0.0.1 and isn't built to be exposed: don't put it behind a tunnel or bind it to another address ([SECURITY.md](SECURITY.md)). If it's ever bound elsewhere, the six routes that delete data, run the scanner or update a copy run from source refuse every caller without this token, including the app's own buttons. Everything else, including reading your whole network, stays open. |
256
276
 
257
277
  ## Contributing
@@ -12,17 +12,20 @@ This app is designed to run **on your own machine, against your own network**.
12
12
 
13
13
  - The web UI binds to localhost. It is not hardened for exposure to the
14
14
  internet, and it has no multi-user authentication or per-user authorization.
15
- - Four API routes (`admin-delete`, `admin-update`, `delete-cluster`,
16
- `setup-profile`) can irreversibly destroy or rewrite data. On localhost they
17
- are allowed without a token — anyone who can reach 127.0.0.1 can already open
18
- the database file directly, so a token there adds friction rather than
19
- safety. Reached from any other host they require `ADMIN_TOKEN` as a bearer,
20
- and refuse to run when it is unset.
15
+ - Six API routes are gated (`admin-delete`, `admin-update`, `delete-cluster`,
16
+ `setup-profile`, `scraper`, `update`). The first four can irreversibly
17
+ destroy or rewrite data, `scraper` starts processes, and `update` changes the
18
+ code that runs next. While the server is bound to 127.0.0.1 they need no
19
+ token — anyone who can reach it can already open the database file directly,
20
+ so a token there adds friction rather than safety. If it is bound to any
21
+ other address, every caller must send `ADMIN_TOKEN` as a bearer, including
22
+ this machine and the app's own buttons, and the routes refuse to run when it
23
+ is unset.
21
24
  - A localhost-bound service is still reachable by any web page you visit — the
22
25
  binding keeps other machines out, not your own browser. Every mutating API
23
26
  request is therefore refused when the browser reports it came from another
24
27
  site (`Sec-Fetch-Site`, with `Origin` as a fallback). Command-line callers
25
- such as the scraper send neither header and are unaffected.
28
+ such as the scanner send neither header and are unaffected.
26
29
  - Values stored in the database are treated as untrusted text and escaped
27
30
  before rendering. Do not reintroduce `innerHTML` (including d3's `.html()`)
28
31
  for anything data-bearing.
@@ -34,10 +37,10 @@ Everything stays local:
34
37
 
35
38
  - A CSV import is parsed in the browser and held for that tab only. It is never
36
39
  uploaded and never written to a database.
37
- - Scraped data and cached avatars are written to your machine and are gitignored.
40
+ - Scanned data and cached avatars are written to your machine and are gitignored.
38
41
  - There is no telemetry, no analytics, and no crash reporting.
39
42
 
40
- If you run the scraper, `chrome-profile/` in the data directory holds a live
43
+ If you run the scanner, `chrome-profile/` in the data directory holds a live
41
44
  logged-in LinkedIn session. Never copy, sync, or commit it.
42
45
 
43
46
  Never commit `public/avatars/` or any exported CSV — they contain real people.
@@ -167,10 +167,11 @@ async function machineChecks() {
167
167
  const usable = await findUsablePython();
168
168
  const anyPython = usable || await findAnyPython();
169
169
 
170
+ // Where Playwright's "chrome" channel looks for Google Chrome (Chromium doesn't count).
170
171
  const chrome =
171
- process.platform === 'darwin'
172
- ? existsSync('/Applications/Google Chrome.app')
173
- : true; // elsewhere Playwright resolves the channel itself
172
+ process.platform === 'darwin' ? existsSync('/Applications/Google Chrome.app')
173
+ : process.platform === 'linux' ? existsSync('/opt/google/chrome/chrome')
174
+ : true; // elsewhere Playwright resolves the channel itself
174
175
 
175
176
  const signedIn = existsSync(path.join(dataDir(), 'chrome-profile', 'Default', 'Cookies'));
176
177
 
@@ -1,5 +1,7 @@
1
1
  import { spawn, execFileSync } from 'node:child_process';
2
- import { projectRoot, isGitCheckout } from '../../../lib/paths';
2
+ import { projectRoot, isGitCheckout, dataDir } from '../../../lib/paths';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
3
5
  import { repoSlug, compareVersions, installKind, updateCommand } from '../../../lib/release';
4
6
  import pkg from '../../../package.json';
5
7
 
@@ -86,6 +88,12 @@ async function localState(root) {
86
88
  // compares it with the one baked into this build. One GET to GitHub, only when
87
89
  // the button is pressed, carrying nothing about the user: the spec's invariant 2
88
90
  // names exactly this as permitted. Never call it from anything that runs on its own.
91
+ // The folder this copy keeps its network in, when it isn't the default one.
92
+ function customDataDir() {
93
+ const dir = dataDir();
94
+ return path.resolve(dir) === path.join(homedir(), '.six-degrees') ? null : dir;
95
+ }
96
+
89
97
  function installedInfo(root) {
90
98
  const slug = repoSlug(pkg.repository);
91
99
  const kind = installKind(process.env, root || '');
@@ -95,7 +103,7 @@ function installedInfo(root) {
95
103
  kind,
96
104
  version: pkg.version,
97
105
  slug,
98
- command: slug ? updateCommand(kind, slug) : null,
106
+ command: slug ? updateCommand(kind, slug, { dataDir: customDataDir() }) : null,
99
107
  };
100
108
  }
101
109
 
@@ -1006,7 +1006,7 @@ function renderDegreesMode(svg, width, height, allD1, degree2, onSelect, tierCol
1006
1006
  overlay.append('text').attr('x', ox).attr('y', oy-68).text('Connect to Unlock')
1007
1007
  .attr('fill', '#000').attr('font-size', 10).attr('font-weight', 700).attr('text-anchor', 'middle').style('pointer-events', 'none');
1008
1008
  } else {
1009
- overlay.append('text').attr('x', ox).attr('y', oy-82).text('Run scraper to check')
1009
+ overlay.append('text').attr('x', ox).attr('y', oy-82).text('Run a scan to check')
1010
1010
  .attr('fill', '#888').attr('font-size', 9).attr('text-anchor', 'middle');
1011
1011
  }
1012
1012
  event.stopPropagation();
@@ -304,7 +304,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
304
304
  </div>
305
305
  <div style={{ fontSize: 13, color: '#ccc', marginBottom: 12 }}>
306
306
  {selected.unlock_status === 'pending'
307
- ? `Waiting for ${selected.name} to accept. Run the scraper to check.`
307
+ ? `Waiting for ${selected.name} to accept. Run a scan to check.`
308
308
  : `Connect with ${selected.name} to unlock their network and extend your 6 degrees.`
309
309
  }
310
310
  </div>
@@ -949,7 +949,7 @@ function CreateClusterCard({ selected, degree2 }) {
949
949
 
950
950
  async function startScrape() {
951
951
  setStatus('checking');
952
- setLog(['Checking the scraper...']);
952
+ setLog(['Checking the scanner...']);
953
953
  const blocked = notReadyMessage(await scraperStatus().catch(() => null));
954
954
  if (blocked) {
955
955
  setStatus('offline');
@@ -970,7 +970,7 @@ function CreateClusterCard({ selected, degree2 }) {
970
970
  }
971
971
  } catch {
972
972
  setStatus('error');
973
- setLog(['Failed to start scrape']);
973
+ setLog(['Failed to start the scan']);
974
974
  }
975
975
  }
976
976
 
@@ -983,7 +983,7 @@ function CreateClusterCard({ selected, degree2 }) {
983
983
  CREATE CLUSTER
984
984
  </div>
985
985
  <div style={{ fontSize: 12, color: '#ccc', marginBottom: 12, textAlign: 'center' }}>
986
- Scrape {selected.name}&apos;s connections to map their network
986
+ Scan {selected.name}&apos;s connections to map their network
987
987
  </div>
988
988
 
989
989
  {status === 'idle' && (
@@ -1002,10 +1002,10 @@ function CreateClusterCard({ selected, degree2 }) {
1002
1002
  {status === 'offline' && (
1003
1003
  <>
1004
1004
  <div style={{ padding: '10px', borderRadius: 8, background: 'rgba(255,80,80,0.1)', border: '1px solid rgba(255,80,80,0.3)', color: '#ff5050', fontSize: 12, marginBottom: 8, textAlign: 'center' }}>
1005
- Scraper server offline
1005
+ Scanner offline
1006
1006
  </div>
1007
1007
  <div style={{ fontSize: 10, color: '#888', textAlign: 'center', marginBottom: 8 }}>
1008
- Open the <strong>Scan</strong> page to finish setting the scraper up
1008
+ Open the <strong>Scan</strong> page to finish setting the scanner up
1009
1009
  </div>
1010
1010
  <button onClick={() => { setStatus('idle'); }} style={{
1011
1011
  width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
@@ -1045,7 +1045,7 @@ function CreateClusterCard({ selected, degree2 }) {
1045
1045
  {status === 'error' && (
1046
1046
  <>
1047
1047
  <div style={{ padding: '10px', borderRadius: 8, background: 'rgba(255,80,80,0.1)', border: '1px solid rgba(255,80,80,0.3)', color: '#ff5050', fontSize: 12, textAlign: 'center', marginBottom: 8 }}>
1048
- Scrape failed — connections may be private
1048
+ Scan failed — connections may be private
1049
1049
  </div>
1050
1050
  <button onClick={() => { setStatus('idle'); setLog([]); }} style={{
1051
1051
  width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
@@ -1348,7 +1348,7 @@ function AutoBridgeButton({ connections, degree2 }) {
1348
1348
  {status === 'offline' && (
1349
1349
  <>
1350
1350
  <div style={{ padding: '8px', borderRadius: 6, background: 'rgba(255,80,80,0.1)', color: '#ff5050', fontSize: 11, marginBottom: 6, textAlign: 'center' }}>
1351
- Scraper offline
1351
+ Scanner offline
1352
1352
  </div>
1353
1353
  <button onClick={() => setStatus('idle')} style={{
1354
1354
  width: '100%', padding: '8px', borderRadius: 6, border: 'none', cursor: 'pointer',
@@ -4,7 +4,9 @@ import { useState, useEffect } from 'react';
4
4
 
5
5
  // Nothing here runs on its own. The spec forbids a silent update check, and
6
6
  // this respects that: the first network call happens when someone presses
7
- // "Check for updates". Loading the panel only reads the local git state.
7
+ // "Check for updates", here or in the Mac app's menu (which opens this page at
8
+ // /setup?check=updates — the same click, made from the menu). Loading the panel
9
+ // only reads the local state.
8
10
 
9
11
  const LINE = '1px solid rgba(255,255,255,0.1)';
10
12
 
@@ -77,7 +79,7 @@ export default function UpdatePanel() {
77
79
  You are on <Mono>{local.sha}</Mono> — “{local.subject}”.
78
80
  </Body>
79
81
 
80
- {result && result.behind === 0 && <Body>Up to date.</Body>}
82
+ {result && result.behind === 0 && <Status ok>You&rsquo;re up to date.</Status>}
81
83
 
82
84
  {result && result.behind > 0 && (
83
85
  <>
@@ -127,9 +129,22 @@ function InstalledUpdates({ local }) {
127
129
  const [result, setResult] = useState(null);
128
130
  const [error, setError] = useState(null);
129
131
  const [copied, setCopied] = useState(false);
132
+ const [checkedAt, setCheckedAt] = useState(null);
133
+
134
+ // Opened from the menu's "Check for Updates…": bring the panel into view and
135
+ // run the check that click asked for.
136
+ useEffect(() => {
137
+ const params = new URLSearchParams(window.location.search);
138
+ if (params.get('check') !== 'updates') return;
139
+ const panel = () => document.getElementById('updates');
140
+ panel()?.scrollIntoView({ block: 'center' });
141
+ check().then(() => panel()?.scrollIntoView({ block: 'end', behavior: 'smooth' }));
142
+ window.history.replaceState(null, '', window.location.pathname);
143
+ }, []);
130
144
 
131
145
  async function check() {
132
146
  setBusy(true);
147
+ setResult(null);
133
148
  setError(null);
134
149
  try {
135
150
  const r = await fetch('/api/update', {
@@ -139,7 +154,7 @@ function InstalledUpdates({ local }) {
139
154
  });
140
155
  const d = await r.json();
141
156
  if (!r.ok) setError(d.error || 'Could not check.');
142
- else setResult(d);
157
+ else { setResult(d); setCheckedAt(new Date()); }
143
158
  } catch (e) {
144
159
  setError(e.message);
145
160
  } finally {
@@ -161,15 +176,20 @@ function InstalledUpdates({ local }) {
161
176
  ? 'Paste this into Terminal. It closes this app, puts the new version in its place and opens it. Your network stays where it is.'
162
177
  : local.kind === 'source'
163
178
  ? 'Stop the app (Ctrl-C), then run this in the six-degrees folder. It fetches the newest code, builds it and starts it again. Your network stays where it is.'
164
- : 'Run this in a terminal to start the newest version. Your network stays where it is.';
179
+ : 'Stop this copy first (Ctrl-C in the terminal it runs in), then run this to start the newest version. Your network stays where it is.';
165
180
 
166
181
  return (
167
182
  <Wrap>
168
183
  <Title>Updates</Title>
169
184
  <Body>You have version <Mono>{local.version}</Mono>.</Body>
170
185
 
171
- {result && !result.latest && <Body>No release has been published yet.</Body>}
172
- {result?.latest && !result.newer && <Body>That is the newest version.</Body>}
186
+ {result && !result.latest && <Status>No release has been published yet.</Status>}
187
+ {result?.latest && !result.newer && (
188
+ <Status ok>
189
+ You&rsquo;re up to date: {result.latest.version} is the newest version.
190
+ {checkedAt && <span style={{ color: '#8b9a9a' }}> Checked at {checkedAt.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}.</span>}
191
+ </Status>
192
+ )}
173
193
 
174
194
  {result?.newer && (
175
195
  <>
@@ -190,7 +210,7 @@ function InstalledUpdates({ local }) {
190
210
 
191
211
  {!result?.newer && (
192
212
  <div style={{ marginTop: 12 }}>
193
- <Btn onClick={check} disabled={busy}>{busy ? 'Checking…' : 'Check for updates'}</Btn>
213
+ <Btn onClick={check} disabled={busy}>{busy ? 'Checking…' : result ? 'Check again' : 'Check for updates'}</Btn>
194
214
  </div>
195
215
  )}
196
216
 
@@ -213,7 +233,18 @@ const pre = {
213
233
 
214
234
  function Wrap({ children }) {
215
235
  return (
216
- <div style={{ marginTop: 32, paddingTop: 24, borderTop: LINE }}>{children}</div>
236
+ <div id="updates" style={{ marginTop: 32, paddingTop: 24, borderTop: LINE }}>{children}</div>
237
+ );
238
+ }
239
+ // The answer to a check, where it can't be missed.
240
+ function Status({ children, ok }) {
241
+ return (
242
+ <div role="status" style={{
243
+ marginTop: 10, padding: '10px 14px', borderRadius: 8, fontSize: 13.5, lineHeight: 1.6,
244
+ color: ok ? '#00ff88' : '#e8e8ee',
245
+ background: ok ? 'rgba(0,255,136,0.08)' : 'rgba(255,255,255,0.05)',
246
+ border: `1px solid ${ok ? 'rgba(0,255,136,0.3)' : 'rgba(255,255,255,0.12)'}`,
247
+ }}>{ok && '✓ '}{children}</div>
217
248
  );
218
249
  }
219
250
  function Title({ children }) {
@@ -27,7 +27,7 @@ export default function ImportPage() {
27
27
  try {
28
28
  const { connections, skipped } = parseConnectionsCsv(text);
29
29
  if (!saveCsvNetwork(connections)) {
30
- setError('That network is too large to hold in this browser tab. Try the local version for big exports.');
30
+ setError('That network is too large to hold in this window. LinkedIn allows up to 30,000 connections and this app holds that many, so please report it with your connection count: github.com/blakeb056/six-degrees/issues');
31
31
  setBusy(false);
32
32
  return;
33
33
  }
@@ -82,7 +82,7 @@ export default function ImportPage() {
82
82
 
83
83
  <Link href="/" style={primaryBtn}>See your galaxy →</Link>
84
84
  <p style={{ color: '#555', fontSize: 12, marginTop: 18, lineHeight: 1.6 }}>
85
- This stays in this browser tab only — nothing was uploaded or saved. Close the tab and it is gone.
85
+ Nothing was uploaded or saved: this lives only in this window (or browser tab), and closing it clears it.
86
86
  </p>
87
87
  </div>
88
88
  </div>
@@ -158,7 +158,7 @@ export default function ImportPage() {
158
158
  haven&rsquo;t met yet.
159
159
  </p>
160
160
  <p style={{ margin: 0, color: '#666', fontSize: 13, lineHeight: 1.7 }}>
161
- Want those? Run the local scraper — it captures 2nd-degree circles and real photos.{' '}
161
+ Want those? Run the local scanner — it captures 2nd-degree circles and real photos.{' '}
162
162
  <Link href="/setup" style={{ color: '#3498DB', textDecoration: 'none' }}>See setup →</Link>
163
163
  </p>
164
164
  </div>
@@ -12,8 +12,8 @@ const geistMono = Geist_Mono({
12
12
  });
13
13
 
14
14
  export const metadata = {
15
- title: "6 Degrees of Separation — LinkedIn Network Research",
16
- description: "Visualizing the six degrees of separation hypothesis through LinkedIn connection analysis. A certified research project mapping power networks.",
15
+ title: "Six Degrees",
16
+ description: "Maps your LinkedIn network: who you know, who they know, and the shortest path to someone you haven't met. Runs on your own computer; nothing is uploaded.",
17
17
  viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no',
18
18
  };
19
19
 
@@ -309,7 +309,7 @@ function HomeInner() {
309
309
  const status = await scraperStatus();
310
310
  const blocked = notReadyMessage(status);
311
311
  if (blocked) { alert(blocked); return; }
312
- if (status.running) { alert('The scraper is already busy.'); return; }
312
+ if (status.running) { alert('The scanner is already busy.'); return; }
313
313
  await startScrape('refresh');
314
314
  alert('Checking for new connections — watch it on the Scan page.');
315
315
  } catch (e) {
@@ -393,7 +393,7 @@ function HomeInner() {
393
393
  LinkedIn&rsquo;s CSV export only covers your own 1st-degree list, so there are no circles to open here.
394
394
  </p>
395
395
  <p style={{ color: '#666', fontSize: 13, lineHeight: 1.7, margin: 0 }}>
396
- The local scraper maps those circles (and captures real photos).{' '}
396
+ The local scanner maps those circles (and captures real photos).{' '}
397
397
  <Link href="/setup" style={{ color: '#3498DB', textDecoration: 'none' }}>Set up scanning &rarr;</Link>
398
398
  </p>
399
399
  </div>
@@ -120,7 +120,7 @@ function PathsInner() {
120
120
  try { localStorage.setItem('six-degrees-company-scan-ok', '1'); } catch {}
121
121
  }
122
122
  setScanning(true);
123
- setScanLog(['Checking the scraper...']);
123
+ setScanLog(['Checking the scanner...']);
124
124
 
125
125
  const blocked = notReadyMessage(await scraperStatus().catch(() => null));
126
126
  if (blocked) {