six-degrees 0.4.2 → 0.4.4
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/app-path-routes-manifest.json +3 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +41 -13
- package/.next/standalone/.next/routes-manifest.json +12 -0
- 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 +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 +19 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +19 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +15 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- 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/legacy/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/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/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 +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/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/social/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/route.js +8 -0
- package/.next/standalone/.next/server/app/api/social/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/route_client-reference-manifest.js +3 -0
- 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 +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 +23 -21
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +23 -21
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +21 -19
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +21 -19
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js +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 +23 -21
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +23 -21
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/page.js +1 -1
- 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 +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 +23 -21
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +23 -21
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/profile/page.js +1 -1
- 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 +23 -21
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +23 -21
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/queue/page.js +1 -1
- 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 +23 -21
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +23 -21
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/scores/page.js +1 -1
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +23 -21
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +23 -21
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/settings/page.js +1 -1
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +23 -21
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +23 -21
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/setup/page.js +1 -1
- 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 +23 -21
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +23 -21
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/social/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/social/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/social/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/social/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/social/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/social/page.js +13 -0
- package/.next/standalone/.next/server/app/social/page.js.map +5 -0
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/social.html +1 -0
- package/.next/standalone/.next/server/app/social.meta +26 -0
- package/.next/standalone/.next/server/app/social.rsc +26 -0
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +26 -0
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +30 -0
- package/.next/standalone/.next/server/app-paths-manifest.json +3 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17kn1mk._.js +8 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0uag-k5._.js → [root-of-the-server]__1dmc7-t._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +5 -5
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_route_actions_1_3c6qn.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16hs8kw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1feicqq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jeztd-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08_03wh._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0hw_qnj._.js → _0wxbq78._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1cyxocj._.js → _1ap1tu_._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1mj739k._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_social_page_actions_176i1kl.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +3 -0
- 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/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +4 -0
- 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/prefetch-hints.json +14 -0
- 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/04a3zockahetx.js +1 -0
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/standalone/.next/static/chunks/{2p6o90z0vgcwr.css → 0bo1dxn5gwzpk.css} +1 -1
- package/.next/standalone/.next/static/chunks/0g_p4p7oci18u.js +1 -0
- package/.next/standalone/.next/static/chunks/{0ax_9zzboyv6d.js → 17-rhp8sg4m1b.js} +2 -2
- package/.next/standalone/.next/static/chunks/1ra4cwcp75tfa.js +1 -0
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +1 -0
- package/.next/standalone/.next/static/chunks/24m745q_85u34.js +6 -0
- package/.next/standalone/.next/static/chunks/2_wcobfqry2uj.js +1 -0
- package/.next/standalone/.next/static/chunks/2dwtoszeq91us.js +1 -0
- package/.next/standalone/.next/static/chunks/2hvcevue9aro4.js +1 -0
- package/.next/standalone/.next/static/chunks/2uo9i03_05rqi.js +18 -0
- package/.next/standalone/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
- package/.next/standalone/.next/static/chunks/3yc3rzp4dxvyo.js +1 -0
- package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +33 -0
- package/.next/standalone/CHANGELOG.md +63 -0
- package/.next/standalone/app/api/network/route.js +11 -0
- package/.next/standalone/app/api/scraper/route.js +19 -0
- package/.next/standalone/app/api/social/route.js +96 -0
- package/.next/standalone/app/components/AutoScanButton.js +41 -0
- package/.next/standalone/app/components/ChainView.js +7 -0
- package/.next/standalone/app/components/FilterPanel.js +37 -5
- package/.next/standalone/app/components/OrbitGraph.js +17 -4
- package/.next/standalone/app/components/ScanStatusBar.js +96 -0
- package/.next/standalone/app/components/SeparationView.js +9 -1
- package/.next/standalone/app/components/Sidebar.js +88 -26
- package/.next/standalone/app/components/SocialAutoSync.js +34 -0
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +85 -0
- package/.next/standalone/app/globals.css +4 -0
- package/.next/standalone/app/layout.js +6 -0
- package/.next/standalone/app/page.js +37 -4
- package/.next/standalone/app/scores/page.js +3 -0
- package/.next/standalone/app/setup/page.js +13 -1
- package/.next/standalone/app/social/page.js +293 -0
- package/.next/standalone/lib/experimental-client.js +38 -0
- package/.next/standalone/lib/insights.js +54 -0
- package/.next/standalone/lib/linkedin-export.js +156 -0
- package/.next/standalone/lib/network-health.js +42 -0
- package/.next/standalone/lib/network.js +1 -0
- package/.next/standalone/lib/scraper-client.js +1 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +335 -5
- package/.next/static/chunks/04a3zockahetx.js +1 -0
- package/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/static/chunks/{2p6o90z0vgcwr.css → 0bo1dxn5gwzpk.css} +1 -1
- package/.next/static/chunks/0g_p4p7oci18u.js +1 -0
- package/.next/static/chunks/{0ax_9zzboyv6d.js → 17-rhp8sg4m1b.js} +2 -2
- package/.next/static/chunks/1ra4cwcp75tfa.js +1 -0
- package/.next/static/chunks/236f0jtp68j_h.js +1 -0
- package/.next/static/chunks/24m745q_85u34.js +6 -0
- package/.next/static/chunks/2_wcobfqry2uj.js +1 -0
- package/.next/static/chunks/2dwtoszeq91us.js +1 -0
- package/.next/static/chunks/2hvcevue9aro4.js +1 -0
- package/.next/static/chunks/2uo9i03_05rqi.js +18 -0
- package/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
- package/.next/static/chunks/3yc3rzp4dxvyo.js +1 -0
- package/.next/static/chunks/3zw8bv7-mc4n6.js +33 -0
- package/CHANGELOG.md +63 -0
- package/lib/experimental-client.js +38 -0
- package/lib/insights.js +54 -0
- package/lib/linkedin-export.js +156 -0
- package/lib/network-health.js +42 -0
- package/lib/network.js +1 -0
- package/lib/scraper-client.js +1 -0
- package/package.json +1 -1
- package/scripts/scrape.py +335 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19ip1qq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1f8t_rp._.js +0 -23
- package/.next/standalone/.next/static/chunks/049vavjsmqgmn.js +0 -33
- package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +0 -1
- package/.next/standalone/.next/static/chunks/0ljxa-wk_iu__.js +0 -18
- package/.next/standalone/.next/static/chunks/0mp9b72fm-jp6.js +0 -6
- package/.next/standalone/.next/static/chunks/1pre1cotntk6-.js +0 -1
- package/.next/standalone/.next/static/chunks/22x2_r768-u5i.js +0 -1
- package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +0 -1
- package/.next/standalone/.next/static/chunks/2jz311xl1pp_8.js +0 -1
- package/.next/standalone/.next/static/chunks/2ot61qsv6_0eq.js +0 -1
- package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +0 -1
- package/.next/standalone/.next/static/chunks/3zs_6-nn0dapb.js +0 -1
- package/.next/static/chunks/049vavjsmqgmn.js +0 -33
- package/.next/static/chunks/0cuuo2jwc9h94.js +0 -1
- package/.next/static/chunks/0ljxa-wk_iu__.js +0 -18
- package/.next/static/chunks/0mp9b72fm-jp6.js +0 -6
- package/.next/static/chunks/1pre1cotntk6-.js +0 -1
- package/.next/static/chunks/22x2_r768-u5i.js +0 -1
- package/.next/static/chunks/28bu26ausqzmn.js +0 -1
- package/.next/static/chunks/2jz311xl1pp_8.js +0 -1
- package/.next/static/chunks/2ot61qsv6_0eq.js +0 -1
- package/.next/static/chunks/2xjho0lroiwlq.js +0 -1
- package/.next/static/chunks/3zs_6-nn0dapb.js +0 -1
- /package/.next/standalone/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_ssgManifest.js +0 -0
- /package/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_buildManifest.js +0 -0
- /package/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// The scan status bar: while a scan runs, every page shows what it's doing,
|
|
4
|
+
// for whom, how far it's got, and today's LinkedIn budget, with Stop. Stop
|
|
5
|
+
// saves what was read, and the Scan page's Resume carries on from that page.
|
|
6
|
+
// It sits just under the header's tabs (Network Circle, Degrees…), on every
|
|
7
|
+
// page, the Scan page too (Blake, 2026-09-29). It only reports: the
|
|
8
|
+
// pacing and the caps live in the scanner (scripts/scrape.py, lib/linkedin-limits.js).
|
|
9
|
+
|
|
10
|
+
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
11
|
+
import Link from 'next/link';
|
|
12
|
+
import { watchScanner, scannerNow, stopScrape } from '../../lib/scraper-client';
|
|
13
|
+
import { watchAllDay, allDayNow } from '../../lib/experimental-client';
|
|
14
|
+
|
|
15
|
+
const WHAT = {
|
|
16
|
+
full: 'Scanning your network',
|
|
17
|
+
refresh: 'Checking for new connections',
|
|
18
|
+
bridge: 'Scanning a circle',
|
|
19
|
+
resume: 'Carrying on with a circle',
|
|
20
|
+
'resume-all': 'Carrying on with circles',
|
|
21
|
+
'auto-bridge': 'Auto-Bridge',
|
|
22
|
+
'auto-bridge-retry': 'Auto-Bridge',
|
|
23
|
+
rescrape: 'Re-scanning a circle',
|
|
24
|
+
login: 'Signing in to LinkedIn',
|
|
25
|
+
setup: 'Setting up the scanner',
|
|
26
|
+
company: 'Scanning a company',
|
|
27
|
+
photos: 'Saving photos',
|
|
28
|
+
messages: 'Reading your messages list',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Green while there's plenty left, amber past 60%, red past 90%.
|
|
32
|
+
const tone = (used, cap) => (!cap ? '#8b9a9a' : used / cap >= 0.9 ? '#ff6b6b' : used / cap >= 0.6 ? '#FFD700' : '#00ff88');
|
|
33
|
+
|
|
34
|
+
export default function ScanStatusBar() {
|
|
35
|
+
const [job, setJob] = useState(() => scannerNow());
|
|
36
|
+
const [stopping, setStopping] = useState(false);
|
|
37
|
+
// The Scan page's experimental all-day Auto-Bridge switch: with it on, the bar
|
|
38
|
+
// stays under the tabs even while nothing runs, so you can see the mode is on.
|
|
39
|
+
const allDay = useSyncExternalStore(watchAllDay, allDayNow, () => false);
|
|
40
|
+
useEffect(() => watchScanner(() => {
|
|
41
|
+
const now = scannerNow();
|
|
42
|
+
setJob(now);
|
|
43
|
+
if (!now.running) setStopping(false);
|
|
44
|
+
}), []);
|
|
45
|
+
const place = {
|
|
46
|
+
position: 'fixed', left: '50%', top: 'var(--scan-bar-top, 94px)', transform: 'translateX(-50%)', zIndex: 60,
|
|
47
|
+
maxWidth: 'calc(100vw - 32px)', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'nowrap', whiteSpace: 'nowrap',
|
|
48
|
+
padding: '8px 12px', borderRadius: 12, fontSize: 12, color: '#cfd8d8', background: 'rgba(14,16,32,0.94)',
|
|
49
|
+
};
|
|
50
|
+
if (!job?.running) {
|
|
51
|
+
if (!allDay) return null;
|
|
52
|
+
return (
|
|
53
|
+
<div role="status" style={{ ...place, opacity: 0.55, border: '1px solid rgba(255,255,255,0.1)' }}>
|
|
54
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#556', flexShrink: 0 }} />
|
|
55
|
+
<b style={{ color: '#b8c4c4' }}>Auto scan</b>
|
|
56
|
+
<span style={{ color: '#8b9a9a' }}>ready · press Auto scan beside Scan to start</span>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const p = job.progress;
|
|
62
|
+
const step = p?.kind === 'batch' && p.total ? `${p.current || p.done || 0} of ${p.total}`
|
|
63
|
+
: p?.kind === 'walk' && p.total ? `${(p.done || 0).toLocaleString()} of ${p.total.toLocaleString()} read`
|
|
64
|
+
: p?.kind === 'saving' ? 'saving' : null;
|
|
65
|
+
const b = job.budget;
|
|
66
|
+
const last = [...(job.log || [])].reverse().find((l) => l && l.trim())?.trim();
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div role="status" aria-live="polite" style={{ ...place, border: '1px solid rgba(0,255,136,0.25)' }}>
|
|
70
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#00ff88', flexShrink: 0 }} />
|
|
71
|
+
<b style={{ color: '#fff' }}>{WHAT[job.action] || 'Scanning'}</b>
|
|
72
|
+
{job.target?.name && <span>{job.target.name}</span>}
|
|
73
|
+
{step && <span style={{ color: '#8b9a9a' }}>{step}</span>}
|
|
74
|
+
{b && (
|
|
75
|
+
<span style={{ color: tone(b.searches, b.cap) }} title="Searches on your LinkedIn account in the last 24 hours, and your daily budget">
|
|
76
|
+
{b.searches}{b.cap ? ` of ${b.cap}` : ''} searches today
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
{last && (
|
|
80
|
+
<span style={{ color: '#667', flex: '0 1 240px', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={last}>
|
|
81
|
+
{last}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
<Link href="/setup" style={{ color: '#3498DB', textDecoration: 'none' }}>Details</Link>
|
|
85
|
+
<button
|
|
86
|
+
onClick={async () => { setStopping(true); try { await stopScrape(); } catch { setStopping(false); } }}
|
|
87
|
+
disabled={stopping}
|
|
88
|
+
title="Stops after saving what's been read; Resume on the Scan page carries on from the same page"
|
|
89
|
+
style={{
|
|
90
|
+
padding: '4px 10px', borderRadius: 8, fontSize: 12, cursor: stopping ? 'default' : 'pointer',
|
|
91
|
+
background: 'rgba(255,107,107,0.12)', color: '#ff9b9b', border: '1px solid rgba(255,107,107,0.35)',
|
|
92
|
+
}}
|
|
93
|
+
>{stopping ? 'Stopping…' : 'Stop'}</button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -66,7 +66,7 @@ function subline(person) {
|
|
|
66
66
|
return person.headline || person.role || person.company || '';
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export default function SeparationView({ connections = [], degree2 = [], fullDegree1 = connections, fullDegree2 = degree2, onSelect, userName, selectedId, tierColors = CLASSIC }) {
|
|
69
|
+
export default function SeparationView({ connections = [], degree2 = [], fullDegree1 = connections, fullDegree2 = degree2, onSelect, userName, selectedId, tierColors = CLASSIC, preset = null }) {
|
|
70
70
|
const isMobile = useIsMobile();
|
|
71
71
|
const ROW_H = isMobile ? 68 : 56;
|
|
72
72
|
const K = isMobile ? 5 : 10;
|
|
@@ -78,6 +78,14 @@ export default function SeparationView({ connections = [], degree2 = [], fullDeg
|
|
|
78
78
|
const [tier, setTier] = useState('all');
|
|
79
79
|
// Rarity beside the tier (lib/rarity.js): any bands switched on; none is everyone.
|
|
80
80
|
const [rarities, setRarities] = useState(() => new Set());
|
|
81
|
+
// Filters a card's Insights asked for ("Show them", "S only"): applied once each.
|
|
82
|
+
const [presetFor, setPresetFor] = useState(null);
|
|
83
|
+
if (preset && preset.id !== presetFor) {
|
|
84
|
+
setPresetFor(preset.id);
|
|
85
|
+
setQuery(preset.query || '');
|
|
86
|
+
setTier(preset.tier || 'all');
|
|
87
|
+
setRarities(new Set(preset.rarity ? [preset.rarity] : []));
|
|
88
|
+
}
|
|
81
89
|
const requests = useRequests();
|
|
82
90
|
const [scrollY, setScrollY] = useState(0);
|
|
83
91
|
const [box, setBox] = useState({ w: 0, h: 800 });
|
|
@@ -12,10 +12,11 @@ import { routeIndex, routesFor } from '../../lib/separation';
|
|
|
12
12
|
import { topCompanies } from '../../lib/scoring';
|
|
13
13
|
import { reachIndex, reachState, circleState, readyToScan, circleScanCost } from '../../lib/reach';
|
|
14
14
|
import { exclusiveReach, bridgeOverlap } from '../../lib/brokerage';
|
|
15
|
+
import { profileInsights } from '../../lib/insights';
|
|
15
16
|
import Avatar from './Avatar';
|
|
16
17
|
import { localPhoto } from '../../lib/photos';
|
|
17
18
|
|
|
18
|
-
export default function Sidebar({ selected, stats, tierColors, connections, degree2 = [], mode, collapsed, filter, pending = [], onToggle, onSelect, onSwitchMode, onFocusNode, onMarkSent, onUndoPending, scanNotes, csvSource = null }) {
|
|
19
|
+
export default function Sidebar({ selected, stats, tierColors, connections, degree2 = [], mode, collapsed, filter, pending = [], onToggle, onSelect, onSwitchMode, onFocusNode, onMarkSent, onUndoPending, scanNotes, csvSource = null, onOpenCircle, onShowInSeparation }) {
|
|
19
20
|
const isDegreesMode = mode === 'degrees';
|
|
20
21
|
// 'sample' or 'csv' while the sample or a CSV import is open in this window,
|
|
21
22
|
// null for your own network. Only your own can be scanned: the scanner looks
|
|
@@ -43,6 +44,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
43
44
|
// Who only each connection reaches (lib/brokerage.js), for their card.
|
|
44
45
|
const exclusive = useMemo(() => exclusiveReach(degree2, connections), [degree2, connections]);
|
|
45
46
|
const overlap = useMemo(() => bridgeOverlap(degree2, connections), [degree2, connections]);
|
|
47
|
+
const insightReach = useMemo(() => reachIndex(connections, degree2, scanNotes), [connections, degree2, scanNotes]);
|
|
48
|
+
// The Insights panel on a connection's card: open or closed, kept as you move between cards.
|
|
49
|
+
const [showInsights, setShowInsights] = useState(false);
|
|
46
50
|
const bridgeById = useMemo(() => new Map(connections.map(c => [c.id, c])), [connections]);
|
|
47
51
|
// Who you've asked, shared with every view: a request sent here shows in
|
|
48
52
|
// Separation, the circle and the Outlink queue at once (lib/requests-client.js).
|
|
@@ -143,33 +147,91 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
143
147
|
/>
|
|
144
148
|
)}
|
|
145
149
|
|
|
146
|
-
{selected.degree !== 2 && (
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
// A twin only when it's real: at least 15% of either's people, and 3 of them.
|
|
150
|
-
const twin = ov && ov.share >= 0.15 && ov.shared >= 3 && connections.find((c) => c.id === ov.with);
|
|
150
|
+
{selected.degree !== 2 && (() => {
|
|
151
|
+
const i = profileInsights(selected, { degree2, exclusive, overlap, reach: insightReach });
|
|
152
|
+
if (!i) return null;
|
|
151
153
|
const first = selected.name?.split(' ')[0] || 'them';
|
|
154
|
+
const twin = i.twin && connections.find((c) => c.id === i.twin.id);
|
|
155
|
+
const row = { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10, padding: '7px 0', borderTop: '1px solid rgba(255,255,255,0.05)' };
|
|
156
|
+
const label = { fontSize: 10.5, color: '#8b9a9a', minWidth: 92 };
|
|
157
|
+
const act = { background: 'none', border: 'none', padding: 0, color: '#3498DB', cursor: 'pointer', fontSize: 11.5, whiteSpace: 'nowrap' };
|
|
158
|
+
const scanned = i.bars == null ? 'Not scanned yet' : i.bars === 5 ? 'Their whole list' : `About ${i.bars * 20}% of their list`;
|
|
152
159
|
return (
|
|
153
|
-
<div style={{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
160
|
+
<div style={{ border: '1px solid rgba(52,152,219,0.25)', borderRadius: 8, marginBottom: 16, background: 'rgba(52,152,219,0.04)' }}>
|
|
161
|
+
<button onClick={() => setShowInsights((v) => !v)} aria-expanded={showInsights}
|
|
162
|
+
style={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '9px 12px', background: 'none', border: 'none', cursor: 'pointer', color: '#cfe6f7' }}>
|
|
163
|
+
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1 }}>INSIGHTS</span>
|
|
164
|
+
<span style={{ fontSize: 11, color: '#8b9a9a' }}>
|
|
165
|
+
{!showInsights && i.circle > 0 && `${i.circle.toLocaleString()} in their circle${i.only ? ` · ${i.only.toLocaleString()} only through ${first}` : ''} `}
|
|
166
|
+
{showInsights ? '▲' : '▼'}
|
|
167
|
+
</span>
|
|
168
|
+
</button>
|
|
169
|
+
{showInsights && (
|
|
170
|
+
<div style={{ padding: '0 12px 8px', fontSize: 12, color: '#ccc' }}>
|
|
171
|
+
<div style={row}>
|
|
172
|
+
<span style={label}>Their circle</span>
|
|
173
|
+
<span style={{ flex: 1 }}>{i.circle ? `${i.circle.toLocaleString()} people` : 'None scanned yet'}</span>
|
|
174
|
+
{i.circle > 0 && onOpenCircle && <button style={act} onClick={() => onOpenCircle(selected.id)}>Explore →</button>}
|
|
175
|
+
</div>
|
|
176
|
+
<div style={row}>
|
|
177
|
+
<span style={label}>Only through {first}</span>
|
|
178
|
+
<span style={{ flex: 1 }}>
|
|
179
|
+
{i.circle ? <><b style={{ color: '#00ff88' }}>{i.only.toLocaleString()}</b> ({Math.round(i.onlyShare * 100)}%): none of your other connections reach them</> : '—'}
|
|
180
|
+
</span>
|
|
181
|
+
{i.only > 0 && onShowInSeparation && (
|
|
182
|
+
<button style={act} onClick={() => onShowInSeparation({ query: selected.name, rarity: 'only' })}>Show them →</button>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
<div style={row}>
|
|
186
|
+
<span style={label}>Closest overlap</span>
|
|
187
|
+
<span style={{ flex: 1 }}>
|
|
188
|
+
{twin ? <>Opens the same doors as <b style={{ color: '#fff' }}>{twin.name}</b> ({Math.round(i.twin.share * 100)}%, {i.twin.shared.toLocaleString()} people)</> : 'No one opens the same doors'}
|
|
189
|
+
</span>
|
|
190
|
+
{twin && <button style={act} onClick={() => onSelect?.(twin)}>Open →</button>}
|
|
191
|
+
</div>
|
|
192
|
+
<div style={row}>
|
|
193
|
+
<span style={label}>Tier mix</span>
|
|
194
|
+
<span style={{ flex: 1, display: 'flex', height: 8, borderRadius: 4, overflow: 'hidden', background: 'rgba(255,255,255,0.05)', alignSelf: 'center' }}>
|
|
195
|
+
{i.circle > 0 && ['S', 'A', 'B', 'C', 'D'].map((t) => i.mix[t] > 0 && (
|
|
196
|
+
<span key={t} title={`${t}: ${i.mix[t]}`} style={{ width: `${(100 * i.mix[t]) / i.circle}%`, background: tierColors[t] }} />
|
|
197
|
+
))}
|
|
198
|
+
</span>
|
|
199
|
+
<span style={{ fontSize: 10.5, color: '#8b9a9a' }}>{i.circle ? `${i.mix.S} S · ${i.mix.A} A` : ''}</span>
|
|
200
|
+
{i.mix.S > 0 && onShowInSeparation && (
|
|
201
|
+
<button style={act} onClick={() => onShowInSeparation({ query: selected.name, tier: 'S' })}>S only →</button>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
{i.companies.length > 0 && (
|
|
205
|
+
<div style={row}>
|
|
206
|
+
<span style={label}>Where they work</span>
|
|
207
|
+
<span style={{ flex: 1 }}>{i.companies.map((c) => `${c.count} at ${c.name}`).join(' · ')}</span>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
<div style={row}>
|
|
211
|
+
<span style={label}>Scanned</span>
|
|
212
|
+
<span style={{ flex: 1 }}>{scanned}</span>
|
|
213
|
+
{i.bars !== 5 && canScan && (
|
|
214
|
+
<a href={`/setup?scan=${encodeURIComponent(selected.id)}`} style={act}>{i.bars == null ? 'Scan →' : 'Finish →'}</a>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
{(() => {
|
|
218
|
+
const since = selected.connected_date ? new Date(selected.connected_date) : null;
|
|
219
|
+
const asked = (bridgeMap[selected.id] || []).filter((p) => hasRequest(p, requests)).length;
|
|
220
|
+
if (!(since && !Number.isNaN(since.getTime())) && !asked) return null;
|
|
221
|
+
return (
|
|
222
|
+
<div style={row}>
|
|
223
|
+
<span style={label}>You and {first}</span>
|
|
224
|
+
<span style={{ flex: 1 }}>
|
|
225
|
+
{since && !Number.isNaN(since.getTime()) ? `Connected ${since.toLocaleDateString(undefined, { month: 'short', year: 'numeric' })}` : ''}
|
|
226
|
+
{asked ? `${since ? ' · ' : ''}${asked} request${asked === 1 ? '' : 's'} out through them` : ''}
|
|
227
|
+
</span>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
})()}
|
|
231
|
+
<div style={row}>
|
|
232
|
+
<span style={label}>Rank</span>
|
|
233
|
+
<span style={{ flex: 1 }}>{i.rank ? `#${i.rank} of ${i.of} by who only they reach` : 'Ranked once their circle is scanned'}</span>
|
|
234
|
+
</div>
|
|
173
235
|
</div>
|
|
174
236
|
)}
|
|
175
237
|
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// The Social tab's "once a day" live messages sync (experimental): once per
|
|
4
|
+
// app session, if it's switched on, the last sync is over a day old, it's
|
|
5
|
+
// daytime and the scanner is free, read the messages list (scripts/scrape.py
|
|
6
|
+
// --messages). Nothing else starts it.
|
|
7
|
+
|
|
8
|
+
import { useEffect } from 'react';
|
|
9
|
+
import { watchScanner, scannerNow, beginScrape } from '../../lib/scraper-client';
|
|
10
|
+
import { IS_DEMO } from '../../lib/demo';
|
|
11
|
+
|
|
12
|
+
let tried = false;
|
|
13
|
+
|
|
14
|
+
export default function SocialAutoSync() {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (IS_DEMO || tried) return undefined;
|
|
17
|
+
let stop = () => {};
|
|
18
|
+
fetch('/api/social').then((r) => r.json()).then(({ social }) => {
|
|
19
|
+
if (!social?.autoSync || tried) return;
|
|
20
|
+
const last = social.liveAt ? Date.parse(social.liveAt) : 0;
|
|
21
|
+
const hour = new Date().getHours();
|
|
22
|
+
if (Date.now() - last < 24 * 3600 * 1000 || hour < 9 || hour >= 19) return;
|
|
23
|
+
stop = watchScanner(() => {
|
|
24
|
+
const now = scannerNow();
|
|
25
|
+
if (!now.known || tried) return;
|
|
26
|
+
tried = true;
|
|
27
|
+
stop();
|
|
28
|
+
if (!now.running) beginScrape('messages').catch(() => {});
|
|
29
|
+
});
|
|
30
|
+
}).catch(() => {});
|
|
31
|
+
return () => stop();
|
|
32
|
+
}, []);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Network health on the Scores tab (lib/network-health.js): how much of your
|
|
4
|
+
// 2nd degree you reach two or more ways, the effective size of your own
|
|
5
|
+
// network from the ties scans have kept, and your top five connections by who
|
|
6
|
+
// only they reach. Only your own numbers, never a percentile against others.
|
|
7
|
+
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import Link from 'next/link';
|
|
10
|
+
import { useUser } from '../UserProvider';
|
|
11
|
+
import { loadNetwork } from '../../../lib/network';
|
|
12
|
+
import { networkHealth } from '../../../lib/network-health';
|
|
13
|
+
import { Body, LINE } from '../ui';
|
|
14
|
+
|
|
15
|
+
const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
|
|
16
|
+
const pct = (x) => `${Math.round(x * 100)}%`;
|
|
17
|
+
|
|
18
|
+
export default function NetworkHealthSection() {
|
|
19
|
+
const { userId } = useUser();
|
|
20
|
+
const [health, setHealth] = useState(null);
|
|
21
|
+
const [failed, setFailed] = useState(false);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!userId) return undefined;
|
|
24
|
+
let live = true;
|
|
25
|
+
loadNetwork(userId)
|
|
26
|
+
.then((net) => { if (live) setHealth(networkHealth(net)); })
|
|
27
|
+
.catch(() => { if (live) setFailed(true); });
|
|
28
|
+
return () => { live = false; };
|
|
29
|
+
}, [userId]);
|
|
30
|
+
|
|
31
|
+
const box = { padding: '12px 14px', borderRadius: 8, border: LINE, background: 'rgba(255,255,255,0.03)' };
|
|
32
|
+
return (
|
|
33
|
+
<section id="health" style={{ marginTop: 20 }}>
|
|
34
|
+
<h2 style={{ fontSize: 16, margin: '0 0 6px' }}>Network health</h2>
|
|
35
|
+
{failed && <Body>Couldn’t read your network just now.</Body>}
|
|
36
|
+
{!failed && !health && <Body>Reading your network…</Body>}
|
|
37
|
+
{health && health.circles === 0 && (
|
|
38
|
+
<Body>Scan a few of your connections’ circles (the Scan page, step 4) and this shows how your network holds together.</Body>
|
|
39
|
+
)}
|
|
40
|
+
{health && health.circles > 0 && (
|
|
41
|
+
<>
|
|
42
|
+
{health.early && (
|
|
43
|
+
<div style={{ fontSize: 12.5, color: '#FFD700', margin: '4px 0 10px' }}>
|
|
44
|
+
An early estimate: {health.circles} {health.circles === 1 ? 'circle' : 'circles'} scanned. These get sharper as you scan more.
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 }}>
|
|
48
|
+
<div style={box}>
|
|
49
|
+
<div style={{ fontSize: 22, fontWeight: 700 }}>{pct(health.twoWays)}</div>
|
|
50
|
+
<div style={{ fontSize: 12.5, color: '#b8c4c4' }}>of your 2nd degree you reach two or more ways</div>
|
|
51
|
+
<div style={{ fontSize: 11.5, color: '#778', marginTop: 4 }}>Higher is safer: lose one connection and they’re still reachable.</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div style={box}>
|
|
54
|
+
<div style={{ fontSize: 22, fontWeight: 700 }}>{Math.round(health.effective.size).toLocaleString()}</div>
|
|
55
|
+
<div style={{ fontSize: 12.5, color: '#b8c4c4' }}>
|
|
56
|
+
effective reach of your {health.effective.n.toLocaleString()} connections ({pct(health.effective.efficiency)} non-overlapping)
|
|
57
|
+
</div>
|
|
58
|
+
<div style={{ fontSize: 11.5, color: '#778', marginTop: 4 }}>
|
|
59
|
+
From {health.effective.ties.toLocaleString()} {health.effective.ties === 1 ? 'tie' : 'ties'} between your connections seen in circle scans so far,
|
|
60
|
+
so it can only come down as you scan more.
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
{health.top.length > 0 && (
|
|
65
|
+
<div style={{ ...box, marginTop: 10, padding: 0 }}>
|
|
66
|
+
<div style={{ padding: '10px 14px', fontSize: 12.5, color: '#b8c4c4', borderBottom: LINE }}>Your connections who reach the most people no one else does</div>
|
|
67
|
+
{health.top.map((b, k) => (
|
|
68
|
+
<div key={b.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', borderBottom: k < health.top.length - 1 ? '1px solid rgba(255,255,255,0.04)' : 'none', fontSize: 13 }}>
|
|
69
|
+
<span style={{ width: 22, color: '#778' }}>#{k + 1}</span>
|
|
70
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: TIER[b.tier] || '#667', flexShrink: 0 }} />
|
|
71
|
+
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
72
|
+
<b>{b.name}</b>{b.company ? <span style={{ color: '#8b9a9a' }}> · {b.company}</span> : null}
|
|
73
|
+
</span>
|
|
74
|
+
<span style={{ color: '#b8c4c4', whiteSpace: 'nowrap' }}>{b.circle.toLocaleString()} in circle</span>
|
|
75
|
+
<span style={{ color: '#00ff88', whiteSpace: 'nowrap' }}>{b.only.toLocaleString()} only them ({pct(b.share)})</span>
|
|
76
|
+
<Link href={`/?chain=${encodeURIComponent(b.id)}`} style={{ color: '#3498DB', textDecoration: 'none', whiteSpace: 'nowrap' }}>Explore →</Link>
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
83
|
+
</section>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -38,3 +38,7 @@ html:has([data-field-question]) {
|
|
|
38
38
|
background: rgba(255, 255, 255, 0.15);
|
|
39
39
|
border-radius: 3px;
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/* Where the scan status bar sits: just under the header (app/components/ScanStatusBar.js). */
|
|
43
|
+
:root { --scan-bar-top: 94px; }
|
|
44
|
+
@media (max-width: 767px) { :root { --scan-bar-top: 66px; } }
|
|
@@ -19,6 +19,8 @@ export const metadata = {
|
|
|
19
19
|
|
|
20
20
|
import UserProvider from './components/UserProvider';
|
|
21
21
|
import StaleServerBanner from './components/StaleServerBanner';
|
|
22
|
+
import ScanStatusBar from './components/ScanStatusBar';
|
|
23
|
+
import SocialAutoSync from './components/SocialAutoSync';
|
|
22
24
|
|
|
23
25
|
export default function RootLayout({ children }) {
|
|
24
26
|
return (
|
|
@@ -30,6 +32,10 @@ export default function RootLayout({ children }) {
|
|
|
30
32
|
is running. Every page, because the answer to "did my update
|
|
31
33
|
work?" should not require finding the right screen. */}
|
|
32
34
|
<StaleServerBanner />
|
|
35
|
+
{/* While a scan runs: what it's doing and today's budget, with Stop. */}
|
|
36
|
+
<ScanStatusBar />
|
|
37
|
+
{/* The Social tab's once-a-day messages sync, when switched on. */}
|
|
38
|
+
<SocialAutoSync />
|
|
33
39
|
</UserProvider>
|
|
34
40
|
</body>
|
|
35
41
|
</html>
|
|
@@ -8,6 +8,7 @@ import useRequests from './components/useRequests';
|
|
|
8
8
|
import { requestCount } from '../lib/requests-client';
|
|
9
9
|
import { loadNetwork } from '../lib/network';
|
|
10
10
|
import { VIEWS, resolveView } from './components/views';
|
|
11
|
+
import AutoScanButton from './components/AutoScanButton';
|
|
11
12
|
import { peopleByDegree, tierCountsOf } from '../lib/degrees';
|
|
12
13
|
import Sidebar from './components/Sidebar';
|
|
13
14
|
import FilterPanel from './components/FilterPanel';
|
|
@@ -73,6 +74,8 @@ function HomeInner() {
|
|
|
73
74
|
const [degree3, setDegree3] = useState([]);
|
|
74
75
|
// Which degrees Network Circle draws: your connections alone until you pick more.
|
|
75
76
|
const [degrees, setDegrees] = useState([1]);
|
|
77
|
+
// Tiers switched off in Network Circle's Filter panel (C and D make a lot of noise).
|
|
78
|
+
const [hiddenTiers, setHiddenTiers] = useState([]);
|
|
76
79
|
const [selected, setSelected] = useState(null);
|
|
77
80
|
const focusNodeRef = useRef(null);
|
|
78
81
|
const [filter, setFilter] = useState('all');
|
|
@@ -151,6 +154,19 @@ function HomeInner() {
|
|
|
151
154
|
|
|
152
155
|
// Followed once: Bridge Chains has opened it, so leaving that view and coming
|
|
153
156
|
// back, or reloading, starts from the overview.
|
|
157
|
+
// A card's Insights → Separation, filtered to that person's circle ("Show them", "S only").
|
|
158
|
+
const [separationPreset, setSeparationPreset] = useState(null);
|
|
159
|
+
const showInSeparation = useCallback((preset) => {
|
|
160
|
+
setSeparationPreset({ ...preset, id: `${preset.query}|${preset.tier || ''}|${preset.rarity || ''}|${Math.random()}` });
|
|
161
|
+
setMode('degrees');
|
|
162
|
+
setVisualMode('separation');
|
|
163
|
+
}, []);
|
|
164
|
+
// A card's Insights → that person's circle in Bridge Chains.
|
|
165
|
+
const openCircle = useCallback((id) => {
|
|
166
|
+
setChainOpen(id);
|
|
167
|
+
setMode('degrees');
|
|
168
|
+
setVisualMode('chain');
|
|
169
|
+
}, []);
|
|
154
170
|
const chainOpened = useCallback(() => {
|
|
155
171
|
setChainOpen(null);
|
|
156
172
|
try { window.history.replaceState(null, '', window.location.pathname); } catch { /* the link stays */ }
|
|
@@ -170,10 +186,10 @@ function HomeInner() {
|
|
|
170
186
|
() => (isDegreesMode ? connections : degrees.flatMap((d) => byDegree[d] || [])),
|
|
171
187
|
[isDegreesMode, connections, degrees, byDegree],
|
|
172
188
|
);
|
|
173
|
-
const filtered = useMemo(
|
|
174
|
-
()
|
|
175
|
-
|
|
176
|
-
);
|
|
189
|
+
const filtered = useMemo(() => {
|
|
190
|
+
if (!isDegreesMode) return hiddenTiers.length ? networkRows.filter(c => !hiddenTiers.includes(c.tier)) : networkRows;
|
|
191
|
+
return filter === 'all' ? networkRows : networkRows.filter(c => c.tier === filter);
|
|
192
|
+
}, [isDegreesMode, networkRows, filter, hiddenTiers]);
|
|
177
193
|
// Lookups built once per load. Every click re-renders this component, and the
|
|
178
194
|
// three places below used to scan one list inside another — degree1.find per
|
|
179
195
|
// 2nd-degree row, degree2.some per connection — about 16M comparisons, twice
|
|
@@ -282,6 +298,17 @@ function HomeInner() {
|
|
|
282
298
|
>
|
|
283
299
|
Paths
|
|
284
300
|
</Link>}
|
|
301
|
+
{!IS_DEMO && <Link
|
|
302
|
+
href="/social"
|
|
303
|
+
title="Experimental: what your own LinkedIn data says about your relationships"
|
|
304
|
+
style={{
|
|
305
|
+
padding: isMobile ? '6px 10px' : '8px 16px', borderRadius: 6, border: 'none', fontSize: isMobile ? 11 : 13, fontWeight: 600,
|
|
306
|
+
background: 'rgba(255,255,255,0.06)', color: '#FFD700', textDecoration: 'none',
|
|
307
|
+
display: 'flex', alignItems: 'center', gap: 4,
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
Social<span style={{ fontSize: 9, opacity: 0.7, marginLeft: 2 }}>beta</span>
|
|
311
|
+
</Link>}
|
|
285
312
|
{!IS_DEMO && !csvMode && <Link
|
|
286
313
|
href="/scores"
|
|
287
314
|
style={{
|
|
@@ -316,6 +343,7 @@ function HomeInner() {
|
|
|
316
343
|
>
|
|
317
344
|
Scan
|
|
318
345
|
</Link>}
|
|
346
|
+
{!IS_DEMO && !csvMode && <AutoScanButton isMobile={isMobile} />}
|
|
319
347
|
</div>
|
|
320
348
|
{/* Settings — also in CSV/sample mode and on phones, so updates stay reachable */}
|
|
321
349
|
{!IS_DEMO && <Link
|
|
@@ -438,6 +466,8 @@ function HomeInner() {
|
|
|
438
466
|
tierCounts={isDegreesMode ? stats?.tiers || {} : tierCountsOf(networkRows)}
|
|
439
467
|
degrees={degrees}
|
|
440
468
|
onDegreesChange={setDegrees}
|
|
469
|
+
hiddenTiers={hiddenTiers}
|
|
470
|
+
onHiddenTiersChange={setHiddenTiers}
|
|
441
471
|
degreeCounts={{ 1: byDegree[1].length, 2: byDegree[2].length, 3: byDegree[3].length }}
|
|
442
472
|
bridgeTierCounts={bridgeTierCounts}
|
|
443
473
|
/>
|
|
@@ -495,6 +525,7 @@ function HomeInner() {
|
|
|
495
525
|
canScan,
|
|
496
526
|
chainOpen,
|
|
497
527
|
onChainOpened: chainOpened,
|
|
528
|
+
preset: separationPreset,
|
|
498
529
|
};
|
|
499
530
|
return <View {...viewProps} />;
|
|
500
531
|
})()}
|
|
@@ -510,6 +541,8 @@ function HomeInner() {
|
|
|
510
541
|
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
511
542
|
onSelect={(node) => { setSelected(node || null); }}
|
|
512
543
|
onSwitchMode={(newMode) => { setMode(newMode); setFilter('all'); }}
|
|
544
|
+
onOpenCircle={openCircle}
|
|
545
|
+
onShowInSeparation={showInSeparation}
|
|
513
546
|
onFocusNode={(nodeId) => { if (focusNodeRef.current) focusNodeRef.current(nodeId); }}
|
|
514
547
|
csvSource={IS_DEMO ? 'sample' : csvMode ? csvSource : null}
|
|
515
548
|
scanNotes={scanNotes}
|
|
@@ -12,6 +12,7 @@ import Link from 'next/link';
|
|
|
12
12
|
import OnboardingGate from '../components/OnboardingGate';
|
|
13
13
|
import SectorSection from '../components/settings/SectorSection';
|
|
14
14
|
import TierSection from '../components/settings/TierSection';
|
|
15
|
+
import NetworkHealthSection from '../components/settings/NetworkHealthSection';
|
|
15
16
|
import CompanyScores from '../components/CompanyScores';
|
|
16
17
|
import { Body, LINE, FONT } from '../components/ui';
|
|
17
18
|
import { IS_DEMO } from '../../lib/demo';
|
|
@@ -60,7 +61,9 @@ function ScoresInner() {
|
|
|
60
61
|
How someone’s power score is worked out, and the three things you can change about it:{' '}
|
|
61
62
|
<a href="#sector" style={jump}>your field</a>, <a href="#tiers" style={jump}>how tiers are graded</a> and{' '}
|
|
62
63
|
<a href="#companies" style={jump}>any company’s score</a>. A change here rescores everyone.
|
|
64
|
+
{' '}First, <a href="#health" style={jump}>how your network holds together</a>.
|
|
63
65
|
</Body>
|
|
66
|
+
<NetworkHealthSection />
|
|
64
67
|
<SectorSection onSaved={saved} />
|
|
65
68
|
<TierSection onSaved={saved} />
|
|
66
69
|
<div id="companies" style={{ marginTop: 28 }}>
|
|
@@ -65,6 +65,8 @@ function SetupInner() {
|
|
|
65
65
|
const [pages, setPages] = useRemembered('six-degrees-bridge-pages-v2', 100);
|
|
66
66
|
// Also finish people mapped before, from the page each one's read stopped at.
|
|
67
67
|
const [finish, setFinish] = useRemembered('six-degrees-bridge-finish', true);
|
|
68
|
+
// Experimental Auto-Bridge: all-day pacing and LinkedIn's own data (scripts/scrape.py --experimental).
|
|
69
|
+
const [experimental, setExperimental] = useRemembered('six-degrees-experimental-auto', false);
|
|
68
70
|
const [error, setError] = useState(null);
|
|
69
71
|
const logRef = useRef(null);
|
|
70
72
|
// What's saved, for the question about your field: undefined while it
|
|
@@ -440,6 +442,16 @@ function SetupInner() {
|
|
|
440
442
|
<input type="checkbox" checked={finish} onChange={(e) => setFinish(e.target.checked)} disabled={running} />
|
|
441
443
|
Also finish people already mapped, from the page each one stopped at
|
|
442
444
|
</label>
|
|
445
|
+
<label style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12.5, color: '#b8c4c4', cursor: running ? 'default' : 'pointer' }}>
|
|
446
|
+
<input type="checkbox" checked={experimental} disabled={running} style={{ marginTop: 3 }}
|
|
447
|
+
onChange={(e) => { setExperimental(e.target.checked); window.dispatchEvent(new Event('six-degrees:experimental')); }} />
|
|
448
|
+
<span>
|
|
449
|
+
<b style={{ color: '#FFD700' }}>Experimental:</b> all-day pacing. Up to 10 pages in a sitting,
|
|
450
|
+
then a 45-minute rest; searches only from 9:00 to 19:00; at the daily budget it waits for it to
|
|
451
|
+
free up instead of stopping; and it saves every page. It also reads LinkedIn’s own data beside
|
|
452
|
+
the page text, to fill gaps and measure how the two compare. It keeps running while the app is open.
|
|
453
|
+
</span>
|
|
454
|
+
</label>
|
|
443
455
|
<div style={{ fontSize: 12, color: '#FFD700', lineHeight: 1.6 }}>
|
|
444
456
|
Every page is a LinkedIn search, so it rests 20 seconds before each one and a
|
|
445
457
|
minute after every 10, and a long list can take
|
|
@@ -449,7 +461,7 @@ function SetupInner() {
|
|
|
449
461
|
what it read and stops, and carries on from that page next time. Keep batches small.
|
|
450
462
|
</div>
|
|
451
463
|
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
452
|
-
<Btn onClick={() => run('auto-bridge', { maxBridges: batch, tiers: order === 'score' ? tiers : [], order, maxPages: pages, deeper: finish })} disabled={!canSearch || (order === 'score' && !tiers.length)} primary>
|
|
464
|
+
<Btn onClick={() => run('auto-bridge', { maxBridges: batch, tiers: order === 'score' ? tiers : [], order, maxPages: pages, deeper: finish, experimental })} disabled={!canSearch || (order === 'score' && !tiers.length)} primary>
|
|
453
465
|
{running && s.action === 'auto-bridge' ? 'Mapping…' : 'Map 2nd degree'}
|
|
454
466
|
</Btn>
|
|
455
467
|
<select
|