six-degrees 0.3.0 → 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/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +35 -7
- package/.next/standalone/.next/required-server-files.json +6 -1
- package/.next/standalone/.next/routes-manifest.json +18 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -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 +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 +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/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/scores/page.js +13 -0
- package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/scores.html +1 -0
- package/.next/standalone/.next/server/app/scores.meta +26 -0
- package/.next/standalone/.next/server/app/scores.rsc +24 -0
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
- 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/app-paths-manifest.json +1 -0
- 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]__0bat8nx._.js +8 -0
- 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/_02twlca._.js +10 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +8 -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 +13 -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/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
- 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/_1cyxocj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.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 +4 -0
- 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 +3 -0
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
- 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/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/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 +286 -0
- package/.next/standalone/CONTRIBUTING.md +15 -4
- package/.next/standalone/README.md +50 -29
- package/.next/standalone/SECURITY.md +10 -0
- package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
- package/.next/standalone/app/api/company-scores/route.js +1 -1
- package/.next/standalone/app/api/ingest/route.js +23 -2
- package/.next/standalone/app/api/outreach/route.js +107 -44
- package/.next/standalone/app/api/scraper/route.js +146 -17
- package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
- package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
- 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/CompanyScores.js +16 -10
- 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/LegacyScoresCard.js +1 -1
- 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 +33 -1
- package/.next/standalone/app/components/OutlinkQuest.js +5 -4
- package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
- package/.next/standalone/app/components/RingsView.js +9 -8
- package/.next/standalone/app/components/SeparationView.js +96 -14
- package/.next/standalone/app/components/Sidebar.js +366 -287
- package/.next/standalone/app/components/TheirCircle.js +300 -0
- package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
- package/.next/standalone/app/components/settings/SectorSection.js +21 -195
- package/.next/standalone/app/components/settings/TierSection.js +99 -0
- package/.next/standalone/app/components/useRequests.js +12 -0
- package/.next/standalone/app/components/useScanner.js +13 -0
- package/.next/standalone/app/components/views.js +3 -5
- package/.next/standalone/app/globals.css +7 -0
- package/.next/standalone/app/launch/page.js +6 -3
- package/.next/standalone/app/page.js +183 -74
- package/.next/standalone/app/paths/page.js +20 -14
- package/.next/standalone/app/profile/page.js +3 -3
- package/.next/standalone/app/queue/page.js +56 -60
- package/.next/standalone/app/scores/page.js +72 -0
- package/.next/standalone/app/settings/page.js +15 -3
- package/.next/standalone/app/setup/page.js +423 -260
- package/.next/standalone/db/schema.js +2 -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/circle.js +169 -0
- package/.next/standalone/lib/companies.js +1 -1
- package/.next/standalone/lib/csv.js +21 -10
- package/.next/standalone/lib/data-export.js +2 -1
- package/.next/standalone/lib/data-folder.js +4 -2
- package/.next/standalone/lib/db-client.js +1 -0
- package/.next/standalone/lib/galaxy.js +12 -0
- package/.next/standalone/lib/ingest.js +27 -8
- package/.next/standalone/lib/legacy-offer.js +7 -6
- package/.next/standalone/lib/legacy-scores.js +2 -2
- 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/rarity.js +80 -0
- package/.next/standalone/lib/reach.js +113 -0
- package/.next/standalone/lib/requests-client.js +113 -0
- package/.next/standalone/lib/rpc.js +26 -15
- package/.next/standalone/lib/scan-state.js +1 -0
- package/.next/standalone/lib/scanner-setup.js +20 -1
- package/.next/standalone/lib/score-guess.js +60 -0
- package/.next/standalone/lib/scoring.js +1761 -69
- package/.next/standalone/lib/scraper-client.js +235 -17
- package/.next/standalone/lib/sector-directory.js +1 -1
- package/.next/standalone/lib/sector-focus.js +17 -3
- package/.next/standalone/lib/separation.js +4 -2
- package/.next/standalone/lib/settings-client.js +21 -0
- package/.next/standalone/lib/settings-effects.js +19 -1
- package/.next/standalone/lib/settings.js +7 -2
- package/.next/standalone/next.config.mjs +25 -3
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/public/demo-data.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/gen-synthetic.mjs +1 -1
- package/.next/standalone/scripts/image_store.py +77 -21
- package/.next/standalone/scripts/scrape.py +305 -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 +286 -0
- package/README.md +50 -29
- package/SECURITY.md +10 -0
- package/db/schema.js +2 -1
- package/lib/chain-layout.js +139 -0
- package/lib/circle.js +169 -0
- package/lib/companies.js +1 -1
- package/lib/csv.js +21 -10
- package/lib/data-export.js +2 -1
- package/lib/data-folder.js +4 -2
- package/lib/db-client.js +1 -0
- package/lib/galaxy.js +12 -0
- package/lib/ingest.js +27 -8
- package/lib/legacy-offer.js +7 -6
- package/lib/legacy-scores.js +2 -2
- package/lib/linkedin-limits.js +30 -8
- package/lib/photos.js +47 -0
- package/lib/quest.js +8 -3
- package/lib/rarity.js +80 -0
- package/lib/reach.js +113 -0
- package/lib/requests-client.js +113 -0
- package/lib/rpc.js +26 -15
- package/lib/scan-state.js +1 -0
- package/lib/scanner-setup.js +20 -1
- package/lib/score-guess.js +60 -0
- package/lib/scoring.js +1761 -69
- package/lib/scraper-client.js +235 -17
- package/lib/sector-directory.js +1 -1
- package/lib/sector-focus.js +17 -3
- package/lib/separation.js +4 -2
- package/lib/settings-client.js +21 -0
- package/lib/settings-effects.js +19 -1
- package/lib/settings.js +7 -2
- package/package.json +2 -2
- package/public/demo-data.json +1 -1
- package/scripts/check-node.mjs +1 -1
- package/scripts/image_store.py +77 -21
- package/scripts/scrape.py +305 -29
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
- 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_0jzisgh.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/_07u8byv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
- 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/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
- package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
- package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
- package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
- package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
- package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
- package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +0 -44
- package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
- package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
- package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
- package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
- package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
- package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
- package/.next/standalone/app/components/BridgeRing.js +0 -753
- package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
- package/.next/static/chunks/08k1crid-05dt.js +0 -1
- package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
- package/.next/static/chunks/0q-_khndfxubv.js +0 -2
- package/.next/static/chunks/1c79veqedvubt.js +0 -1
- package/.next/static/chunks/1chp3nx45meg5.js +0 -44
- package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
- package/.next/static/chunks/1uadqylrod5_v.js +0 -1
- package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
- package/.next/static/chunks/289ktj97di6zg.js +0 -1
- package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
- package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
|
@@ -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,80 @@
|
|
|
1
|
+
// How rare the way in to someone is: a distinction, never a score.
|
|
2
|
+
//
|
|
3
|
+
// Blake, 2026-09-28: "if i have like 20 or 30 mututals with someone … they are
|
|
4
|
+
// most likely to add me but that means they are not that hard to reach … no
|
|
5
|
+
// mutuals at all besides the person im adding from then they must be a rarer
|
|
6
|
+
// connection". Mutual connections say how much your world and theirs overlap.
|
|
7
|
+
// Many means they'll probably accept; few means the path to them is rare and
|
|
8
|
+
// the bridge who knows them is worth more. Neither says anything about who they
|
|
9
|
+
// are, so rarity never enters the power score or the tier. It is shown and
|
|
10
|
+
// filtered beside them: "S and only one way in" is a rare find, "S and warm"
|
|
11
|
+
// an easy win.
|
|
12
|
+
//
|
|
13
|
+
// The count is LinkedIn's own mutual count where a scan saved one
|
|
14
|
+
// (mutual_count), else the ways in the app has seen: how many of your scanned
|
|
15
|
+
// connections know them. That second one is only a floor. With 14 of 150
|
|
16
|
+
// circles scanned, a "1" may really be 20, and it can only go up as you scan
|
|
17
|
+
// more, so everything that shows it says where it came from.
|
|
18
|
+
|
|
19
|
+
export const RARITY = [
|
|
20
|
+
{ key: 'only', label: 'Only way in', range: '1', max: 1, color: '#00E5FF' },
|
|
21
|
+
{ key: 'rare', label: 'Rare', range: '2–3', max: 3, color: '#4FC3F7' },
|
|
22
|
+
{ key: 'uncommon', label: 'Uncommon', range: '4–10', max: 10, color: '#B0BEC5' },
|
|
23
|
+
{ key: 'common', label: 'Common', range: '11–30', max: 30, color: '#FFB74D' },
|
|
24
|
+
{ key: 'warm', label: 'Warm', range: '31+', max: Infinity, color: '#FF7043' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const BY_KEY = new Map(RARITY.map((r) => [r.key, r]));
|
|
28
|
+
|
|
29
|
+
/** The band for a number of mutual connections (the bridge counts: at least 1). */
|
|
30
|
+
export function rarityFor(count) {
|
|
31
|
+
const n = Math.max(1, Math.floor(Number(count) || 1));
|
|
32
|
+
return RARITY.find((r) => n <= r.max).key;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function rarityInfo(key) {
|
|
36
|
+
return BY_KEY.get(key) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How many mutual connections you share with someone, and where that number
|
|
41
|
+
* came from: { count, from: 'linkedin' | 'scans' }. `waysIn` is how many of
|
|
42
|
+
* your scanned connections know them (lib/separation.js).
|
|
43
|
+
*/
|
|
44
|
+
export function mutualsOf(row, waysIn = 1) {
|
|
45
|
+
const saved = Number(row?.mutual_count);
|
|
46
|
+
if (Number.isFinite(saved) && saved >= 1) return { count: Math.floor(saved), from: 'linkedin' };
|
|
47
|
+
return { count: Math.max(1, Math.floor(Number(waysIn) || 1)), from: 'scans' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Someone's rarity: { key, count, from }. */
|
|
51
|
+
export function rarityOf(row, waysIn = 1) {
|
|
52
|
+
const m = mutualsOf(row, waysIn);
|
|
53
|
+
return { key: rarityFor(m.count), ...m };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Does a person pass both filters? Each filter is a Set of what's switched on;
|
|
58
|
+
* an empty set lets everyone through. Tier and rarity must BOTH match, so
|
|
59
|
+
* "S" + "Only way in" is exactly the rare finds.
|
|
60
|
+
*/
|
|
61
|
+
export function passes({ tier, rarity }, { tiers = new Set(), rarities = new Set() } = {}) {
|
|
62
|
+
if (tiers.size && !tiers.has(tier)) return false;
|
|
63
|
+
if (rarities.size && !(rarity && rarities.has(rarity))) return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** People per band, for the chips' counts. `items` carry `rarity` (a key). */
|
|
68
|
+
export function countByRarity(items = []) {
|
|
69
|
+
const out = Object.fromEntries(RARITY.map((r) => [r.key, 0]));
|
|
70
|
+
for (const it of items) if (it?.rarity && out[it.rarity] !== undefined) out[it.rarity]++;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Turn one key on or off in a filter set, as a new Set (React state). */
|
|
75
|
+
export function toggle(set, key) {
|
|
76
|
+
const next = new Set(set);
|
|
77
|
+
if (next.has(key)) next.delete(key);
|
|
78
|
+
else next.add(key);
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// A request you've sent, everywhere at once.
|
|
2
|
+
//
|
|
3
|
+
// Blake, 2026-09-28: "once a person is sent our a connection request hitting
|
|
4
|
+
// that button then it should commuiniacte in the app all around". It didn't.
|
|
5
|
+
// Each place that sent one kept its own note of it: the card's link told
|
|
6
|
+
// nothing else on the page (and kept offering itself), the Outlink queue kept
|
|
7
|
+
// a list of row ids, and Separation, the circle and Orbit read nothing at all,
|
|
8
|
+
// even after a reload. Now there is one list, per person (their profile URL,
|
|
9
|
+
// so every bridge's copy of them agrees), loaded once and changed only through
|
|
10
|
+
// markRequested / undoRequest here, and every view reads it (useRequests).
|
|
11
|
+
|
|
12
|
+
import { keyFor } from './separation.js';
|
|
13
|
+
|
|
14
|
+
export const REQUESTS_UNKNOWN = Object.freeze({ known: false, sent: new Set(), mine: new Map(), version: 0 });
|
|
15
|
+
|
|
16
|
+
const listeners = new Set();
|
|
17
|
+
let now = REQUESTS_UNKNOWN;
|
|
18
|
+
let loading = null;
|
|
19
|
+
// What this page did itself, person → true (sent) / false (undone). It wins
|
|
20
|
+
// over the list as loaded, so a send made while the list was on its way stays.
|
|
21
|
+
const mine = new Map();
|
|
22
|
+
|
|
23
|
+
function publish(sent = now.sent, known = now.known) {
|
|
24
|
+
now = Object.freeze({ known, sent, mine: new Map(mine), version: now.version + 1 });
|
|
25
|
+
for (const listener of [...listeners]) listener();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function load() {
|
|
29
|
+
if (loading || now.known) return loading;
|
|
30
|
+
loading = fetch('/api/outreach')
|
|
31
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
32
|
+
.then((d) => { if (d) publish(new Set((d.pending || []).map(keyFor)), true); })
|
|
33
|
+
.catch(() => { /* the sample network, or the app restarting: the rows' own columns stand in */ })
|
|
34
|
+
.finally(() => { loading = null; });
|
|
35
|
+
return loading;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Call `listener` whenever the list changes. Returns the way to stop. */
|
|
39
|
+
export function watchRequests(listener) {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
load();
|
|
42
|
+
return () => listeners.delete(listener);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function requestsNow() {
|
|
46
|
+
return now;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Is a request out to this person? What this page did wins; then the list
|
|
51
|
+
* as loaded; until that arrives, the row's own columns.
|
|
52
|
+
*/
|
|
53
|
+
export function hasRequest(row, state = now) {
|
|
54
|
+
if (!row) return false;
|
|
55
|
+
const key = keyFor(row);
|
|
56
|
+
if (state.mine.has(key)) return state.mine.get(key);
|
|
57
|
+
if (state.known) return state.sent.has(key);
|
|
58
|
+
return row.unlock_status === 'pending' || row.outreach_status === 'sent';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** How many people have a request out. */
|
|
62
|
+
export function requestCount(state = now) {
|
|
63
|
+
const keys = new Set(state.known ? state.sent : []);
|
|
64
|
+
for (const [key, on] of state.mine) (on ? keys.add(key) : keys.delete(key));
|
|
65
|
+
return keys.size;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function post(body) {
|
|
69
|
+
const r = await fetch('/api/outreach', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
});
|
|
74
|
+
const d = await r.json().catch(() => ({}));
|
|
75
|
+
if (!r.ok) throw new Error(d.error || 'Could not save that.');
|
|
76
|
+
return d;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* You sent this person a request. Every view shows it at once; the save
|
|
81
|
+
* follows, and is taken back on screen if it fails. `bridgeId` is the
|
|
82
|
+
* connection you asked through, kept for when they accept.
|
|
83
|
+
*/
|
|
84
|
+
export async function markRequested(row, { bridgeId } = {}) {
|
|
85
|
+
const key = keyFor(row);
|
|
86
|
+
const before = mine.has(key) ? mine.get(key) : undefined;
|
|
87
|
+
mine.set(key, true);
|
|
88
|
+
publish();
|
|
89
|
+
try {
|
|
90
|
+
return await post({ action: 'mark-sent', connectionId: row.id, profileUrl: row.profile_url, bridgeId });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (before === undefined) mine.delete(key);
|
|
93
|
+
else mine.set(key, before);
|
|
94
|
+
publish();
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Take a request back, everywhere. */
|
|
100
|
+
export async function undoRequest(row) {
|
|
101
|
+
const key = keyFor(row);
|
|
102
|
+
const before = mine.has(key) ? mine.get(key) : undefined;
|
|
103
|
+
mine.set(key, false);
|
|
104
|
+
publish();
|
|
105
|
+
try {
|
|
106
|
+
return await post({ action: 'undo', connectionId: row.id, profileUrl: row.profile_url });
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (before === undefined) mine.delete(key);
|
|
109
|
+
else mine.set(key, before);
|
|
110
|
+
publish();
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -6,7 +6,7 @@ import { getDb, newId, nowIso } from './db-client.js';
|
|
|
6
6
|
// results back. rpc('score_new_connections') is that procedure's name, kept
|
|
7
7
|
// for the routes that call it; it rescores with lib/scoring.js like the rest.
|
|
8
8
|
|
|
9
|
-
import { readNetwork, scoreNetwork, explainScore, SCORING_VERSION, KNOWN_COMPANIES, RULE_TABLES } from './scoring.js';
|
|
9
|
+
import { readNetwork, scoreNetwork, explainScore, SCORING_VERSION, KNOWN_COMPANIES, COMPANY_DATA, RULE_TABLES, TIER_SCALE_SETTING } from './scoring.js';
|
|
10
10
|
import { industryKeyOf, INDUSTRIES } from './companies.js';
|
|
11
11
|
import { readSettings } from './settings.js';
|
|
12
12
|
import { SECTOR_FOCUS_SETTING, focusFingerprint, previewSectorFocus } from './sector-focus.js';
|
|
@@ -14,7 +14,8 @@ import { LEGACY_OFFER_KEY } from './legacy-scores.js';
|
|
|
14
14
|
import { sectorMatcher, sectorGroup, sectorLabel } from './sector-directory.js';
|
|
15
15
|
|
|
16
16
|
// The word lists and rules the stored scores were computed with: a fingerprint
|
|
17
|
-
// of every entry on the curated company list
|
|
17
|
+
// of every entry on the curated company list and the public dataset (name,
|
|
18
|
+
// score, alias, industry),
|
|
18
19
|
// of the broad industries' words and labels (lib/companies.js INDUSTRIES), and
|
|
19
20
|
// of every rule table scoring reads headlines and names with (lib/scoring.js
|
|
20
21
|
// RULE_TABLES: titles, students and clubs, what isn't a company, the reach
|
|
@@ -26,12 +27,14 @@ import { sectorMatcher, sectorGroup, sectorLabel } from './sector-directory.js';
|
|
|
26
27
|
// does, with no number to remember to bump. Copies from before the list was
|
|
27
28
|
// made neutral stamped none, which is also how rescoreAll() knows to offer
|
|
28
29
|
// keeping their old scores (lib/legacy-offer.js).
|
|
29
|
-
export function scoringStamp({ known = KNOWN_COMPANIES, industries = INDUSTRIES, rules = RULE_TABLES } = {}) {
|
|
30
|
+
export function scoringStamp({ known = KNOWN_COMPANIES, data = COMPANY_DATA, industries = INDUSTRIES, rules = RULE_TABLES } = {}) {
|
|
30
31
|
// JSON has no patterns or sets: a pattern counts by its source and flags, a set by its members.
|
|
31
32
|
const plain = (key, value) => (value instanceof RegExp ? String(value) : value instanceof Set ? [...value] : value);
|
|
32
33
|
return createHash('sha256')
|
|
33
34
|
.update(JSON.stringify([
|
|
34
35
|
known.map(([name, score, alias, industry]) => [name, score, String(alias), industry]),
|
|
36
|
+
// The public dataset too: an entry added or rescored rescores stored scores.
|
|
37
|
+
data.map(([name, score, alias, industry]) => [name, score, String(alias), industry]),
|
|
35
38
|
industries.map(({ key, label, words }) => [key, label, String(words)]),
|
|
36
39
|
rules,
|
|
37
40
|
], plain))
|
|
@@ -45,7 +48,7 @@ export function companyOverrides(db = getDb()) {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
|
-
* Set company scores as your own ([[name, score], …]), the way
|
|
51
|
+
* Set company scores as your own ([[name, score], …]), the way the Scores tab
|
|
49
52
|
* does: one row per name, replacing any you had. Rescoring is the caller's
|
|
50
53
|
* job, so a batch rescores once.
|
|
51
54
|
*/
|
|
@@ -61,6 +64,11 @@ export function sectorFocusOf(db = getDb()) {
|
|
|
61
64
|
return readSettings(db, { sectorFocus: SECTOR_FOCUS_SETTING }).sectorFocus;
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/** Scores → Tiers: 'curve' (graded on your network's curve, the default) or 'fixed'. */
|
|
68
|
+
export function tierScaleOf(db = getDb()) {
|
|
69
|
+
return readSettings(db, { tierScale: TIER_SCALE_SETTING }).tierScale;
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
/**
|
|
65
73
|
* What rescoring reads: every row, as scoring needs it. `withPeople` adds each
|
|
66
74
|
* row's name, for naming who a change would move.
|
|
@@ -74,7 +82,7 @@ export function scoringRows(db = getDb(), { withPeople = false } = {}) {
|
|
|
74
82
|
* Every headline read once, the way scoring reads it: each company's
|
|
75
83
|
* headcount, its one industry, the sector directory's sectors it matches and
|
|
76
84
|
* the industries those sit under. Rescoring, Settings' preview and
|
|
77
|
-
* suggestions, and
|
|
85
|
+
* suggestions, and the Scores tab all read the rows through this, so they
|
|
78
86
|
* agree on which company is in which sector.
|
|
79
87
|
*/
|
|
80
88
|
export function readForScoring(rows) {
|
|
@@ -85,9 +93,9 @@ export function readForScoring(rows) {
|
|
|
85
93
|
* Rescore everyone. The score depends on the whole network (how many of your
|
|
86
94
|
* people work at a company, how strong a bridge's circle is), so after any
|
|
87
95
|
* ingest or company-score change every row is recomputed — ~0.1s for 4,000.
|
|
88
|
-
* Your company scores
|
|
89
|
-
* every path that rescores (an import, a company
|
|
90
|
-
* model) applies
|
|
96
|
+
* Your company scores, your sector focus and how tiers are graded are read
|
|
97
|
+
* here, not passed in, so every path that rescores (an import, a company
|
|
98
|
+
* score, Settings, a stale model) applies them all.
|
|
91
99
|
*
|
|
92
100
|
* `compareWith` is another sector focus: Settings' save passes the one it
|
|
93
101
|
* replaced. The same rows are then also scored with it, in memory, and the
|
|
@@ -102,12 +110,13 @@ export function rescoreAll({ compareWith } = {}) {
|
|
|
102
110
|
const rows = scoringRows(db);
|
|
103
111
|
const overrides = companyOverrides(db);
|
|
104
112
|
const focus = sectorFocusOf(db);
|
|
113
|
+
const tierScale = tierScaleOf(db);
|
|
105
114
|
// Every headline is read once, however many ways the rows are then scored.
|
|
106
115
|
const read = readForScoring(rows);
|
|
107
|
-
const { scores } = scoreNetwork(rows, { overrides, focus, read });
|
|
116
|
+
const { scores } = scoreNetwork(rows, { overrides, focus, tierScale, read });
|
|
108
117
|
// Worked out before anything is written, so a failure leaves nothing half done.
|
|
109
118
|
const change = compareWith === undefined ? null
|
|
110
|
-
: previewSectorFocus(rows, { overrides, read, from: compareWith, to: focus, examples: 0 });
|
|
119
|
+
: previewSectorFocus(rows, { overrides, read, from: compareWith, to: focus, tierScale, examples: 0 });
|
|
111
120
|
const update = db.prepare(`UPDATE linkedin_connections SET seniority_score = ?, company_prestige_score = ?, power_score = ?, tier = ?,
|
|
112
121
|
score_why = ?, circle_power = ?, circle_s_count = ?, circle_a_count = ?, circle_elite_pct = ?, is_catalyst = ?, catalyst_score = ?, updated_at = ?
|
|
113
122
|
WHERE id = ?`);
|
|
@@ -131,9 +140,10 @@ export function rescoreAll({ compareWith } = {}) {
|
|
|
131
140
|
}
|
|
132
141
|
const meta = db.prepare('INSERT INTO app_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value');
|
|
133
142
|
meta.run('scoring_version', String(SCORING_VERSION));
|
|
134
|
-
// Which curated list
|
|
143
|
+
// Which curated list, sector focus and tier scale these scores were computed with (see rescoreIfStale).
|
|
135
144
|
meta.run('scoring_list', KNOWN_LIST_STAMP);
|
|
136
145
|
meta.run('scoring_focus', focusFingerprint(focus));
|
|
146
|
+
meta.run('scoring_tiers', tierScale);
|
|
137
147
|
db.exec('COMMIT');
|
|
138
148
|
} catch (err) {
|
|
139
149
|
db.exec('ROLLBACK');
|
|
@@ -144,16 +154,17 @@ export function rescoreAll({ compareWith } = {}) {
|
|
|
144
154
|
|
|
145
155
|
/**
|
|
146
156
|
* Rescore once if the stored scores came from an older scoring model or
|
|
147
|
-
* curated list, or from a sector focus other than the one saved
|
|
148
|
-
* update, a database restored or brought from another computer, or a
|
|
149
|
-
* save whose rescore never finished.
|
|
157
|
+
* curated list, or from a sector focus or tier scale other than the one saved
|
|
158
|
+
* now: an update, a database restored or brought from another computer, or a
|
|
159
|
+
* Settings save whose rescore never finished.
|
|
150
160
|
*/
|
|
151
161
|
export function rescoreIfStale() {
|
|
152
162
|
const db = getDb();
|
|
153
163
|
const meta = (key) => db.prepare('SELECT value FROM app_meta WHERE key = ?').get(key)?.value;
|
|
154
164
|
const fresh = meta('scoring_version') === String(SCORING_VERSION)
|
|
155
165
|
&& meta('scoring_list') === KNOWN_LIST_STAMP
|
|
156
|
-
&& meta('scoring_focus') === focusFingerprint(sectorFocusOf(db))
|
|
166
|
+
&& meta('scoring_focus') === focusFingerprint(sectorFocusOf(db))
|
|
167
|
+
&& meta('scoring_tiers') === tierScaleOf(db);
|
|
157
168
|
return fresh ? { scored: 0 } : rescoreAll();
|
|
158
169
|
}
|
|
159
170
|
|
|
@@ -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
|
+
}
|