six-degrees 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +32 -4
- package/.next/standalone/.next/routes-manifest.json +6 -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.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 +16 -16
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- 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 +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 +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 +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 +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 +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/backup/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/csv/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/restore/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 +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 +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 +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 +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 +3 -3
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js +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 +4 -4
- 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 +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/crm/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +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 +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 +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 +18 -18
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +18 -18
- 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 +16 -16
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +19 -19
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +19 -19
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/insights/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/insights/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/insights/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/insights/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/insights/page.js +13 -0
- package/.next/standalone/.next/server/app/insights/page.js.map +5 -0
- package/.next/standalone/.next/server/app/insights/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/insights/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/insights.html +1 -0
- package/.next/standalone/.next/server/app/insights.meta +26 -0
- package/.next/standalone/.next/server/app/insights.rsc +35 -0
- package/.next/standalone/.next/server/app/insights.segments/_full.segment.rsc +35 -0
- package/.next/standalone/.next/server/app/insights.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/insights.segments/insights/__PAGE__.segment.rsc +33 -0
- 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 +18 -18
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +18 -18
- 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 +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +19 -19
- 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 +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +19 -19
- 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 +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +19 -19
- 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 +21 -20
- 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 +18 -18
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +18 -18
- 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 +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +19 -19
- 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 +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +19 -19
- 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 +16 -16
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +18 -18
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +18 -18
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +16 -16
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__063s0x7._.js → [root-of-the-server]__0r-0t-9._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ra6xyg._.js +15 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20rwfdw._.js → [root-of-the-server]__0rcpjad._.js} +5 -2
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_02iz_9q._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_05agvk3._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_0v84u66._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_db-client_0_dch35.js +9 -9
- package/.next/standalone/.next/server/chunks/{lib_rpc_0jnfp2e.js → lib_rpc_00nndai.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0b17r6v.js → lib_rpc_03ibb-x.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0doq0t-.js → lib_rpc_03ri3ji.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0fd2rlw.js → lib_rpc_078ce48.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0a69mgr.js +3 -0
- package/.next/standalone/.next/server/chunks/{lib_rpc_005abye.js → lib_rpc_0cej3t8.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0cmh4g6.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0nx2_pc.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0r79-w1.js +3 -0
- package/.next/standalone/.next/server/chunks/{lib_rpc_0is4qoc.js → lib_rpc_0t552z6.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0yx8ynd.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_12p-o9-.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1466dnb.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1sryxv6.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1trn-x5.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1ygu_mv.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egamyq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_04am_h8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_07zb7lu._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0_79idq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0gds903._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0klqt9z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1k9qfsk._.js → _14jak5s._.js} +6 -2
- package/.next/standalone/.next/server/chunks/ssr/_14nxd3q._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1cj2yog._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fm1ih_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1hdqw0-._.js +7 -0
- package/.next/standalone/.next/server/chunks/ssr/_1k2nmbw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0gy11ar._.js → _1r62six._.js} +6 -2
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_insights_page_actions_0-0tf75.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_0uuff77._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_components_1j3q-h5._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_01o3i5y._.js → app_components_settings_SectorPicker_1c-29qw.js} +2 -6
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_insights_page_1d_chja.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +14 -14
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +3 -3
- 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/02wnf8dkxegxg.js +34 -0
- package/.next/standalone/.next/static/chunks/0838_s6w-i_ik.js +1 -0
- package/.next/{static/chunks/2eo9pqq9gay3r.js → standalone/.next/static/chunks/0ka4prpk3w9wn.js} +4 -4
- package/.next/standalone/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
- package/.next/standalone/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
- package/.next/standalone/.next/static/chunks/112mv-com-mqa.js +21 -0
- package/.next/{static/chunks/44yn0tcd9dfk3.js → standalone/.next/static/chunks/1bkt_hhjzvbh8.js} +1 -1
- package/.next/standalone/.next/static/chunks/1cmphb3kh2ers.css +3 -0
- package/.next/{static/chunks/1qbjermbf68xj.js → standalone/.next/static/chunks/1d8yv3k_atka7.js} +1 -1
- package/.next/standalone/.next/static/chunks/23hc51oa4vkhc.js +5 -0
- package/.next/standalone/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
- package/.next/standalone/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
- package/.next/{static/chunks/2zf4jmz9l3li2.js → standalone/.next/static/chunks/2w-y83mjlpnse.js} +1 -1
- package/.next/standalone/.next/static/chunks/2ws1xf293j-tm.js +5 -0
- package/.next/standalone/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
- package/.next/standalone/.next/static/chunks/3opgg1efrx8q6.js +1 -0
- package/.next/standalone/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
- package/.next/{static/chunks/0an_b6zi1r_qq.js → standalone/.next/static/chunks/3y40z6of3r9py.js} +1 -5
- package/.next/standalone/.next/static/chunks/403dpflln8ah5.js +1 -0
- package/.next/standalone/CHANGELOG.md +60 -0
- package/.next/standalone/README.md +25 -1
- package/.next/standalone/SECURITY.md +8 -0
- package/.next/standalone/app/api/outreach/route.js +13 -73
- package/.next/standalone/app/api/scraper/route.js +97 -4
- package/.next/standalone/app/components/AppHeader.js +22 -6
- package/.next/standalone/app/components/AppTabs.js +4 -0
- package/.next/standalone/app/components/AutoConnect.js +286 -0
- package/.next/standalone/app/components/ChainView.js +75 -2
- package/.next/standalone/app/components/FilterPanel.js +8 -1
- package/.next/standalone/app/components/LinkedInLimits.js +38 -13
- package/.next/standalone/app/components/OutlinkQuest.js +32 -11
- package/.next/standalone/app/components/ScanStatusBar.js +18 -3
- package/.next/standalone/app/components/ScanTrail.js +13 -3
- package/.next/standalone/app/components/SeparationView.js +6 -22
- package/.next/standalone/app/components/Sidebar.js +147 -38
- package/.next/standalone/app/components/insights/Boards.js +275 -0
- package/.next/standalone/app/components/insights/PeopleView.js +266 -0
- package/.next/standalone/app/components/insights/Rail.js +156 -0
- package/.next/standalone/app/components/insights/Report.js +382 -0
- package/.next/standalone/app/components/insights/parts.js +239 -0
- package/.next/standalone/app/components/settings/UsageSection.js +50 -3
- package/.next/standalone/app/globals.css +2 -3
- package/.next/standalone/app/insights/page.js +220 -0
- package/.next/standalone/app/page.js +1 -17
- package/.next/standalone/app/queue/page.js +30 -18
- package/.next/standalone/app/setup/page.js +16 -9
- package/.next/standalone/lib/auto-connect.js +145 -0
- package/.next/standalone/lib/companies.js +1 -1
- package/.next/standalone/lib/in-progress.js +46 -0
- package/.next/standalone/lib/insights-board.js +613 -0
- package/.next/standalone/lib/linkedin-limits.js +36 -11
- package/.next/standalone/lib/requests-client.js +10 -0
- package/.next/standalone/lib/requests.js +124 -0
- package/.next/standalone/lib/scan-state.js +1 -0
- package/.next/standalone/lib/scraper-client.js +18 -5
- package/.next/standalone/lib/settings.js +3 -0
- package/.next/standalone/lib/usage.js +5 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +569 -9
- package/.next/static/chunks/02wnf8dkxegxg.js +34 -0
- package/.next/static/chunks/0838_s6w-i_ik.js +1 -0
- package/.next/{standalone/.next/static/chunks/2eo9pqq9gay3r.js → static/chunks/0ka4prpk3w9wn.js} +4 -4
- package/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
- package/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
- package/.next/static/chunks/112mv-com-mqa.js +21 -0
- package/.next/{standalone/.next/static/chunks/44yn0tcd9dfk3.js → static/chunks/1bkt_hhjzvbh8.js} +1 -1
- package/.next/static/chunks/1cmphb3kh2ers.css +3 -0
- package/.next/{standalone/.next/static/chunks/1qbjermbf68xj.js → static/chunks/1d8yv3k_atka7.js} +1 -1
- package/.next/static/chunks/23hc51oa4vkhc.js +5 -0
- package/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
- package/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
- package/.next/{standalone/.next/static/chunks/2zf4jmz9l3li2.js → static/chunks/2w-y83mjlpnse.js} +1 -1
- package/.next/static/chunks/2ws1xf293j-tm.js +5 -0
- package/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
- package/.next/static/chunks/3opgg1efrx8q6.js +1 -0
- package/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
- package/.next/{standalone/.next/static/chunks/0an_b6zi1r_qq.js → static/chunks/3y40z6of3r9py.js} +1 -5
- package/.next/static/chunks/403dpflln8ah5.js +1 -0
- package/CHANGELOG.md +60 -0
- package/README.md +25 -1
- package/SECURITY.md +8 -0
- package/lib/auto-connect.js +145 -0
- package/lib/companies.js +1 -1
- package/lib/in-progress.js +46 -0
- package/lib/insights-board.js +613 -0
- package/lib/linkedin-limits.js +36 -11
- package/lib/requests-client.js +10 -0
- package/lib/requests.js +124 -0
- package/lib/scan-state.js +1 -0
- package/lib/scraper-client.js +18 -5
- package/lib/settings.js +3 -0
- package/lib/usage.js +5 -0
- package/package.json +1 -1
- package/scripts/scrape.py +569 -9
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_4y8z7._.js +0 -8
- package/.next/standalone/.next/server/chunks/_02twlca._.js +0 -10
- package/.next/standalone/.next/server/chunks/lib_rpc_0nef_n7.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0plrzra.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_13a4m0c.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1if17c-.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1j-zw7v.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1sngt-q.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1v2sgme.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1vh-tgx.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1zaubz7.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_02sband._.js +0 -7
- package/.next/standalone/.next/server/chunks/ssr/_189g8lg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1f_n00p._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_0t908t-._.js +0 -3
- package/.next/standalone/.next/static/chunks/0e3g0sil79cus.js +0 -21
- package/.next/standalone/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
- package/.next/standalone/.next/static/chunks/163b-20vwa06i.js +0 -34
- package/.next/standalone/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
- package/.next/standalone/.next/static/chunks/23_1z47al92nz.js +0 -57
- package/.next/standalone/.next/static/chunks/260bomtr214m8.js +0 -34
- package/.next/standalone/.next/static/chunks/2xk2ra4piiuod.js +0 -34
- package/.next/standalone/.next/static/chunks/3b0-14cxqsprt.js +0 -1
- package/.next/standalone/.next/static/chunks/3jarus1_38s2z.js +0 -1
- package/.next/standalone/.next/static/chunks/3ssu341hav_sq.css +0 -3
- package/.next/standalone/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
- package/.next/static/chunks/0e3g0sil79cus.js +0 -21
- package/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
- package/.next/static/chunks/163b-20vwa06i.js +0 -34
- package/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
- package/.next/static/chunks/23_1z47al92nz.js +0 -57
- package/.next/static/chunks/260bomtr214m8.js +0 -34
- package/.next/static/chunks/2xk2ra4piiuod.js +0 -34
- package/.next/static/chunks/3b0-14cxqsprt.js +0 -1
- package/.next/static/chunks/3jarus1_38s2z.js +0 -1
- package/.next/static/chunks/3ssu341hav_sq.css +0 -3
- package/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
- /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_ssgManifest.js +0 -0
- /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_buildManifest.js +0 -0
- /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_ssgManifest.js +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,94657,e=>{"use strict";var t=e.i(43476),n=e.i(71645),r=e.i(7837);let o=e=>`${e.name} ${e.was} → ${e.estimated?"estimated":e.now}`,l=e=>({padding:"6px 12px",borderRadius:7,cursor:"pointer",fontSize:12,fontWeight:700,border:e?"none":"1px solid rgba(var(--sd-ink, 255, 255, 255), 0.15)",background:e?"linear-gradient(135deg, #00ff88, #3498DB)":"transparent",color:e?"#000":"var(--sd-fg-2, #cfd8d8)"});function s({onAnswered:e}){var a;let i,c,[d,u]=(0,n.useState)(null),[f,p]=(0,n.useState)(!1),[m,g]=(0,n.useState)(()=>new Set),[h,y]=(0,n.useState)(!1),[b,x]=(0,n.useState)(null);if((0,n.useEffect)(()=>{let e=!1;return(0,r.csvNetworkSource)().then(t=>t||e?null:fetch("/api/company-scores/legacy").then(e=>e.json())).then(t=>{!e&&t?.offer?.companies?.length&&(u(t.offer),g(new Set(t.offer.companies.map(e=>e.name))))}).catch(()=>{}),()=>{e=!0}},[]),!d)return null;let v=async t=>{y(!0),x(null);try{let n=await fetch("/api/company-scores/legacy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({keep:t})}),r=await n.json();if(!n.ok&&!r.kept)throw Error(r.error||"Could not save your answer.");u(null),e?.({kept:r.kept||[],scored:r.scored||0,error:n.ok?null:r.error})}catch(e){x(e.message)}finally{y(!1)}};return(0,t.jsxs)("section",{"aria-label":"Built-in scores changed",style:{marginBottom:14,padding:14,borderRadius:10,border:"1px solid rgba(255,215,0,0.35)",background:"rgba(255,215,0,0.06)",fontSize:12.5,color:"var(--sd-fg-2, #cfd8d8)",lineHeight:1.6},children:[(0,t.jsx)("b",{style:{color:"var(--sd-fg-1, #fff)"},children:"Built-in scores changed in this version:"})," ",(i=(a=d.companies).slice(0,3).map(o).join(", "),(c=a.length-3)>0?`${i} and ${c} more`:i),"."," ","Keep any of the old ones as your own?",(0,t.jsx)("div",{style:{marginTop:4,color:"var(--sd-fg-3, #8b9a9a)"},children:"The built-in list now holds only companies most professionals would recognise, scored the same for everyone; anything else is estimated from your network. A score you keep is yours, like one you set below, and Auto hands it back."}),f&&(0,t.jsx)("ul",{style:{listStyle:"none",padding:0,margin:"10px 0 0",display:"grid",gap:4},children:d.companies.map(e=>(0,t.jsx)("li",{children:(0,t.jsxs)("label",{style:{display:"flex",gap:8,alignItems:"baseline",cursor:"pointer"},children:[(0,t.jsx)("input",{type:"checkbox",checked:m.has(e.name),disabled:h,onChange:()=>{let t;return t=e.name,g(e=>{let n=new Set(e);return n.has(t)?n.delete(t):n.add(t),n})}}),(0,t.jsxs)("span",{children:[(0,t.jsx)("b",{style:{color:"var(--sd-fg-1, #fff)"},children:e.name})," ",e.was," → ",e.estimated?`estimated (${e.now})`:e.now,(0,t.jsxs)("span",{style:{color:"var(--sd-fg-4, #778)"},children:[` \xb7 ${e.people} ${1===e.people?"person":"people"} there now`,e.names.length>1&&` \xb7 for ${e.names.join(", ")}`]})]})]})},e.name))}),(0,t.jsxs)("div",{style:{display:"flex",gap:8,marginTop:10,flexWrap:"wrap"},children:[f?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("button",{disabled:h||!m.size,onClick:()=>v([...m]),style:{...l(!0),opacity:h||!m.size?.5:1},children:["Keep ",m.size," ",1===m.size?"score":"scores"]}),(0,t.jsx)("button",{disabled:h,onClick:()=>p(!1),style:l(!1),children:"Back"})]}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("button",{disabled:h,onClick:()=>v(d.companies.map(e=>e.name)),style:l(!0),children:"Keep all"}),(0,t.jsx)("button",{disabled:h,onClick:()=>p(!0),style:l(!1),children:"Choose…"})]}),(0,t.jsx)("button",{disabled:h,onClick:()=>v([]),style:l(!1),children:"No thanks"})]}),b&&(0,t.jsx)("div",{role:"alert",style:{marginTop:8,color:"#e74c3c"},children:b})]})}let a="1px solid rgba(var(--sd-ink, 255, 255, 255), 0.1)",i={yours:{label:"your score",color:"var(--sd-green, #00ff88)"},known:{label:"known list",color:"var(--sd-blue, #3498DB)"},data:{label:"public data",color:"#1abc9c"},network:{label:"estimated: many of your people",color:"var(--sd-orange, #FF6B35)"},default:{label:"unknown company",color:"var(--sd-fg-4, #778)"}};async function c(){let e=await fetch("/api/company-scores"),t=await e.json();if(!e.ok)throw Error(t.error||"Could not load company scores");return t.companies}function d(){let[e,t]=(0,n.useState)(null),[r,o]=(0,n.useState)(null);return(0,n.useEffect)(()=>{let e=!1;return c().then(n=>{e||t(n)}).catch(t=>{e||o(t.message)}),()=>{e=!0}},[]),{companies:e,error:r,setScore:(0,n.useCallback)(async(e,n)=>{let r=await fetch("/api/company-scores",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,score:n})}),o=await r.json();if(!r.ok)throw Error(o.error||"Could not save");return t(await c()),o},[]),reload:(0,n.useCallback)(async()=>t(await c()),[])}}function u({company:e,onSet:r,compact:o}){let[l,s]=(0,n.useState)(!1),i=async t=>{s(!0);try{await r(e.name,"auto"===t?null:Number(t))}finally{s(!1)}};return(0,t.jsxs)("select",{disabled:l,value:"yours"===e.source?String(e.score):"auto",onChange:e=>i(e.target.value),title:"Set this company's score. Auto uses the known list or an estimate.",style:{padding:o?"2px 4px":"4px 6px",borderRadius:6,border:a,background:"var(--sd-card, #11131c)",color:"var(--sd-fg-1, #dfe6e9)",fontSize:12},children:[(0,t.jsxs)("option",{value:"auto",children:["Auto (","yours"===e.source?e.suggested??"est.":e.score,")"]}),[10,9,8,7,6,5,4,3,2,1].map(e=>(0,t.jsx)("option",{value:e,children:e},e))]})}e.s(["ScorePicker",0,u,"default",0,function({onRescored:e,version:r=0}){let{companies:o,error:l,setScore:c,reload:f}=d();(0,n.useEffect)(()=>{r&&f().catch(()=>{})},[r,f]);let[p,m]=(0,n.useState)(""),[g,h]=(0,n.useState)("all"),[y,b]=(0,n.useState)(150),[x,v]=(0,n.useState)(null),w=e=>{v(e),setTimeout(()=>v(null),3500)},S=async(t,n)=>{let r=await c(t,n);w(`${t}: ${null==n?"back to auto":`set to ${n}`} \xb7 rescored ${r.scored} people`),e?.()},k=async({kept:t,scored:n,error:r})=>{if(!t.length)return w("Keeping the new built-in scores.");w(r||`Kept ${t.length} old ${1===t.length?"score":"scores"} as your own \xb7 rescored ${n} people`),await f().catch(()=>{}),e?.()},j=(0,n.useMemo)(()=>{if(!o)return[];let e=p.trim().toLowerCase();return o.filter(t=>(!e||t.name.toLowerCase().includes(e))&&("all"===g||t.source===g))},[o,p,g]);if(l)return(0,t.jsx)("div",{style:{padding:24,color:"#e74c3c"},children:l});if(!o)return(0,t.jsx)("div",{style:{padding:24,color:"var(--sd-fg-4, #778)"},children:"Loading companies…"});let C=e=>o.filter(t=>t.source===e).length;return(0,t.jsxs)("div",{style:{flex:1,overflow:"auto",padding:16},children:[(0,t.jsxs)("div",{style:{maxWidth:980,margin:"0 auto"},children:[(0,t.jsx)(s,{onAnswered:k}),(0,t.jsxs)("div",{style:{padding:14,borderRadius:10,border:a,background:"rgba(var(--sd-ink, 255, 255, 255), 0.03)",fontSize:12.5,color:"var(--sd-fg-2, #cfd8d8)",lineHeight:1.6},children:[(0,t.jsx)("b",{style:{color:"var(--sd-fg-1, #fff)"},children:"How a power score works."})," power = ",(0,t.jsx)("b",{children:"title"})," × ",(0,t.jsx)("b",{children:"company weight"})," + bonus. The title (student 1 … founder or C-suite 10) comes from someone’s roles, with former roles at 70%; an audience of their own counts like a title (1M+ followers like a director). The company weight runs from 0.725 for a company we don’t know (or none found), the middle of the scale, to 1.0 (a company scored 10), so seniority counts for more at a bigger company. The bonus is at most +1.5, for investor, YC, an audience in the millions or the top award of any field, and counts half at an unknown company. A 1st-degree person whose circle is unusually strong gets up to +2 more: up to +1 for a high share of it at A or S, or +1 for every 25 people there. Tiers are graded on your network’s curve unless you choose the fixed scale (",(0,t.jsx)("a",{href:"#tiers",style:{color:"var(--sd-blue, #3498DB)"},children:"Tiers"}),", above): your top 3% are S, the next 12% A, the next 25% B (it only lifts, and never lifts anyone under 4 into S or A). On the fixed scale, S ≥ 7.5 · A ≥ 5.5 · B ≥ 4 · C ≥ 2.5.",(0,t.jsxs)("div",{style:{marginTop:6,color:"var(--sd-fg-3, #8b9a9a)"},children:["So a VP at a 10 scores 9.0, a founder at an unknown company 7.3, a director at an unknown company 5.4, and an intern at a 10 scores 2.0. Set a company’s score below and everyone there is rescored. Companies in the sectors you pick (",(0,t.jsx)("a",{href:"#sector",style:{color:"var(--sd-blue, #3498DB)"},children:"Your sector"}),", above) get +1 or +2; a score you set is never changed."]})]}),(0,t.jsxs)("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap",margin:"14px 0 10px"},children:[(0,t.jsx)("input",{value:p,onChange:e=>m(e.target.value),placeholder:"Find a company",style:{padding:"6px 10px",borderRadius:7,border:a,background:"rgba(var(--sd-ink, 255, 255, 255), 0.05)",color:"var(--sd-fg-1, #fff)",fontSize:13,minWidth:180}}),[["all",`All ${o.length}`],["known",`Known list ${C("known")}`],["data",`Public data ${C("data")}`],["yours",`Yours ${C("yours")}`],["network",`Estimated ${C("network")}`],["default",`Unknown ${C("default")}`]].map(([e,n])=>(0,t.jsx)("button",{onClick:()=>h(e),style:{padding:"5px 10px",borderRadius:7,border:a,cursor:"pointer",fontSize:11.5,fontWeight:600,background:g===e?"rgba(52,152,219,0.25)":"transparent",color:g===e?"var(--sd-fg-1, #cfe6f7)":"var(--sd-fg-3, #8b9a9a)"},children:n},e))]}),(0,t.jsxs)("div",{style:{borderRadius:10,border:a,overflow:"hidden"},children:[(0,t.jsxs)("div",{style:{display:"grid",gridTemplateColumns:"1fr 70px 70px 70px 60px 180px 110px",gap:8,padding:"8px 12px",fontSize:10.5,letterSpacing:.6,color:"var(--sd-fg-3, #8b9a9a)",background:"rgba(var(--sd-ink, 255, 255, 255), 0.04)"},children:[(0,t.jsx)("span",{children:"COMPANY"}),(0,t.jsx)("span",{style:{textAlign:"right"},children:"PEOPLE"}),(0,t.jsx)("span",{style:{textAlign:"right"},children:"YOU KNOW"}),(0,t.jsx)("span",{style:{textAlign:"right"},children:"DIRECTOR+"}),(0,t.jsx)("span",{style:{textAlign:"right"},children:"SCORE"}),(0,t.jsx)("span",{children:"SOURCE"}),(0,t.jsx)("span",{children:"SET"})]}),j.slice(0,y).map(e=>(0,t.jsxs)("div",{style:{display:"grid",gridTemplateColumns:"1fr 70px 70px 70px 60px 180px 110px",gap:8,padding:"7px 12px",alignItems:"center",borderTop:"1px solid rgba(var(--sd-ink, 255, 255, 255), 0.05)",fontSize:12.5},children:[(0,t.jsxs)("span",{style:{display:"flex",alignItems:"center",gap:8,minWidth:0},children:[(0,t.jsx)("span",{title:`${e.industry.label} (inferred)`,style:{width:8,height:8,borderRadius:"50%",background:e.industry.color,flexShrink:0}}),(0,t.jsx)("span",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.name})]}),(0,t.jsx)("span",{style:{textAlign:"right"},children:e.people}),(0,t.jsx)("span",{style:{textAlign:"right",color:e.d1?"var(--sd-green, #00ff88)":"var(--sd-fg-5, #556)"},children:e.d1}),(0,t.jsx)("span",{style:{textAlign:"right",color:e.senior?"var(--sd-gold, #FFD700)":"var(--sd-fg-5, #556)"},children:e.senior}),(0,t.jsx)("b",{style:{textAlign:"right",fontSize:15},children:e.score}),(0,t.jsxs)("span",{style:{fontSize:11,color:i[e.source]?.color},children:[i[e.source]?.label,e.sectorBonus>0&&(0,t.jsxs)("span",{style:{color:"var(--sd-green, #00ff88)"},children:[" + ",e.sectorBonus," your sector"]})]}),(0,t.jsx)(u,{company:e,onSet:S,compact:!0})]},e.name))]}),j.length>y&&(0,t.jsxs)("button",{onClick:()=>b(e=>e+300),style:{marginTop:10,padding:"7px 14px",borderRadius:8,border:a,background:"rgba(var(--sd-ink, 255, 255, 255), 0.05)",color:"var(--sd-fg-1, #dfe6e9)",cursor:"pointer",fontSize:12},children:["Show more (",j.length-y," left)"]})]}),x&&(0,t.jsx)("div",{role:"status",style:{position:"fixed",bottom:24,left:"50%",transform:"translateX(-50%)",padding:"10px 18px",borderRadius:24,background:"linear-gradient(135deg, #00ff88, #3498DB)",color:"#000",fontWeight:800,fontSize:13,zIndex:50},children:x})]})},"useCompanyScores",0,d],94657)},39160,84278,e=>{"use strict";var t=e.i(7214),n=e.i(7837);function r(e){let t=String(e||"").trim();if(!t)return null;let n=t.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\s*UTC)?$/);if(n)return Date.UTC(+n[1],n[2]-1,+n[3],+n[4],+n[5],+(n[6]||0));if(n=t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4}),?\s+(\d{1,2}):(\d{2})\s*(AM|PM)?$/i)){let e=100>+n[3]?2e3+ +n[3]:+n[3],t=n[4]%12;return"PM"===(n[6]||"").toUpperCase()&&(t+=12),Date.UTC(e,n[1]-1,+n[2],t,+n[5])}if(n=t.match(/^([A-Za-z]{3,9})\s+(\d{4})$/)){let e=Date.parse(`1 ${n[1]} ${n[2]} UTC`);return Number.isNaN(e)?null:e}let r=Date.parse(/\d{4}$/.test(t)?`${t} UTC`:t);return Number.isNaN(r)?null:r}let o=e=>(0,t.keyFor)({profile_url:e});function l(e=[]){let t=new Map;for(let n of e){let e=o(n["sender profile url"]),r=n["conversation id"];r&&e&&(t.has(e)||t.set(e,new Set),t.get(e).add(r))}let n=null;for(let[e,r]of t)(!n||r.size>t.get(n).size)&&(n=e);return n}let s=e=>{let t=f(e);return t?`name:${t}`:null},a=/sponsored/i,i=/in\s?mail/i,c=/linkedin\.com\/(company|showcase|school)\//i,d=e=>/^(yes|true|1)$/i.test(String(e||"").trim());function u(e,t){if(!e||!e.total)return"never";let n=t-e.last;return n<=2592e6||e.recent>=3?"warm":n<=31536e6?"cool":"dormant"}let f=e=>String(e||"").split(",")[0].normalize("NFD").replace(/[̀-ͯ]/g,"").toLowerCase().replace(/[^a-z0-9 ]+/g," ").replace(/\s+/g," ").trim();e.s(["buildConversations",0,function(e=[],{keepText:t=!1}={}){let n=l(e),u=new Map;for(let t of e)n&&o(t["sender profile url"])===n&&t.from&&u.set(t.from,(u.get(t.from)||0)+1);let p=f([...u].sort((e,t)=>t[1]-e[1])[0]?.[0]||""),m=e=>!!e&&(e===n||!!p&&e===`name:${p}`),g={},h=new Map,y=0,b=0;for(let n of e){let e=n["conversation id"],l=r(n.date);if(!e||null==l){b+=1;continue}if(d(n["is message draft"])){y+=1;continue}let u=o(n["sender profile url"])||s(n.from);u&&n.from&&!g[u]&&(g[u]=String(n.from).trim());let f=String(n["recipient profile urls"]||"").split(/[,;\s]+/).filter(Boolean).map(o),p=String(n.to||"").split(",").map(e=>e.trim()).filter(Boolean),x=f.length?f:p.map(s).filter(Boolean);f.length&&f.length===p.length&&f.forEach((e,t)=>{g[e]||(g[e]=p[t])}),f.length||p.forEach(e=>{let t=s(e);t&&!g[t]&&(g[t]=e)}),h.has(e)||h.set(e,{id:e,title:"",everyone:new Set,messages:[],kind:null,folder:"",newest:-1/0});let v=h.get(e);u&&v.everyone.add(u),x.forEach(e=>v.everyone.add(e)),v.kind=v.kind||function(e){let t=String(e.folder||""),n=String(e["conversation title"]||"");return a.test(t)||a.test(n)||c.test(String(e["sender profile url"]||""))?"sponsored":i.test(t)||i.test(n)?"inmail":null}(n),l>=v.newest&&(v.newest=l,v.folder=String(n.folder||"").trim().toLowerCase().slice(0,30));let w={t:l,fromMe:m(u)};t&&(w.text=[n.subject,n.content].map(e=>String(e||"").trim()).filter(Boolean).join("\n\n"),!v.title&&n["conversation title"]&&(v.title=String(n["conversation title"]).trim())),v.messages.push(w)}let x=[];for(let e of h.values()){let n=[...e.everyone].filter(e=>e&&!m(e)).sort();e.messages.sort((e,t)=>e.t-t.t);let r={id:e.id,people:n,group:n.length>1,kind:e.kind,folder:e.folder,messages:e.messages};t&&(r.title=e.title),x.push(r)}for(let e of Object.keys(g))m(e)&&delete g[e];return{self:n,conversations:x,names:g,drafts:y,skipped:b}},"buildInvitations",0,function(e=[],{keepText:t=!1}={}){let n=[];for(let l of e){let e=String(l.direction||"").toUpperCase();if("INCOMING"!==e&&"OUTGOING"!==e)continue;let a="INCOMING"===e?"in":"out",i=String(("in"===a?l.from:l.to)||"").trim(),c=o("in"===a?l.inviterprofileurl:l.inviteeprofileurl),d=(c&&/\/in\//.test(c)?c:null)||s(i);if(!d)continue;let u=r(l["sent at"]),f={id:`${a}|${d}|${u??""}`,dir:a,key:d,name:i.slice(0,120),t:u};t&&String(l.message||"").trim()&&(f.note=String(l.message).trim().slice(0,2e3)),n.push(f)}return n.sort((e,t)=>(t.t||0)-(e.t||0))},"careerChapters",0,function(e=[],t=[],n=Date.now()){let l=e.map(e=>({company:e["company name"]||"",title:e.title||"",from:r(e["started on"]),to:r(e["finished on"])})).filter(e=>e.company&&null!=e.from).sort((e,t)=>e.from-t.from),s=t.map(e=>({url:o(e.url||e.profile_url),t:r(e["connected on"]||e.connected_on)})).filter(e=>null!=e.t);return l.map(e=>{let t=e.to??n,r=s.filter(n=>n.t>=e.from&&n.t<t);return{...e,to:e.to,made:r.length,people:r.map(e=>e.url)}})},"conversationSummary",0,function(e){let t=e.messages[e.messages.length-1];return{id:e.id,people:e.people,group:e.group,kind:e.kind||null,folder:e.folder||"",last:t?t.t:null,lastFromThem:t?!t.fromMe:null,count:e.messages.length,mine:e.messages.filter(e=>e.fromMe).length}},"invitationSplit",0,function(e=[]){let t=0,n=0;for(let r of e){let e=String(r.direction||"").toUpperCase();"INCOMING"===e?t+=1:"OUTGOING"===e&&(n+=1)}return{incoming:t,outgoing:n,total:t+n}},"messageStats",0,function(e=[]){let t=l(e),n=new Map;for(let t of e){let e=o(t["sender profile url"]),l=t["conversation id"];if(!l||!e)continue;let s=String(t["recipient profile urls"]||"").split(/[,;\s]+/).filter(Boolean).map(o),a=r(t.date);if(null==a)continue;n.has(l)||n.set(l,{people:new Set,msgs:[]});let i=n.get(l);i.people.add(e),s.forEach(e=>i.people.add(e)),i.msgs.push({from:e,t:a})}let s=0,a=new Map;for(let e of n.values()){let n=[...e.people].filter(e=>e!==t);if(1!==n.length||!e.people.has(t))continue;let r=n[0],o=a.get(r)||{last:0,lastFromThem:!1,total:0,times:[]};for(let n of e.msgs)o.total+=1,o.times.push(n.t),n.t>=o.last&&(o.last=n.t,o.lastFromThem=n.from!==t),n.t>s&&(s=n.t);a.set(r,o)}for(let e of a.values())e.recent=e.times.filter(e=>e>s-7776e6).length,delete e.times;return{self:t,asOf:s||null,people:a}},"nameKey",0,f,"postingEffect",0,function(e=[],t=[]){let n=e.map(e=>({t:r(e.date),link:e.sharelink||e["share link"]||""})).filter(e=>null!=e.t).sort((e,t)=>e.t-t.t),o=t.map(e=>r(e["connected on"]||e.connected_on)).filter(e=>null!=e).sort((e,t)=>e-t);if(!o.length)return{posts:[],usualWeek:0,afterPosts:0};let l=Math.max(6048e5,o[o.length-1]-o[0]),s=o.length/l*6048e5,a=n.map(e=>({...e,week:o.filter(t=>t>=e.t&&t<e.t+6048e5).length})),i=a.length?a.reduce((e,t)=>e+t.week,0)/a.length:0;return{posts:a,usualWeek:s,afterPosts:i}},"readTable",0,function(e,t){let r=(0,n.parseCsv)(String(e||"")),o=r.findIndex(e=>e.some(e=>e.trim().toLowerCase()===t.toLowerCase()));if(o<0)return[];let l=r[o].map(e=>e.trim().toLowerCase());return r.slice(o+1).filter(e=>e.some(e=>e.trim())).map(e=>Object.fromEntries(l.map((t,n)=>[t,(e[n]??"").trim()])))},"warmthOf",0,u],84278);let p=["gravity","rings","push","pull","distance","orbit","dotSize","lines","sizeBy","names","branch","colourBy","speed","loop"],m=e=>"boolean"==typeof e||"number"==typeof e&&Number.isFinite(e)&&1e3>=Math.abs(e)||"string"==typeof e&&/^[a-z]{1,20}$/i.test(e);function g(e){if(!Array.isArray(e))return[];let t=new Map;for(let n of e){let e="string"==typeof n?.name?n.name.trim().slice(0,40):"";if(!e||!n.settings||"object"!=typeof n.settings||Array.isArray(n.settings))continue;let r={};for(let e of p)Object.hasOwn(n.settings,e)&&m(n.settings[e])&&(r[e]=n.settings[e]);t.set(e,{name:e,settings:r})}return[...t.values()].slice(0,50)}var h=e.i(56899),y=e.i(40827);let b="six-degrees-galaxy-lab",x="six-degrees-galaxy-layouts",v=Object.freeze({on:!0,gravity:0,rings:1,push:15,pull:1,distance:1,dotSize:1,lines:1,sizeBy:"power",orbit:0,names:"key",branch:!1,colourBy:"tier",speed:15,loop:!1,labels:!0,v:2}),w=["gravity","rings","push","pull","distance"],S=Object.freeze({gravity:.8,rings:0,push:6,pull:6,distance:.15,sizeBy:"links"}),k=Object.freeze({rings:{lines:1},orbit:{lines:1},clusters:{lines:3}}),j=Object.freeze({gravity:0,rings:3,push:6,pull:4,distance:.22,sizeBy:"power"}),C=new Set,O=null;function T(){return O||(O=function(){try{let e=JSON.parse(localStorage.getItem(b)||"null");if(!e||"object"!=typeof e)return v;return{...v,...e,...e.v===v.v?{}:{branch:!1},v:v.v,on:!0}}catch{return v}}()),O}function F(e){O=null===e?{...v,labels:T().labels}:{...T(),...e,on:!0};try{localStorage.setItem(b,JSON.stringify(O))}catch{}C.forEach(e=>e())}let A=null,N=new Set;function M(){if(!A){try{A=JSON.parse(localStorage.getItem(x)||"[]")}catch{A=[]}Array.isArray(A)||(A=[])}return A}function $(e,{save:t=!0}={}){A=e;try{localStorage.setItem(x,JSON.stringify(e))}catch{}N.forEach(e=>e()),t&&(0,h.saveSettings)({galaxyLayouts:e})}let B=new Set,E={at:null,playing:!1,min:null,max:null,of:0,find:"",fly:0,found:0,fit:0,social:void 0},z=null;function L(e){let t={...E,...e};Object.keys(e).every(e=>Object.is(t[e],E[e]))||(E=t,B.forEach(e=>e()))}function D(){cancelAnimationFrame(z),z=null,L({playing:!1})}let I=null;function R(e){let n=new Map;for(let[r,o]of Object.entries(e?.people||{}))n.set((0,t.keyFor)({profile_url:r}),{...o});for(let[r,o]of Object.entries(e?.live||{})){let e=(0,t.keyFor)({profile_url:r}),l=n.get(e)||{total:0,recent:0};o.last&&(!l.last||o.last>l.last)&&(l.last=o.last,"boolean"==typeof o.lastFromThem&&(l.lastFromThem=o.lastFromThem)),l.unread=o.unread,l.total=Math.max(l.total||0,1),n.set(e,l)}let r=Math.max(e?.asOf||0,e?.liveAt&&Date.parse(e.liveAt)||0);for(let e of n.values())(e.last||0)>r&&(r=e.last);return{merged:n,asOf:r,chapters:e?.chapters||[],posts:e?.posts?.posts||[]}}let U={1:"#4FC3F7",2:"#BA68C8",3:"#FFB74D"},P=["#FF6B6B","#4ECDC4","#FFD93D","#A29BFE","#00B894","#FD79A8","#74B9FF","#E17055"],K={warm:"#FF7043",cool:"#4FC3F7",dormant:"#8D6E63",never:"#546E7A"},W="#4a5568",_=["#2b1b6b","#6a1b9a","#c2185b","#f4511e","#ffb300","#fff3b0"];function J(e){let t=Math.max(0,Math.min(1,Number(e)||0))*(_.length-1),n=Math.min(_.length-2,Math.floor(t)),r=t-n,o=_[n],l=_[n+1];return`#${[1,3,5].map(e=>Math.round(parseInt(o.slice(e,e+2),16)*(1-r)+parseInt(l.slice(e,e+2),16)*r).toString(16).padStart(2,"0")).join("")}`}function Y(e=[],t=()=>0){let n=[...new Set(e.map(t))].sort((e,t)=>e-t),r=new Map(n.map((e,t)=>[e,n.length>1?t/(n.length-1):1]));return e=>r.get(t(e))??0}let G=e=>String(e||"").trim().toLowerCase();e.s(["CLUSTERS",0,S,"FORCE_KEYS",0,w,"LAB_DEFAULTS",0,v,"LAYOUT_LOOKS",0,k,"ORBIT",0,j,"adoptSavedLayouts",0,function(e){let t=g(M()),{use:n,save:r,changed:o}=(0,y.reconcileSaved)({saved:null==e?null:g(e),cached:t,isDefault:e=>0===e.length,same:(e,t)=>JSON.stringify(e)===JSON.stringify(t)});r&&(0,h.saveSettings)({galaxyLayouts:t}),o&&$(n,{save:!1})},"applyLayout",0,function(e){let t=M().find(t=>t.name===e);t&&F(Object.fromEntries(p.filter(e=>e in t.settings).map(e=>[e,t.settings[e]])))},"bornTimes",0,function(e,t){let n=new Map(e.map(e=>[e.id,e])),r=new Map,o=(e,l=0)=>{if(r.has(e.id))return r.get(e.id);let s=-1/0,a=t.get(e.id);if(1===e.degree||null==a||l>6){let t=e.connected_date?Date.parse(e.connected_date):NaN;s=Number.isNaN(t)?-1/0:t}return null!=a&&n.has(a)&&l<=6&&(s=Math.max(s,o(n.get(a),l+1))),r.set(e.id,s),s};return e.map(e=>o(e))},"chapterAt",0,function(e,t){let n=null;for(let r of e?.chapters||[])Number.isFinite(r.from)&&r.from<=t&&(!n||r.from>n.from)&&(n=r);return n&&(!Number.isFinite(n.to)||n.to>t)?n:null},"clockNow",0,()=>E,"colourScheme",0,function(e,n,r,o){if("degree"===e)return{of:e=>U[Math.min(e.degree||1,3)],legend:[["1st",U[1]],["2nd",U[2]],["3rd+",U[3]]]};if("company"===e){let e=new Map;for(let t of n){let n=G(t.company);if(!n)continue;let r=e.get(n)||{name:String(t.company).trim(),n:0};r.n+=1,e.set(n,r)}let t=[...e.entries()].filter(([,e])=>e.n>1).sort((e,t)=>t[1].n-e[1].n).slice(0,P.length),r=new Map(t.map(([e],t)=>[e,P[t]]));return{of:e=>r.get(G(e.company))||W,legend:[...t.map(([,e],t)=>[e.name,P[t]]),["Other",W]]}}if("heat"===e){let e=function(e=[]){return Y(e,t.score)}(n);return{of:t=>J(e(t)),heat:e,legend:[["Cold",J(.1)],["Warm",J(.45)],["Hot",J(.75)],["Hottest",J(1)]]}}return"warmth"===e&&o?{of:e=>1===(e.degree||1)?K[u(o.merged.get((0,t.keyFor)(e)),o.asOf)]:"#2d3748",legend:[["Warm",K.warm],["Cool",K.cool],["Dormant",K.dormant],["Never messaged",K.never]]}:{of:e=>r[e.tier]||"#666",legend:Object.entries(r)}},"effectiveLab",0,e=>e.on?e:e.labels===v.labels?v:{...v,labels:e.labels},"findMatches",0,function(e,t){let n=String(t||"").trim().toLowerCase();if(n.length<2)return null;let r=new Set;for(let t of e)[t.name,t.company,t.role,t.headline].some(e=>e&&String(e).toLowerCase().includes(n))&&r.add(t.id);return r},"forgetLayout",0,function(e){$(M().filter(t=>t.name!==e))},"heatBy",0,Y,"heatColour",0,J,"labNow",0,T,"layoutOf",0,function(e){let t=t=>Object.entries(t).every(([t,n])=>e[t]===n);return w.every(t=>e[t]===v[t])?"rings":t(S)?"clusters":t(j)?"orbit":null},"layoutsNow",0,M,"loadSocial",0,function(){return I||(I=fetch("/api/social").then(e=>e.ok?e.json():{}).then(e=>{L({social:e.social?R(e.social):null})}).catch(()=>L({social:null}))),I},"mergeSocial",0,R,"milestones",0,function(e){let t=[];for(let n of e?.chapters||[])Number.isFinite(n.from)&&t.push({t:n.from,kind:"job",label:n.company});for(let n of e?.posts||[])Number.isFinite(n.t)&&t.push({t:n.t,kind:"post",label:"You posted"});return t.sort((e,t)=>e.t-t.t)},"pause",0,D,"play",0,function e(){if(null==E.min||null==E.max||E.max<=E.min)return;let t=E.max-E.min,n=1e3*Math.max(1,Number(T().speed)||15),r=null==E.at||E.at>=E.max?E.min:E.at,o=performance.now()-(r-E.min)/t*n;L({at:r,playing:!0});let l=r=>{let s=Math.min(E.max,E.min+(r-o)/n*t);if(L({at:s}),s>=E.max){z=null,L({playing:!1}),T().loop&&setTimeout(()=>{z||E.at!==E.max||(L({at:E.min}),e())},1200);return}z=requestAnimationFrame(l)};cancelAnimationFrame(z),z=requestAnimationFrame(l)},"reachCounts",0,function(e,t){let n=new Map(e.map(e=>[e.id,0]));for(let r of e){let e=t.get(r.id);for(let r=0;null!=e&&r<7;r++)n.set(e,(n.get(e)||0)+1),e=t.get(e)}return n},"saveLayout",0,function(e){let t=String(e||"").trim().slice(0,40);if(!t)return;let n=T(),r=Object.fromEntries(p.map(e=>[e,n[e]]));$([...M().filter(e=>e.name!==t),{name:t,settings:r}])},"setClock",0,L,"setLab",0,F,"stopReplay",0,function(){D(),L({at:null})},"watchClock",0,function(e){return B.add(e),()=>B.delete(e)},"watchLab",0,function(e){C.add(e);let t=t=>{t.key===b&&(O=null,e())};return window.addEventListener("storage",t),()=>{C.delete(e),window.removeEventListener("storage",t)}},"watchLayouts",0,function(e){return N.add(e),()=>N.delete(e)}],39160)}]);
|
|
@@ -6,6 +6,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.0] - 2026-10-03
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Insights: your network ranked by power.** A new tab between Paths and Outlink. The Power
|
|
13
|
+
Index ranks everyone within two steps of you, each person once, with shared ranks for ties and
|
|
14
|
+
a line saying why a big tie shares its score; filter by degree, tier and rarity, search by name,
|
|
15
|
+
company or who knows them, and from any row open their circle, scan it, or ask (their LinkedIn
|
|
16
|
+
profile opens and the request is marked sent, as on their card). Beside it: Untapped (S and A
|
|
17
|
+
in your 2nd degree with no request out), Kingmakers (the connections with the most S and A
|
|
18
|
+
behind them), Gatekeepers (who your S and A reach hangs on), Hidden giants (S-tier with one way
|
|
19
|
+
in), Company power, How sure (how much of the ranking rests on a guess), and Richest?, which
|
|
20
|
+
says plainly that the app never sees money. Kingmakers, Gatekeepers, Companies, Industries and
|
|
21
|
+
a scrolling Report are views of their own in the notch. Every number says where it came from;
|
|
22
|
+
a CSV import gets the boards that need no circles, and says so for the rest. On your own network every 2nd-degree row offers Auto beside Ask.
|
|
23
|
+
- **Auto: one press sends someone a connection request for you.** Next to every Connect (a person's
|
|
24
|
+
card, and Outlink's lists and next best moves) there's now an Auto button, the bright one. Press it
|
|
25
|
+
and the scanner's Chrome opens their profile, presses Connect and sends the request without a note,
|
|
26
|
+
then shows "Request sent" and marks it pending everywhere in the app. Connect still opens their
|
|
27
|
+
profile for you to do it yourself. One press is one person: never a batch, never on a timer, and it
|
|
28
|
+
waits while a scan runs. It asks once before the first one. When LinkedIn wants their email address
|
|
29
|
+
first, Auto closes that and sends nothing ("Use Connect to add them yourself"); when LinkedIn offers
|
|
30
|
+
a personal note (Premium), it sends without one. It only calls a request sent once their profile
|
|
31
|
+
shows it pending, and says so plainly when it can't tell. It stops at 15 requests in any 24 hours and
|
|
32
|
+
80 in any 7 days (LinkedIn doesn't publish its limit; about 100 a week is commonly reported), opens
|
|
33
|
+
their profile once like a scan does, holds during a pause after LinkedIn pushed back, and shows in
|
|
34
|
+
Settings → LinkedIn usage as Connection requests (Auto).
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- **A scan that stopped partway says so, on the person's card and on the Scan page.** Open someone
|
|
38
|
+
whose circle scan didn't finish and a line by their name says which page it stopped after, with
|
|
39
|
+
Resume right there; their scan box now reads Scan in progress, with Resume from page N as its main
|
|
40
|
+
button, instead of Cluster active. The card's Insights offer Resume for them too, not a scan that
|
|
41
|
+
reads the list again from page 1, and an empty circle in Bridge Chains whose scan stopped partway
|
|
42
|
+
says so, with Resume from page N, instead of saying their list has been read. On the Scan page, everyone stopped partway is in a new In progress
|
|
43
|
+
card right after step 4, with Resume for each person and Resume all, and each name opens their circle
|
|
44
|
+
on the map. It used to be folded away under Fine-tune the scanner.
|
|
45
|
+
- **The tabs sit on the header's line, and the notch hangs from them.** On a computer the row of
|
|
46
|
+
tabs rests on the line under the header, so a page's own buttons in the notch below join onto
|
|
47
|
+
it. The header is the same height on every tab: the Pending count that added a row on Degrees
|
|
48
|
+
and Separation is gone (sent requests are still under Outlink → Pending).
|
|
49
|
+
- **Separation starts with its slider.** The line counting people two steps away and the tier
|
|
50
|
+
strip under it are gone; the search box stays at the top.
|
|
51
|
+
- **The notch comes with the page.** Switching tabs, it waits for the next page to appear instead
|
|
52
|
+
of hanging over a blank screen while it loads.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- **A company called "Foods" or "Beverages" is in Retail, Consumer & Hospitality.** The word list
|
|
56
|
+
had "food" and "beverage" but not the plurals, so a company like Northwind Foods took its
|
|
57
|
+
industry from its people's headlines instead (tech, on a test network). Scores are worked out
|
|
58
|
+
again once after the update, since a company's industry can change its estimate and your sector.
|
|
59
|
+
- **Scan dots are whole on Paths.** The dots along the header's line while a scan runs were cut in
|
|
60
|
+
half above Paths' left panel; they also stop short of the tabs now that the tabs sit on the line.
|
|
61
|
+
- **The 3rd-degree dot in Degrees and Separation says why it can't be tapped.** It said "No one at
|
|
62
|
+
3rd degree yet" even with people found by company scans; it now says those are drawn on Network
|
|
63
|
+
Circle.
|
|
64
|
+
- **The ring round a scanned connection's dot shows how much of their list was read.** It showed 2
|
|
65
|
+
of 5 bars, "partly read", for everyone scanned, even when their whole list was in, and the card's
|
|
66
|
+
Insights offered to finish lists that were finished. The app now takes in how far each list was
|
|
67
|
+
read, which the scanner had been noting all along.
|
|
68
|
+
|
|
9
69
|
## [0.7.0] - 2026-10-03
|
|
10
70
|
|
|
11
71
|
### Added
|
|
@@ -193,6 +193,16 @@ in, and its most powerful people.
|
|
|
193
193
|
|
|
194
194
|
<img src="docs/img/paths.png" alt="Paths → Map with the company analyzer open: companies grouped by industry, and one company's people by level and ways in" width="100%">
|
|
195
195
|
|
|
196
|
+
**Insights.** Everyone within two steps of you in one table, ranked by power, each person
|
|
197
|
+
once: the **Power Index**, with filters for degree, tier and rarity, and Open circle, Scan
|
|
198
|
+
circle or Ask on every row. Beside it, and each a board of its own in the notch:
|
|
199
|
+
**Kingmakers** (your connections with the most S and A people behind them), **Gatekeepers**
|
|
200
|
+
(who your S and A reach hangs on), **Hidden giants** (S-tier with one way in), **Untapped**
|
|
201
|
+
(S and A with no request out), **Company power** and **Industries**. **Report** is the same
|
|
202
|
+
numbers as a page you scroll. Every number says where it came from, and nothing is about
|
|
203
|
+
money: *Richest?* says plainly that the app never sees it. Power ranks reachability, not
|
|
204
|
+
people.
|
|
205
|
+
|
|
196
206
|
**Scores.** How a power score is worked out, and the three things you can change about
|
|
197
207
|
it, in one place: your field (the sectors you work in), how tiers are graded (on your
|
|
198
208
|
network's curve or the fixed scale), and every scanned company's score.
|
|
@@ -202,6 +212,15 @@ circle offers its best five people at a time; mark invites as sent to fill the r
|
|
|
202
212
|
unlock the next five. It also shows the next best moves, levels and points (from
|
|
203
213
|
invites sent and people who accepted, never from browsing).
|
|
204
214
|
|
|
215
|
+
**Auto** *(needs a scan)*. Next to every Connect (a person's card, Outlink), Auto sends
|
|
216
|
+
that one person a connection request for you, from the scanner's Chrome, without a note.
|
|
217
|
+
One press, one person: never a batch, never on a timer. It asks once before the first one.
|
|
218
|
+
If LinkedIn wants their email address first, Auto closes that and sends nothing (use
|
|
219
|
+
Connect to add them yourself); if it offers a personal note (Premium), Auto sends without
|
|
220
|
+
one. It calls a request sent only once their profile shows it pending. It stops at 15 in
|
|
221
|
+
any 24 hours and 80 in any 7 days, opens their profile once (a profile view), and counts in
|
|
222
|
+
Settings → LinkedIn usage.
|
|
223
|
+
|
|
205
224
|
**Scan.** The guided scanner. It shows progress, the budget that's left, the cooldown
|
|
206
225
|
lock, and a **Paused** list you can carry on from.
|
|
207
226
|
|
|
@@ -287,6 +306,10 @@ Something out of date, or unfair to another tool? [Open an issue](https://github
|
|
|
287
306
|
>
|
|
288
307
|
> The app asks for an explicit "I understand" once, before the first scan.
|
|
289
308
|
>
|
|
309
|
+
> **The scanner reads; the one thing it ever sends is a connection request, and only when
|
|
310
|
+
> you press Auto** on a person (one each press, without a note, at most 15 a day and 80 a
|
|
311
|
+
> week). Auto asks once before its first request.
|
|
312
|
+
>
|
|
290
313
|
> Six Degrees is not affiliated with or endorsed by LinkedIn.
|
|
291
314
|
|
|
292
315
|
## First run
|
|
@@ -395,7 +418,8 @@ It estimates **network position**, not what anyone is worth as a person.
|
|
|
395
418
|
data folder as `csv-network.json` (names, positions, companies, profile links and when
|
|
396
419
|
you connected; never email addresses) until you click × beside *Your CSV*.
|
|
397
420
|
- The app contacts only these, and only when you act:
|
|
398
|
-
- **LinkedIn**, while you scan
|
|
421
|
+
- **LinkedIn**, while you scan, and when you press **Auto** (one connection request,
|
|
422
|
+
sent from the scanner's Chrome). Each profile photo is saved on your computer as the
|
|
399
423
|
scan reads it, and the app shows photos only from there, so looking at your network
|
|
400
424
|
never contacts LinkedIn. (Photos an older version kept as links to LinkedIn show
|
|
401
425
|
initials until the next scan, or *Save photos* on the Scan page, saves them.)
|
|
@@ -129,6 +129,14 @@ Everything stays local:
|
|
|
129
129
|
If you run the scanner, `chrome-profile/` in the data directory holds a live
|
|
130
130
|
logged-in LinkedIn session. Never copy, sync, or commit it.
|
|
131
131
|
|
|
132
|
+
The scanner reads LinkedIn. The one thing it ever sends there is a connection request,
|
|
133
|
+
and only when you press **Auto** on a person: one request per press, without a note, at
|
|
134
|
+
most 15 in any 24 hours and 80 in any 7 days. The app looks the person up by the id the
|
|
135
|
+
page sends and passes the scanner their profile URL from the database, never a URL or name
|
|
136
|
+
from the request, and refuses before anything starts during a cooldown, past those caps,
|
|
137
|
+
or before Auto's one-time question has been answered (`app/api/scraper/route.js`,
|
|
138
|
+
`lib/auto-connect.js`).
|
|
139
|
+
|
|
132
140
|
### Moving your data (Settings → Your data)
|
|
133
141
|
|
|
134
142
|
- **An export** is one `.sixdegrees` file the user saves and carries. It is
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { db as supabase } from '../../../lib/db';
|
|
2
1
|
import { getDb, decodeRow } from '../../../lib/db-client';
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
XP_SEND, XP_ACCEPT, awardXP, whose, copiesOf, inList, bestTier, markSent,
|
|
4
|
+
} from '../../../lib/requests';
|
|
4
5
|
|
|
5
6
|
// Requests you've sent, kept per PERSON.
|
|
6
7
|
//
|
|
@@ -13,56 +14,8 @@ import { resolveProfile } from '../../../lib/profile';
|
|
|
13
14
|
// asked through is kept (so once they accept, their card and circle say who
|
|
14
15
|
// introduced you: lib/promote.js keeps it), XP comes once per person, and Undo
|
|
15
16
|
// takes back all of it. lib/requests-client.js is the one way the pages call this.
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
const XP_SEND = { S: 25, A: 15, B: 10, C: 5, D: 2 };
|
|
19
|
-
const XP_ACCEPT = { S: 100, A: 60, B: 35, C: 15, D: 5 };
|
|
20
|
-
const TIER_ORDER = ['S', 'A', 'B', 'C', 'D'];
|
|
21
|
-
|
|
22
|
-
async function awardXP(userId, amount) {
|
|
23
|
-
if (!userId || !amount) return;
|
|
24
|
-
// Upsert: create if not exists, change if exists. Never below zero: an Undo
|
|
25
|
-
// takes back what a send gave, including sends from before sends gave any.
|
|
26
|
-
const { data: existing } = await supabase.from('user_stats').select('xp').eq('id', userId).single();
|
|
27
|
-
if (existing) {
|
|
28
|
-
await supabase.from('user_stats').update({ xp: Math.max(0, (existing.xp || 0) + amount) }).eq('id', userId);
|
|
29
|
-
} else if (amount > 0) {
|
|
30
|
-
await supabase.from('user_stats').insert([{ id: userId, xp: amount, level: 1 }]).catch(() => {});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** The profile a request belongs to: the one sent, else this machine's. */
|
|
35
|
-
function whose(userId) {
|
|
36
|
-
if (userId) return String(userId);
|
|
37
|
-
try { return resolveProfile({ create: false })?.id || null; } catch { return null; }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Every copy of one person: by the clicked row's profile URL, or the URL given. */
|
|
41
|
-
function copiesOf(db, { connectionId, profileUrl, userId }) {
|
|
42
|
-
let url = profileUrl ? String(profileUrl) : null;
|
|
43
|
-
let user = userId ? String(userId) : null;
|
|
44
|
-
let clicked = null;
|
|
45
|
-
if (connectionId != null) {
|
|
46
|
-
clicked = db.prepare('SELECT id, profile_url, user_id FROM linkedin_connections WHERE id = ?').get(String(connectionId)) || null;
|
|
47
|
-
if (clicked) {
|
|
48
|
-
url = clicked.profile_url || url;
|
|
49
|
-
user = user || clicked.user_id || null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const cols = 'id, degree, tier, outreach_status, unlock_status, unlocked_from_bridge_id';
|
|
53
|
-
let rows = [];
|
|
54
|
-
if (url) {
|
|
55
|
-
rows = user
|
|
56
|
-
? db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE profile_url = ? AND user_id = ?`).all(url, user)
|
|
57
|
-
: db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE profile_url = ?`).all(url);
|
|
58
|
-
} else if (clicked) {
|
|
59
|
-
rows = db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE id = ?`).all(clicked.id);
|
|
60
|
-
}
|
|
61
|
-
return { url, user, rows };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const inList = (ids) => ids.map(() => '?').join(', ');
|
|
65
|
-
const bestTier = (rows) => TIER_ORDER.find((t) => rows.some((r) => r.tier === t)) || 'D';
|
|
17
|
+
// The marking itself is lib/requests.js markSent, which Auto's sends use too
|
|
18
|
+
// (app/api/scraper/route.js).
|
|
66
19
|
|
|
67
20
|
export async function POST(request) {
|
|
68
21
|
try {
|
|
@@ -72,33 +25,20 @@ export async function POST(request) {
|
|
|
72
25
|
return Response.json({ error: 'Unknown action' }, { status: 400 });
|
|
73
26
|
}
|
|
74
27
|
const db = getDb();
|
|
28
|
+
|
|
29
|
+
if (action === 'mark-sent') {
|
|
30
|
+
const { saved, xp, already } = await markSent(db, { connectionId, profileUrl, userId, bridgeId });
|
|
31
|
+
// Nobody on file (the sample network lives in the browser): nothing to keep.
|
|
32
|
+
if (!saved) return Response.json({ success: true, saved: 0, xp: 0 });
|
|
33
|
+
return Response.json({ success: true, saved, xp, already });
|
|
34
|
+
}
|
|
35
|
+
|
|
75
36
|
const { user, rows } = copiesOf(db, { connectionId, profileUrl, userId });
|
|
76
37
|
// Nobody on file (the sample network lives in the browser): nothing to keep.
|
|
77
38
|
if (!rows.length) return Response.json({ success: true, saved: 0, xp: 0 });
|
|
78
39
|
const ids = rows.map((r) => r.id);
|
|
79
40
|
const now = new Date().toISOString();
|
|
80
41
|
|
|
81
|
-
if (action === 'mark-sent') {
|
|
82
|
-
const already = rows.some((r) => r.outreach_status === 'sent' || r.outreach_status === 'accepted');
|
|
83
|
-
db.prepare(`UPDATE linkedin_connections SET outreach_status = 'sent', updated_at = ?
|
|
84
|
-
WHERE id IN (${inList(ids)}) AND (outreach_status IS NULL OR outreach_status NOT IN ('sent', 'accepted'))`).run(now, ...ids);
|
|
85
|
-
db.prepare(`UPDATE linkedin_connections SET unlock_status = 'pending', updated_at = ?
|
|
86
|
-
WHERE id IN (${inList(ids)}) AND (unlock_status IS NULL OR unlock_status = 'locked')`).run(now, ...ids);
|
|
87
|
-
// Who you asked through: one of your own connections, or nothing.
|
|
88
|
-
if (bridgeId != null) {
|
|
89
|
-
const bridge = db.prepare(
|
|
90
|
-
`SELECT id, name FROM linkedin_connections WHERE id = ? AND degree = 1${user ? ' AND user_id = ?' : ''}`,
|
|
91
|
-
).get(...[String(bridgeId), ...(user ? [user] : [])]);
|
|
92
|
-
if (bridge) {
|
|
93
|
-
db.prepare(`UPDATE linkedin_connections SET unlocked_from_bridge_id = ?, unlocked_from_name = ?, updated_at = ?
|
|
94
|
-
WHERE id IN (${inList(ids)}) AND degree = 2 AND unlocked_from_bridge_id IS NULL`).run(bridge.id, bridge.name, now, ...ids);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const xp = already ? 0 : XP_SEND[bestTier(rows)] || 2;
|
|
98
|
-
await awardXP(whose(user), xp);
|
|
99
|
-
return Response.json({ success: true, saved: ids.length, xp, already });
|
|
100
|
-
}
|
|
101
|
-
|
|
102
42
|
if (action === 'mark-accepted') {
|
|
103
43
|
db.prepare(`UPDATE linkedin_connections SET outreach_status = 'accepted', updated_at = ? WHERE id IN (${inList(ids)})`).run(now, ...ids);
|
|
104
44
|
return Response.json({ success: true, saved: ids.length, xp: 0, accepts: XP_ACCEPT[bestTier(rows)] || 5 });
|
|
@@ -9,13 +9,15 @@ import { pausedList, readProgress, readUnclear } from '../../../lib/paused';
|
|
|
9
9
|
import { getDb, backUpDailyIfDue } from '../../../lib/db-client';
|
|
10
10
|
import { db as notesDb } from '../../../lib/db';
|
|
11
11
|
import { scanDoneNotification } from '../../../lib/notifications';
|
|
12
|
-
import { registerScanState } from '../../../lib/scan-state';
|
|
12
|
+
import { registerScanState, runningNow } from '../../../lib/scan-state';
|
|
13
13
|
import { pendingImport } from '../../../lib/data-import';
|
|
14
14
|
import { waitingPhotoCount } from '../../../lib/photos';
|
|
15
15
|
import { readSettings } from '../../../lib/settings';
|
|
16
16
|
import { riskAccepted, touchesLinkedIn, RISK_REFUSAL } from '../../../lib/scan-risk';
|
|
17
17
|
import { reachIndex, circleState } from '../../../lib/reach';
|
|
18
18
|
import { chromeInstalled, signedInFrom, SIGNED_IN_FILE, CHROME_REFUSAL } from '../../../lib/scanner-setup';
|
|
19
|
+
import { AUTO_REFUSAL, connectResultIn, connectMarks, inviteRefusal, personRefusal } from '../../../lib/auto-connect';
|
|
20
|
+
import { markSentRows, awardXP, whose, requestStanding } from '../../../lib/requests';
|
|
19
21
|
import {
|
|
20
22
|
pythonLooker, thisHostKey, scannerCommand, installSteps, downloadedPython, downloadVerified,
|
|
21
23
|
placeDownloadedPython, sweepSetupLeftovers, megabytes, SETUP_WORK_PREFIX, ScannerSetupError,
|
|
@@ -51,6 +53,8 @@ const state = registerScanState({
|
|
|
51
53
|
failure: null, // the last lines of stderr from a run that failed — its reason
|
|
52
54
|
pages: 0, // pages the running scan has read: the dots along the header's line (app/components/ScanTrail.js)
|
|
53
55
|
found: [], // how many people each of those pages found: the Scan page's cluster (app/components/ScanRadar.js)
|
|
56
|
+
invitee: null, // Auto's person: { id, name, profileUrl, userId, bridgeId }, looked up here (connectTarget)
|
|
57
|
+
outcome: null, // how Auto's request went: the scanner's "Connect result: …" line (lib/auto-connect.js)
|
|
54
58
|
});
|
|
55
59
|
|
|
56
60
|
// A page read, as scripts/scrape.py prints it: " Page 3... " as a circle or a
|
|
@@ -122,6 +126,9 @@ function push(line) {
|
|
|
122
126
|
}
|
|
123
127
|
const got = t.match(PAGE_FOUND);
|
|
124
128
|
if (got && state.found.length) state.found[state.found.length - 1] = Number(got[1]);
|
|
129
|
+
// Auto's one line saying how the request went (scrape.py main, --connect).
|
|
130
|
+
const result = state.action === 'connect' && connectResultIn(t);
|
|
131
|
+
if (result) state.outcome = result;
|
|
125
132
|
}
|
|
126
133
|
if (state.log.length > MAX_LOG) state.log.splice(0, state.log.length - MAX_LOG);
|
|
127
134
|
}
|
|
@@ -529,8 +536,48 @@ const ACTIONS = {
|
|
|
529
536
|
// 1,000 conversations at most), for the Social tab's "Read my whole history".
|
|
530
537
|
'messages-full': { flag: '--messages --full-history', label: 'Reading your whole messages history' },
|
|
531
538
|
photos: { flag: '--save-photos', label: 'Saving profile photos to this computer' },
|
|
539
|
+
// Auto (Blake, 2026-10-03): one connection request, without a note, to one
|
|
540
|
+
// person pressed on a card. By id, looked up here (connectTarget); the URL
|
|
541
|
+
// and name that reach the command line come from the database, never the page.
|
|
542
|
+
connect: { flag: '--connect', label: 'Sending a connection request to' },
|
|
532
543
|
};
|
|
533
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Auto's person, by id: { person: { id, name, profileUrl, userId, bridgeId } },
|
|
547
|
+
* or why not: { error, status }. Anyone in your network on this computer with a
|
|
548
|
+
* LinkedIn profile who isn't already one of your connections (no copy of them
|
|
549
|
+
* at degree 1) and has no request out. The URL is checked like every other
|
|
550
|
+
* that reaches a command line.
|
|
551
|
+
*/
|
|
552
|
+
function connectTarget(id) {
|
|
553
|
+
try {
|
|
554
|
+
const me = resolveProfile({ create: false });
|
|
555
|
+
const db = getDb();
|
|
556
|
+
const row = me && db.prepare(
|
|
557
|
+
'SELECT id, name, degree, profile_url, source_connection_id FROM linkedin_connections WHERE id = ? AND user_id = ?',
|
|
558
|
+
).get(String(id ?? ''), me.id);
|
|
559
|
+
if (!row) return personRefusal(null);
|
|
560
|
+
const why = personRefusal(row, requestStanding(db, { profileUrl: row.profile_url, userId: me.id }));
|
|
561
|
+
if (why) return why;
|
|
562
|
+
const profileUrl = cleanProfileUrl(row.profile_url);
|
|
563
|
+
if (!profileUrl) return { status: 400, error: `${row.name || 'Their'} LinkedIn profile address on file can’t be used.` };
|
|
564
|
+
return {
|
|
565
|
+
person: {
|
|
566
|
+
id: row.id, name: cleanName(row.name) || null, profileUrl, userId: me.id,
|
|
567
|
+
// Who you'd be asking through: the connection whose circle they're in.
|
|
568
|
+
bridgeId: row.degree === 2 ? row.source_connection_id ?? null : null,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
} catch {
|
|
572
|
+
return personRefusal(null);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Auto's one-time yes (lib/auto-connect.js), given on the page that first offers it. */
|
|
577
|
+
function autoAccepted() {
|
|
578
|
+
try { return Boolean(readSettings(getDb()).autoConnectAccepted); } catch { return false; }
|
|
579
|
+
}
|
|
580
|
+
|
|
534
581
|
/** Only a plain linkedin.com/in/ profile URL becomes an argument. */
|
|
535
582
|
function cleanProfileUrl(raw) {
|
|
536
583
|
const url = String(raw ?? '').trim();
|
|
@@ -651,6 +698,15 @@ export async function POST(request) {
|
|
|
651
698
|
if (!Object.hasOwn(ACTIONS, action)) {
|
|
652
699
|
return Response.json({ error: `Unknown action '${action}'` }, { status: 400 });
|
|
653
700
|
}
|
|
701
|
+
// Auto: who, by id, and whether a request can go to them at all, before
|
|
702
|
+
// anything else is asked. Then its own one-time yes (the page asks it).
|
|
703
|
+
let invitee = null;
|
|
704
|
+
if (action === 'connect') {
|
|
705
|
+
const found = connectTarget(body.id);
|
|
706
|
+
if (found.error) return Response.json({ error: found.error }, { status: found.status });
|
|
707
|
+
invitee = found.person;
|
|
708
|
+
if (!autoAccepted()) return Response.json({ error: AUTO_REFUSAL, needsAutoAcceptance: true }, { status: 409 });
|
|
709
|
+
}
|
|
654
710
|
// Nothing opens LinkedIn before the one-time "I understand" on the Scan page,
|
|
655
711
|
// whichever button asked (lib/scan-risk.js).
|
|
656
712
|
if (touchesLinkedIn(action) && !scanRisk()) {
|
|
@@ -707,12 +763,14 @@ export async function POST(request) {
|
|
|
707
763
|
// never reaches the command line (a name or the URL looked up for it does),
|
|
708
764
|
// and anything not shaped like one of our ids is dropped.
|
|
709
765
|
const hintId = typeof body.id === 'string' && /^[\w-]{1,64}$/.test(body.id) ? body.id : null;
|
|
710
|
-
const target =
|
|
766
|
+
const target = invitee ? { id: invitee.id, name: invitee.name }
|
|
767
|
+
: name || profileUrl ? { id: hintId, name: name || person?.name || null } : null;
|
|
711
768
|
// Nothing that searches LinkedIn starts during a cooldown; the scanner checks
|
|
712
769
|
// too, this just says so before a process is spawned.
|
|
713
770
|
// The 1st-degree scans aren't searches, but they open LinkedIn with automation too.
|
|
771
|
+
// Auto's request too: a pause after LinkedIn pushed back holds everything.
|
|
714
772
|
const searches = spec.searches || action.startsWith('auto-bridge')
|
|
715
|
-
|| ['bridge', 'rescrape', 'company', 'full', 'refresh'].includes(action);
|
|
773
|
+
|| ['bridge', 'rescrape', 'company', 'full', 'refresh', 'connect'].includes(action);
|
|
716
774
|
// A staged import replaces the network at the next start, so anything scanned
|
|
717
775
|
// now would land in the copy that is about to be set aside. Setting the
|
|
718
776
|
// scanner up touches no network data.
|
|
@@ -723,7 +781,15 @@ export async function POST(request) {
|
|
|
723
781
|
if (searches && cooldown) {
|
|
724
782
|
return Response.json({ error: `Scanning is paused until ${new Date(cooldown.until).toLocaleString()}: ${cooldown.reason}.`, cooldown }, { status: 409 });
|
|
725
783
|
}
|
|
784
|
+
// Auto's caps (15 in any 24 hours, 80 in any 7 days) and the profile view it
|
|
785
|
+
// takes, said before a process starts; the scanner checks them again.
|
|
786
|
+
if (invitee) {
|
|
787
|
+
const capped = inviteRefusal(linkedinState(dataDir()));
|
|
788
|
+
if (capped) return Response.json({ error: capped }, { status: 409 });
|
|
789
|
+
}
|
|
726
790
|
if (state.running) {
|
|
791
|
+
// One thing at a time: Auto waits for a scan like any other job.
|
|
792
|
+
if (invitee) return Response.json({ error: `${runningNow(state.action)}. Press Auto again once it has finished.`, action: state.action }, { status: 409 });
|
|
727
793
|
return Response.json({ error: 'Something is already running.', action: state.action }, { status: 409 });
|
|
728
794
|
}
|
|
729
795
|
|
|
@@ -759,7 +825,8 @@ export async function POST(request) {
|
|
|
759
825
|
// "-" can then never be read as a flag of its own. Anyone found by
|
|
760
826
|
// id is --bridge-url, which carries on where their last read stopped
|
|
761
827
|
// unless told to start at page 1.
|
|
762
|
-
...(
|
|
828
|
+
...(invitee ? [`--connect=${invitee.profileUrl}`, ...(invitee.name ? [`--connect-name=${invitee.name}`] : [])]
|
|
829
|
+
: name ? [`${spec.flag}=${name}`] : profileUrl ? [`--bridge-url=${profileUrl}`] : spec.flag.split(' ')),
|
|
763
830
|
...(profileUrl && action === 'bridge' && !deeper ? ['--from-start'] : []),
|
|
764
831
|
...(maxBridges && (action.startsWith('auto-bridge') || action === 'resume-all') ? [`--max-bridges=${maxBridges}`] : []),
|
|
765
832
|
...(tiers.length && action.startsWith('auto-bridge') ? [`--tiers=${tiers.join(',')}`] : []),
|
|
@@ -793,6 +860,8 @@ export async function POST(request) {
|
|
|
793
860
|
state.found = [];
|
|
794
861
|
state.stderrTail = [];
|
|
795
862
|
state.failure = null;
|
|
863
|
+
state.invitee = invitee;
|
|
864
|
+
state.outcome = null;
|
|
796
865
|
forgetChecks();
|
|
797
866
|
|
|
798
867
|
// Name the profile outright. The scraper would otherwise ask the app which one
|
|
@@ -810,7 +879,29 @@ export async function POST(request) {
|
|
|
810
879
|
SIX_DEGREES_FROM_APP: '1',
|
|
811
880
|
};
|
|
812
881
|
|
|
882
|
+
/**
|
|
883
|
+
* Auto: a request LinkedIn showed as pending (or one already pending there)
|
|
884
|
+
* is marked as sent through the same store as the Connect button
|
|
885
|
+
* (lib/requests.js), before the job is seen to end, so every view that looks
|
|
886
|
+
* next finds it; its XP follows. Anything else marks nothing (TRAPS §7).
|
|
887
|
+
*/
|
|
888
|
+
function markAutoSent() {
|
|
889
|
+
const who = state.invitee;
|
|
890
|
+
if (state.action !== 'connect' || !who || who.marked || !connectMarks(state.outcome)) return;
|
|
891
|
+
who.marked = true;
|
|
892
|
+
try {
|
|
893
|
+
const { saved, xp, user } = markSentRows(getDb(), {
|
|
894
|
+
connectionId: who.id, profileUrl: who.profileUrl, userId: who.userId, bridgeId: who.bridgeId,
|
|
895
|
+
});
|
|
896
|
+
if (saved) Promise.resolve(awardXP(whose(user), xp)).catch(() => {});
|
|
897
|
+
push('Marked as sent in Six Degrees.');
|
|
898
|
+
} catch (err) {
|
|
899
|
+
push(`Sent on LinkedIn, but it couldn’t be marked as sent here: ${err.message}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
813
903
|
function finish(code) {
|
|
904
|
+
markAutoSent();
|
|
814
905
|
// A failure must say why. stderr is filtered while running because pip and
|
|
815
906
|
// Playwright are noisy there, and that filter once swallowed the only line
|
|
816
907
|
// explaining a failed scan, leaving just "exit 1". On a failure, show the
|
|
@@ -834,6 +925,8 @@ export async function POST(request) {
|
|
|
834
925
|
state.exitCode = code;
|
|
835
926
|
state.recent = [{
|
|
836
927
|
action: state.action, target: state.target, startedAt: state.startedAt, exitCode: code, failure: state.failure,
|
|
928
|
+
// How Auto's request went, for the button that started it (lib/auto-connect.js connectOutcome).
|
|
929
|
+
...(state.action === 'connect' ? { outcome: state.outcome } : {}),
|
|
837
930
|
}, ...state.recent].slice(0, 5);
|
|
838
931
|
// A scan that finished leaves a notification ("Scan done: …"); a stop or a failure doesn't.
|
|
839
932
|
const done = scanDoneNotification({ action: state.action, target: state.target, exitCode: code, stopped, log: state.log.slice(-12), userId: profileId || null });
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
// The row is a grid of 1fr | auto | 1fr: the two sides share what's left
|
|
11
11
|
// equally, so the tabs sit in the middle whatever is beside them. A window too
|
|
12
12
|
// narrow for that lets the sides shrink to fit before anything overlaps.
|
|
13
|
+
//
|
|
14
|
+
// On a computer the tabs sit on the header's bottom line, so the notch that
|
|
15
|
+
// hangs under the line (ScanStatusBar) hangs from the tabs themselves (Blake,
|
|
16
|
+
// 2026-10-03: "lower the section of buttons right to the line so the notch is
|
|
17
|
+
// touching"). They're moved down, not laid out lower, so the header keeps its
|
|
18
|
+
// height and every page under it stays where it was.
|
|
13
19
|
|
|
14
20
|
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
15
21
|
import Link from 'next/link';
|
|
@@ -36,6 +42,11 @@ function useIsMobile() {
|
|
|
36
42
|
return m;
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
// The header's padding top and bottom, and the gap under the row: the tabs
|
|
46
|
+
// move down by the last two to reach the line.
|
|
47
|
+
const PAD_Y = 20;
|
|
48
|
+
const ROW_GAP = 8;
|
|
49
|
+
|
|
39
50
|
const TITLE = { fontWeight: 700, margin: 0, whiteSpace: 'nowrap', background: 'linear-gradient(135deg, #FFD700, #9B59B6, #3498DB)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' };
|
|
40
51
|
|
|
41
52
|
/**
|
|
@@ -53,22 +64,27 @@ export default function AppHeader({ active, isMobile: mobileProp, csvMode = fals
|
|
|
53
64
|
const mobileSeen = useIsMobile();
|
|
54
65
|
const isMobile = mobileProp ?? mobileSeen;
|
|
55
66
|
const own = !IS_DEMO && !csvMode; // scanning and notifications are for your own network
|
|
67
|
+
const tabs = (
|
|
68
|
+
<AppTabs active={active} isMobile={isMobile} csvMode={csvMode} onMode={onMode}
|
|
69
|
+
after={own ? <AutoScanButton isMobile={isMobile} /> : null}>
|
|
70
|
+
{chips}
|
|
71
|
+
</AppTabs>
|
|
72
|
+
);
|
|
56
73
|
return (
|
|
57
74
|
<header style={{
|
|
58
|
-
position: 'relative', padding: isMobile ? '10px 12px' :
|
|
75
|
+
position: 'relative', padding: isMobile ? '10px 12px' : `${PAD_Y}px 30px`, borderBottom: '1px solid rgba(var(--sd-ink, 255, 255, 255), 0.1)', flexShrink: 0,
|
|
59
76
|
// Clear, or frosted glass on a glass theme (lib/themes.js).
|
|
60
77
|
background: 'var(--sd-header)', backdropFilter: 'var(--sd-header-blur)', WebkitBackdropFilter: 'var(--sd-header-blur)',
|
|
61
78
|
}}>
|
|
62
79
|
<div style={isMobile
|
|
63
80
|
? { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 6, marginBottom: 4 }
|
|
64
|
-
: { display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', gap: 16, marginBottom:
|
|
81
|
+
: { display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', gap: 16, marginBottom: ROW_GAP }}>
|
|
65
82
|
<Link href="/" style={{ textDecoration: 'none', justifySelf: 'start' }}>
|
|
66
83
|
<Brand style={{ ...TITLE, fontSize: isMobile ? 16 : 28 }}>Six Degrees</Brand>
|
|
67
84
|
</Link>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
</AppTabs>
|
|
85
|
+
{isMobile ? tabs : (
|
|
86
|
+
<div style={{ alignSelf: 'end', position: 'relative', top: ROW_GAP + PAD_Y, zIndex: 2 }}>{tabs}</div>
|
|
87
|
+
)}
|
|
72
88
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', justifySelf: 'end', gap: isMobile ? 4 : 8 }}>
|
|
73
89
|
{/* Settings: also in CSV or sample mode and on phones, so updates stay reachable */}
|
|
74
90
|
{!IS_DEMO && <Link href="/settings" title="Settings" aria-label="Settings" style={{
|
|
@@ -17,6 +17,10 @@ const MODES = [
|
|
|
17
17
|
];
|
|
18
18
|
const PAGES = [
|
|
19
19
|
{ key: 'paths', href: '/paths', label: 'Paths', color: 'var(--sd-green, #00ff88)', on: 'linear-gradient(135deg, #00ff88, #3498DB)', demo: true },
|
|
20
|
+
// Your network ranked by power, and the boards that read it (Blake, 2026-10-03:
|
|
21
|
+
// "stats and insights … based off their circles"). Purple to gold, its own colour.
|
|
22
|
+
// The sample and a CSV import have something to rank too, so it isn't `own`.
|
|
23
|
+
{ key: 'insights', href: '/insights', label: 'Insights', color: 'var(--sd-purple, #c39bd3)', on: 'linear-gradient(135deg, #9B59B6, #FFD700)' },
|
|
20
24
|
// Social is part of Outlink now (Messages & follow-ups); /social forwards there.
|
|
21
25
|
{ key: 'outlink', href: '/queue', label: 'Outlink', color: 'var(--sd-orange, #FF6B35)', on: 'linear-gradient(135deg, #FF6B35, #FFD700)', own: true },
|
|
22
26
|
{ key: 'scan', href: '/setup', label: 'Scan', color: 'var(--sd-fg-4, #666)', on: 'linear-gradient(135deg, #00ff88, #1abc9c)', own: true },
|