six-degrees 0.4.0-beta.1 → 0.4.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/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +5 -5
- package/.next/standalone/.next/required-server-files.json +6 -1
- package/.next/standalone/.next/routes-manifest.json +12 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
- 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/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 +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 +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 +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 +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +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 +13 -13
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
- 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 +13 -13
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +13 -13
- 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 +12 -12
- 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 +13 -13
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
- 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 +12 -12
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0rc27za._.js → [root-of-the-server]__0bat8nx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -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]__0o5-tcg._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -6
- package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +12 -8
- package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0cu6-an._.js → _1vpfyd-._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.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/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
- package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
- package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
- package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
- package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
- package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
- package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
- package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
- package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
- package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
- package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
- package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
- package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
- package/.next/standalone/CHANGELOG.md +152 -0
- package/.next/standalone/CONTRIBUTING.md +15 -4
- package/.next/standalone/README.md +21 -15
- package/.next/standalone/SECURITY.md +10 -0
- package/.next/standalone/app/api/ingest/route.js +6 -1
- package/.next/standalone/app/api/scraper/route.js +79 -11
- package/.next/standalone/app/api/update-images/route.js +41 -7
- package/.next/standalone/app/avatars/[file]/route.js +6 -1
- package/.next/standalone/app/components/Avatar.js +5 -2
- package/.next/standalone/app/components/ChainView.js +659 -469
- package/.next/standalone/app/components/EmptyState.js +1 -1
- package/.next/standalone/app/components/FieldStep.js +94 -0
- package/.next/standalone/app/components/ForceGraph.js +183 -768
- package/.next/standalone/app/components/GridView.js +9 -7
- package/.next/standalone/app/components/LinkedInLimits.js +22 -7
- package/.next/standalone/app/components/ListView.js +6 -4
- package/.next/standalone/app/components/MappingProgress.js +5 -6
- package/.next/standalone/app/components/OnboardingGate.js +1 -1
- package/.next/standalone/app/components/OrbitGraph.js +3 -1
- package/.next/standalone/app/components/OutlinkQuest.js +5 -4
- package/.next/standalone/app/components/RingsView.js +9 -8
- package/.next/standalone/app/components/Sidebar.js +172 -112
- package/.next/standalone/app/components/TheirCircle.js +18 -11
- package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
- package/.next/standalone/app/components/settings/SectorSection.js +17 -191
- package/.next/standalone/app/components/settings/TierSection.js +7 -12
- package/.next/standalone/app/components/views.js +2 -2
- package/.next/standalone/app/globals.css +7 -0
- package/.next/standalone/app/launch/page.js +2 -2
- package/.next/standalone/app/page.js +130 -25
- package/.next/standalone/app/paths/page.js +3 -2
- package/.next/standalone/app/queue/page.js +18 -10
- package/.next/standalone/app/setup/page.js +423 -260
- package/.next/standalone/db/schema.js +1 -1
- package/.next/standalone/eslint.config.mjs +3 -3
- package/.next/standalone/install.sh +3 -3
- package/.next/standalone/lib/chain-layout.js +139 -0
- package/.next/standalone/lib/csv.js +4 -6
- package/.next/standalone/lib/data-export.js +2 -1
- package/.next/standalone/lib/data-folder.js +4 -2
- package/.next/standalone/lib/galaxy.js +12 -0
- package/.next/standalone/lib/ingest.js +15 -8
- package/.next/standalone/lib/linkedin-limits.js +30 -8
- package/.next/standalone/lib/photos.js +47 -0
- package/.next/standalone/lib/quest.js +8 -3
- package/.next/standalone/lib/reach.js +113 -0
- package/.next/standalone/lib/scan-state.js +1 -0
- package/.next/standalone/lib/scanner-setup.js +20 -1
- package/.next/standalone/lib/scoring.js +769 -18
- package/.next/standalone/lib/scraper-client.js +27 -0
- package/.next/standalone/lib/sector-focus.js +13 -0
- package/.next/standalone/lib/separation.js +4 -2
- package/.next/standalone/lib/settings-client.js +21 -0
- package/.next/standalone/lib/settings.js +3 -1
- package/.next/standalone/next.config.mjs +25 -3
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/build-app.mjs +11 -2
- package/.next/standalone/scripts/check-node.mjs +1 -1
- package/.next/standalone/scripts/image_store.py +77 -21
- package/.next/standalone/scripts/scrape.py +243 -29
- package/.next/standalone/server.js +1 -1
- package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
- package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
- package/.next/static/chunks/0l1568qff0efj.js +1 -0
- package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
- package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
- package/.next/static/chunks/135n87ydxjrra.js +2 -0
- package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
- package/.next/static/chunks/28bu26ausqzmn.js +1 -0
- package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
- package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
- package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
- package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
- package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
- package/CHANGELOG.md +152 -0
- package/README.md +21 -15
- package/SECURITY.md +10 -0
- package/db/schema.js +1 -1
- package/lib/chain-layout.js +139 -0
- package/lib/csv.js +4 -6
- package/lib/data-export.js +2 -1
- package/lib/data-folder.js +4 -2
- package/lib/galaxy.js +12 -0
- package/lib/ingest.js +15 -8
- package/lib/linkedin-limits.js +30 -8
- package/lib/photos.js +47 -0
- package/lib/quest.js +8 -3
- package/lib/reach.js +113 -0
- package/lib/scan-state.js +1 -0
- package/lib/scanner-setup.js +20 -1
- package/lib/scoring.js +769 -18
- package/lib/scraper-client.js +27 -0
- package/lib/sector-focus.js +13 -0
- package/lib/separation.js +4 -2
- package/lib/settings-client.js +21 -0
- package/lib/settings.js +3 -1
- package/package.json +1 -1
- package/scripts/check-node.mjs +1 -1
- package/scripts/image_store.py +77 -21
- package/scripts/scrape.py +243 -29
- package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0223v9s._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0bcf13g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0wxbq78._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0z88zcl._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
- package/.next/standalone/.next/static/chunks/0_yurn4q91vq8.js +0 -16
- package/.next/standalone/.next/static/chunks/0d_mh-06srh2k.js +0 -1
- package/.next/standalone/.next/static/chunks/0x3oadfikvb1c.js +0 -1
- package/.next/standalone/.next/static/chunks/1d5hl1io798n2.js +0 -1
- package/.next/standalone/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
- package/.next/standalone/.next/static/chunks/1roznk7hkjes1.js +0 -44
- package/.next/standalone/.next/static/chunks/2_bf2n4gstad9.js +0 -33
- package/.next/standalone/.next/static/chunks/36gbwek52x_w-.js +0 -2
- package/.next/standalone/.next/static/chunks/3a55suosthntc.js +0 -1
- package/.next/standalone/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
- package/.next/standalone/.next/static/chunks/3nbnio971zlji.js +0 -2
- package/.next/standalone/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
- package/.next/static/chunks/0_yurn4q91vq8.js +0 -16
- package/.next/static/chunks/0d_mh-06srh2k.js +0 -1
- package/.next/static/chunks/0x3oadfikvb1c.js +0 -1
- package/.next/static/chunks/1d5hl1io798n2.js +0 -1
- package/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
- package/.next/static/chunks/1roznk7hkjes1.js +0 -44
- package/.next/static/chunks/2_bf2n4gstad9.js +0 -33
- package/.next/static/chunks/36gbwek52x_w-.js +0 -2
- package/.next/static/chunks/3a55suosthntc.js +0 -1
- package/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
- package/.next/static/chunks/3nbnio971zlji.js +0 -2
- package/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
- /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
- /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Every statement is CREATE ... IF NOT EXISTS, so applying it is both
|
|
10
10
|
// first-run setup and a no-op on every later boot.
|
|
11
11
|
|
|
12
|
-
export const SCHEMA_SQL = `--
|
|
12
|
+
export const SCHEMA_SQL = `-- Six Degrees — local SQLite schema.
|
|
13
13
|
--
|
|
14
14
|
-- Ported from the Postgres schema this project ran on before it went
|
|
15
15
|
-- local-first. Type mapping: uuid -> TEXT (generated in JS), jsonb and
|
|
@@ -23,9 +23,9 @@ const eslintConfig = defineConfig([
|
|
|
23
23
|
// surface in review.
|
|
24
24
|
"react-hooks/set-state-in-effect": "warn",
|
|
25
25
|
|
|
26
|
-
// Avatars are
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// Avatars are small local files the scanner saved (lib/photos.js), with
|
|
27
|
+
// graceful initial-letter fallbacks. next/image would add a loader and
|
|
28
|
+
// per-request cost for no benefit here.
|
|
29
29
|
"@next/next/no-img-element": "off",
|
|
30
30
|
},
|
|
31
31
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Install
|
|
2
|
+
# Install Six Degrees on a Mac with one line:
|
|
3
3
|
#
|
|
4
4
|
# curl -fsSL https://raw.githubusercontent.com/blakeb056/six-degrees/main/install.sh | bash
|
|
5
5
|
#
|
|
@@ -108,7 +108,7 @@ stop_running_copy() {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
main() {
|
|
111
|
-
printf '\n
|
|
111
|
+
printf '\n Six Degrees — installer\n\n'
|
|
112
112
|
|
|
113
113
|
[ "$(uname -s)" = "Darwin" ] || fail "This installer is for macOS. On Linux, run: npx six-degrees (needs Node 22.13 or later). Windows isn't supported yet."
|
|
114
114
|
|
|
@@ -182,7 +182,7 @@ main() {
|
|
|
182
182
|
|
|
183
183
|
if [ -z "${SIX_DEGREES_NO_OPEN:-}" ]; then
|
|
184
184
|
open "$target"
|
|
185
|
-
printf '\n ✓ Opening
|
|
185
|
+
printf '\n ✓ Opening Six Degrees. Next time, open it from Applications or Spotlight.\n'
|
|
186
186
|
else
|
|
187
187
|
printf '\n ✓ Done.\n'
|
|
188
188
|
fi
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Where Bridge Chains puts a circle's dots.
|
|
2
|
+
//
|
|
3
|
+
// Blake, 2026-09-28: "when i scan someone and have a lot of 2nd degree its
|
|
4
|
+
// almost a solid line and needs to expand more". Every dot sat on one ring, so
|
|
5
|
+
// 200 people round one circle were a band and 800 were a solid line. Here a
|
|
6
|
+
// circle fills rings from the inside out instead, as many as its count needs:
|
|
7
|
+
// a small circle is one ring, a big one spreads into more, and neighbours keep
|
|
8
|
+
// their distance. Plain maths, no React; tests/chain-layout.test.mjs.
|
|
9
|
+
|
|
10
|
+
const TAU = Math.PI * 2;
|
|
11
|
+
|
|
12
|
+
/** How many dots fit on a ring of `radius` across `sweep` radians, `spacing` apart. */
|
|
13
|
+
function capacity(radius, spacing, sweep) {
|
|
14
|
+
if (radius <= 0) return 1;
|
|
15
|
+
const arc = sweep * radius;
|
|
16
|
+
// A whole circle closes on itself; an arc has a dot at each end.
|
|
17
|
+
return Math.max(1, sweep >= TAU - 1e-9 ? Math.floor(arc / spacing) : Math.floor(arc / spacing) + 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The rings from `inner` outwards, `spacing` apart, until `n` dots fit. */
|
|
21
|
+
function ringsFor(n, inner, spacing, sweep) {
|
|
22
|
+
const radii = [];
|
|
23
|
+
let room = 0;
|
|
24
|
+
for (let r = inner; room < n; r += spacing) {
|
|
25
|
+
radii.push(r);
|
|
26
|
+
room += capacity(r, spacing, sweep);
|
|
27
|
+
}
|
|
28
|
+
return radii;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const outerOf = (n, inner, spacing, sweep) => {
|
|
32
|
+
const radii = ringsFor(n, inner, spacing, sweep);
|
|
33
|
+
return radii[radii.length - 1];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Places `n` dots in rings round a centre at (0, 0).
|
|
38
|
+
*
|
|
39
|
+
* The first ring sits at `inner`, and each next one `spacing` further out.
|
|
40
|
+
* When that would run past `outer`, the rings first start further in (down to
|
|
41
|
+
* `innerMin`), then close up (down to `minSpacing`); past that they carry on
|
|
42
|
+
* beyond `outer` rather than overlap, and zooming out shows them. The dots on
|
|
43
|
+
* each ring share it evenly, in proportion to its length, so every ring is
|
|
44
|
+
* about as full as the rest; the first `n` of a list sorted best-first land
|
|
45
|
+
* nearest the centre. Every other ring is turned by half a step, so dots on
|
|
46
|
+
* neighbouring rings don't line up in spokes.
|
|
47
|
+
*
|
|
48
|
+
* `start` and `sweep` fill an arc instead of the whole circle, each ring from
|
|
49
|
+
* its middle outwards.
|
|
50
|
+
*
|
|
51
|
+
* @returns {{ spacing: number, inner: number, rings: { radius: number, count: number }[],
|
|
52
|
+
* points: { x: number, y: number, angle: number, ring: number }[] }}
|
|
53
|
+
*/
|
|
54
|
+
export function ringLayout(n, {
|
|
55
|
+
inner, outer, spacing, minSpacing = spacing / 3, innerMin = inner,
|
|
56
|
+
start = -Math.PI / 2, sweep = TAU,
|
|
57
|
+
} = {}) {
|
|
58
|
+
const count = Math.max(0, Math.floor(n) || 0);
|
|
59
|
+
if (!count) return { spacing, inner, rings: [], points: [] };
|
|
60
|
+
|
|
61
|
+
let s = spacing;
|
|
62
|
+
let r0 = inner;
|
|
63
|
+
if (outerOf(count, r0, s, sweep) > outer) {
|
|
64
|
+
// Start further in, as far as innerMin, before closing anything up.
|
|
65
|
+
if (outerOf(count, innerMin, s, sweep) <= outer) {
|
|
66
|
+
let lo = innerMin, hi = inner;
|
|
67
|
+
for (let i = 0; i < 24; i++) {
|
|
68
|
+
const mid = (lo + hi) / 2;
|
|
69
|
+
if (outerOf(count, mid, s, sweep) <= outer) lo = mid; else hi = mid;
|
|
70
|
+
}
|
|
71
|
+
r0 = lo;
|
|
72
|
+
} else {
|
|
73
|
+
r0 = innerMin;
|
|
74
|
+
let lo = minSpacing, hi = spacing;
|
|
75
|
+
if (outerOf(count, r0, lo, sweep) <= outer) {
|
|
76
|
+
for (let i = 0; i < 24; i++) {
|
|
77
|
+
const mid = (lo + hi) / 2;
|
|
78
|
+
if (outerOf(count, r0, mid, sweep) <= outer) lo = mid; else hi = mid;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
s = lo;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const radii = ringsFor(count, r0, s, sweep);
|
|
86
|
+
const caps = radii.map((r) => capacity(r, s, sweep));
|
|
87
|
+
const room = caps.reduce((a, b) => a + b, 0);
|
|
88
|
+
|
|
89
|
+
// Share the dots in proportion to each ring's room, never over it; what the
|
|
90
|
+
// rounding leaves goes to the rings with the most room left.
|
|
91
|
+
const counts = caps.map((c) => Math.min(c, Math.floor((count * c) / room)));
|
|
92
|
+
let left = count - counts.reduce((a, b) => a + b, 0);
|
|
93
|
+
while (left > 0) {
|
|
94
|
+
let best = -1;
|
|
95
|
+
for (let k = 0; k < caps.length; k++) {
|
|
96
|
+
if (counts[k] < caps[k] && (best < 0 || caps[k] - counts[k] > caps[best] - counts[best])) best = k;
|
|
97
|
+
}
|
|
98
|
+
counts[best]++;
|
|
99
|
+
left--;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const whole = sweep >= TAU - 1e-9;
|
|
103
|
+
const points = [];
|
|
104
|
+
const rings = [];
|
|
105
|
+
radii.forEach((radius, k) => {
|
|
106
|
+
const m = counts[k];
|
|
107
|
+
if (!m) return;
|
|
108
|
+
rings.push({ radius, count: m });
|
|
109
|
+
// Round a whole circle evenly; across an arc from one edge to the other, a
|
|
110
|
+
// lone dot in the middle of it. An arc fills from its middle outwards, so
|
|
111
|
+
// the first in the list sit nearest whatever the arc fans out from.
|
|
112
|
+
const step = whole ? TAU / m : m > 1 ? sweep / (m - 1) : 0;
|
|
113
|
+
const from = whole ? start + (k % 2 ? step / 2 : 0) : m > 1 ? start : start + sweep / 2;
|
|
114
|
+
const slots = whole ? null : middleOut(m);
|
|
115
|
+
for (let j = 0; j < m; j++) {
|
|
116
|
+
const angle = from + (slots ? slots[j] : j) * step;
|
|
117
|
+
points.push({ x: radius * Math.cos(angle), y: radius * Math.sin(angle), angle, ring: rings.length - 1 });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return { spacing: s, inner: r0, rings, points };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 0 … m-1 from the middle outwards: for 5, 2 3 1 4 0. */
|
|
124
|
+
function middleOut(m) {
|
|
125
|
+
let lo = Math.floor((m - 1) / 2);
|
|
126
|
+
let hi = lo + 1;
|
|
127
|
+
const out = [lo--];
|
|
128
|
+
while (out.length < m) {
|
|
129
|
+
if (hi < m) out.push(hi++);
|
|
130
|
+
if (out.length < m && lo >= 0) out.push(lo--);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A dot's radius for a layout's spacing: bigger for higher tiers, never touching a neighbour. */
|
|
136
|
+
export function dotRadius(spacing, tier) {
|
|
137
|
+
const scale = tier === 'S' ? 1 : tier === 'A' ? 0.86 : tier === 'B' ? 0.72 : 0.6;
|
|
138
|
+
return Math.max(1.4, Math.min(7, spacing * 0.42 * scale));
|
|
139
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { scorePerson, curveCutoffs, curvedTier } from './scoring.js';
|
|
2
|
+
import { toIsoDate } from './ingest.js';
|
|
2
3
|
// LinkedIn Connections.csv import — parsed ENTIRELY in the browser.
|
|
3
4
|
//
|
|
4
5
|
// Nothing is uploaded and nothing is written to the database: an imported
|
|
@@ -136,12 +137,9 @@ export function parseConnectionsCsv(text) {
|
|
|
136
137
|
if (seen.has(id)) { skipped++; continue; }
|
|
137
138
|
seen.add(id);
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const d = new Date(raw);
|
|
143
|
-
if (!isNaN(d.getTime())) connected_date = d.toISOString().split('T')[0];
|
|
144
|
-
}
|
|
140
|
+
// "28 Sep 2026", read as the calendar date it is. new Date(raw).toISOString()
|
|
141
|
+
// made it local midnight in UTC, a day early anywhere east of London.
|
|
142
|
+
const connected_date = toIsoDate(cell(iConnected));
|
|
145
143
|
|
|
146
144
|
connections.push(connectionFrom({ id, name, role, company, profileUrl, connected_date }));
|
|
147
145
|
}
|
|
@@ -111,7 +111,8 @@ export function buildExport(db, { dir, outFile, includePhotos = true, appVersion
|
|
|
111
111
|
// file the other computer doesn't have: some views draw an empty circle
|
|
112
112
|
// for it. With no photo, they show initials, and the next scan of that
|
|
113
113
|
// person saves a new one. A photo not saved yet (LinkedIn's own link,
|
|
114
|
-
// which expires in a few weeks) is left as it is, as it is here
|
|
114
|
+
// which expires in a few weeks) is left as it is, as it is here: neither
|
|
115
|
+
// computer shows it, and either saves it once (lib/photos.js).
|
|
115
116
|
if (!includePhotos) {
|
|
116
117
|
out.exec("UPDATE linkedin_connections SET profile_image_url = NULL WHERE profile_image_url LIKE '/avatars/%'");
|
|
117
118
|
}
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
import { readdirSync, lstatSync, existsSync, rmSync } from 'node:fs';
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
+
import { AVATAR_FILE } from './photos.js';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
// A photo the /avatars route serves (lib/photos.js). An export carries and an
|
|
17
|
+
// import accepts exactly those names.
|
|
18
|
+
export { AVATAR_FILE };
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* The scanner's notes about the network itself: without them, people whose
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The Galaxy's view, as numbers, kept apart from the drawing so it can be tested
|
|
2
|
+
// (app/components/ForceGraph.js).
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The same view in a box of a new size. It moves by half the change, at the
|
|
6
|
+
* same zoom, so whatever was in the middle of the box is in the middle still.
|
|
7
|
+
* Opening a 320-pixel panel slides the view 160 pixels left, and closing it
|
|
8
|
+
* slides the view back to exactly where it was.
|
|
9
|
+
*/
|
|
10
|
+
export function recentre({ x, y, k }, from, to) {
|
|
11
|
+
return { x: x + (to.width - from.width) / 2, y: y + (to.height - from.height) / 2, k };
|
|
12
|
+
}
|
|
@@ -43,22 +43,29 @@ const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july',
|
|
|
43
43
|
'august', 'september', 'october', 'november', 'december'];
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* A date as LinkedIn writes it → "2026-09-22": "September 22, 2026" (a profile's
|
|
47
|
+
* "Connected on …", read by the scanner) or "22 Sep 2026" (the "Connected On"
|
|
48
|
+
* column of the Connections.csv export, read by lib/csv.js).
|
|
47
49
|
*
|
|
48
50
|
* Parsed by hand rather than with new Date(text).toISOString(): that reads the
|
|
49
51
|
* text as local midnight and converts to UTC, which moves the date back a day
|
|
50
52
|
* anywhere east of Greenwich — and throws outright on text it cannot read.
|
|
51
|
-
*
|
|
53
|
+
* A calendar date has no time zone, so nothing here uses one.
|
|
54
|
+
* Returns null for anything it does not recognise, or a day the month doesn't have.
|
|
52
55
|
*/
|
|
53
56
|
export function toIsoDate(text) {
|
|
54
57
|
const t = String(text || '').trim();
|
|
55
58
|
if (/^\d{4}-\d{2}-\d{2}$/.test(t)) return t;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
let name, day, year;
|
|
60
|
+
let m = t.match(/^([A-Za-z]+)\.?\s+(\d{1,2}),?\s+(\d{4})$/); // September 22, 2026
|
|
61
|
+
if (m) [, name, day, year] = m;
|
|
62
|
+
else if ((m = t.match(/^(\d{1,2})\s+([A-Za-z]+)\.?\s+(\d{4})$/))) [, day, name, year] = m; // 22 Sep 2026
|
|
63
|
+
else return null;
|
|
64
|
+
const month = MONTHS.findIndex((full) => full.startsWith(name.toLowerCase()) && name.length >= 3);
|
|
65
|
+
day = Number(day);
|
|
66
|
+
// Day 0 of the next month is this month's last, counted in UTC so no zone moves it.
|
|
67
|
+
if (month < 0 || day < 1 || day > new Date(Date.UTC(Number(year), month + 1, 0)).getUTCDate()) return null;
|
|
68
|
+
return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
// Tiers that make a new connection worth a notification of its own.
|
|
@@ -4,15 +4,20 @@
|
|
|
4
4
|
// reads them with the same rules, and writes only the two things a person
|
|
5
5
|
// changes here — the budget, and lifting a cooldown.
|
|
6
6
|
//
|
|
7
|
+
// The budget is searches (a day and a month) and profile views (a day). Profile
|
|
8
|
+
// views are what got an account restricted (TRAPS §16), so their cap has no
|
|
9
|
+
// "no limit", and the scanner also keeps any two a minute apart.
|
|
10
|
+
//
|
|
7
11
|
// They belong to the LinkedIn account, not to a profile in the app. TRAPS §35.
|
|
8
12
|
|
|
9
13
|
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, lstatSync } from 'node:fs';
|
|
10
14
|
import path from 'node:path';
|
|
11
15
|
import { durable } from './durable.js';
|
|
12
16
|
|
|
13
|
-
export const DEFAULT_LIMITS = { daily: 50, monthly: 250 };
|
|
17
|
+
export const DEFAULT_LIMITS = { daily: 50, monthly: 250, profiles: 50 };
|
|
14
18
|
export const DAILY_CHOICES = [25, 50, 100, 200, 500];
|
|
15
19
|
export const MONTHLY_CHOICES = [100, 250, 500, 1000, 0]; // 0 = no monthly cap (Premium)
|
|
20
|
+
export const PROFILE_CHOICES = [10, 25, 50, 100]; // profile views a day; never unlimited
|
|
16
21
|
const DAY = 24 * 3600;
|
|
17
22
|
const PACIFIC = 'America/Los_Angeles';
|
|
18
23
|
|
|
@@ -56,15 +61,25 @@ export function nextMonthStartPacific(ms = Date.now()) {
|
|
|
56
61
|
export function readLimits(dir) {
|
|
57
62
|
const raw = readJson(path.join(dir, 'scan-limits.json')) || {};
|
|
58
63
|
const num = (v, d) => (Number.isInteger(v) && v >= 0 ? v : d);
|
|
59
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
daily: num(raw.daily, DEFAULT_LIMITS.daily),
|
|
66
|
+
monthly: num(raw.monthly, DEFAULT_LIMITS.monthly),
|
|
67
|
+
// 0 is "no limit" for searches; for profile views the scanner reads it as the default.
|
|
68
|
+
profiles: num(raw.profiles, 0) || DEFAULT_LIMITS.profiles,
|
|
69
|
+
};
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Only the offered choices are accepted, like everything else that reaches the
|
|
74
|
+
* scanner. A value not given keeps what is saved: every key is written back,
|
|
75
|
+
* or saving one would drop the others to their defaults.
|
|
76
|
+
*/
|
|
77
|
+
export function writeLimits(dir, { daily, monthly, profiles } = {}) {
|
|
64
78
|
const cur = readLimits(dir);
|
|
65
79
|
const next = {
|
|
66
80
|
daily: DAILY_CHOICES.includes(daily) ? daily : cur.daily,
|
|
67
81
|
monthly: MONTHLY_CHOICES.includes(monthly) ? monthly : cur.monthly,
|
|
82
|
+
profiles: PROFILE_CHOICES.includes(profiles) ? profiles : cur.profiles,
|
|
68
83
|
};
|
|
69
84
|
writeJsonAtomic(path.join(dir, 'scan-limits.json'), next);
|
|
70
85
|
return next;
|
|
@@ -72,13 +87,14 @@ export function writeLimits(dir, { daily, monthly }) {
|
|
|
72
87
|
|
|
73
88
|
/**
|
|
74
89
|
* Searches in the last 24 hours and since LinkedIn's month began, and profile
|
|
75
|
-
* views
|
|
76
|
-
*
|
|
90
|
+
* views in the last 24 hours. An unreadable record reads as the day used up,
|
|
91
|
+
* searches and profile views both, as the scanner treats it — failing open
|
|
92
|
+
* would say nothing had been searched.
|
|
77
93
|
*/
|
|
78
94
|
export function usage(dir, now = Date.now()) {
|
|
79
95
|
const file = path.join(dir, 'linkedin-activity.json');
|
|
80
96
|
const data = existsSync(file) ? readJson(file) : { searches: [], profiles: [] };
|
|
81
|
-
if (!data || typeof data !== 'object') return { searchesToday: Infinity, searchesMonth: 0, profilesToday:
|
|
97
|
+
if (!data || typeof data !== 'object') return { searchesToday: Infinity, searchesMonth: 0, profilesToday: Infinity, unreadable: true };
|
|
82
98
|
const s = (Array.isArray(data.searches) ? data.searches : []).map(Number);
|
|
83
99
|
const p = (Array.isArray(data.profiles) ? data.profiles : []).map(Number);
|
|
84
100
|
const dayAgo = now / 1000 - DAY;
|
|
@@ -113,6 +129,7 @@ export function linkedinState(dir, now = Date.now()) {
|
|
|
113
129
|
limits,
|
|
114
130
|
leftToday: left(limits.daily, use.searchesToday),
|
|
115
131
|
leftMonth: left(limits.monthly, use.searchesMonth),
|
|
132
|
+
profilesLeftToday: left(limits.profiles, use.profilesToday),
|
|
116
133
|
monthResets: nextMonthStartPacific(now),
|
|
117
134
|
cooldown: readCooldown(dir, now),
|
|
118
135
|
};
|
|
@@ -138,7 +155,8 @@ const isTime = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
|
138
155
|
* Why one of these files from another computer can't be used here, in words
|
|
139
156
|
* for the page, or null when it can. The limits follow writeLimits: only the
|
|
140
157
|
* menu's own choices. A value the app never offers is refused rather than
|
|
141
|
-
* passed to the scanner, which reads a daily limit of 0 as "no limit at all"
|
|
158
|
+
* passed to the scanner, which reads a daily limit of 0 as "no limit at all"
|
|
159
|
+
* (and 1000 profile views a day would be as good as none).
|
|
142
160
|
*/
|
|
143
161
|
export function budgetFileProblem(name, data) {
|
|
144
162
|
if (!isObject(data)) return 'it is not a JSON object';
|
|
@@ -155,6 +173,9 @@ export function budgetFileProblem(name, data) {
|
|
|
155
173
|
if (data.monthly !== undefined && !MONTHLY_CHOICES.includes(data.monthly)) {
|
|
156
174
|
return `its monthly limit (${String(data.monthly).slice(0, 20)}) is not one Six Degrees offers`;
|
|
157
175
|
}
|
|
176
|
+
if (data.profiles !== undefined && !PROFILE_CHOICES.includes(data.profiles)) {
|
|
177
|
+
return `its limit on profile views (${String(data.profiles).slice(0, 20)}) is not one Six Degrees offers`;
|
|
178
|
+
}
|
|
158
179
|
}
|
|
159
180
|
return null;
|
|
160
181
|
}
|
|
@@ -236,6 +257,7 @@ export function mergeBudgetFiles({ dir, from }) {
|
|
|
236
257
|
durable.writeJsonDurably(here(LIMITS), {
|
|
237
258
|
daily: limits.daily ?? DEFAULT_LIMITS.daily,
|
|
238
259
|
monthly: limits.monthly ?? DEFAULT_LIMITS.monthly,
|
|
260
|
+
profiles: limits.profiles ?? DEFAULT_LIMITS.profiles,
|
|
239
261
|
});
|
|
240
262
|
}
|
|
241
263
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Profile photos are shown from this computer only.
|
|
2
|
+
//
|
|
3
|
+
// The scanner saves each photo into the data folder as it reads it
|
|
4
|
+
// (scripts/image_store.py), and the row points at the saved file,
|
|
5
|
+
// `/avatars/<name>`, which app/avatars/[file] serves. A row can also hold a
|
|
6
|
+
// link: LinkedIn's own, which older versions stored until the scanner replaced
|
|
7
|
+
// it, and which a copy of a network made by one carries too. A page never loads
|
|
8
|
+
// a link: it would contact LinkedIn while someone is only looking at their map,
|
|
9
|
+
// and the app goes online only when you act. That person shows initials until
|
|
10
|
+
// the photo is saved, at the next scan or with Save photos on the Scan page.
|
|
11
|
+
// The Content-Security-Policy in next.config.mjs holds the same line in the
|
|
12
|
+
// browser.
|
|
13
|
+
//
|
|
14
|
+
// No imports, so the views and the server share it: every view and the routes
|
|
15
|
+
// that store photos check with localPhoto, and the /avatars route serves the
|
|
16
|
+
// names AVATAR_FILE allows.
|
|
17
|
+
|
|
18
|
+
/** A photo the /avatars route serves: a plain file name, no folders. The route uses this same pattern. */
|
|
19
|
+
export const AVATAR_FILE = /^[A-Za-z0-9_-]+\.(webp|jpg|jpeg|png)$/;
|
|
20
|
+
|
|
21
|
+
const PREFIX = '/avatars/';
|
|
22
|
+
|
|
23
|
+
/** The photo to show for a row's `profile_image_url`: its saved file's path, or null. */
|
|
24
|
+
export function localPhoto(url) {
|
|
25
|
+
if (typeof url !== 'string' || !url.startsWith(PREFIX)) return null;
|
|
26
|
+
return AVATAR_FILE.test(url.slice(PREFIX.length)) ? url : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The photos still kept as links, one per person and link:
|
|
31
|
+
* [{ profileUrl, imageUrl }]. `db` is an open node:sqlite database. The
|
|
32
|
+
* scanner saves each (`GET /api/update-images`), or forgets the link on a
|
|
33
|
+
* definite no, so a link is tried until there's an answer.
|
|
34
|
+
*/
|
|
35
|
+
export function waitingPhotos(db) {
|
|
36
|
+
return db.prepare(`SELECT DISTINCT profile_url AS profileUrl, profile_image_url AS imageUrl
|
|
37
|
+
FROM linkedin_connections
|
|
38
|
+
WHERE profile_image_url IS NOT NULL AND profile_image_url <> ''
|
|
39
|
+
AND substr(profile_image_url, 1, 9) <> '/avatars/'
|
|
40
|
+
ORDER BY profile_url`).all()
|
|
41
|
+
.filter((r) => r.profileUrl && !localPhoto(r.imageUrl));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** How many people have a photo waiting to be saved. */
|
|
45
|
+
export function waitingPhotoCount(db) {
|
|
46
|
+
return new Set(waitingPhotos(db).map((r) => r.profileUrl)).size;
|
|
47
|
+
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// sent, people who actually joined your network), never from browsing.
|
|
10
10
|
// Plain functions; tests/quest.test.mjs.
|
|
11
11
|
|
|
12
|
+
import { circleState } from './reach.js';
|
|
13
|
+
|
|
12
14
|
export const STAGE_SIZE = 5;
|
|
13
15
|
export const XP_SEND = { S: 25, A: 15, B: 10, C: 5, D: 2 };
|
|
14
16
|
export const XP_ADDED = { S: 100, A: 60, B: 35, C: 15, D: 5 };
|
|
@@ -26,8 +28,9 @@ export function levelFor(points) {
|
|
|
26
28
|
* @param sentIds ids marked "invite sent"
|
|
27
29
|
* @param added 1st-degree people who came through a cluster (unlocked_from_bridge_id set)
|
|
28
30
|
* @param mappedIds ids of 1st-degree people whose own circle is mapped
|
|
31
|
+
* @param reach lib/reach.js reachIndex(), which also knows hidden lists; mappedIds stands in without it
|
|
29
32
|
*/
|
|
30
|
-
export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set() }) {
|
|
33
|
+
export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedIds = new Set(), reach = null }) {
|
|
31
34
|
const byBridge = new Map();
|
|
32
35
|
const seen = new Set();
|
|
33
36
|
for (const r of [...recs].sort((a, b) => (b.priority || 0) - (a.priority || 0))) {
|
|
@@ -112,8 +115,10 @@ export function buildQuest({ recs = [], sentIds = new Set(), added = [], mappedI
|
|
|
112
115
|
level: levelFor(points),
|
|
113
116
|
sentTotal,
|
|
114
117
|
addedTotal,
|
|
115
|
-
// People you've added whose own circle
|
|
116
|
-
|
|
118
|
+
// People you've added whose own circle can be scanned: the next degree.
|
|
119
|
+
// Hidden lists aren't doors (lib/reach.js, the rule Bridge Chains uses).
|
|
120
|
+
newDoors: added.filter((a) => a.unlocked_from_bridge_id
|
|
121
|
+
&& circleState(a, reach || { scanned: mappedIds, hidden: new Set() }) === 'todo'),
|
|
117
122
|
clearedStages: clusters.reduce((s, c) => s + (c.stage - 1) + (c.complete ? 1 : 0), 0),
|
|
118
123
|
};
|
|
119
124
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Who you reached through a circle, and whether their own circle is next.
|
|
2
|
+
//
|
|
3
|
+
// Blake, 2026-09-25: "i would like to sigingy if a orb in the 2nd degree is
|
|
4
|
+
// added already so it shows they are ready for a 2nd degree scan to get the 3rd
|
|
5
|
+
// degree so it needs to be intuitive with the orb glowing".
|
|
6
|
+
//
|
|
7
|
+
// "Ready" is one rule, kept here once. It used to live in three slightly
|
|
8
|
+
// different copies (Outlink's new doors, the profile page's mapping bar and the
|
|
9
|
+
// Degrees panel's Auto-Bridge Next), and only one of them knew about hidden
|
|
10
|
+
// lists. Someone is ready for a circle scan when all of these are true:
|
|
11
|
+
// - they're your connection now (degree 1);
|
|
12
|
+
// - they came to you through a circle: unlocked_from_bridge_id, which
|
|
13
|
+
// lib/promote.js keeps when it clears source_connection_id, and which a
|
|
14
|
+
// request sent from the app sets to the circle you asked through;
|
|
15
|
+
// - their own circle hasn't been scanned: nobody in it is saved, and the
|
|
16
|
+
// scanner has no note of reading it (bridge-progress.json);
|
|
17
|
+
// - their list isn't known to be hidden (bridge-skips.json).
|
|
18
|
+
// A glow that fired on hidden lists would teach you to ignore it, so a hidden
|
|
19
|
+
// list is its own state, never "ready".
|
|
20
|
+
//
|
|
21
|
+
// Plain functions, no React; tests/reach.test.mjs.
|
|
22
|
+
|
|
23
|
+
import { keyFor, compareBridges } from './separation.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What the rules read, built once per load.
|
|
27
|
+
*
|
|
28
|
+
* @param connections 1st-degree rows
|
|
29
|
+
* @param degree2 2nd-degree rows: whose circle each sits in
|
|
30
|
+
* @param notes what the scanner noted, from /api/scraper?reach=1:
|
|
31
|
+
* { skips: [{ profileUrl }] (hidden lists), read: [profileUrl] (lists it has read) }
|
|
32
|
+
* @returns {{ scanned: Set, read: Set, hidden: Set, reachedFrom: Map }}
|
|
33
|
+
* `reachedFrom` is circle id → the connections you reached through it.
|
|
34
|
+
*/
|
|
35
|
+
export function reachIndex(connections = [], degree2 = [], notes = {}) {
|
|
36
|
+
const scanned = new Set();
|
|
37
|
+
for (const row of degree2 || []) {
|
|
38
|
+
if (row?.source_connection_id != null) scanned.add(row.source_connection_id);
|
|
39
|
+
}
|
|
40
|
+
const hidden = new Set();
|
|
41
|
+
for (const s of notes?.skips || []) {
|
|
42
|
+
if (s?.profileUrl) hidden.add(keyFor({ profile_url: s.profileUrl }));
|
|
43
|
+
}
|
|
44
|
+
const read = new Set();
|
|
45
|
+
for (const url of notes?.read || []) {
|
|
46
|
+
if (url) read.add(keyFor({ profile_url: url }));
|
|
47
|
+
}
|
|
48
|
+
const reachedFrom = new Map();
|
|
49
|
+
for (const row of connections || []) {
|
|
50
|
+
const from = row?.unlocked_from_bridge_id;
|
|
51
|
+
if (from == null || row.degree !== 1) continue;
|
|
52
|
+
let list = reachedFrom.get(from);
|
|
53
|
+
if (!list) reachedFrom.set(from, (list = []));
|
|
54
|
+
list.push(row);
|
|
55
|
+
}
|
|
56
|
+
return { scanned, read, hidden, reachedFrom };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Their own circle: 'scanned' (someone in it is saved, or the scanner read
|
|
61
|
+
* their list and everyone on it was already yours), 'hidden' (they keep their
|
|
62
|
+
* connections private) or 'todo'.
|
|
63
|
+
*/
|
|
64
|
+
export function circleState(person, reach) {
|
|
65
|
+
if (!person) return 'todo';
|
|
66
|
+
if (reach.scanned.has(person.id)) return 'scanned';
|
|
67
|
+
if (person.profile_url && reach.read?.has(keyFor(person))) return 'scanned';
|
|
68
|
+
if (person.profile_url && reach.hidden.has(keyFor(person))) return 'hidden';
|
|
69
|
+
return 'todo';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* For someone you reached through a circle: 'ready' (their circle can be
|
|
74
|
+
* scanned next), 'hidden' or 'scanned'. Null for anyone else: people who were
|
|
75
|
+
* always your connections, and 2nd-degree rows (a request sent from the app
|
|
76
|
+
* marks those with the circle it asked through, but they aren't yours yet).
|
|
77
|
+
*/
|
|
78
|
+
export function reachState(person, reach) {
|
|
79
|
+
if (!person || person.degree !== 1 || person.unlocked_from_bridge_id == null) return null;
|
|
80
|
+
const state = circleState(person, reach);
|
|
81
|
+
return state === 'todo' ? 'ready' : state;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Everyone ready for a circle scan, strongest first. */
|
|
85
|
+
export function readyToScan(connections, reach) {
|
|
86
|
+
return (connections || []).filter((c) => reachState(c, reach) === 'ready').sort(compareBridges);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** How many people you reached through each circle are ready, by circle id. */
|
|
90
|
+
export function readyByCircle(reach) {
|
|
91
|
+
const out = new Map();
|
|
92
|
+
for (const [from, people] of reach.reachedFrom) {
|
|
93
|
+
const n = people.filter((p) => reachState(p, reach) === 'ready').length;
|
|
94
|
+
if (n) out.set(from, n);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What scanning one circle asks of LinkedIn: one profile view to find their
|
|
101
|
+
* list, then a search for every page of it, `pages` at most (LinkedIn shows
|
|
102
|
+
* 100). About 0.55 minutes a page with the scanner's rests (20 s before each
|
|
103
|
+
* page, a minute more after every 10), so about 55 minutes for a whole list.
|
|
104
|
+
* The Scan page and the Degrees panel both say it from here.
|
|
105
|
+
*/
|
|
106
|
+
export function circleScanCost(pages = 100) {
|
|
107
|
+
const searches = Math.max(1, Math.min(100, Math.round(pages) || 100));
|
|
108
|
+
return {
|
|
109
|
+
profileViews: 1,
|
|
110
|
+
searches,
|
|
111
|
+
minutes: searches >= 100 ? 55 : Math.max(5, Math.round(searches * 0.55)),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -40,6 +40,7 @@ export function runningNow(job) {
|
|
|
40
40
|
case 'install': return 'The scanner\'s packages are being installed';
|
|
41
41
|
case 'setup': return 'The scanner is being set up';
|
|
42
42
|
case 'login': return 'The LinkedIn sign-in window is open';
|
|
43
|
+
case 'photos': return 'Profile photos are being saved';
|
|
43
44
|
default: return 'A scan is running';
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// What the Scan page's first step says and offers, from the checks GET
|
|
2
|
-
// /api/scraper reports (app/api/scraper/route.js, lib/scanner-python.js)
|
|
2
|
+
// /api/scraper reports (app/api/scraper/route.js, lib/scanner-python.js), and
|
|
3
|
+
// whether it asks for your field before that (askForField).
|
|
3
4
|
// Plain data in, plain data out: the page draws it, and
|
|
4
5
|
// tests/scanner-setup.test.mjs checks every state without a browser. No Node
|
|
5
6
|
// imports here; the page runs in the browser.
|
|
@@ -102,3 +103,21 @@ function stepOnly(status) {
|
|
|
102
103
|
button: null,
|
|
103
104
|
};
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether the Scan page asks for your field first (app/components/FieldStep.js):
|
|
109
|
+
* once, before the first scan. Never to someone who already has a network, has
|
|
110
|
+
* picked a sector (Scores → Your sector), or answered or skipped the question
|
|
111
|
+
* before (`fieldAsked`). `status` is GET /api/scraper's answer (null while it
|
|
112
|
+
* loads), `settings` GET /api/settings' `settings` (undefined while it loads,
|
|
113
|
+
* null when it couldn't be read). null means not known yet: the page waits,
|
|
114
|
+
* rather than show the steps and then swap them for the question.
|
|
115
|
+
*/
|
|
116
|
+
export function askForField(status, settings) {
|
|
117
|
+
if (!status) return null;
|
|
118
|
+
const n = status.network;
|
|
119
|
+
// No counts means the app couldn't be asked; a scan running is past asking.
|
|
120
|
+
if (!n || n.first || n.second || n.third || status.running) return false;
|
|
121
|
+
if (settings === undefined) return null;
|
|
122
|
+
return Boolean(settings && !settings.fieldAsked && !settings.sectorFocus?.sectors?.length);
|
|
123
|
+
}
|