six-degrees 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +35 -7
- package/.next/standalone/.next/required-server-files.json +6 -1
- package/.next/standalone/.next/routes-manifest.json +18 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/scores/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/scores/page.js +13 -0
- package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/scores.html +1 -0
- package/.next/standalone/.next/server/app/scores.meta +26 -0
- package/.next/standalone/.next/server/app/scores.rsc +24 -0
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +13 -13
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
- package/.next/standalone/.next/server/chunks/_02twlca._.js +10 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +8 -6
- package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +13 -8
- package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1cyxocj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +4 -0
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/prefetch-hints.json +14 -0
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
- package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
- package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
- package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
- package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
- package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
- package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
- package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
- package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
- package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
- package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
- package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
- package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
- package/.next/standalone/CHANGELOG.md +286 -0
- package/.next/standalone/CONTRIBUTING.md +15 -4
- package/.next/standalone/README.md +50 -29
- package/.next/standalone/SECURITY.md +10 -0
- package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
- package/.next/standalone/app/api/company-scores/route.js +1 -1
- package/.next/standalone/app/api/ingest/route.js +23 -2
- package/.next/standalone/app/api/outreach/route.js +107 -44
- package/.next/standalone/app/api/scraper/route.js +146 -17
- package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
- package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
- package/.next/standalone/app/api/update-images/route.js +41 -7
- package/.next/standalone/app/avatars/[file]/route.js +6 -1
- package/.next/standalone/app/components/Avatar.js +5 -2
- package/.next/standalone/app/components/ChainView.js +659 -469
- package/.next/standalone/app/components/CompanyScores.js +16 -10
- package/.next/standalone/app/components/EmptyState.js +1 -1
- package/.next/standalone/app/components/FieldStep.js +94 -0
- package/.next/standalone/app/components/ForceGraph.js +183 -768
- package/.next/standalone/app/components/GridView.js +9 -7
- package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
- package/.next/standalone/app/components/LinkedInLimits.js +22 -7
- package/.next/standalone/app/components/ListView.js +6 -4
- package/.next/standalone/app/components/MappingProgress.js +5 -6
- package/.next/standalone/app/components/OnboardingGate.js +1 -1
- package/.next/standalone/app/components/OrbitGraph.js +33 -1
- package/.next/standalone/app/components/OutlinkQuest.js +5 -4
- package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
- package/.next/standalone/app/components/RingsView.js +9 -8
- package/.next/standalone/app/components/SeparationView.js +96 -14
- package/.next/standalone/app/components/Sidebar.js +366 -287
- package/.next/standalone/app/components/TheirCircle.js +300 -0
- package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
- package/.next/standalone/app/components/settings/SectorSection.js +21 -195
- package/.next/standalone/app/components/settings/TierSection.js +99 -0
- package/.next/standalone/app/components/useRequests.js +12 -0
- package/.next/standalone/app/components/useScanner.js +13 -0
- package/.next/standalone/app/components/views.js +3 -5
- package/.next/standalone/app/globals.css +7 -0
- package/.next/standalone/app/launch/page.js +6 -3
- package/.next/standalone/app/page.js +183 -74
- package/.next/standalone/app/paths/page.js +20 -14
- package/.next/standalone/app/profile/page.js +3 -3
- package/.next/standalone/app/queue/page.js +56 -60
- package/.next/standalone/app/scores/page.js +72 -0
- package/.next/standalone/app/settings/page.js +15 -3
- package/.next/standalone/app/setup/page.js +423 -260
- package/.next/standalone/db/schema.js +2 -1
- package/.next/standalone/eslint.config.mjs +3 -3
- package/.next/standalone/install.sh +3 -3
- package/.next/standalone/lib/chain-layout.js +139 -0
- package/.next/standalone/lib/circle.js +169 -0
- package/.next/standalone/lib/companies.js +1 -1
- package/.next/standalone/lib/csv.js +21 -10
- package/.next/standalone/lib/data-export.js +2 -1
- package/.next/standalone/lib/data-folder.js +4 -2
- package/.next/standalone/lib/db-client.js +1 -0
- package/.next/standalone/lib/galaxy.js +12 -0
- package/.next/standalone/lib/ingest.js +27 -8
- package/.next/standalone/lib/legacy-offer.js +7 -6
- package/.next/standalone/lib/legacy-scores.js +2 -2
- package/.next/standalone/lib/linkedin-limits.js +30 -8
- package/.next/standalone/lib/photos.js +47 -0
- package/.next/standalone/lib/quest.js +8 -3
- package/.next/standalone/lib/rarity.js +80 -0
- package/.next/standalone/lib/reach.js +113 -0
- package/.next/standalone/lib/requests-client.js +113 -0
- package/.next/standalone/lib/rpc.js +26 -15
- package/.next/standalone/lib/scan-state.js +1 -0
- package/.next/standalone/lib/scanner-setup.js +20 -1
- package/.next/standalone/lib/score-guess.js +60 -0
- package/.next/standalone/lib/scoring.js +1761 -69
- package/.next/standalone/lib/scraper-client.js +235 -17
- package/.next/standalone/lib/sector-directory.js +1 -1
- package/.next/standalone/lib/sector-focus.js +17 -3
- package/.next/standalone/lib/separation.js +4 -2
- package/.next/standalone/lib/settings-client.js +21 -0
- package/.next/standalone/lib/settings-effects.js +19 -1
- package/.next/standalone/lib/settings.js +7 -2
- package/.next/standalone/next.config.mjs +25 -3
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/public/demo-data.json +1 -1
- package/.next/standalone/scripts/build-app.mjs +11 -2
- package/.next/standalone/scripts/check-node.mjs +1 -1
- package/.next/standalone/scripts/gen-synthetic.mjs +1 -1
- package/.next/standalone/scripts/image_store.py +77 -21
- package/.next/standalone/scripts/scrape.py +305 -29
- package/.next/standalone/server.js +1 -1
- package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
- package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
- package/.next/static/chunks/0l1568qff0efj.js +1 -0
- package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
- package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
- package/.next/static/chunks/135n87ydxjrra.js +2 -0
- package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
- package/.next/static/chunks/28bu26ausqzmn.js +1 -0
- package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
- package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
- package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
- package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
- package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
- package/CHANGELOG.md +286 -0
- package/README.md +50 -29
- package/SECURITY.md +10 -0
- package/db/schema.js +2 -1
- package/lib/chain-layout.js +139 -0
- package/lib/circle.js +169 -0
- package/lib/companies.js +1 -1
- package/lib/csv.js +21 -10
- package/lib/data-export.js +2 -1
- package/lib/data-folder.js +4 -2
- package/lib/db-client.js +1 -0
- package/lib/galaxy.js +12 -0
- package/lib/ingest.js +27 -8
- package/lib/legacy-offer.js +7 -6
- package/lib/legacy-scores.js +2 -2
- package/lib/linkedin-limits.js +30 -8
- package/lib/photos.js +47 -0
- package/lib/quest.js +8 -3
- package/lib/rarity.js +80 -0
- package/lib/reach.js +113 -0
- package/lib/requests-client.js +113 -0
- package/lib/rpc.js +26 -15
- package/lib/scan-state.js +1 -0
- package/lib/scanner-setup.js +20 -1
- package/lib/score-guess.js +60 -0
- package/lib/scoring.js +1761 -69
- package/lib/scraper-client.js +235 -17
- package/lib/sector-directory.js +1 -1
- package/lib/sector-focus.js +17 -3
- package/lib/separation.js +4 -2
- package/lib/settings-client.js +21 -0
- package/lib/settings-effects.js +19 -1
- package/lib/settings.js +7 -2
- package/package.json +2 -2
- package/public/demo-data.json +1 -1
- package/scripts/check-node.mjs +1 -1
- package/scripts/image_store.py +77 -21
- package/scripts/scrape.py +305 -29
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
- package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
- package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
- package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
- package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
- package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
- package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
- package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +0 -44
- package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
- package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
- package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
- package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
- package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
- package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
- package/.next/standalone/app/components/BridgeRing.js +0 -753
- package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
- package/.next/static/chunks/08k1crid-05dt.js +0 -1
- package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
- package/.next/static/chunks/0q-_khndfxubv.js +0 -2
- package/.next/static/chunks/1c79veqedvubt.js +0 -1
- package/.next/static/chunks/1chp3nx45meg5.js +0 -44
- package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
- package/.next/static/chunks/1uadqylrod5_v.js +0 -1
- package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
- package/.next/static/chunks/289ktj97di6zg.js +0 -1
- package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
- package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
|
@@ -1,80 +1,143 @@
|
|
|
1
1
|
import { db as supabase } from '../../../lib/db';
|
|
2
|
+
import { getDb, decodeRow } from '../../../lib/db-client';
|
|
3
|
+
import { resolveProfile } from '../../../lib/profile';
|
|
4
|
+
|
|
5
|
+
// Requests you've sent, kept per PERSON.
|
|
6
|
+
//
|
|
7
|
+
// A person has one row per bridge whose circle holds them (the table's key is
|
|
8
|
+
// profile + bridge + user), and a request used to mark only the copy that was
|
|
9
|
+
// clicked. The other copies still offered "Add", the Pending count counted
|
|
10
|
+
// copies, and every click awarded XP again. And the card's link wrote a second
|
|
11
|
+
// column (unlock_status) through another route, which Undo never took back.
|
|
12
|
+
// Now a request marks every copy of the person, in both columns, the bridge you
|
|
13
|
+
// asked through is kept (so once they accept, their card and circle say who
|
|
14
|
+
// introduced you: lib/promote.js keeps it), XP comes once per person, and Undo
|
|
15
|
+
// takes back all of it. lib/requests-client.js is the one way the pages call this.
|
|
2
16
|
|
|
3
17
|
// XP rewards by tier
|
|
4
18
|
const XP_SEND = { S: 25, A: 15, B: 10, C: 5, D: 2 };
|
|
5
19
|
const XP_ACCEPT = { S: 100, A: 60, B: 35, C: 15, D: 5 };
|
|
20
|
+
const TIER_ORDER = ['S', 'A', 'B', 'C', 'D'];
|
|
6
21
|
|
|
7
22
|
async function awardXP(userId, amount) {
|
|
8
23
|
if (!userId || !amount) return;
|
|
9
|
-
// Upsert: create if not exists,
|
|
24
|
+
// Upsert: create if not exists, change if exists. Never below zero: an Undo
|
|
25
|
+
// takes back what a send gave, including sends from before sends gave any.
|
|
10
26
|
const { data: existing } = await supabase.from('user_stats').select('xp').eq('id', userId).single();
|
|
11
27
|
if (existing) {
|
|
12
|
-
await supabase.from('user_stats').update({ xp: (existing.xp || 0) + amount }).eq('id', userId);
|
|
13
|
-
} else {
|
|
28
|
+
await supabase.from('user_stats').update({ xp: Math.max(0, (existing.xp || 0) + amount) }).eq('id', userId);
|
|
29
|
+
} else if (amount > 0) {
|
|
14
30
|
await supabase.from('user_stats').insert([{ id: userId, xp: amount, level: 1 }]).catch(() => {});
|
|
15
31
|
}
|
|
16
32
|
}
|
|
17
33
|
|
|
34
|
+
/** The profile a request belongs to: the one sent, else this machine's. */
|
|
35
|
+
function whose(userId) {
|
|
36
|
+
if (userId) return String(userId);
|
|
37
|
+
try { return resolveProfile({ create: false })?.id || null; } catch { return null; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Every copy of one person: by the clicked row's profile URL, or the URL given. */
|
|
41
|
+
function copiesOf(db, { connectionId, profileUrl, userId }) {
|
|
42
|
+
let url = profileUrl ? String(profileUrl) : null;
|
|
43
|
+
let user = userId ? String(userId) : null;
|
|
44
|
+
let clicked = null;
|
|
45
|
+
if (connectionId != null) {
|
|
46
|
+
clicked = db.prepare('SELECT id, profile_url, user_id FROM linkedin_connections WHERE id = ?').get(String(connectionId)) || null;
|
|
47
|
+
if (clicked) {
|
|
48
|
+
url = clicked.profile_url || url;
|
|
49
|
+
user = user || clicked.user_id || null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const cols = 'id, degree, tier, outreach_status, unlock_status, unlocked_from_bridge_id';
|
|
53
|
+
let rows = [];
|
|
54
|
+
if (url) {
|
|
55
|
+
rows = user
|
|
56
|
+
? db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE profile_url = ? AND user_id = ?`).all(url, user)
|
|
57
|
+
: db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE profile_url = ?`).all(url);
|
|
58
|
+
} else if (clicked) {
|
|
59
|
+
rows = db.prepare(`SELECT ${cols} FROM linkedin_connections WHERE id = ?`).all(clicked.id);
|
|
60
|
+
}
|
|
61
|
+
return { url, user, rows };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const inList = (ids) => ids.map(() => '?').join(', ');
|
|
65
|
+
const bestTier = (rows) => TIER_ORDER.find((t) => rows.some((r) => r.tier === t)) || 'D';
|
|
66
|
+
|
|
18
67
|
export async function POST(request) {
|
|
19
68
|
try {
|
|
20
69
|
const body = await request.json();
|
|
21
|
-
const { action, connectionId, profileUrl, userId } = body;
|
|
70
|
+
const { action, connectionId, profileUrl, userId, bridgeId } = body;
|
|
71
|
+
if (!['mark-sent', 'mark-accepted', 'undo'].includes(action)) {
|
|
72
|
+
return Response.json({ error: 'Unknown action' }, { status: 400 });
|
|
73
|
+
}
|
|
74
|
+
const db = getDb();
|
|
75
|
+
const { user, rows } = copiesOf(db, { connectionId, profileUrl, userId });
|
|
76
|
+
// Nobody on file (the sample network lives in the browser): nothing to keep.
|
|
77
|
+
if (!rows.length) return Response.json({ success: true, saved: 0, xp: 0 });
|
|
78
|
+
const ids = rows.map((r) => r.id);
|
|
79
|
+
const now = new Date().toISOString();
|
|
22
80
|
|
|
23
81
|
if (action === 'mark-sent') {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
82
|
+
const already = rows.some((r) => r.outreach_status === 'sent' || r.outreach_status === 'accepted');
|
|
83
|
+
db.prepare(`UPDATE linkedin_connections SET outreach_status = 'sent', updated_at = ?
|
|
84
|
+
WHERE id IN (${inList(ids)}) AND (outreach_status IS NULL OR outreach_status NOT IN ('sent', 'accepted'))`).run(now, ...ids);
|
|
85
|
+
db.prepare(`UPDATE linkedin_connections SET unlock_status = 'pending', updated_at = ?
|
|
86
|
+
WHERE id IN (${inList(ids)}) AND (unlock_status IS NULL OR unlock_status = 'locked')`).run(now, ...ids);
|
|
87
|
+
// Who you asked through: one of your own connections, or nothing.
|
|
88
|
+
if (bridgeId != null) {
|
|
89
|
+
const bridge = db.prepare(
|
|
90
|
+
`SELECT id, name FROM linkedin_connections WHERE id = ? AND degree = 1${user ? ' AND user_id = ?' : ''}`,
|
|
91
|
+
).get(...[String(bridgeId), ...(user ? [user] : [])]);
|
|
92
|
+
if (bridge) {
|
|
93
|
+
db.prepare(`UPDATE linkedin_connections SET unlocked_from_bridge_id = ?, unlocked_from_name = ?, updated_at = ?
|
|
94
|
+
WHERE id IN (${inList(ids)}) AND degree = 2 AND unlocked_from_bridge_id IS NULL`).run(bridge.id, bridge.name, now, ...ids);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const xp = already ? 0 : XP_SEND[bestTier(rows)] || 2;
|
|
98
|
+
await awardXP(whose(user), xp);
|
|
99
|
+
return Response.json({ success: true, saved: ids.length, xp, already });
|
|
40
100
|
}
|
|
41
101
|
|
|
42
102
|
if (action === 'mark-accepted') {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (userId) query = query.eq('user_id', userId);
|
|
46
|
-
const { error } = await query;
|
|
47
|
-
if (error) return Response.json({ error: error.message }, { status: 500 });
|
|
48
|
-
return Response.json({ success: true });
|
|
103
|
+
db.prepare(`UPDATE linkedin_connections SET outreach_status = 'accepted', updated_at = ? WHERE id IN (${inList(ids)})`).run(now, ...ids);
|
|
104
|
+
return Response.json({ success: true, saved: ids.length, xp: 0, accepts: XP_ACCEPT[bestTier(rows)] || 5 });
|
|
49
105
|
}
|
|
50
106
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return Response.json({ error: 'Unknown action' }, { status: 400 });
|
|
107
|
+
// undo
|
|
108
|
+
const wasSent = rows.some((r) => r.outreach_status === 'sent');
|
|
109
|
+
db.prepare(`UPDATE linkedin_connections SET outreach_status = NULL, updated_at = ?
|
|
110
|
+
WHERE id IN (${inList(ids)}) AND outreach_status = 'sent'`).run(now, ...ids);
|
|
111
|
+
db.prepare(`UPDATE linkedin_connections SET unlock_status = 'locked', updated_at = ?
|
|
112
|
+
WHERE id IN (${inList(ids)}) AND unlock_status = 'pending'`).run(now, ...ids);
|
|
113
|
+
const xp = wasSent ? -(XP_SEND[bestTier(rows)] || 2) : 0;
|
|
114
|
+
await awardXP(whose(user), xp);
|
|
115
|
+
return Response.json({ success: true, saved: ids.length, xp });
|
|
61
116
|
} catch (err) {
|
|
62
117
|
console.error('Outreach error:', err);
|
|
63
118
|
return Response.json({ error: err.message }, { status: 500 });
|
|
64
119
|
}
|
|
65
120
|
}
|
|
66
121
|
|
|
122
|
+
/** Everyone with a request out, once each (the newest copy), newest first. */
|
|
67
123
|
export async function GET(request) {
|
|
68
124
|
try {
|
|
69
125
|
const { searchParams } = new URL(request.url);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
126
|
+
const user = whose(searchParams.get('userId'));
|
|
127
|
+
const rows = getDb().prepare(
|
|
128
|
+
`SELECT * FROM linkedin_connections
|
|
129
|
+
WHERE (outreach_status = 'sent' OR unlock_status = 'pending')${user ? ' AND user_id = ?' : ''}
|
|
130
|
+
ORDER BY created_at DESC`,
|
|
131
|
+
).all(...(user ? [user] : []));
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
const pending = [];
|
|
134
|
+
for (const row of rows) {
|
|
135
|
+
const key = row.profile_url || `id:${row.id}`;
|
|
136
|
+
if (seen.has(key)) continue;
|
|
137
|
+
seen.add(key);
|
|
138
|
+
pending.push(decodeRow('linkedin_connections', row));
|
|
139
|
+
}
|
|
140
|
+
return Response.json({ pending });
|
|
78
141
|
} catch (err) {
|
|
79
142
|
return Response.json({ error: err.message }, { status: 500 });
|
|
80
143
|
}
|
|
@@ -9,6 +9,8 @@ import { pausedList, readProgress, readUnclear } from '../../../lib/paused';
|
|
|
9
9
|
import { getDb } from '../../../lib/db-client';
|
|
10
10
|
import { registerScanState } from '../../../lib/scan-state';
|
|
11
11
|
import { pendingImport } from '../../../lib/data-import';
|
|
12
|
+
import { waitingPhotoCount } from '../../../lib/photos';
|
|
13
|
+
import { reachIndex, circleState } from '../../../lib/reach';
|
|
12
14
|
import {
|
|
13
15
|
pythonLooker, thisHostKey, scannerCommand, installSteps, downloadedPython, downloadVerified,
|
|
14
16
|
placeDownloadedPython, sweepSetupLeftovers, megabytes, SETUP_WORK_PREFIX, ScannerSetupError,
|
|
@@ -32,6 +34,8 @@ const MAX_LOG = 500;
|
|
|
32
34
|
const state = registerScanState({
|
|
33
35
|
running: false,
|
|
34
36
|
action: null,
|
|
37
|
+
target: null, // who a scan is of: { id, name } (either may be null), or null for scans of no one
|
|
38
|
+
recent: [], // the last few finished jobs, newest first: how each one ended
|
|
35
39
|
log: [],
|
|
36
40
|
startedAt: null,
|
|
37
41
|
exitCode: null,
|
|
@@ -126,6 +130,53 @@ function bridgeSkips() {
|
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
/**
|
|
134
|
+
* What lib/reach.js needs from the scanner's files to tell who is ready for a
|
|
135
|
+
* circle scan: whose list is hidden, and whose list has been read even though
|
|
136
|
+
* nothing in it was saved (everyone on it was already yours). Read on its own,
|
|
137
|
+
* without the machine checks, because the map asks for it when it loads and
|
|
138
|
+
* again after every scan.
|
|
139
|
+
*/
|
|
140
|
+
function scanNotes() {
|
|
141
|
+
let read = [];
|
|
142
|
+
try {
|
|
143
|
+
const me = resolveProfile({ create: false });
|
|
144
|
+
if (me) read = Object.keys(readProgress(dataDir(), me.id));
|
|
145
|
+
} catch {}
|
|
146
|
+
return { skips: bridgeSkips(), read };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* One of your connections, for the Scan page's "Scan one circle" box (Bridge
|
|
151
|
+
* Chains and the Degrees panel send people there by id): who they are, and how
|
|
152
|
+
* their circle stands by the same rule the map uses. Null when not found.
|
|
153
|
+
*/
|
|
154
|
+
function pickedPerson(id) {
|
|
155
|
+
try {
|
|
156
|
+
const me = resolveProfile({ create: false });
|
|
157
|
+
if (!me) return { person: null };
|
|
158
|
+
const db = getDb();
|
|
159
|
+
const row = db.prepare(
|
|
160
|
+
`SELECT id, name, tier, power_score, degree, profile_url, unlocked_from_bridge_id, unlocked_from_name
|
|
161
|
+
FROM linkedin_connections WHERE id = ? AND user_id = ? AND degree = 1`,
|
|
162
|
+
).get(String(id ?? ''), me.id);
|
|
163
|
+
if (!row) return { person: null };
|
|
164
|
+
const mapped = db.prepare(
|
|
165
|
+
'SELECT 1 FROM linkedin_connections WHERE user_id = ? AND degree = 2 AND source_connection_id = ? LIMIT 1',
|
|
166
|
+
).get(me.id, row.id);
|
|
167
|
+
const reach = reachIndex([], mapped ? [{ source_connection_id: row.id }] : [], scanNotes());
|
|
168
|
+
return {
|
|
169
|
+
person: {
|
|
170
|
+
id: row.id, name: row.name, tier: row.tier, power_score: row.power_score,
|
|
171
|
+
unlocked_from_bridge_id: row.unlocked_from_bridge_id, unlocked_from_name: row.unlocked_from_name,
|
|
172
|
+
},
|
|
173
|
+
circle: circleState(row, reach),
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
return { person: null };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
129
180
|
// The machine checks (Python, Chrome, signed in) spawn processes, so they are
|
|
130
181
|
// cached for a few seconds, and callers that arrive while a look is under way
|
|
131
182
|
// share it rather than each starting their own Pythons. Everything about the
|
|
@@ -225,6 +276,10 @@ async function status() {
|
|
|
225
276
|
me = resolveProfile({ create: false });
|
|
226
277
|
if (me) network = networkCounts(me.id);
|
|
227
278
|
} catch {}
|
|
279
|
+
// People whose photo is still a link, which the app doesn't load: Save
|
|
280
|
+
// photos appears while there are any (lib/photos.js).
|
|
281
|
+
let photosWaiting = 0;
|
|
282
|
+
try { photosWaiting = waitingPhotoCount(getDb()); } catch {}
|
|
228
283
|
|
|
229
284
|
const py = m.python;
|
|
230
285
|
return {
|
|
@@ -251,20 +306,69 @@ async function status() {
|
|
|
251
306
|
signedIn: m.signedIn,
|
|
252
307
|
},
|
|
253
308
|
network,
|
|
309
|
+
photosWaiting,
|
|
310
|
+
...job(),
|
|
311
|
+
skips: bridgeSkips(),
|
|
312
|
+
linkedin: linkedinState(dataDir()),
|
|
313
|
+
paused: paused(me?.id),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The running job alone, from memory: no checks, no database. Every Scan
|
|
319
|
+
* button in the app asks this (lib/scraper-client.js watchScanner), every few
|
|
320
|
+
* seconds, so it must cost nothing.
|
|
321
|
+
*
|
|
322
|
+
* `recent` is how the last few jobs ended. A page following one job can then
|
|
323
|
+
* learn how it ended even when another job started before it looked again;
|
|
324
|
+
* with only the latest job's exit code, that one would never seem to finish.
|
|
325
|
+
*/
|
|
326
|
+
function job() {
|
|
327
|
+
return {
|
|
254
328
|
running: state.running,
|
|
255
329
|
action: state.action,
|
|
330
|
+
target: state.target,
|
|
256
331
|
startedAt: state.startedAt,
|
|
257
332
|
exitCode: state.exitCode,
|
|
258
333
|
failure: state.running ? null : state.failure,
|
|
259
334
|
progress: state.running ? scanProgress(state.log, state.action) : null,
|
|
260
335
|
log: state.log.slice(-120),
|
|
261
|
-
|
|
262
|
-
linkedin: linkedinState(dataDir()),
|
|
263
|
-
paused: paused(me?.id),
|
|
336
|
+
recent: state.recent,
|
|
264
337
|
};
|
|
265
338
|
}
|
|
266
339
|
|
|
267
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Where Resume would carry on with one of your connections, for their profile
|
|
342
|
+
* card: { nextPage, pagesRead, legacy }, or null when there is nothing to carry
|
|
343
|
+
* on with (never mapped, read to the end, hidden). The Paused list's own rules
|
|
344
|
+
* (lib/paused.js), so the card and the Scan page can't disagree.
|
|
345
|
+
*/
|
|
346
|
+
function resumePoint(id) {
|
|
347
|
+
try {
|
|
348
|
+
const me = resolveProfile({ create: false });
|
|
349
|
+
if (!me) return null;
|
|
350
|
+
const db = getDb();
|
|
351
|
+
const row = db.prepare(
|
|
352
|
+
`SELECT id, name, tier, profile_url, connected_date, created_at
|
|
353
|
+
FROM linkedin_connections WHERE id = ? AND user_id = ? AND degree = 1`,
|
|
354
|
+
).get(String(id ?? ''), me.id);
|
|
355
|
+
if (!row) return null;
|
|
356
|
+
const mapped = db.prepare(
|
|
357
|
+
'SELECT 1 FROM linkedin_connections WHERE user_id = ? AND degree = 2 AND source_connection_id = ? LIMIT 1',
|
|
358
|
+
).get(me.id, row.id);
|
|
359
|
+
const [p] = pausedList([row], new Set(mapped ? [row.id] : []), readProgress(dataDir(), me.id), readUnclear(dataDir()));
|
|
360
|
+
return p ? { nextPage: p.nextPage, pagesRead: p.pagesRead, legacy: p.legacy } : null;
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function GET(request) {
|
|
367
|
+
const q = new URL(request?.url || 'http://127.0.0.1/api/scraper').searchParams;
|
|
368
|
+
if (q.has('job')) return Response.json(job());
|
|
369
|
+
if (q.has('resume')) return Response.json({ resume: resumePoint(q.get('resume')) });
|
|
370
|
+
if (q.has('reach')) return Response.json(scanNotes());
|
|
371
|
+
if (q.has('person')) return Response.json(pickedPerson(q.get('person')));
|
|
268
372
|
return Response.json(await status());
|
|
269
373
|
}
|
|
270
374
|
|
|
@@ -283,7 +387,10 @@ const ACTIONS = {
|
|
|
283
387
|
// Hidden profiles are remembered so they are not retried forever; this is
|
|
284
388
|
// the way back in without a terminal.
|
|
285
389
|
'auto-bridge-retry': { flag: '--auto-bridge --retry-private', label: 'Mapping every bridge, hidden ones included' },
|
|
286
|
-
|
|
390
|
+
// One person's circle, from page 1. By profile URL when the page sends their
|
|
391
|
+
// id, as every Scan button does: two connections can share a name, and by name
|
|
392
|
+
// the scanner reads whichever was saved first. By name for a caller with no id.
|
|
393
|
+
bridge: { flag: '--bridge', needsName: true, byId: true, label: 'Mapping the circle behind' },
|
|
287
394
|
// Carry on with one person whose read was cut short. By profile URL, not name:
|
|
288
395
|
// two connections can share a name, and Resume must reach the one clicked.
|
|
289
396
|
resume: { flag: '--bridge-url', needsId: true, label: 'Carrying on with', searches: true },
|
|
@@ -291,6 +398,9 @@ const ACTIONS = {
|
|
|
291
398
|
'resume-all': { flag: '--auto-bridge --only-unfinished', label: 'Carrying on with every paused list', searches: true },
|
|
292
399
|
rescrape: { flag: '--rescrape', needsName: true, label: 'Re-mapping the circle behind' },
|
|
293
400
|
company: { flag: '--company', needsName: true, label: 'Scanning' },
|
|
401
|
+
// Photos an older version kept as links to LinkedIn: saved here, once each.
|
|
402
|
+
// No browser, no search; every scan does the same at its end.
|
|
403
|
+
photos: { flag: '--save-photos', label: 'Saving profile photos to this computer' },
|
|
294
404
|
};
|
|
295
405
|
|
|
296
406
|
/** Only a plain linkedin.com/in/ profile URL becomes an argument. */
|
|
@@ -402,7 +512,7 @@ export async function POST(request) {
|
|
|
402
512
|
|
|
403
513
|
// The two settings a person changes here. Neither starts anything.
|
|
404
514
|
if (action === 'set-limits') {
|
|
405
|
-
const limits = writeLimits(dataDir(), { daily: body.daily, monthly: body.monthly });
|
|
515
|
+
const limits = writeLimits(dataDir(), { daily: body.daily, monthly: body.monthly, profiles: body.profiles });
|
|
406
516
|
return Response.json({ ok: true, limits });
|
|
407
517
|
}
|
|
408
518
|
if (action === 'lift-cooldown') {
|
|
@@ -431,24 +541,34 @@ export async function POST(request) {
|
|
|
431
541
|
const readsCircles = action.startsWith('auto-bridge') || ['bridge', 'rescrape', 'resume', 'resume-all'].includes(action);
|
|
432
542
|
// Carry on with people already mapped, from the page each one stopped at.
|
|
433
543
|
const deeper = body.deeper === true && (action.startsWith('auto-bridge') || action === 'bridge');
|
|
544
|
+
// Resume, and a circle scan of someone picked by id, go by the connection's
|
|
545
|
+
// profile URL rather than a name.
|
|
546
|
+
const byId = spec.needsId || (spec.byId && Boolean(body.id));
|
|
434
547
|
let name = null;
|
|
435
|
-
if (spec.needsName) {
|
|
548
|
+
if (spec.needsName && !byId) {
|
|
436
549
|
name = cleanName(body.name);
|
|
437
550
|
if (!name) return Response.json({ error: 'A name is required for this action.' }, { status: 400 });
|
|
438
551
|
}
|
|
439
|
-
//
|
|
440
|
-
//
|
|
552
|
+
// The id comes from the page; the URL is looked up here, for this profile, so
|
|
553
|
+
// nothing from the request itself reaches the command line.
|
|
441
554
|
let profileUrl = null;
|
|
442
|
-
|
|
555
|
+
let person = null;
|
|
556
|
+
if (byId) {
|
|
443
557
|
try {
|
|
444
558
|
const me = resolveProfile({ create: false });
|
|
445
|
-
|
|
446
|
-
'SELECT profile_url FROM linkedin_connections WHERE id = ? AND user_id = ? AND degree = 1',
|
|
559
|
+
person = me && getDb().prepare(
|
|
560
|
+
'SELECT name, profile_url FROM linkedin_connections WHERE id = ? AND user_id = ? AND degree = 1',
|
|
447
561
|
).get(String(body.id ?? ''), me.id);
|
|
448
|
-
profileUrl =
|
|
562
|
+
profileUrl = person?.profile_url && cleanProfileUrl(person.profile_url);
|
|
449
563
|
} catch { profileUrl = null; }
|
|
450
564
|
if (!profileUrl) return Response.json({ error: 'That connection could not be found.' }, { status: 400 });
|
|
451
565
|
}
|
|
566
|
+
// Who the job is of, so every Scan button can say whose scan is running and a
|
|
567
|
+
// profile card can show its own person's progress. Only ever shown: the id
|
|
568
|
+
// never reaches the command line (a name or the URL looked up for it does),
|
|
569
|
+
// and anything not shaped like one of our ids is dropped.
|
|
570
|
+
const hintId = typeof body.id === 'string' && /^[\w-]{1,64}$/.test(body.id) ? body.id : null;
|
|
571
|
+
const target = name || profileUrl ? { id: hintId, name: name || person?.name || null } : null;
|
|
452
572
|
// Nothing that searches LinkedIn starts during a cooldown; the scanner checks
|
|
453
573
|
// too, this just says so before a process is spawned.
|
|
454
574
|
// The 1st-degree scans aren't searches, but they open LinkedIn with automation too.
|
|
@@ -497,8 +617,11 @@ export async function POST(request) {
|
|
|
497
617
|
// app is inside the signed app (lib/scanner-python.js scannerCommand).
|
|
498
618
|
plan = [scannerCommand(found.run, path.join(root, 'scripts', 'scrape.py'), [
|
|
499
619
|
// `--flag=value` is one token on purpose: a name beginning with
|
|
500
|
-
// "-" can then never be read as a flag of its own.
|
|
501
|
-
|
|
620
|
+
// "-" can then never be read as a flag of its own. Anyone found by
|
|
621
|
+
// id is --bridge-url, which carries on where their last read stopped
|
|
622
|
+
// unless told to start at page 1.
|
|
623
|
+
...(name ? [`${spec.flag}=${name}`] : profileUrl ? [`--bridge-url=${profileUrl}`] : spec.flag.split(' ')),
|
|
624
|
+
...(profileUrl && action === 'bridge' && !deeper ? ['--from-start'] : []),
|
|
502
625
|
...(maxBridges && (action.startsWith('auto-bridge') || action === 'resume-all') ? [`--max-bridges=${maxBridges}`] : []),
|
|
503
626
|
...(tiers.length && action.startsWith('auto-bridge') ? [`--tiers=${tiers.join(',')}`] : []),
|
|
504
627
|
...(action.startsWith('auto-bridge') ? [`--order=${order}`] : []),
|
|
@@ -517,9 +640,10 @@ export async function POST(request) {
|
|
|
517
640
|
state.running = true;
|
|
518
641
|
state.stopping = false;
|
|
519
642
|
state.action = action;
|
|
643
|
+
state.target = target;
|
|
520
644
|
state.exitCode = null;
|
|
521
645
|
state.startedAt = Date.now();
|
|
522
|
-
state.log = [spec.label + (name ? ` ${name}…` : '…')];
|
|
646
|
+
state.log = [spec.label + (target?.name ? ` ${target.name}…` : '…')];
|
|
523
647
|
state.stderrTail = [];
|
|
524
648
|
state.failure = null;
|
|
525
649
|
forgetChecks();
|
|
@@ -560,6 +684,9 @@ export async function POST(request) {
|
|
|
560
684
|
state.child = null;
|
|
561
685
|
state.abort = null;
|
|
562
686
|
state.exitCode = code;
|
|
687
|
+
state.recent = [{
|
|
688
|
+
action: state.action, target: state.target, startedAt: state.startedAt, exitCode: code, failure: state.failure,
|
|
689
|
+
}, ...state.recent].slice(0, 5);
|
|
563
690
|
// A setup's private folder (the download and what was unpacked from it).
|
|
564
691
|
if (cleanup) {
|
|
565
692
|
try { cleanup(); } catch { /* swept at the next setup (sweepSetupLeftovers) */ }
|
|
@@ -625,5 +752,7 @@ export async function POST(request) {
|
|
|
625
752
|
|
|
626
753
|
runStep(0);
|
|
627
754
|
|
|
628
|
-
|
|
755
|
+
// Which job this is, so the page that started it can tell its end from the
|
|
756
|
+
// end of whatever runs next (lib/scraper-client.js runScrape).
|
|
757
|
+
return Response.json({ ok: true, action, startedAt: state.startedAt });
|
|
629
758
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getDb } from '../../../../lib/db-client';
|
|
2
|
-
import { companyOverrides, readForScoring, scoringRows, sectorFocusOf } from '../../../../lib/rpc';
|
|
2
|
+
import { companyOverrides, readForScoring, scoringRows, sectorFocusOf, tierScaleOf } from '../../../../lib/rpc';
|
|
3
3
|
import { parseSectorFocus, previewSectorFocus } from '../../../../lib/sector-focus';
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Scores → Your sector, before you save: what a sector focus would change,
|
|
6
6
|
// against the one saved now. It reads the network once and scores it twice in
|
|
7
7
|
// memory, with the inputs rescoreAll() uses (readForScoring: each company's
|
|
8
8
|
// industry and its sectors from the directory), and writes nothing. A save
|
|
@@ -31,6 +31,7 @@ export async function POST(request) {
|
|
|
31
31
|
read: readForScoring(rows),
|
|
32
32
|
from: sectorFocusOf(db),
|
|
33
33
|
to,
|
|
34
|
+
tierScale: tierScaleOf(db),
|
|
34
35
|
}));
|
|
35
36
|
} catch (err) {
|
|
36
37
|
return Response.json({ error: err.message }, { status: 500 });
|
|
@@ -2,7 +2,7 @@ import { getDb } from '../../../../lib/db-client';
|
|
|
2
2
|
import { readForScoring, scoringRows } from '../../../../lib/rpc';
|
|
3
3
|
import { suggestSectors } from '../../../../lib/sector-directory';
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Scores → Your sector: "Suggested from your network". The directory's
|
|
6
6
|
// sectors most of your scanned people work in, counted here from the rows
|
|
7
7
|
// with the same read rescoring uses, so a suggestion's companies are the ones
|
|
8
8
|
// picking it would lean. Its own read-only GET, not part of GET
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { db as supabase } from '../../../lib/db';
|
|
2
|
+
import { getDb } from '../../../lib/db-client';
|
|
3
|
+
import { localPhoto, waitingPhotos } from '../../../lib/photos';
|
|
2
4
|
|
|
3
5
|
const CORS_HEADERS = {
|
|
4
6
|
'Access-Control-Allow-Origin': '*',
|
|
@@ -10,29 +12,61 @@ export async function OPTIONS() {
|
|
|
10
12
|
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
// The photos still kept as links, for the scanner to save: at the end of a
|
|
16
|
+
// scan, and for Save photos on the Scan page (scrape.py save_waiting_photos).
|
|
17
|
+
// A read, so no CORS header: another site's page can't see the answer.
|
|
18
|
+
export async function GET() {
|
|
19
|
+
try {
|
|
20
|
+
return Response.json({ waiting: waitingPhotos(getDb()) });
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return Response.json({ error: err.message }, { status: 500 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// `images`: [{ profileUrl, imageUrl }], each a photo the scanner has saved,
|
|
27
|
+
// attached to every row of that person. Only a saved file's path is stored,
|
|
28
|
+
// never a link (lib/photos.js).
|
|
29
|
+
//
|
|
30
|
+
// `forget`: [{ profileUrl, imageUrl }], links the scanner got a definite no for
|
|
31
|
+
// (expired, not LinkedIn's, not a picture, or the same picture as someone
|
|
32
|
+
// else's), never one it couldn't reach. The link is cleared, and only where it
|
|
33
|
+
// is still that link, so each is tried once and the person shows initials.
|
|
13
34
|
export async function POST(request) {
|
|
14
35
|
try {
|
|
15
|
-
const { images } = await request.json();
|
|
36
|
+
const { images, forget } = await request.json();
|
|
16
37
|
|
|
17
|
-
if (!images || !Array.isArray(images)) {
|
|
38
|
+
if ((!images || !Array.isArray(images)) && !Array.isArray(forget)) {
|
|
18
39
|
return Response.json({ error: 'Missing images array' }, { status: 400, headers: CORS_HEADERS });
|
|
19
40
|
}
|
|
20
41
|
|
|
21
42
|
let updated = 0;
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
const saved = Array.isArray(images) ? images : [];
|
|
44
|
+
for (let i = 0; i < saved.length; i += 20) {
|
|
45
|
+
const batch = saved.slice(i, i + 20);
|
|
24
46
|
const promises = batch.map(({ profileUrl, imageUrl }) => {
|
|
25
|
-
|
|
47
|
+
const photo = localPhoto(imageUrl);
|
|
48
|
+
if (!profileUrl || !photo) return Promise.resolve();
|
|
26
49
|
return supabase
|
|
27
50
|
.from('linkedin_connections')
|
|
28
|
-
.update({ profile_image_url:
|
|
51
|
+
.update({ profile_image_url: photo })
|
|
29
52
|
.eq('profile_url', profileUrl)
|
|
30
53
|
.then(({ error }) => { if (!error) updated++; });
|
|
31
54
|
});
|
|
32
55
|
await Promise.all(promises);
|
|
33
56
|
}
|
|
34
57
|
|
|
35
|
-
|
|
58
|
+
let forgotten = 0;
|
|
59
|
+
for (const { profileUrl, imageUrl } of Array.isArray(forget) ? forget : []) {
|
|
60
|
+
if (typeof profileUrl !== 'string' || typeof imageUrl !== 'string' || !imageUrl || localPhoto(imageUrl)) continue;
|
|
61
|
+
const { data, error } = await supabase
|
|
62
|
+
.from('linkedin_connections')
|
|
63
|
+
.update({ profile_image_url: null })
|
|
64
|
+
.eq('profile_url', profileUrl)
|
|
65
|
+
.eq('profile_image_url', imageUrl);
|
|
66
|
+
if (!error && data?.length) forgotten++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Response.json({ success: true, updated, forgotten }, { headers: CORS_HEADERS });
|
|
36
70
|
} catch (err) {
|
|
37
71
|
return Response.json({ error: err.message }, { status: 500, headers: CORS_HEADERS });
|
|
38
72
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { dataDir } from '../../../lib/db-client';
|
|
4
|
-
import { AVATAR_FILE } from '../../../lib/
|
|
4
|
+
import { AVATAR_FILE } from '../../../lib/photos';
|
|
5
5
|
|
|
6
6
|
// Avatars are captured by the scraper and live in the user's data directory,
|
|
7
7
|
// never inside the app package — an installed copy must not write into its own
|
|
8
8
|
// files, and real faces must never end up in the repo. Rows store the path
|
|
9
9
|
// `/avatars/<name>.webp`, so this route resolves that to ~/.six-degrees/avatars.
|
|
10
|
+
// It is the only place a page gets a photo from (lib/photos.js): reading one
|
|
11
|
+
// never goes online.
|
|
10
12
|
|
|
11
13
|
export async function GET(_request, { params }) {
|
|
12
14
|
const { file } = await params;
|
|
@@ -26,6 +28,9 @@ export async function GET(_request, { params }) {
|
|
|
26
28
|
'Content-Type': type,
|
|
27
29
|
// Content-addressed filenames, so they can be cached indefinitely.
|
|
28
30
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
31
|
+
// A photo that came in with an import is only as good as its name: never
|
|
32
|
+
// let the browser read one as anything but the image type it claims.
|
|
33
|
+
'X-Content-Type-Options': 'nosniff',
|
|
29
34
|
},
|
|
30
35
|
});
|
|
31
36
|
} catch {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { initialsFor } from '../../lib/tiers';
|
|
4
|
+
import { localPhoto } from '../../lib/photos';
|
|
4
5
|
|
|
5
6
|
const FALLBACK = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
|
|
6
7
|
|
|
@@ -9,8 +10,10 @@ const FALLBACK = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#
|
|
|
9
10
|
// Most people have no photo, so the initials disc is the common case, not the
|
|
10
11
|
// fallback — and it sits UNDER the photo, so a photo that fails to load just
|
|
11
12
|
// reveals it. No state, and no reaching into a sibling node to un-hide it.
|
|
13
|
+
// Only a photo saved on this computer is drawn (lib/photos.js).
|
|
12
14
|
export default function Avatar({ person, size = 32, tierColors = FALLBACK }) {
|
|
13
15
|
const c = tierColors[person?.tier] || '#666';
|
|
16
|
+
const photo = localPhoto(person?.profile_image_url);
|
|
14
17
|
return (
|
|
15
18
|
<span style={{
|
|
16
19
|
position: 'relative', display: 'inline-block', width: size, height: size, flexShrink: 0,
|
|
@@ -20,9 +23,9 @@ export default function Avatar({ person, size = 32, tierColors = FALLBACK }) {
|
|
|
20
23
|
position: 'absolute', inset: 0, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
21
24
|
background: `${c}2e`, color: c, fontWeight: 800, fontSize: Math.max(7, Math.round(size * 0.36)), lineHeight: 1,
|
|
22
25
|
}}>{initialsFor(person?.name)}</span>
|
|
23
|
-
{
|
|
26
|
+
{photo && (
|
|
24
27
|
<img
|
|
25
|
-
src={
|
|
28
|
+
src={photo}
|
|
26
29
|
alt=""
|
|
27
30
|
loading="lazy"
|
|
28
31
|
decoding="async"
|