six-degrees 0.4.0-beta.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +14 -14
- 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/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
- 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 +2 -2
- 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 +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
- 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/favicon.ico/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
- 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 +14 -14
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
- 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 +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- 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 +14 -14
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
- 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 +14 -14
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
- 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 +14 -14
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
- 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 +14 -14
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
- 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 +14 -14
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
- 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 +14 -14
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
- 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 +14 -14
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
- 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]__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/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__20tmem7._.js} +2 -2
- 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/_1mxn_lb._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +26 -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]__00d8snt._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +1 -1
- 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 +6 -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/10784r7w7t5gw.js +18 -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/2p5yt1o9tnxjn.js +2 -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/3ol1xux1-ofm4.js +6 -0
- package/.next/standalone/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
- package/.next/standalone/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
- package/.next/standalone/CHANGELOG.md +211 -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/admin-delete/route.js +3 -0
- package/.next/standalone/app/api/ingest/route.js +19 -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 +713 -469
- package/.next/standalone/app/components/EmptyState.js +1 -1
- package/.next/standalone/app/components/FieldStep.js +94 -0
- package/.next/standalone/app/components/FilterPanel.js +37 -1
- package/.next/standalone/app/components/ForceGraph.js +228 -774
- package/.next/standalone/app/components/GridView.js +9 -7
- package/.next/standalone/app/components/LinkedInLimits.js +40 -8
- 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 +209 -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 +6 -4
- package/.next/standalone/app/favicon.ico +0 -0
- package/.next/standalone/app/globals.css +7 -0
- package/.next/standalone/app/launch/page.js +2 -2
- package/.next/standalone/app/page.js +153 -31
- package/.next/standalone/app/paths/page.js +11 -3
- package/.next/standalone/app/queue/page.js +18 -10
- package/.next/standalone/app/setup/page.js +423 -260
- package/.next/standalone/db/schema.js +15 -1
- package/.next/standalone/eslint.config.mjs +3 -3
- package/.next/standalone/install.sh +3 -3
- package/.next/standalone/lib/brokerage.js +94 -0
- package/.next/standalone/lib/chain-layout.js +228 -0
- package/.next/standalone/lib/circle.js +23 -4
- 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/degrees.js +30 -0
- package/.next/standalone/lib/galaxy.js +12 -0
- package/.next/standalone/lib/ingest.js +18 -11
- package/.next/standalone/lib/linkedin-limits.js +35 -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/search-risk.js +38 -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/lib/ties.js +23 -0
- 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 +271 -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/10784r7w7t5gw.js +18 -0
- package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
- package/.next/static/chunks/28bu26ausqzmn.js +1 -0
- package/.next/static/chunks/2p5yt1o9tnxjn.js +2 -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/3ol1xux1-ofm4.js +6 -0
- package/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
- package/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
- package/CHANGELOG.md +211 -0
- package/README.md +21 -15
- package/SECURITY.md +10 -0
- package/db/schema.js +15 -1
- package/lib/brokerage.js +94 -0
- package/lib/chain-layout.js +228 -0
- package/lib/circle.js +23 -4
- package/lib/csv.js +4 -6
- package/lib/data-export.js +2 -1
- package/lib/data-folder.js +4 -2
- package/lib/degrees.js +30 -0
- package/lib/galaxy.js +12 -0
- package/lib/ingest.js +18 -11
- package/lib/linkedin-limits.js +35 -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/search-risk.js +38 -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/lib/ties.js +23 -0
- package/package.json +1 -1
- package/scripts/check-node.mjs +1 -1
- package/scripts/image_store.py +77 -21
- package/scripts/scrape.py +271 -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/standalone/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- 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/static/media/favicon.2vob68tjqpejf.ico +0 -0
- /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_ssgManifest.js +0 -0
- /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_buildManifest.js +0 -0
- /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → LlDgA9g8ToBkDLlo9Qtov}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Where a bridge's circle is previewed on hover: a band beyond every ring of
|
|
143
|
+
* bridges. With many bridges the overview grows extra rings, past where the
|
|
144
|
+
* preview used to start, and it drew over them. `clear` leaves room for a
|
|
145
|
+
* hovered bridge's dot (20) and its name under it.
|
|
146
|
+
*/
|
|
147
|
+
export function previewBand(maxR, bridgeRings = [], clear = 34) {
|
|
148
|
+
const edge = Math.max(0, ...bridgeRings.map((r) => r.radius));
|
|
149
|
+
const inner = Math.max(maxR * 0.66, edge ? edge + clear : 0);
|
|
150
|
+
return { inner, outer: Math.max(maxR * 0.8, inner + maxR * 0.14) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const TIER_BANDS = ['S', 'A', 'B', 'C', 'D'];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* An opened circle's people in tier bands round the person: S nearest, then A,
|
|
157
|
+
* B, C and D, with a gap between bands like Network Circle's tier orbits, so a
|
|
158
|
+
* big circle reads as layers instead of one clump. Points come back in the
|
|
159
|
+
* order of `rows`, each with `slot`: the angle its ring gives each dot. The
|
|
160
|
+
* spacing closes up, down to `minSpacing`, until every band fits inside
|
|
161
|
+
* `outer`; past that the bands carry on outwards and zooming out shows them.
|
|
162
|
+
*
|
|
163
|
+
* @returns {{ spacing: number, edge: number, bands: { tier: string, inner: number, outer: number }[],
|
|
164
|
+
* points: { x: number, y: number, angle: number, slot: number }[] }}
|
|
165
|
+
*/
|
|
166
|
+
export function tierBandLayout(rows, { inner, outer, spacing, minSpacing = spacing / 3, gap = spacing }) {
|
|
167
|
+
const groups = TIER_BANDS.map((tier) => ({ tier, idx: [] }));
|
|
168
|
+
rows.forEach((row, i) => {
|
|
169
|
+
const t = TIER_BANDS.indexOf(row?.tier);
|
|
170
|
+
groups[t < 0 ? 4 : t].idx.push(i); // no tier: with D
|
|
171
|
+
});
|
|
172
|
+
const used = groups.filter((g) => g.idx.length);
|
|
173
|
+
const place = (s) => {
|
|
174
|
+
const points = new Array(rows.length);
|
|
175
|
+
const bands = [];
|
|
176
|
+
let r = inner;
|
|
177
|
+
for (const g of used) {
|
|
178
|
+
const l = ringLayout(g.idx.length, { inner: r, outer: Infinity, spacing: s, minSpacing: s });
|
|
179
|
+
g.idx.forEach((rowIndex, j) => {
|
|
180
|
+
const p = l.points[j];
|
|
181
|
+
points[rowIndex] = { x: p.x, y: p.y, angle: p.angle, slot: TAU / l.rings[p.ring].count };
|
|
182
|
+
});
|
|
183
|
+
const edge = Math.max(r, ...l.rings.map((x) => x.radius));
|
|
184
|
+
bands.push({ tier: g.tier, inner: r, outer: edge });
|
|
185
|
+
r = edge + gap * (s / spacing);
|
|
186
|
+
}
|
|
187
|
+
return { spacing: s, points, bands, edge: bands.length ? bands[bands.length - 1].outer : inner };
|
|
188
|
+
};
|
|
189
|
+
let best = place(spacing);
|
|
190
|
+
if (best.edge <= outer) return best;
|
|
191
|
+
let lo = minSpacing;
|
|
192
|
+
let hi = spacing;
|
|
193
|
+
best = place(lo);
|
|
194
|
+
if (best.edge > outer) return best;
|
|
195
|
+
for (let i = 0; i < 20; i++) {
|
|
196
|
+
const mid = (lo + hi) / 2;
|
|
197
|
+
const tryMid = place(mid);
|
|
198
|
+
if (tryMid.edge <= outer) { lo = mid; best = tryMid; } else hi = mid;
|
|
199
|
+
}
|
|
200
|
+
return best;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The people behind each person in an opened circle: their own circle, drawn
|
|
205
|
+
* as a fan of small dots beyond `from`, centred on that person's angle and no
|
|
206
|
+
* wider than their slot (at most a sixth of the circle), so each fan sits
|
|
207
|
+
* behind its own person. At most `cap` dots a fan and about `total` in all, so
|
|
208
|
+
* a big network stays quick; `more` is how many weren't drawn.
|
|
209
|
+
*
|
|
210
|
+
* @param {{ angle: number, slot: number, count: number }[]} anchors
|
|
211
|
+
* @returns {({ points: { x: number, y: number }[], more: number } | null)[]}
|
|
212
|
+
*/
|
|
213
|
+
export function outerFans(anchors, { from, spacing = 7, cap = 40, total = 1200 } = {}) {
|
|
214
|
+
const fans = anchors.filter((a) => a?.count > 0).length;
|
|
215
|
+
const each = Math.max(6, Math.min(cap, Math.floor(total / Math.max(1, fans))));
|
|
216
|
+
return anchors.map((a) => {
|
|
217
|
+
if (!(a?.count > 0)) return null;
|
|
218
|
+
const n = Math.min(a.count, each);
|
|
219
|
+
// Only as wide as the dots need at `spacing`, never wider than their slot:
|
|
220
|
+
// more rows once it's full, so a small fan stays a tight cluster.
|
|
221
|
+
const room = Math.min(a.slot || TAU, TAU / 6);
|
|
222
|
+
const width = Math.min(room, Math.max((spacing * 3) / from, (n * spacing) / from));
|
|
223
|
+
const l = ringLayout(n, {
|
|
224
|
+
inner: from, outer: Infinity, spacing, minSpacing: spacing, start: a.angle - width / 2, sweep: width,
|
|
225
|
+
});
|
|
226
|
+
return { points: l.points.map(({ x, y }) => ({ x, y })), more: a.count - n };
|
|
227
|
+
});
|
|
228
|
+
}
|
package/lib/circle.js
CHANGED
|
@@ -137,17 +137,34 @@ export function circleLayout(rings = [], size = 240) {
|
|
|
137
137
|
for (let k = 0; k < R; k++) {
|
|
138
138
|
const radius = radiusOf(k);
|
|
139
139
|
const people = rings[k].people;
|
|
140
|
-
|
|
140
|
+
// More than their slice holds without the dots touching: rows, neighbours
|
|
141
|
+
// alternating between them, so a big circle never draws as a solid line.
|
|
142
|
+
// Each keeps its angle behind who they hang off. Rows go out towards the
|
|
143
|
+
// next ring, or for the outermost, in towards the one before (less than
|
|
144
|
+
// half way either side, so two rings' rows never meet).
|
|
145
|
+
const gap = Math.max(2.4, size / 100);
|
|
146
|
+
const inward = k === R - 1 && R > 1;
|
|
147
|
+
const band = (inward ? radius - radiusOf(k - 1) : (k + 1 < R ? radiusOf(k + 1) : half * 0.97) - radius) * 0.45;
|
|
148
|
+
const maxRows = Math.max(1, Math.floor(band / gap) + 1);
|
|
149
|
+
const rowGap = maxRows > 1 ? (inward ? -1 : 1) * Math.min(gap * 1.2, band / (maxRows - 1)) : 0;
|
|
150
|
+
const rowsFor = (count, arc) => clamp(Math.ceil((count * gap) / Math.max(1e-9, radius * arc)), 1, maxRows);
|
|
151
|
+
let rows = 1;
|
|
152
|
+
let room = Infinity; // the least room any one dot gets along its row
|
|
141
153
|
const items = [];
|
|
142
154
|
for (const [parentId, group] of byParent[k]) {
|
|
143
155
|
const [a0, a1] = k === 0 ? [START, START + 2 * Math.PI] : span.get(parentId) || [START, START + 2 * Math.PI];
|
|
144
156
|
const total = group.reduce((s, p) => s + weight.get(p), 0);
|
|
157
|
+
const groupRows = rowsFor(group.length, a1 - a0);
|
|
158
|
+
rows = Math.max(rows, groupRows);
|
|
159
|
+
room = Math.min(room, (radius * (a1 - a0) * groupRows) / Math.max(1, group.length));
|
|
160
|
+
let n = 0;
|
|
145
161
|
let a = a0;
|
|
146
162
|
for (const p of group) {
|
|
147
163
|
const w = ((a1 - a0) * weight.get(p)) / total;
|
|
148
164
|
const angle = a + w / 2;
|
|
149
|
-
const
|
|
150
|
-
const
|
|
165
|
+
const r = radius + (n++ % groupRows) * rowGap;
|
|
166
|
+
const x = half + r * Math.cos(angle);
|
|
167
|
+
const y = half + r * Math.sin(angle);
|
|
151
168
|
items.push({ ...p, angle, x, y });
|
|
152
169
|
if (p.state === 'connected') span.set(p.row.id, [a, a + w]);
|
|
153
170
|
a += w;
|
|
@@ -163,7 +180,9 @@ export function circleLayout(rings = [], size = 240) {
|
|
|
163
180
|
spokes.push({ x1: it.x, y1: it.y, x2: half + nextR * Math.cos(mid), y2: half + nextR * Math.sin(mid) });
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
|
-
|
|
183
|
+
// Sized by the most crowded slice, and never wider than the gap between rows.
|
|
184
|
+
const dotR = clamp(Math.min(room * 0.32, rows > 1 ? Math.abs(rowGap) * 0.42 : Infinity), 0.8, size / 60);
|
|
185
|
+
out.push({ degree: rings[k].degree, radius, dotR, rows, items });
|
|
167
186
|
}
|
|
168
187
|
return { size, center: { x: half, y: half }, rings: out, spokes };
|
|
169
188
|
}
|
package/lib/csv.js
CHANGED
|
@@ -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
|
}
|
package/lib/data-export.js
CHANGED
|
@@ -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
|
}
|
package/lib/data-folder.js
CHANGED
|
@@ -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
|
package/lib/degrees.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Everyone the app knows, by degree, for Network Circle's degree filter.
|
|
2
|
+
//
|
|
3
|
+
// 1st: your connections. 2nd: anyone in a scanned circle, including the people
|
|
4
|
+
// behind someone you added, since you're connected to them now. 3rd: anyone a
|
|
5
|
+
// company scan found who is neither. Each person counts once, at the nearest
|
|
6
|
+
// degree they're found (by profile, lib/separation.js keyFor), whichever
|
|
7
|
+
// circle or scan found them first.
|
|
8
|
+
|
|
9
|
+
import { keyFor } from './separation.js';
|
|
10
|
+
|
|
11
|
+
export function peopleByDegree(degree1 = [], degree2 = [], degree3 = []) {
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const take = (rows, degree) => {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const row of rows || []) {
|
|
16
|
+
const key = keyFor(row);
|
|
17
|
+
if (seen.has(key)) continue;
|
|
18
|
+
seen.add(key);
|
|
19
|
+
out.push(row.degree === degree ? row : { ...row, degree });
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
};
|
|
23
|
+
return { 1: take(degree1, 1), 2: take(degree2, 2), 3: take(degree3, 3) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function tierCountsOf(rows = []) {
|
|
27
|
+
const counts = {};
|
|
28
|
+
for (const row of rows) counts[row.tier] = (counts[row.tier] || 0) + 1;
|
|
29
|
+
return counts;
|
|
30
|
+
}
|
package/lib/galaxy.js
ADDED
|
@@ -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
|
+
}
|
package/lib/ingest.js
CHANGED
|
@@ -31,34 +31,41 @@ export function uniqueByProfile(records) {
|
|
|
31
31
|
*/
|
|
32
32
|
export function splitAlreadyConnected(records, firstDegreeUrls) {
|
|
33
33
|
const keep = [];
|
|
34
|
-
|
|
34
|
+
const connected = []; // their profile URLs: ties between your connections (lib/ties.js)
|
|
35
35
|
for (const r of records) {
|
|
36
|
-
if (firstDegreeUrls.has(r.profile_url))
|
|
36
|
+
if (firstDegreeUrls.has(r.profile_url)) connected.push(r.profile_url);
|
|
37
37
|
else keep.push(r);
|
|
38
38
|
}
|
|
39
|
-
return { keep, alreadyConnected };
|
|
39
|
+
return { keep, alreadyConnected: connected.length, connected };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
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.
|
package/lib/linkedin-limits.js
CHANGED
|
@@ -4,15 +4,21 @@
|
|
|
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 got an account restricted (TRAPS §16), so their cap has no "no limit",
|
|
9
|
+
// and the scanner also keeps any two a minute apart. Searches did too, at 373 in
|
|
10
|
+
// 24 hours: the page asks before a risky search budget (lib/search-risk.js).
|
|
11
|
+
//
|
|
7
12
|
// They belong to the LinkedIn account, not to a profile in the app. TRAPS §35.
|
|
8
13
|
|
|
9
14
|
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, lstatSync } from 'node:fs';
|
|
10
15
|
import path from 'node:path';
|
|
11
16
|
import { durable } from './durable.js';
|
|
12
17
|
|
|
13
|
-
export const DEFAULT_LIMITS = { daily: 50, monthly: 250 };
|
|
18
|
+
export const DEFAULT_LIMITS = { daily: 50, monthly: 250, profiles: 50 };
|
|
14
19
|
export const DAILY_CHOICES = [25, 50, 100, 200, 500];
|
|
15
20
|
export const MONTHLY_CHOICES = [100, 250, 500, 1000, 0]; // 0 = no monthly cap (Premium)
|
|
21
|
+
export const PROFILE_CHOICES = [10, 25, 50, 100]; // profile views a day; never unlimited
|
|
16
22
|
const DAY = 24 * 3600;
|
|
17
23
|
const PACIFIC = 'America/Los_Angeles';
|
|
18
24
|
|
|
@@ -53,18 +59,32 @@ export function nextMonthStartPacific(ms = Date.now()) {
|
|
|
53
59
|
return monthStartPacific(start + 40 * DAY * 1000);
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
const profileChoice = (n) => PROFILE_CHOICES.filter((c) => c <= n).pop() ?? PROFILE_CHOICES[0];
|
|
63
|
+
|
|
56
64
|
export function readLimits(dir) {
|
|
57
65
|
const raw = readJson(path.join(dir, 'scan-limits.json')) || {};
|
|
58
66
|
const num = (v, d) => (Number.isInteger(v) && v >= 0 ? v : d);
|
|
59
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
daily: num(raw.daily, DEFAULT_LIMITS.daily),
|
|
69
|
+
monthly: num(raw.monthly, DEFAULT_LIMITS.monthly),
|
|
70
|
+
// 0 is "no limit" for searches; for profile views the scanner reads it as the
|
|
71
|
+
// default. A number typed in by hand counts as the largest choice under it,
|
|
72
|
+
// as the scanner reads it (scrape.py search_limits).
|
|
73
|
+
profiles: profileChoice(num(raw.profiles, 0) || DEFAULT_LIMITS.profiles),
|
|
74
|
+
};
|
|
60
75
|
}
|
|
61
76
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Only the offered choices are accepted, like everything else that reaches the
|
|
79
|
+
* scanner. A value not given keeps what is saved: every key is written back,
|
|
80
|
+
* or saving one would drop the others to their defaults.
|
|
81
|
+
*/
|
|
82
|
+
export function writeLimits(dir, { daily, monthly, profiles } = {}) {
|
|
64
83
|
const cur = readLimits(dir);
|
|
65
84
|
const next = {
|
|
66
85
|
daily: DAILY_CHOICES.includes(daily) ? daily : cur.daily,
|
|
67
86
|
monthly: MONTHLY_CHOICES.includes(monthly) ? monthly : cur.monthly,
|
|
87
|
+
profiles: PROFILE_CHOICES.includes(profiles) ? profiles : cur.profiles,
|
|
68
88
|
};
|
|
69
89
|
writeJsonAtomic(path.join(dir, 'scan-limits.json'), next);
|
|
70
90
|
return next;
|
|
@@ -72,13 +92,14 @@ export function writeLimits(dir, { daily, monthly }) {
|
|
|
72
92
|
|
|
73
93
|
/**
|
|
74
94
|
* Searches in the last 24 hours and since LinkedIn's month began, and profile
|
|
75
|
-
* views
|
|
76
|
-
*
|
|
95
|
+
* views in the last 24 hours. An unreadable record reads as the day used up,
|
|
96
|
+
* searches and profile views both, as the scanner treats it — failing open
|
|
97
|
+
* would say nothing had been searched.
|
|
77
98
|
*/
|
|
78
99
|
export function usage(dir, now = Date.now()) {
|
|
79
100
|
const file = path.join(dir, 'linkedin-activity.json');
|
|
80
101
|
const data = existsSync(file) ? readJson(file) : { searches: [], profiles: [] };
|
|
81
|
-
if (!data || typeof data !== 'object') return { searchesToday: Infinity, searchesMonth: 0, profilesToday:
|
|
102
|
+
if (!data || typeof data !== 'object') return { searchesToday: Infinity, searchesMonth: 0, profilesToday: Infinity, unreadable: true };
|
|
82
103
|
const s = (Array.isArray(data.searches) ? data.searches : []).map(Number);
|
|
83
104
|
const p = (Array.isArray(data.profiles) ? data.profiles : []).map(Number);
|
|
84
105
|
const dayAgo = now / 1000 - DAY;
|
|
@@ -113,6 +134,7 @@ export function linkedinState(dir, now = Date.now()) {
|
|
|
113
134
|
limits,
|
|
114
135
|
leftToday: left(limits.daily, use.searchesToday),
|
|
115
136
|
leftMonth: left(limits.monthly, use.searchesMonth),
|
|
137
|
+
profilesLeftToday: left(limits.profiles, use.profilesToday),
|
|
116
138
|
monthResets: nextMonthStartPacific(now),
|
|
117
139
|
cooldown: readCooldown(dir, now),
|
|
118
140
|
};
|
|
@@ -138,7 +160,8 @@ const isTime = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
|
138
160
|
* Why one of these files from another computer can't be used here, in words
|
|
139
161
|
* for the page, or null when it can. The limits follow writeLimits: only the
|
|
140
162
|
* 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"
|
|
163
|
+
* passed to the scanner, which reads a daily limit of 0 as "no limit at all"
|
|
164
|
+
* (and 1000 profile views a day would be as good as none).
|
|
142
165
|
*/
|
|
143
166
|
export function budgetFileProblem(name, data) {
|
|
144
167
|
if (!isObject(data)) return 'it is not a JSON object';
|
|
@@ -155,6 +178,9 @@ export function budgetFileProblem(name, data) {
|
|
|
155
178
|
if (data.monthly !== undefined && !MONTHLY_CHOICES.includes(data.monthly)) {
|
|
156
179
|
return `its monthly limit (${String(data.monthly).slice(0, 20)}) is not one Six Degrees offers`;
|
|
157
180
|
}
|
|
181
|
+
if (data.profiles !== undefined && !PROFILE_CHOICES.includes(data.profiles)) {
|
|
182
|
+
return `its limit on profile views (${String(data.profiles).slice(0, 20)}) is not one Six Degrees offers`;
|
|
183
|
+
}
|
|
158
184
|
}
|
|
159
185
|
return null;
|
|
160
186
|
}
|
|
@@ -236,6 +262,7 @@ export function mergeBudgetFiles({ dir, from }) {
|
|
|
236
262
|
durable.writeJsonDurably(here(LIMITS), {
|
|
237
263
|
daily: limits.daily ?? DEFAULT_LIMITS.daily,
|
|
238
264
|
monthly: limits.monthly ?? DEFAULT_LIMITS.monthly,
|
|
265
|
+
profiles: limits.profiles ?? DEFAULT_LIMITS.profiles,
|
|
239
266
|
});
|
|
240
267
|
}
|
|
241
268
|
}
|
package/lib/photos.js
ADDED
|
@@ -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
|
+
}
|
package/lib/quest.js
CHANGED
|
@@ -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
|
}
|