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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +11 -11
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jrmmj-._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qx548g._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1an3leo._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m8zedk._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +2 -2
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bf3x4q._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1djpk8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0jk64vo17avsb.js +1 -0
- package/.next/standalone/.next/static/chunks/0s65dsrthylo6.js +2 -0
- package/.next/standalone/.next/static/chunks/0uyfdhi8dek85.js +16 -0
- package/.next/standalone/.next/static/chunks/{1zmpcmngg5c7-.js → 229haorf0f19b.js} +1 -1
- package/.next/{static/chunks/3ts-_4i6bfkh2.js → standalone/.next/static/chunks/2g54uafnwuakc.js} +1 -1
- package/.next/standalone/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
- package/.next/{static/chunks/3pfyuezyqd9ob.js → standalone/.next/static/chunks/2spwqs6b6dx13.js} +1 -1
- package/.next/{static/chunks/1odow92u6wxz0.js → standalone/.next/static/chunks/38dlepuubiox0.js} +1 -1
- package/.next/standalone/AGENTS.md +10 -0
- package/.next/standalone/CHANGELOG.md +41 -0
- package/.next/standalone/CONTRIBUTING.md +9 -0
- package/.next/standalone/README.md +49 -29
- package/.next/standalone/SECURITY.md +12 -9
- package/.next/standalone/app/api/scraper/route.js +4 -3
- package/.next/standalone/app/api/update/route.js +10 -2
- package/.next/standalone/app/components/ForceGraph.js +1 -1
- package/.next/standalone/app/components/Sidebar.js +8 -8
- package/.next/standalone/app/components/UpdatePanel.js +39 -8
- package/.next/standalone/app/import/page.js +3 -3
- package/.next/standalone/app/layout.js +2 -2
- package/.next/standalone/app/page.js +2 -2
- package/.next/standalone/app/paths/page.js +1 -1
- package/.next/standalone/app/profile/page.js +0 -178
- package/.next/standalone/app/setup/page.js +4 -4
- package/.next/standalone/bin/six-degrees.mjs +7 -2
- package/.next/standalone/install.sh +1 -1
- package/.next/standalone/lib/csv.js +53 -27
- package/.next/standalone/lib/release.js +5 -2
- package/.next/standalone/lib/scraper-client.js +3 -3
- package/.next/standalone/package-lock.json +103 -9
- package/.next/standalone/package.json +12 -10
- package/.next/standalone/public/demo-data.json +1 -1
- package/.next/standalone/scripts/gen-synthetic.mjs +16 -1
- package/.next/standalone/scripts/readme_buttons.py +89 -0
- package/.next/standalone/scripts/screenshots.py +2 -0
- package/.next/standalone/site/app.js +84 -0
- package/.next/standalone/site/index.html +221 -0
- package/.next/standalone/site/style.css +155 -0
- package/.next/standalone/site/video/install-mac-poster.jpg +0 -0
- package/.next/standalone/site/video/install-mac.mp4 +0 -0
- package/.next/standalone/site/video/linux-poster.jpg +0 -0
- package/.next/standalone/site/video/linux.mp4 +0 -0
- package/.next/standalone/site/video/scan-poster.jpg +0 -0
- package/.next/standalone/site/video/scan.mp4 +0 -0
- package/.next/static/chunks/0jk64vo17avsb.js +1 -0
- package/.next/static/chunks/0s65dsrthylo6.js +2 -0
- package/.next/static/chunks/0uyfdhi8dek85.js +16 -0
- package/.next/static/chunks/{1zmpcmngg5c7-.js → 229haorf0f19b.js} +1 -1
- package/.next/{standalone/.next/static/chunks/3ts-_4i6bfkh2.js → static/chunks/2g54uafnwuakc.js} +1 -1
- package/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
- package/.next/{standalone/.next/static/chunks/3pfyuezyqd9ob.js → static/chunks/2spwqs6b6dx13.js} +1 -1
- package/.next/{standalone/.next/static/chunks/1odow92u6wxz0.js → static/chunks/38dlepuubiox0.js} +1 -1
- package/CHANGELOG.md +41 -0
- package/README.md +49 -29
- package/SECURITY.md +12 -9
- package/bin/six-degrees.mjs +7 -2
- package/lib/csv.js +53 -27
- package/lib/release.js +5 -2
- package/lib/scraper-client.js +3 -3
- package/package.json +12 -10
- package/public/demo-data.json +1 -1
- package/.next/standalone/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
- package/.next/standalone/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
- package/.next/standalone/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
- package/.next/standalone/.next/static/chunks/3hsi90cejq2dg.js +0 -1
- package/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
- package/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
- package/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
- package/.next/static/chunks/3hsi90cejq2dg.js +0 -1
- /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_ssgManifest.js +0 -0
- /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_buildManifest.js +0 -0
- /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → ZGTiDrZ5NHGETgGdlhssu}/_ssgManifest.js +0 -0
package/.next/{static/chunks/1odow92u6wxz0.js → standalone/.next/static/chunks/38dlepuubiox0.js}
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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"><
|
|
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/
|
|
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>
|
|
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** (
|
|
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:**
|
|
76
|
-
|
|
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 (
|
|
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
|
-
> [
|
|
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
|
|
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
|
|
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
|
-
-
|
|
16
|
-
`setup-profile`)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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('/
|
|
173
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
986
|
+
Scan {selected.name}'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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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 && <
|
|
82
|
+
{result && result.behind === 0 && <Status ok>You’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
|
-
: '
|
|
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 && <
|
|
172
|
-
{result?.latest && !result.newer &&
|
|
186
|
+
{result && !result.latest && <Status>No release has been published yet.</Status>}
|
|
187
|
+
{result?.latest && !result.newer && (
|
|
188
|
+
<Status ok>
|
|
189
|
+
You’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
|
|
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
|
-
|
|
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’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
|
|
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: "
|
|
16
|
-
description: "
|
|
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
|
|
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’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
|
|
396
|
+
The local scanner maps those circles (and captures real photos).{' '}
|
|
397
397
|
<Link href="/setup" style={{ color: '#3498DB', textDecoration: 'none' }}>Set up scanning →</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
|
|
123
|
+
setScanLog(['Checking the scanner...']);
|
|
124
124
|
|
|
125
125
|
const blocked = notReadyMessage(await scraperStatus().catch(() => null));
|
|
126
126
|
if (blocked) {
|