six-degrees 0.4.10 → 0.5.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 +9 -9
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- 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 +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 +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 +3 -2
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js +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 +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 +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- 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 +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__05_3-22._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__13dx4sd._.js} +7 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
- package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
- 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 +6 -5
- 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 +19 -3
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.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/03bziijw5srol.js +1 -0
- package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{static/chunks/1xptlvtcz6654.css → standalone/.next/static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/.next/standalone/CHANGELOG.md +235 -0
- package/.next/standalone/app/api/data/export/route.js +2 -1
- package/.next/standalone/app/api/ingest/route.js +40 -1
- package/.next/standalone/app/api/scraper/route.js +12 -2
- package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
- package/.next/standalone/app/api/social/crm/route.js +37 -0
- package/.next/standalone/app/api/social/messages/route.js +44 -8
- package/.next/standalone/app/api/social/route.js +80 -22
- package/.next/standalone/app/components/AppTabs.js +59 -0
- package/.next/standalone/app/components/ChainView.js +303 -89
- package/.next/standalone/app/components/CirclePanel.js +162 -0
- package/.next/standalone/app/components/CompanyScores.js +2 -2
- package/.next/standalone/app/components/FieldStep.js +4 -4
- package/.next/standalone/app/components/FilterPanel.js +121 -183
- package/.next/standalone/app/components/ForceGraph.js +41 -3
- package/.next/standalone/app/components/GalaxyLab.js +26 -31
- package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
- package/.next/standalone/app/components/MapControls.js +154 -0
- package/.next/standalone/app/components/NoteDetail.js +118 -0
- package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
- package/.next/standalone/app/components/PeopleMap.js +308 -0
- package/.next/standalone/app/components/ScanRadar.js +129 -0
- package/.next/standalone/app/components/ScanStatusBar.js +72 -27
- package/.next/standalone/app/components/SeparationView.js +626 -118
- package/.next/standalone/app/components/Sidebar.js +28 -2
- package/.next/standalone/app/components/settings/DataSection.js +12 -3
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
- package/.next/standalone/app/components/settings/SectorSection.js +2 -2
- package/.next/standalone/app/components/settings/TierSection.js +1 -1
- package/.next/standalone/app/components/settings/TitleSection.js +149 -0
- package/.next/standalone/app/components/views.js +15 -14
- package/.next/standalone/app/globals.css +11 -0
- package/.next/standalone/app/page.js +89 -156
- package/.next/standalone/app/paths/page.js +39 -27
- package/.next/standalone/app/profile/page.js +33 -5
- package/.next/standalone/app/queue/page.js +56 -34
- package/.next/standalone/app/scores/page.js +16 -66
- package/.next/standalone/app/settings/page.js +27 -13
- package/.next/standalone/app/setup/page.js +298 -205
- package/.next/standalone/app/social/Crm.js +683 -0
- package/.next/standalone/app/social/SocialHub.js +374 -0
- package/.next/standalone/app/social/Thread.js +90 -0
- package/.next/standalone/app/social/page.js +10 -351
- package/.next/standalone/lib/chain-layout.js +105 -35
- package/.next/standalone/lib/data-export.js +10 -4
- package/.next/standalone/lib/data-folder.js +9 -0
- package/.next/standalone/lib/data-import.js +13 -3
- package/.next/standalone/lib/degrees.js +0 -6
- package/.next/standalone/lib/dot-rings.js +25 -0
- package/.next/standalone/lib/galaxy-lab.js +65 -6
- package/.next/standalone/lib/ingest.js +2 -0
- package/.next/standalone/lib/island.js +40 -0
- package/.next/standalone/lib/linkedin-export.js +267 -46
- package/.next/standalone/lib/linkedin-limits.js +6 -2
- package/.next/standalone/lib/map-heat.js +20 -0
- package/.next/standalone/lib/network-health.js +1 -1
- package/.next/standalone/lib/notifications.js +121 -0
- package/.next/standalone/lib/people-map.js +82 -0
- package/.next/standalone/lib/rarity.js +45 -3
- package/.next/standalone/lib/reach.js +7 -6
- package/.next/standalone/lib/rpc.js +15 -4
- package/.next/standalone/lib/scan-pace.js +46 -0
- package/.next/standalone/lib/scoring.js +39 -8
- package/.next/standalone/lib/scraper-client.js +19 -6
- package/.next/standalone/lib/sector-focus.js +4 -4
- package/.next/standalone/lib/separation.js +137 -10
- package/.next/standalone/lib/settings-effects.js +20 -11
- package/.next/standalone/lib/settings.js +3 -0
- package/.next/standalone/lib/social-crm.js +415 -0
- package/.next/standalone/lib/social-store.js +13 -6
- package/.next/standalone/lib/tier-grid.js +99 -0
- package/.next/standalone/lib/title-ranking.js +83 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +177 -35
- package/.next/static/chunks/03bziijw5srol.js +1 -0
- package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{standalone/.next/static/chunks/1xptlvtcz6654.css → static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/CHANGELOG.md +235 -0
- package/lib/chain-layout.js +105 -35
- package/lib/data-export.js +10 -4
- package/lib/data-folder.js +9 -0
- package/lib/data-import.js +13 -3
- package/lib/degrees.js +0 -6
- package/lib/dot-rings.js +25 -0
- package/lib/galaxy-lab.js +65 -6
- package/lib/ingest.js +2 -0
- package/lib/island.js +40 -0
- package/lib/linkedin-export.js +267 -46
- package/lib/linkedin-limits.js +6 -2
- package/lib/map-heat.js +20 -0
- package/lib/network-health.js +1 -1
- package/lib/notifications.js +121 -0
- package/lib/people-map.js +82 -0
- package/lib/rarity.js +45 -3
- package/lib/reach.js +7 -6
- package/lib/rpc.js +15 -4
- package/lib/scan-pace.js +46 -0
- package/lib/scoring.js +39 -8
- package/lib/scraper-client.js +19 -6
- package/lib/sector-focus.js +4 -4
- package/lib/separation.js +137 -10
- package/lib/settings-effects.js +20 -11
- package/lib/settings.js +3 -0
- package/lib/social-crm.js +415 -0
- package/lib/social-store.js +13 -6
- package/lib/tier-grid.js +99 -0
- package/lib/title-ranking.js +83 -0
- package/package.json +1 -1
- package/scripts/scrape.py +177 -35
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
- package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/standalone/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- package/.next/standalone/app/components/OrbitGraph.js +0 -737
- package/.next/standalone/app/social/Conversations.js +0 -246
- package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
package/lib/separation.js
CHANGED
|
@@ -86,11 +86,6 @@ export function comparePower(a, b) {
|
|
|
86
86
|
}
|
|
87
87
|
const NO_ROUTE = { id: null, bridge: null };
|
|
88
88
|
|
|
89
|
-
/** "Ways in" order: most ways in first, then the power order. The rank shown stays the power rank. */
|
|
90
|
-
export function compareWaysIn(a, b) {
|
|
91
|
-
return b.waysIn - a.waysIn || comparePower(a, b);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
89
|
/**
|
|
95
90
|
* @param {object[]} degree2 2nd-degree rows (one per bridge per person)
|
|
96
91
|
* @param {object[]} connections 1st-degree rows, the bridges
|
|
@@ -236,6 +231,10 @@ export function dotRadius(waysIn) {
|
|
|
236
231
|
|
|
237
232
|
export const MIN_BRIDGE_GAP = 18;
|
|
238
233
|
const TOP_PAD = 16;
|
|
234
|
+
/** Grey dots for mutual connections in circles not scanned yet, at most this many drawn. */
|
|
235
|
+
export const GHOST_MAX = 60;
|
|
236
|
+
const GHOST_LABEL = 40; // room under the last connection for the line that counts them
|
|
237
|
+
const PILL_W_LAYOUT = 236; // SeparationView's PILL_W: the connection pills' width as cards
|
|
239
238
|
|
|
240
239
|
/**
|
|
241
240
|
* The summit map: You → the bridges → the top of the list, every route drawn.
|
|
@@ -249,10 +248,13 @@ const TOP_PAD = 16;
|
|
|
249
248
|
*
|
|
250
249
|
* @param {object[]} top the first K people of whatever the list shows
|
|
251
250
|
* @param {number} width map width in px
|
|
251
|
+
* @param {{ cards?: boolean }} opts cards: people drawn as cards and
|
|
252
|
+
* connections as pills (Blake, 2026-10-02), which need more room per row
|
|
252
253
|
* @returns {{ width, height, you, people, bridges, links, spokes }}
|
|
253
254
|
*/
|
|
254
|
-
export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
255
|
-
const gap = isMobile ? 36 : 26;
|
|
255
|
+
export function summitLayout(top = [], width = 800, isMobile = false, { cards = false } = {}) {
|
|
256
|
+
const gap = cards ? 50 : isMobile ? 36 : 26;
|
|
257
|
+
const bridgeGap = cards ? 36 : MIN_BRIDGE_GAP;
|
|
256
258
|
const youX = 20;
|
|
257
259
|
const bx = Math.round(width * 0.42);
|
|
258
260
|
const px = Math.round(width * 0.58);
|
|
@@ -262,7 +264,7 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
|
262
264
|
p,
|
|
263
265
|
x: px,
|
|
264
266
|
y: TOP_PAD + i * gap,
|
|
265
|
-
r: dotRadius(p.waysIn),
|
|
267
|
+
r: cards ? 0 : dotRadius(p.waysIn),
|
|
266
268
|
label: fit(`#${p.rank}${p.tied ? '=' : ''} ${p.person.name || ''}`, Math.floor((width - px - 60) / 6.2)),
|
|
267
269
|
}));
|
|
268
270
|
|
|
@@ -288,11 +290,16 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
|
288
290
|
|
|
289
291
|
const y0 = TOP_PAD;
|
|
290
292
|
const y1 = people.length ? people[people.length - 1].y : TOP_PAD;
|
|
291
|
-
|
|
293
|
+
// Each connection sits level with the people they lead to (the middle of
|
|
294
|
+
// them), moved no more than it takes to keep `bridgeGap` from the next one,
|
|
295
|
+
// so a connection with one person sits on that person's row and the line
|
|
296
|
+
// between them runs straight (Blake, 2026-10-02: "the lines should line up
|
|
297
|
+
// with every box").
|
|
298
|
+
const ys = stackColumn(bridges.map((b) => b.bary), bridgeGap, y0);
|
|
292
299
|
const nameBudget = Math.max(4, Math.floor((bx - 10 - (youX + 22)) / 6));
|
|
293
300
|
bridges.forEach((b, j) => {
|
|
294
301
|
b.x = bx;
|
|
295
|
-
b.y =
|
|
302
|
+
b.y = ys[j];
|
|
296
303
|
b.r = 5;
|
|
297
304
|
if (b.unresolved) b.label = fit(`can't be named (${b.count})`, nameBudget);
|
|
298
305
|
else {
|
|
@@ -302,6 +309,15 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
|
302
309
|
delete b.ys;
|
|
303
310
|
});
|
|
304
311
|
const nodeFor = new Map(bridges.map((b) => [b.unresolved ? UNRESOLVED : b.id, b]));
|
|
312
|
+
// What each connection is to the people on the map: the only door to some,
|
|
313
|
+
// the best way in to others, or only another way in.
|
|
314
|
+
for (const b of bridges) { b.onlyFor = 0; b.bestFor = 0; }
|
|
315
|
+
for (const pp of people) {
|
|
316
|
+
const first = pp.p.routes[0];
|
|
317
|
+
const bestNode = first ? nodeFor.get(first.bridge ? first.id : UNRESOLVED) : null;
|
|
318
|
+
if (bestNode) bestNode.bestFor++;
|
|
319
|
+
if (pp.p.waysIn === 1 && bestNode) bestNode.onlyFor++;
|
|
320
|
+
}
|
|
305
321
|
|
|
306
322
|
// Routes, per person: routes[0] is the top-scored bridge. Two unnamed routes
|
|
307
323
|
// for one person are one line to the one grey node.
|
|
@@ -326,3 +342,114 @@ export function summitLayout(top = [], width = 800, isMobile = false) {
|
|
|
326
342
|
const spokes = bridges.map((b) => ({ id: b.id, x1: you.x, y1: you.y, x2: b.x, y2: b.y }));
|
|
327
343
|
return { width, height, you, people, bridges, links, spokes };
|
|
328
344
|
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Heights as near the ones asked for as they can be while staying `gap`
|
|
348
|
+
* apart, in the order given (already sorted), never above `top`: neighbours
|
|
349
|
+
* that would overlap move apart by the same amount each way.
|
|
350
|
+
*/
|
|
351
|
+
export function stackColumn(wanted = [], gap = MIN_BRIDGE_GAP, top = 0) {
|
|
352
|
+
const blocks = [];
|
|
353
|
+
wanted.forEach((y, k) => {
|
|
354
|
+
blocks.push({ sum: y - k * gap, count: 1 });
|
|
355
|
+
while (blocks.length > 1 && blocks[blocks.length - 2].sum / blocks[blocks.length - 2].count > blocks[blocks.length - 1].sum / blocks[blocks.length - 1].count) {
|
|
356
|
+
const last = blocks.pop();
|
|
357
|
+
blocks[blocks.length - 1].sum += last.sum;
|
|
358
|
+
blocks[blocks.length - 1].count += last.count;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
const out = blocks.flatMap((b) => new Array(b.count).fill(b.sum / b.count)).map((q, k) => q + k * gap);
|
|
362
|
+
const lift = out.length && out[0] < top ? top - out[0] : 0;
|
|
363
|
+
return out.map((y) => y + lift);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Who the map aims at with the slider all the way to easy: whoever you share the
|
|
368
|
+
* most mutual connections with, scanned or not (Blake, 2026-10-02: "if we have
|
|
369
|
+
* mutuals but not scanned thats still a high probability … i dont want to hinder
|
|
370
|
+
* the algorithm since we havent scanned that person"). `mutualsOf` is LinkedIn's
|
|
371
|
+
* count where a scan saved one, never less than the ways in the app has drawn
|
|
372
|
+
* (lib/rarity.js). Then the most drawn, then the stronger person. Narrowing the
|
|
373
|
+
* tiers first (S only, say) keeps it to the people worth it.
|
|
374
|
+
*/
|
|
375
|
+
export function mostMutuals(people = [], mutualsOf = () => 0, limit = 7) {
|
|
376
|
+
return [...people].sort((a, b) => (Math.max(mutualsOf(b) || 0, b.waysIn) - Math.max(mutualsOf(a) || 0, a.waysIn))
|
|
377
|
+
|| b.waysIn - a.waysIn
|
|
378
|
+
|| comparePower(a, b)).slice(0, limit);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* How many people the map draws with the slider at `at` (lib/rarity.js): all
|
|
383
|
+
* `k` from the rare end to the middle, then fewer and fewer, down to the one
|
|
384
|
+
* person you're aiming at with the slider all the way to easy. Blake,
|
|
385
|
+
* 2026-10-02: at the rare end "it will show that one door"; at the other "all
|
|
386
|
+
* the connections going into that one person".
|
|
387
|
+
*/
|
|
388
|
+
export function mapCount(at, k = 10) {
|
|
389
|
+
const v = Math.max(0, Math.min(100, Number(at) || 0));
|
|
390
|
+
if (v <= 50) return k;
|
|
391
|
+
return Math.max(1, Math.round((k * (100 - v)) / 50));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The map for one person: You → every connection of yours who leads to them →
|
|
396
|
+
* them, with rings round them. Routes the map can't name share one grey node,
|
|
397
|
+
* as in summitLayout, and it comes back in summitLayout's shape (`people` holds
|
|
398
|
+
* the one person, marked `big`), so one map draws both and can glide from one
|
|
399
|
+
* to the other. Pure, so it is tested without a browser.
|
|
400
|
+
*
|
|
401
|
+
* @returns {{ width, height, you, target, people, bridges, links, spokes }}
|
|
402
|
+
*/
|
|
403
|
+
export function convergeLayout(p, width = 800, isMobile = false, { cards = false, mutuals = 0 } = {}) {
|
|
404
|
+
const gap = cards ? 46 : isMobile ? 40 : 34;
|
|
405
|
+
const youX = 20;
|
|
406
|
+
const bx = Math.round(width * (isMobile ? 0.44 : 0.36));
|
|
407
|
+
const tx = Math.round(width * (isMobile ? 0.8 : cards ? 0.6 : 0.62));
|
|
408
|
+
const nodes = [];
|
|
409
|
+
let unnamed = null;
|
|
410
|
+
for (const r of p?.routes || []) {
|
|
411
|
+
if (r.bridge) nodes.push({ id: r.id, bridge: r.bridge, unresolved: false, count: 1 });
|
|
412
|
+
else if (unnamed) unnamed.count++;
|
|
413
|
+
else nodes.push((unnamed = { id: null, bridge: null, unresolved: true, count: 1 }));
|
|
414
|
+
}
|
|
415
|
+
// Mutual connections the app can't draw yet: they're in circles you haven't
|
|
416
|
+
// scanned. Grey dots under the connections who do lead there, each with a faint
|
|
417
|
+
// line in, so the count reads at a glance (GHOST_MAX at most, then "+N").
|
|
418
|
+
const unscanned = Math.max(0, Math.floor(Number(mutuals) || 0) - (p?.waysIn || 0));
|
|
419
|
+
const ghostCount = Math.min(GHOST_MAX, unscanned);
|
|
420
|
+
const ghostStep = cards ? 12 : 11;
|
|
421
|
+
const ghostW = cards ? PILL_W_LAYOUT - 16 : isMobile ? 96 : 120;
|
|
422
|
+
const perRow = Math.max(1, Math.floor(ghostW / ghostStep) + 1);
|
|
423
|
+
const ghostRows = Math.ceil(ghostCount / perRow);
|
|
424
|
+
const ghostH = ghostCount ? GHOST_LABEL + ghostRows * ghostStep : 0;
|
|
425
|
+
const block = Math.max(0, nodes.length - 1) * gap + ghostH;
|
|
426
|
+
const height = Math.max(isMobile ? 190 : cards ? 250 : 220, TOP_PAD * 2 + block + 48);
|
|
427
|
+
const mid = height / 2;
|
|
428
|
+
const top = mid - block / 2;
|
|
429
|
+
const nameBudget = Math.max(4, Math.floor((bx - 10 - (youX + 22)) / 6));
|
|
430
|
+
const bridges = nodes.map((n, j) => {
|
|
431
|
+
const full = n.unresolved ? `can't be named (${n.count})` : n.bridge.name || '';
|
|
432
|
+
return {
|
|
433
|
+
...n, x: bx, y: top + j * gap, r: 6,
|
|
434
|
+
primary: j === 0 && !n.unresolved,
|
|
435
|
+
label: full.length <= nameBudget ? full : fit(n.unresolved ? full : shortName(full), nameBudget),
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
const target = { x: tx, y: mid, r: cards ? 0 : 17, rings: cards ? [] : [32, 50, 70].filter((r) => r <= mid - 4) };
|
|
439
|
+
const links = bridges.map((b) => ({
|
|
440
|
+
key: p?.key, bridgeId: b.id, primary: b.primary, unresolved: b.unresolved, tier: b.bridge?.tier ?? null,
|
|
441
|
+
x1: b.x, y1: b.y, x2: target.x, y2: target.y,
|
|
442
|
+
}));
|
|
443
|
+
const you = { x: youX, y: mid, r: 14 };
|
|
444
|
+
const spokes = bridges.map((b) => ({ id: b.id, x1: you.x, y1: you.y, x2: b.x, y2: b.y }));
|
|
445
|
+
const people = p ? [{ key: p.key, p, x: target.x, y: target.y, r: target.r, rings: target.rings, big: true, label: '' }] : [];
|
|
446
|
+
const gy = top + Math.max(0, nodes.length - 1) * gap + GHOST_LABEL;
|
|
447
|
+
const gx = cards ? bx - PILL_W_LAYOUT + 8 : bx - ghostW;
|
|
448
|
+
const ghosts = Array.from({ length: ghostCount }, (_, i) => ({ x: gx + (i % perRow) * ghostStep, y: gy + Math.floor(i / perRow) * ghostStep }));
|
|
449
|
+
// How bright the person is: the share of your mutual connections with them that the map can draw.
|
|
450
|
+
const drawn = p?.waysIn ? p.waysIn / (p.waysIn + unscanned) : 1;
|
|
451
|
+
return {
|
|
452
|
+
width, height, you, target, people, bridges, links, spokes,
|
|
453
|
+
ghosts, unscanned, ghostLabel: ghostCount ? { x: gx, y: gy - 13 } : null, drawn,
|
|
454
|
+
};
|
|
455
|
+
}
|
package/lib/settings-effects.js
CHANGED
|
@@ -5,28 +5,37 @@
|
|
|
5
5
|
|
|
6
6
|
import { rescoreAll } from './rpc.js';
|
|
7
7
|
import { sameFocus, NO_FOCUS, tierMoves } from './sector-focus.js';
|
|
8
|
+
import { rankingFingerprint } from './title-ranking.js';
|
|
8
9
|
|
|
9
10
|
// Each entry: `changed(before, after)` says whether a change matters (by
|
|
10
11
|
// default, any change to the stored value), and `run(db, after, before)` does
|
|
11
12
|
// the work and returns what to tell the page.
|
|
13
|
+
// Rescore everyone and count who changed tier, from the stored tiers before and after.
|
|
14
|
+
function rescoreCountingMoves(db) {
|
|
15
|
+
const rows = () => db.prepare('SELECT id, degree, profile_url, tier FROM linkedin_connections').all();
|
|
16
|
+
try {
|
|
17
|
+
const before = new Map(rows().map((r) => [r.id, r.tier]));
|
|
18
|
+
const { scored } = rescoreAll();
|
|
19
|
+
const { people, up, down } = tierMoves(rows(), (r) => before.get(r.id), (r) => r.tier, { examples: 0 });
|
|
20
|
+
return { scored, people, moved: up + down, up, down };
|
|
21
|
+
} catch (err) {
|
|
22
|
+
throw new Error(`Saved, but rescoring your network failed (${err.message}). It will try again the next time the map loads.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
const EFFECTS = {
|
|
27
|
+
// Title points are part of everyone's score, so a new ranking rescores everyone, as a new tier scale does.
|
|
28
|
+
titleRanking: {
|
|
29
|
+
changed: (before, after) => rankingFingerprint(before) !== rankingFingerprint(after),
|
|
30
|
+
run: (db) => rescoreCountingMoves(db),
|
|
31
|
+
},
|
|
13
32
|
tierScale: {
|
|
14
33
|
// Tiers are graded when everyone is scored, so a new scale rescores
|
|
15
34
|
// everyone. The answer counts who changed tier, from the stored tiers
|
|
16
35
|
// before and after, with the sector focus's own counter (tierMoves): a
|
|
17
36
|
// person reachable through two bridges is two rows but one person, counted
|
|
18
37
|
// once at their closest degree.
|
|
19
|
-
run(db)
|
|
20
|
-
const rows = () => db.prepare('SELECT id, degree, profile_url, tier FROM linkedin_connections').all();
|
|
21
|
-
try {
|
|
22
|
-
const before = new Map(rows().map((r) => [r.id, r.tier]));
|
|
23
|
-
const { scored } = rescoreAll();
|
|
24
|
-
const { people, up, down } = tierMoves(rows(), (r) => before.get(r.id), (r) => r.tier, { examples: 0 });
|
|
25
|
-
return { scored, people, moved: up + down, up, down };
|
|
26
|
-
} catch (err) {
|
|
27
|
-
throw new Error(`Saved, but rescoring your network failed (${err.message}). It will try again the next time the map loads.`);
|
|
28
|
-
}
|
|
29
|
-
},
|
|
38
|
+
run: (db) => rescoreCountingMoves(db),
|
|
30
39
|
},
|
|
31
40
|
sectorFocus: {
|
|
32
41
|
// Only a change to what scoring uses: a strength with no sectors picked
|
package/lib/settings.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { SECTOR_FOCUS_SETTING, FIELD_ASKED_SETTING } from './sector-focus.js';
|
|
14
14
|
import { TIER_SCALE_SETTING } from './scoring.js';
|
|
15
|
+
import { TITLE_RANKING_SETTING } from './title-ranking.js';
|
|
15
16
|
|
|
16
17
|
export const SETTINGS = {
|
|
17
18
|
// Scores → Your sector: industries whose companies count for more (lib/sector-focus.js).
|
|
@@ -20,6 +21,8 @@ export const SETTINGS = {
|
|
|
20
21
|
fieldAsked: FIELD_ASKED_SETTING,
|
|
21
22
|
// Scores → Tiers: graded on your network's curve, or on the fixed scale (lib/scoring.js).
|
|
22
23
|
tierScale: TIER_SCALE_SETTING,
|
|
24
|
+
// Scores → Titles: how much each kind of title counts, and roles you're looking for (lib/title-ranking.js).
|
|
25
|
+
titleRanking: TITLE_RANKING_SETTING,
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
const KEY = 'settings';
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// The Social tab's CRM: one place for everyone you've been in touch with on
|
|
2
|
+
// LinkedIn, and your own notes on them.
|
|
3
|
+
//
|
|
4
|
+
// Blake, 2026-09-29: "it doesn't show all the conversations and sent messages;
|
|
5
|
+
// this needs to be a full CRM with past conversations, current ones, sent out,
|
|
6
|
+
// etc." The Conversations list showed only connections, and nothing you'd
|
|
7
|
+
// sent that wasn't a message. Here every person you've messaged, asked to
|
|
8
|
+
// connect, been asked by, or tracked a request to in the app is one contact,
|
|
9
|
+
// connection or not, with:
|
|
10
|
+
//
|
|
11
|
+
// what LinkedIn says their conversations (export and live sync), requests
|
|
12
|
+
// both ways (Invitations.csv), the app's own tracked
|
|
13
|
+
// requests (the Outlink queue's "Mark sent")
|
|
14
|
+
// what you say a stage, tags, notes and a next follow-up date: yours,
|
|
15
|
+
// kept per profile in crm-<profile>.json
|
|
16
|
+
// (app/api/social/crm), always, whatever the Keep my
|
|
17
|
+
// messages switch says, because you wrote them
|
|
18
|
+
//
|
|
19
|
+
// and the views: Inbox (waiting on you), Awaiting reply (you wrote last, no
|
|
20
|
+
// answer for N days), Follow-ups due, Pipeline (by stage), Sent, Received, All.
|
|
21
|
+
//
|
|
22
|
+
// Plain functions, no React; tests/social-crm.test.mjs.
|
|
23
|
+
|
|
24
|
+
import { keyFor, routeIndex, routesFor } from './separation.js';
|
|
25
|
+
import { nameKey } from './linkedin-export.js';
|
|
26
|
+
|
|
27
|
+
const DAY = 86400000;
|
|
28
|
+
|
|
29
|
+
export const STAGES = Object.freeze([
|
|
30
|
+
{ id: 'new', label: 'New' },
|
|
31
|
+
{ id: 'contacted', label: 'Contacted' },
|
|
32
|
+
{ id: 'replied', label: 'Replied' },
|
|
33
|
+
{ id: 'meeting', label: 'Meeting' },
|
|
34
|
+
{ id: 'won', label: 'Won/Partner' },
|
|
35
|
+
{ id: 'not-now', label: 'Not now' },
|
|
36
|
+
]);
|
|
37
|
+
const STAGE_IDS = new Set(STAGES.map((s) => s.id));
|
|
38
|
+
export const stageLabel = (id) => STAGES.find((s) => s.id === id)?.label || '';
|
|
39
|
+
|
|
40
|
+
/** Awaiting reply: you wrote last, and nothing came back for this many days. */
|
|
41
|
+
export const AWAIT_DAYS = 7;
|
|
42
|
+
const MAX_NOTES = 20000;
|
|
43
|
+
const MAX_TAGS = 30;
|
|
44
|
+
const MAX_TAG = 40;
|
|
45
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
46
|
+
const KEY_OK = (k) => typeof k === 'string' && k.length > 0 && k.length < 300;
|
|
47
|
+
|
|
48
|
+
// ── The store: crm-<profile>.json ─────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export function emptyCrm() {
|
|
51
|
+
return { version: 1, settings: { awaitDays: AWAIT_DAYS }, people: {} };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Tags as typed, without commas or control characters, once each whatever the case. */
|
|
55
|
+
export function cleanTags(list) {
|
|
56
|
+
const raw = Array.isArray(list) ? list : typeof list === 'string' ? list.split(',') : [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const t of raw) {
|
|
60
|
+
const tag = String(t ?? '').replace(/[\u0000-\u001f,]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, MAX_TAG);
|
|
61
|
+
if (!tag || seen.has(tag.toLowerCase())) continue;
|
|
62
|
+
seen.add(tag.toLowerCase());
|
|
63
|
+
out.push(tag);
|
|
64
|
+
if (out.length >= MAX_TAGS) break;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A real calendar day as YYYY-MM-DD, or null. */
|
|
70
|
+
function cleanDay(s) {
|
|
71
|
+
if (typeof s !== 'string' || !DATE.test(s)) return null;
|
|
72
|
+
const t = Date.parse(`${s}T00:00:00Z`);
|
|
73
|
+
return Number.isNaN(t) || new Date(t).toISOString().slice(0, 10) !== s ? null : s;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One person's CRM entry with only what it may hold, or null when there's nothing in it. */
|
|
77
|
+
export function cleanEntry(e) {
|
|
78
|
+
if (!e || typeof e !== 'object') return null;
|
|
79
|
+
const out = {};
|
|
80
|
+
if (STAGE_IDS.has(e.stage)) out.stage = e.stage;
|
|
81
|
+
const tags = cleanTags(e.tags);
|
|
82
|
+
if (tags.length) out.tags = tags;
|
|
83
|
+
if (typeof e.notes === 'string' && e.notes.trim()) out.notes = e.notes.slice(0, MAX_NOTES);
|
|
84
|
+
const followUp = cleanDay(e.followUp);
|
|
85
|
+
if (followUp) out.followUp = followUp;
|
|
86
|
+
if (!Object.keys(out).length) return null;
|
|
87
|
+
if (typeof e.updatedAt === 'string' && !Number.isNaN(Date.parse(e.updatedAt))) out.updatedAt = e.updatedAt;
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cleanDays = (n) => {
|
|
92
|
+
const d = Math.round(Number(n));
|
|
93
|
+
return Number.isFinite(d) && d >= 1 && d <= 365 ? d : AWAIT_DAYS;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** The whole file, read back: anything that isn't a CRM entry is dropped. */
|
|
97
|
+
export function cleanCrm(raw) {
|
|
98
|
+
const out = emptyCrm();
|
|
99
|
+
if (!raw || typeof raw !== 'object') return out;
|
|
100
|
+
out.settings.awaitDays = cleanDays(raw.settings?.awaitDays);
|
|
101
|
+
for (const [key, e] of Object.entries(raw.people && typeof raw.people === 'object' ? raw.people : {})) {
|
|
102
|
+
if (!KEY_OK(key)) continue;
|
|
103
|
+
const entry = cleanEntry(e);
|
|
104
|
+
if (entry) out.people[key] = entry;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One change to the store: { key, set } changes that person's fields (only the
|
|
111
|
+
* ones sent; null or '' clears one), { settings } the settings. A person left
|
|
112
|
+
* with nothing is taken out. Returns the new store; the old one isn't touched.
|
|
113
|
+
*/
|
|
114
|
+
export function patchCrm(store, { key, set, settings } = {}, now = Date.now()) {
|
|
115
|
+
const out = cleanCrm(store);
|
|
116
|
+
if (settings && typeof settings === 'object' && 'awaitDays' in settings) out.settings.awaitDays = cleanDays(settings.awaitDays);
|
|
117
|
+
if (KEY_OK(key) && set && typeof set === 'object') {
|
|
118
|
+
const merged = { ...(out.people[key] || {}) };
|
|
119
|
+
for (const f of ['stage', 'tags', 'notes', 'followUp']) {
|
|
120
|
+
if (!(f in set)) continue;
|
|
121
|
+
if (set[f] == null || set[f] === '' || (Array.isArray(set[f]) && !set[f].length)) delete merged[f];
|
|
122
|
+
else merged[f] = set[f];
|
|
123
|
+
}
|
|
124
|
+
delete merged.updatedAt;
|
|
125
|
+
const entry = cleanEntry(merged);
|
|
126
|
+
if (entry) out.people[key] = { ...entry, updatedAt: new Date(now).toISOString() };
|
|
127
|
+
else delete out.people[key];
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Contacts: everything known about each person ─────────────────────────
|
|
133
|
+
|
|
134
|
+
/** Today on this computer's calendar, as YYYY-MM-DD (a follow-up date is a day, not a moment). */
|
|
135
|
+
export function localDay(d = new Date()) {
|
|
136
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
137
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const PROFILE = /^https:\/\/(www\.)?linkedin\.com\/in\/[^/?#\s]+$/;
|
|
141
|
+
const slug = (u) => String(u || '').replace(/\/+$/, '').split('/').pop();
|
|
142
|
+
|
|
143
|
+
/** Who a key is, for showing: their name in the network, else as the export or the sync spelt it, else the end of their link. */
|
|
144
|
+
function nameFor(key, row, names) {
|
|
145
|
+
if (row?.name) return row.name;
|
|
146
|
+
if (names[key]) return names[key];
|
|
147
|
+
if (key.startsWith('name:')) return key.slice(5).replace(/\b\w/g, (c) => c.toUpperCase());
|
|
148
|
+
return slug(key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const TIER_ORDER = { S: 0, A: 1, B: 2, C: 3, D: 4 };
|
|
152
|
+
const betterRow = (a, b) => (a.degree ?? 9) < (b.degree ?? 9)
|
|
153
|
+
|| ((a.degree ?? 9) === (b.degree ?? 9) && (TIER_ORDER[a.tier] ?? 9) < (TIER_ORDER[b.tier] ?? 9));
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Everyone you've been in touch with, one contact each, and the group
|
|
157
|
+
* conversations beside them:
|
|
158
|
+
*
|
|
159
|
+
* conversations the Conversations list (conversationList): 1:1 ones make
|
|
160
|
+
* contacts, groups are returned as they are
|
|
161
|
+
* invitations buildInvitations' records
|
|
162
|
+
* network { degree1, degree2, degree3 } rows, as /api/network sends them
|
|
163
|
+
* crm the store (cleanCrm)
|
|
164
|
+
* names names by key, from the export and the live sync
|
|
165
|
+
* also keys to make a contact for even with nothing on file yet
|
|
166
|
+
* (someone found by search, to add a note to)
|
|
167
|
+
*
|
|
168
|
+
* A person the export knows only by name (no link) is joined to the network's
|
|
169
|
+
* person of that name when only one has it. A contact:
|
|
170
|
+
*
|
|
171
|
+
* { key, name, row, connection, degree, profileUrl, introducers,
|
|
172
|
+
* conversations, groups, last, lastFromThem, unread, threadUrl, count,
|
|
173
|
+
* theyWrote, kind, invitesIn, invitesOut, request, crm, lastContact }
|
|
174
|
+
*/
|
|
175
|
+
export function buildContacts({ conversations = [], invitations = [], network = {}, crm = emptyCrm(), names = {}, also = [] } = {}) {
|
|
176
|
+
const degree1 = network.degree1 || [];
|
|
177
|
+
const degree2 = network.degree2 || [];
|
|
178
|
+
const rows = [...degree1, ...degree2, ...(network.degree3 || [])];
|
|
179
|
+
const byKey = new Map();
|
|
180
|
+
const byName = new Map();
|
|
181
|
+
for (const r of rows) {
|
|
182
|
+
const k = keyFor(r);
|
|
183
|
+
const had = byKey.get(k);
|
|
184
|
+
if (!had || betterRow(r, had)) byKey.set(k, r);
|
|
185
|
+
}
|
|
186
|
+
for (const [k, r] of byKey) {
|
|
187
|
+
const n = nameKey(r.name);
|
|
188
|
+
if (n) byName.set(n, byName.has(n) && byName.get(n) !== k ? null : k);
|
|
189
|
+
}
|
|
190
|
+
const connected = new Set(degree1.map(keyFor));
|
|
191
|
+
const routes = routeIndex(degree2);
|
|
192
|
+
const bridgeById = new Map(degree1.map((r) => [r.id, r]));
|
|
193
|
+
// A name-only key becomes the network's person of that name, when there's one.
|
|
194
|
+
const resolve = (k) => (k.startsWith('name:') ? byName.get(k.slice(5)) || k : k);
|
|
195
|
+
|
|
196
|
+
const contacts = new Map();
|
|
197
|
+
const contact = (rawKey) => {
|
|
198
|
+
const key = resolve(rawKey);
|
|
199
|
+
let c = contacts.get(key);
|
|
200
|
+
if (c) return c;
|
|
201
|
+
const row = byKey.get(key) || null;
|
|
202
|
+
c = {
|
|
203
|
+
key,
|
|
204
|
+
name: nameFor(key, row, names),
|
|
205
|
+
row,
|
|
206
|
+
connection: connected.has(key),
|
|
207
|
+
degree: row?.degree ?? null,
|
|
208
|
+
profileUrl: PROFILE.test(key) ? key : (row?.profile_url && PROFILE.test(keyFor(row)) ? keyFor(row) : null),
|
|
209
|
+
introducers: row && row.degree === 2
|
|
210
|
+
? [...new Set(routesFor(row, routes, bridgeById).map((x) => x.bridge?.name).filter(Boolean))]
|
|
211
|
+
: [],
|
|
212
|
+
conversations: [],
|
|
213
|
+
groups: [],
|
|
214
|
+
last: null,
|
|
215
|
+
lastFromThem: null,
|
|
216
|
+
unread: null,
|
|
217
|
+
threadUrl: null,
|
|
218
|
+
count: 0,
|
|
219
|
+
theyWrote: false,
|
|
220
|
+
kind: null,
|
|
221
|
+
invitesIn: [],
|
|
222
|
+
invitesOut: [],
|
|
223
|
+
request: null,
|
|
224
|
+
crm: crm?.people?.[key] || null,
|
|
225
|
+
lastContact: null,
|
|
226
|
+
};
|
|
227
|
+
contacts.set(key, c);
|
|
228
|
+
return c;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const groups = [];
|
|
232
|
+
for (const conv of conversations) {
|
|
233
|
+
if (!conv || !Array.isArray(conv.people)) continue;
|
|
234
|
+
if (conv.group) {
|
|
235
|
+
groups.push({ ...conv, people: conv.people.map(resolve) });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!conv.people.length) {
|
|
239
|
+
// Only you wrote, and the export doesn't say to whom: kept as a group-like entry of its own.
|
|
240
|
+
groups.push({ ...conv, unknown: true });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const c = contact(conv.people[0]);
|
|
244
|
+
c.conversations.push(conv);
|
|
245
|
+
c.count += conv.count || 0;
|
|
246
|
+
if (conv.lastFromThem === true || (conv.count != null && conv.mine != null && conv.count > conv.mine)) c.theyWrote = true;
|
|
247
|
+
if (conv.last && (!c.last || conv.last >= c.last)) {
|
|
248
|
+
c.last = conv.last;
|
|
249
|
+
c.lastFromThem = conv.lastFromThem;
|
|
250
|
+
c.unread = conv.unread;
|
|
251
|
+
c.threadUrl = conv.threadUrl || c.threadUrl;
|
|
252
|
+
} else if (!c.last && !conv.last) {
|
|
253
|
+
c.threadUrl = conv.threadUrl || c.threadUrl;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const c of contacts.values()) {
|
|
257
|
+
c.conversations.sort((a, b) => (b.last || 0) - (a.last || 0));
|
|
258
|
+
// An advert or an InMail only when every conversation with them is one.
|
|
259
|
+
const kinds = new Set(c.conversations.map((x) => x.kind || null));
|
|
260
|
+
c.kind = kinds.size === 1 ? [...kinds][0] : null;
|
|
261
|
+
}
|
|
262
|
+
for (const g of groups) for (const k of g.people) contacts.get(k)?.groups.push(g.id);
|
|
263
|
+
|
|
264
|
+
for (const inv of invitations) {
|
|
265
|
+
if (!inv?.key) continue;
|
|
266
|
+
const c = contact(inv.key);
|
|
267
|
+
if (inv.name && !c.row && !names[c.key]) c.name = inv.name;
|
|
268
|
+
(inv.dir === 'in' ? c.invitesIn : c.invitesOut).push(inv);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Requests the app tracks: the Outlink queue's "Mark sent", once per person.
|
|
272
|
+
for (const r of rows) {
|
|
273
|
+
const sent = r.outreach_status === 'sent' || r.outreach_status === 'accepted' || r.unlock_status === 'pending';
|
|
274
|
+
if (!sent) continue;
|
|
275
|
+
const c = contact(keyFor(r));
|
|
276
|
+
const status = c.connection || r.outreach_status === 'accepted' ? 'accepted' : 'pending';
|
|
277
|
+
if (!c.request || status === 'accepted') c.request = { status };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const key of Object.keys(crm?.people || {})) contact(key);
|
|
281
|
+
for (const key of also) if (KEY_OK(key)) contact(key);
|
|
282
|
+
|
|
283
|
+
for (const c of contacts.values()) {
|
|
284
|
+
const times = [c.last, ...c.invitesIn.map((i) => i.t), ...c.invitesOut.map((i) => i.t)].filter(Boolean);
|
|
285
|
+
c.lastContact = times.length ? Math.max(...times) : null;
|
|
286
|
+
}
|
|
287
|
+
return { contacts, groups };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Where a contact seems to be, from what happened, for when you haven't set a stage. */
|
|
291
|
+
export function suggestedStage(c) {
|
|
292
|
+
if (c.theyWrote) return 'replied';
|
|
293
|
+
if (c.conversations.length || c.invitesOut.length || c.request) return 'contacted';
|
|
294
|
+
return 'new';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const isAdvert = (c) => c.kind === 'sponsored';
|
|
298
|
+
|
|
299
|
+
/** Waiting on you: they wrote last, or there's something unread. Unread first, then the newest. */
|
|
300
|
+
export function inboxOf(contacts) {
|
|
301
|
+
return [...contacts].filter((c) => !isAdvert(c) && (c.lastFromThem === true || c.unread > 0))
|
|
302
|
+
.sort((a, b) => (b.unread > 0) - (a.unread > 0) || (b.last || 0) - (a.last || 0));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Awaiting reply: you wrote last, in a 1:1 conversation, and `days` or more
|
|
307
|
+
* have gone by with nothing back, as of `asOf` (the newest thing the data
|
|
308
|
+
* knows, so an older export reads as it was). Each with `waited` (days).
|
|
309
|
+
*/
|
|
310
|
+
export function awaitingOf(contacts, asOf, days = AWAIT_DAYS) {
|
|
311
|
+
const out = [];
|
|
312
|
+
for (const c of contacts) {
|
|
313
|
+
if (isAdvert(c) || c.lastFromThem !== false || !c.last) continue;
|
|
314
|
+
const waited = Math.floor((asOf - c.last) / DAY);
|
|
315
|
+
if (waited >= days) out.push({ contact: c, waited });
|
|
316
|
+
}
|
|
317
|
+
return out.sort((a, b) => a.waited - b.waited);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Follow-ups due: a next follow-up date of today or before. Oldest first. */
|
|
321
|
+
export function followUpsDue(contacts, today = localDay()) {
|
|
322
|
+
return [...contacts].filter((c) => c.crm?.followUp && c.crm.followUp <= today)
|
|
323
|
+
.sort((a, b) => a.crm.followUp.localeCompare(b.crm.followUp));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The pipeline: people you've given a stage, by stage, most recently in touch first. */
|
|
327
|
+
export function pipelineOf(contacts) {
|
|
328
|
+
const out = Object.fromEntries(STAGES.map((s) => [s.id, []]));
|
|
329
|
+
for (const c of contacts) if (c.crm?.stage) out[c.crm.stage].push(c);
|
|
330
|
+
for (const list of Object.values(out)) list.sort((a, b) => (b.lastContact || 0) - (a.lastContact || 0));
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Sent: what went out from you, per person, newest first.
|
|
336
|
+
* invite a connection request (Invitations.csv, OUTGOING): accepted once
|
|
337
|
+
* they're a connection, pending until then
|
|
338
|
+
* request one the app tracked (Mark sent): the same
|
|
339
|
+
* awaiting a message with no reply for `days` or more (awaitingOf)
|
|
340
|
+
* Each: { contact, t, items: [{ type, t?, status?, waited?, id? }] }.
|
|
341
|
+
*/
|
|
342
|
+
export function sentOf(contacts, asOf, days = AWAIT_DAYS) {
|
|
343
|
+
const list = [...contacts];
|
|
344
|
+
const waiting = new Map(awaitingOf(list, asOf, days).map((a) => [a.contact.key, a.waited]));
|
|
345
|
+
const out = [];
|
|
346
|
+
for (const c of list) {
|
|
347
|
+
const items = [];
|
|
348
|
+
for (const inv of c.invitesOut) items.push({ type: 'invite', id: inv.id, t: inv.t, status: c.connection ? 'accepted' : 'pending' });
|
|
349
|
+
if (c.request) items.push({ type: 'request', status: c.request.status });
|
|
350
|
+
if (waiting.has(c.key)) items.push({ type: 'awaiting', t: c.last, waited: waiting.get(c.key) });
|
|
351
|
+
if (!items.length) continue;
|
|
352
|
+
out.push({ contact: c, items, t: Math.max(0, ...items.map((i) => i.t || 0)) });
|
|
353
|
+
}
|
|
354
|
+
return out.sort((a, b) => b.t - a.t);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Received: requests to you from people who aren't connections yet (so you haven't accepted them). Newest first. */
|
|
358
|
+
export function receivedOf(contacts) {
|
|
359
|
+
const out = [];
|
|
360
|
+
for (const c of contacts) {
|
|
361
|
+
if (c.connection || !c.invitesIn.length) continue;
|
|
362
|
+
out.push({ contact: c, t: Math.max(0, ...c.invitesIn.map((i) => i.t || 0)) });
|
|
363
|
+
}
|
|
364
|
+
return out.sort((a, b) => b.t - a.t);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Search across names, companies, roles, tags and notes, and what was said
|
|
369
|
+
* when the messages are kept: `said` is the set of conversation ids whose
|
|
370
|
+
* kept messages match (GET /api/social/messages?q=).
|
|
371
|
+
*/
|
|
372
|
+
export function matchesContact(c, q, said = null) {
|
|
373
|
+
const words = String(q || '').trim().toLowerCase();
|
|
374
|
+
if (!words) return true;
|
|
375
|
+
const hay = [c.name, c.row?.company, c.row?.role, c.row?.headline, ...(c.crm?.tags || []), c.crm?.notes]
|
|
376
|
+
.filter(Boolean).join('\n').toLowerCase();
|
|
377
|
+
return hay.includes(words) || (!!said && c.conversations.some((conv) => said.has(conv.id)));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── The CSV export ───────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* One cell. Quoted when it holds a comma, a quote or a line break; and a
|
|
384
|
+
* leading =, +, - or @ gets a ' in front, so a spreadsheet shows it as text
|
|
385
|
+
* instead of running it as a formula (a note or a name is written by someone).
|
|
386
|
+
*/
|
|
387
|
+
export function csvCell(v) {
|
|
388
|
+
let s = v == null ? '' : String(v);
|
|
389
|
+
if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
|
|
390
|
+
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const isoDay = (t) => (t ? new Date(t).toISOString().slice(0, 10) : '');
|
|
394
|
+
|
|
395
|
+
export const CSV_COLUMNS = ['Name', 'Company', 'Profile', 'Connection', 'Stage', 'Tags', 'Last contact', 'Who wrote last', 'Next follow-up', 'Notes'];
|
|
396
|
+
|
|
397
|
+
/** The CRM table as a CSV file's text: one row per contact, as Excel and Numbers read it (a BOM, CRLF). */
|
|
398
|
+
export function crmCsv(contacts) {
|
|
399
|
+
const lines = [CSV_COLUMNS.map(csvCell).join(',')];
|
|
400
|
+
for (const c of contacts) {
|
|
401
|
+
lines.push([
|
|
402
|
+
c.name,
|
|
403
|
+
c.row?.company || '',
|
|
404
|
+
c.profileUrl || '',
|
|
405
|
+
c.connection ? 'Yes' : 'No',
|
|
406
|
+
stageLabel(c.crm?.stage),
|
|
407
|
+
(c.crm?.tags || []).join('; '),
|
|
408
|
+
isoDay(c.lastContact),
|
|
409
|
+
c.lastFromThem == null ? '' : c.lastFromThem ? 'Them' : 'You',
|
|
410
|
+
c.crm?.followUp || '',
|
|
411
|
+
c.crm?.notes || '',
|
|
412
|
+
].map(csvCell).join(','));
|
|
413
|
+
}
|
|
414
|
+
return `${lines.join('\r\n')}\r\n`;
|
|
415
|
+
}
|