six-degrees 0.4.8 → 0.4.9
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 +3 -3
- 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 +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +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.nft.json +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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +2 -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 +2 -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.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.nft.json +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.nft.json +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.nft.json +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 +2 -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 +3 -2
- 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 +2 -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 +2 -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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/messages/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/messages/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +9 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/messages/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/route.js +4 -2
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +14 -14
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +14 -14
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +14 -14
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +14 -14
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +14 -14
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +14 -14
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +14 -14
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +14 -14
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
- 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 +14 -14
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06mi9k2._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +6 -6
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_messages_route_actions_0skh09r.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +1 -1
- package/.next/standalone/.next/server/chunks/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 +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_1zc5q0a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1feicqq._.js → [root-of-the-server]__0qdeuyo._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1mj739k._.js → _0l8cxmo._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0wxbq78._.js → app_components_19c2d9c._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_1wax83z._.js → node_modules_0h91jdk._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_1nkgb45._.js → node_modules_1f8t_rp._.js} +1 -1
- 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/{static/chunks/1z-zp93cyoxvz.js → standalone/.next/static/chunks/04bbi55urkrd6.js} +1 -1
- package/.next/{static/chunks/3q3pmt5vv-4pi.js → standalone/.next/static/chunks/054zdthcnyd2h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{236f0jtp68j_h.js → 1_mohm2643nxp.js} +1 -1
- package/.next/{static/chunks/3zw8bv7-mc4n6.js → standalone/.next/static/chunks/1z_rt4jteewqp.js} +1 -1
- package/.next/standalone/.next/static/chunks/2bftn9gubwf0w.js +1 -0
- package/.next/{static/chunks/0g_p4p7oci18u.js → standalone/.next/static/chunks/2kfhc58df2xyo.js} +1 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +1 -0
- package/.next/standalone/.next/static/chunks/39csdcx7yv227.js +1 -0
- package/.next/standalone/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
- package/.next/standalone/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
- package/.next/standalone/CHANGELOG.md +39 -0
- package/.next/standalone/app/api/social/messages/route.js +130 -0
- package/.next/standalone/app/api/social/route.js +89 -47
- package/.next/standalone/app/components/ScanStatusBar.js +40 -3
- package/.next/standalone/app/components/settings/DataSection.js +2 -1
- package/.next/standalone/app/paths/page.js +2 -0
- package/.next/standalone/app/profile/page.js +2 -0
- package/.next/standalone/app/queue/page.js +2 -0
- package/.next/standalone/app/scores/page.js +2 -0
- package/.next/standalone/app/settings/page.js +2 -0
- package/.next/standalone/app/setup/page.js +6 -3
- package/.next/standalone/app/social/Conversations.js +246 -0
- package/.next/standalone/app/social/page.js +90 -9
- package/.next/standalone/lib/galaxy-lab.js +6 -1
- package/.next/standalone/lib/linkedin-export.js +175 -5
- package/.next/standalone/lib/social-store.js +73 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +156 -23
- package/.next/{standalone/.next/static/chunks/1z-zp93cyoxvz.js → static/chunks/04bbi55urkrd6.js} +1 -1
- package/.next/{standalone/.next/static/chunks/3q3pmt5vv-4pi.js → static/chunks/054zdthcnyd2h.js} +1 -1
- package/.next/static/chunks/{236f0jtp68j_h.js → 1_mohm2643nxp.js} +1 -1
- package/.next/{standalone/.next/static/chunks/3zw8bv7-mc4n6.js → static/chunks/1z_rt4jteewqp.js} +1 -1
- package/.next/static/chunks/2bftn9gubwf0w.js +1 -0
- package/.next/{standalone/.next/static/chunks/0g_p4p7oci18u.js → static/chunks/2kfhc58df2xyo.js} +1 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +1 -0
- package/.next/static/chunks/39csdcx7yv227.js +1 -0
- package/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
- package/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
- package/CHANGELOG.md +39 -0
- package/lib/galaxy-lab.js +6 -1
- package/lib/linkedin-export.js +175 -5
- package/lib/social-store.js +73 -0
- package/package.json +1 -1
- package/scripts/scrape.py +156 -23
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17kn1mk._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dmc7-t._.js +0 -8
- package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_217nddo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_0i3fw_m._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +0 -3
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +0 -1
- package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +0 -1
- package/.next/standalone/.next/static/chunks/15qbli9udo0vh.js +0 -1
- package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/static/chunks/0y1yms5kez2f3.js +0 -1
- package/.next/static/chunks/14m1zu-8warte.js +0 -1
- package/.next/static/chunks/15qbli9udo0vh.js +0 -1
- /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
- /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
- /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
|
@@ -6,6 +6,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.9] - 2026-09-29
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Conversations, in the Social tab.** Every conversation with one of your connections, from your
|
|
13
|
+
export and the live messages sync: who it's with (name and tier), when it was last active, who
|
|
14
|
+
wrote last, unread, how many messages, and *Open on LinkedIn* (the thread when the sync found
|
|
15
|
+
its link, else their profile). Waiting on you comes first, then S and A tier, then the most
|
|
16
|
+
recent. Group conversations are shown and marked, and aren't counted in warmth. A search box
|
|
17
|
+
finds people by name.
|
|
18
|
+
- **Keep my messages on this computer**, a switch that's off unless you turn it on. When it's on,
|
|
19
|
+
the messages themselves, what other people wrote to you included, are kept in the app's data
|
|
20
|
+
folder (a file of their own beside the Social tab's), and a conversation opens as a thread,
|
|
21
|
+
newest at the bottom, a page at a time so a thread of thousands opens as fast as a short one.
|
|
22
|
+
The search box then finds what was said, too. They're never sent anywhere, and "Save a copy of
|
|
23
|
+
my network" doesn't carry them (Settings says so). Switching it off asks first, then deletes
|
|
24
|
+
them; *Forget it* deletes them with everything else.
|
|
25
|
+
- **The live messages sync reads a little more of the list.** Each conversation's link, whether
|
|
26
|
+
it's a group, and its newest message: who wrote last, which now feeds warmth and Waiting on you
|
|
27
|
+
where your export is missing or older, and its words, which go to the app only while Keep my
|
|
28
|
+
messages is on. It still opens no conversation, and its saved samples still keep no words.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **Auto scan is slower and has its own ceilings.** Sittings of 8 pages, then an hour's rest;
|
|
32
|
+
searches only from 9:00 to 18:00; never more than 40 searches in 24 hours or 200 in 7 days,
|
|
33
|
+
however high the daily budget is set (the account restricted on 28 September did 373 in a
|
|
34
|
+
day with the budget at 500); and two days with no searches after any check from LinkedIn.
|
|
35
|
+
- **The scan status bar sits under the buttons.** On each page it now sits in its own row
|
|
36
|
+
under the header, pushing the page down instead of covering it. On Network Circle it floats
|
|
37
|
+
just under the header, measured, so a header that wraps to two rows no longer hides it.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **The live messages sync keeps what it finds.** It read your conversations but matched none of
|
|
41
|
+
them: the pattern for a profile link never matched, and LinkedIn's messaging gives a member
|
|
42
|
+
link your connections list doesn't have. It now matches each person to your connections by
|
|
43
|
+
link, or by a name only one connection has (the name isn't kept), and scrolls the list's own
|
|
44
|
+
panel so more than the first 20 conversations load.
|
|
45
|
+
- **Messaging samples keep no words.** The sync's saved samples of LinkedIn's data (for tuning
|
|
46
|
+
the reader) now drop every piece of writing and every name before they're saved.
|
|
47
|
+
|
|
9
48
|
## [0.4.8] - 2026-09-29
|
|
10
49
|
|
|
11
50
|
### Changed
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { conversationList } from '../../../../lib/linkedin-export';
|
|
3
|
+
import { socialFiles, readJson, writeJsonAtomic, readMessages, deleteMessages } from '../../../../lib/social-store';
|
|
4
|
+
|
|
5
|
+
// The Social tab's Conversations: the list, one thread at a time, and the
|
|
6
|
+
// messages themselves when Keep my messages is on (lib/social-store.js has the
|
|
7
|
+
// two files and why they're apart).
|
|
8
|
+
//
|
|
9
|
+
// GET the list, no words: who, when, who wrote last,
|
|
10
|
+
// unread, how many, the thread's link
|
|
11
|
+
// GET ?id=<id> | ?with=<key> one thread, a page at a time (before, limit):
|
|
12
|
+
// its messages only if they're kept
|
|
13
|
+
// GET ?q=<words> the conversations whose kept messages say that
|
|
14
|
+
// POST an import's messages, in parts
|
|
15
|
+
// DELETE the messages gone, the list kept
|
|
16
|
+
//
|
|
17
|
+
// Nothing here goes anywhere but this computer's browser: no route of the app
|
|
18
|
+
// sends messages on, and a copy of the network doesn't carry this file.
|
|
19
|
+
|
|
20
|
+
const PAGE = 100;
|
|
21
|
+
const MAX_TEXT = 20000;
|
|
22
|
+
|
|
23
|
+
function state() {
|
|
24
|
+
const files = socialFiles();
|
|
25
|
+
if (!files) return null;
|
|
26
|
+
const social = readJson(files.social, {}) || {};
|
|
27
|
+
const keep = social.keepMessages === true;
|
|
28
|
+
const kept = keep ? readMessages(files.messages) : { threads: {}, previews: {} };
|
|
29
|
+
return { files, social, keep, kept, list: conversationList(social.conversations, social.live) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A conversation's kept messages, oldest first, with the live sync's newest one after them when it's newer. */
|
|
33
|
+
function messagesOf(c, kept) {
|
|
34
|
+
const msgs = kept.threads[c.id]?.messages || [];
|
|
35
|
+
const preview = !c.group && c.people.length === 1 ? kept.previews[c.people[0]] : null;
|
|
36
|
+
if (preview?.text && (!msgs.length || (preview.t || 0) > (msgs[msgs.length - 1].t || 0))) return [...msgs, preview];
|
|
37
|
+
return msgs;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function GET(request) {
|
|
41
|
+
const s = state();
|
|
42
|
+
if (!s) return Response.json({ keepMessages: false, conversations: [] });
|
|
43
|
+
const url = new URL(request.url);
|
|
44
|
+
const id = url.searchParams.get('id');
|
|
45
|
+
const withKey = url.searchParams.get('with');
|
|
46
|
+
|
|
47
|
+
if (id || withKey) {
|
|
48
|
+
const c = id
|
|
49
|
+
? s.list.find((x) => x.id === id)
|
|
50
|
+
: s.list.filter((x) => !x.group && x.people[0] === withKey).sort((a, b) => (b.last || 0) - (a.last || 0))[0];
|
|
51
|
+
if (!c) return Response.json({ error: 'That conversation isn’t here.' }, { status: 404 });
|
|
52
|
+
if (!s.keep) return Response.json({ kept: false, id: c.id, total: c.count });
|
|
53
|
+
const msgs = messagesOf(c, s.kept);
|
|
54
|
+
const total = msgs.length;
|
|
55
|
+
const limit = Math.min(500, Math.max(1, Number(url.searchParams.get('limit')) || PAGE));
|
|
56
|
+
const before = url.searchParams.has('before') ? Math.max(0, Math.min(total, Number(url.searchParams.get('before')) || 0)) : total;
|
|
57
|
+
const start = Math.max(0, before - limit);
|
|
58
|
+
return Response.json({ kept: true, id: c.id, title: s.kept.threads[c.id]?.title || '', total, start, messages: msgs.slice(start, before) });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const q = String(url.searchParams.get('q') || '').trim().toLowerCase();
|
|
62
|
+
if (url.searchParams.has('q')) {
|
|
63
|
+
if (!s.keep || q.length < 2) return Response.json({ ids: [] });
|
|
64
|
+
const ids = s.list.filter((c) => messagesOf(c, s.kept).some((m) => String(m.text || '').toLowerCase().includes(q))).map((c) => c.id);
|
|
65
|
+
return Response.json({ ids });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const conversations = s.list.map((c) => ({
|
|
69
|
+
...c,
|
|
70
|
+
title: s.kept.threads[c.id]?.title || '',
|
|
71
|
+
hasText: s.keep && messagesOf(c, s.kept).length > 0,
|
|
72
|
+
}));
|
|
73
|
+
return Response.json({ keepMessages: s.keep, conversations });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One message as it's kept, or null when it isn't one. */
|
|
77
|
+
function cleanMessage(m) {
|
|
78
|
+
if (!m || typeof m !== 'object' || !Number.isFinite(Number(m.t))) return null;
|
|
79
|
+
return { t: Number(m.t), fromMe: m.fromMe === true, text: typeof m.text === 'string' ? m.text.slice(0, MAX_TEXT) : '' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// An import's messages: { part, parts, threads: { id: { title, messages } } }.
|
|
83
|
+
// Each part stays under the 10 MB a request can carry here, so a long history
|
|
84
|
+
// comes in several. They gather in the `incoming` file and replace the kept
|
|
85
|
+
// messages only when the last one lands, so a half-sent import never shows as
|
|
86
|
+
// the whole thing. Refused while Keep my messages is off, whatever the page sent.
|
|
87
|
+
export async function POST(request) {
|
|
88
|
+
const s = state();
|
|
89
|
+
if (!s) return Response.json({ error: 'Pick a profile first.' }, { status: 400 });
|
|
90
|
+
if (!s.keep) return Response.json({ error: 'Keep my messages is off, so nothing was saved.' }, { status: 409 });
|
|
91
|
+
let body;
|
|
92
|
+
try { body = await request.json(); } catch { return Response.json({ error: 'That isn’t readable.' }, { status: 400 }); }
|
|
93
|
+
const part = Number(body?.part);
|
|
94
|
+
const parts = Number(body?.parts);
|
|
95
|
+
if (!Number.isInteger(part) || !Number.isInteger(parts) || part < 0 || part >= parts) {
|
|
96
|
+
return Response.json({ error: 'That isn’t a part of an import.' }, { status: 400 });
|
|
97
|
+
}
|
|
98
|
+
let into;
|
|
99
|
+
if (part === 0) {
|
|
100
|
+
// A new import replaces the export's messages; the live sync's previews stay.
|
|
101
|
+
into = { threads: {}, previews: s.kept.previews || {} };
|
|
102
|
+
} else {
|
|
103
|
+
into = readJson(s.files.incoming, null);
|
|
104
|
+
if (!into?.threads) return Response.json({ error: 'The start of this import is missing. Choose the export folder again.' }, { status: 409 });
|
|
105
|
+
}
|
|
106
|
+
const threads = body?.threads && typeof body.threads === 'object' ? body.threads : {};
|
|
107
|
+
for (const [id, t] of Object.entries(threads)) {
|
|
108
|
+
if (!id || id.length > 200 || !t || !Array.isArray(t.messages)) continue;
|
|
109
|
+
const to = into.threads[id] || (into.threads[id] = { title: '', messages: [] });
|
|
110
|
+
if (typeof t.title === 'string' && t.title && !to.title) to.title = t.title.slice(0, 200);
|
|
111
|
+
for (const m of t.messages) {
|
|
112
|
+
const c = cleanMessage(m);
|
|
113
|
+
if (c) to.messages.push(c);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (part === parts - 1) {
|
|
117
|
+
writeJsonAtomic(s.files.messages, into);
|
|
118
|
+
rmSync(s.files.incoming, { force: true });
|
|
119
|
+
} else {
|
|
120
|
+
writeJsonAtomic(s.files.incoming, into);
|
|
121
|
+
}
|
|
122
|
+
return Response.json({ ok: true, part, parts, done: part === parts - 1 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The words gone, the list and the numbers kept: what switching Keep my
|
|
126
|
+
// messages off does (the PATCH that turns it off deletes them as well).
|
|
127
|
+
export async function DELETE() {
|
|
128
|
+
deleteMessages(socialFiles());
|
|
129
|
+
return Response.json({ ok: true });
|
|
130
|
+
}
|
|
@@ -1,40 +1,66 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { dataDir } from '../../../lib/db-client';
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { db } from '../../../lib/db';
|
|
4
3
|
import { resolveProfile } from '../../../lib/profile';
|
|
4
|
+
import { matchLive } from '../../../lib/linkedin-export';
|
|
5
|
+
import { socialFiles, readJson, writeJsonAtomic, readMessages, deleteMessages } from '../../../lib/social-store';
|
|
5
6
|
|
|
6
7
|
// The Social tab's saved findings (lib/linkedin-export.js): numbers and dates
|
|
7
|
-
// from your own LinkedIn export
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// from your own LinkedIn export and the live messages sync, one file per
|
|
9
|
+
// profile in the data folder. No message text is kept in this file: the words,
|
|
10
|
+
// when Keep my messages is on, go to their own file (lib/social-store.js,
|
|
11
|
+
// app/api/social/messages). GET reads it, POST replaces it, PUT takes a live
|
|
12
|
+
// sync, PATCH flips a switch, DELETE forgets it all.
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// A person's key: their profile link, as lib/separation.js keyFor makes it.
|
|
15
|
+
const KEY_OK = (k) => typeof k === 'string' && k.length > 0 && k.length < 300;
|
|
16
|
+
|
|
17
|
+
/** An import's list of conversations, with nothing but the fields the list shows. */
|
|
18
|
+
function cleanConversations(list) {
|
|
19
|
+
if (!Array.isArray(list)) return [];
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const c of list) {
|
|
22
|
+
if (!c || typeof c.id !== 'string' || !c.id || c.id.length > 200 || !Array.isArray(c.people)) continue;
|
|
23
|
+
const people = c.people.filter(KEY_OK).slice(0, 200);
|
|
24
|
+
if (!people.length) continue;
|
|
25
|
+
out.push({
|
|
26
|
+
id: c.id,
|
|
27
|
+
people,
|
|
28
|
+
group: c.group === true,
|
|
29
|
+
last: Number(c.last) || null,
|
|
30
|
+
lastFromThem: typeof c.lastFromThem === 'boolean' ? c.lastFromThem : null,
|
|
31
|
+
count: Number.isFinite(Number(c.count)) ? Number(c.count) : null,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
15
35
|
}
|
|
16
36
|
|
|
17
37
|
export async function GET() {
|
|
18
|
-
const
|
|
19
|
-
if (!
|
|
20
|
-
|
|
38
|
+
const files = socialFiles();
|
|
39
|
+
if (!files) return Response.json({ social: null });
|
|
40
|
+
const social = readJson(files.social, null);
|
|
41
|
+
if (!social) return Response.json({ social: null });
|
|
42
|
+
// The list of conversations can run to thousands; the Galaxy and the tab's
|
|
43
|
+
// counts don't need it, and GET /api/social/messages serves it.
|
|
44
|
+
const { conversations, ...rest } = social;
|
|
45
|
+
return Response.json({ social: { ...rest, conversationCount: Array.isArray(conversations) ? conversations.length : 0 } });
|
|
21
46
|
}
|
|
22
47
|
|
|
23
48
|
export async function POST(request) {
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
49
|
+
const files = socialFiles();
|
|
50
|
+
if (!files) return Response.json({ error: 'Pick a profile first.' }, { status: 400 });
|
|
26
51
|
let body;
|
|
27
52
|
try { body = await request.json(); } catch { return Response.json({ error: 'That isn’t readable.' }, { status: 400 }); }
|
|
28
53
|
// Only the known fields, so nothing else rides along into the file.
|
|
29
|
-
|
|
30
|
-
try { before = JSON.parse(readFileSync(file, 'utf8')) || {}; } catch { /* none yet */ }
|
|
54
|
+
const before = readJson(files.social, {}) || {};
|
|
31
55
|
const clean = {
|
|
32
56
|
live: before.live || {},
|
|
33
57
|
liveAt: before.liveAt || null,
|
|
34
58
|
autoSync: before.autoSync === true,
|
|
59
|
+
keepMessages: before.keepMessages === true,
|
|
35
60
|
importedAt: new Date().toISOString(),
|
|
36
61
|
asOf: Number(body?.asOf) || null,
|
|
37
62
|
people: body?.people && typeof body.people === 'object' ? body.people : {},
|
|
63
|
+
conversations: cleanConversations(body?.conversations),
|
|
38
64
|
chapters: Array.isArray(body?.chapters) ? body.chapters : [],
|
|
39
65
|
posts: body?.posts && typeof body.posts === 'object' ? body.posts : null,
|
|
40
66
|
invites: body?.invites && typeof body.invites === 'object' ? body.invites : null,
|
|
@@ -46,51 +72,67 @@ export async function POST(request) {
|
|
|
46
72
|
if (!clean.emails) delete p.email;
|
|
47
73
|
}
|
|
48
74
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
renameSync(tmp, file);
|
|
52
|
-
return Response.json({ ok: true, people: Object.keys(clean.people).length });
|
|
75
|
+
writeJsonAtomic(files.social, clean);
|
|
76
|
+
return Response.json({ ok: true, people: Object.keys(clean.people).length, conversations: clean.conversations.length });
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
// From the scanner's experimental messages sync (scripts/scrape.py sync_messages):
|
|
56
|
-
// per person, when your 1:1 conversation was last active
|
|
57
|
-
//
|
|
80
|
+
// per person, when your 1:1 conversation was last active, unread count, the
|
|
81
|
+
// thread's link, who wrote last and the newest message. Merged in beside what
|
|
82
|
+
// the export gave, kept under `live`. The newest message's words go to the
|
|
83
|
+
// messages file, and only while Keep my messages is on.
|
|
58
84
|
export async function PUT(request) {
|
|
59
|
-
const
|
|
60
|
-
if (!
|
|
85
|
+
const files = socialFiles();
|
|
86
|
+
if (!files) return Response.json({ error: 'Pick a profile first.' }, { status: 400 });
|
|
61
87
|
let body;
|
|
62
88
|
try { body = await request.json(); } catch { return Response.json({ error: 'That isn’t readable.' }, { status: 400 }); }
|
|
63
|
-
|
|
64
|
-
|
|
89
|
+
const saved = readJson(files.social, {}) || {};
|
|
90
|
+
// Matched to your connections here, so the names the sync sends go no further.
|
|
91
|
+
const me = resolveProfile({ create: false });
|
|
92
|
+
let conns = [];
|
|
93
|
+
try {
|
|
94
|
+
const q = db.from('linkedin_connections').select('name, profile_url, id').eq('degree', 1);
|
|
95
|
+
if (me?.id) q.eq('user_id', me.id);
|
|
96
|
+
conns = (await q).data || [];
|
|
97
|
+
} catch { /* no connections yet */ }
|
|
98
|
+
const { live: matched, unmatched } = matchLive(body?.live || {}, conns);
|
|
65
99
|
const live = {};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
live[
|
|
100
|
+
const previews = {};
|
|
101
|
+
for (const [key, { preview, ...rest }] of Object.entries(matched)) {
|
|
102
|
+
live[key] = rest;
|
|
103
|
+
if (saved.keepMessages === true && preview?.text) previews[key] = preview;
|
|
104
|
+
}
|
|
105
|
+
writeJsonAtomic(files.social, { ...saved, live, liveAt: new Date().toISOString() });
|
|
106
|
+
if (Object.keys(previews).length) {
|
|
107
|
+
const kept = readJson(files.messages, {}) || {};
|
|
108
|
+
writeJsonAtomic(files.messages, { ...kept, previews: { ...(kept.previews || {}), ...previews } });
|
|
69
109
|
}
|
|
70
|
-
|
|
71
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
72
|
-
writeFileSync(tmp, JSON.stringify(next));
|
|
73
|
-
renameSync(tmp, file);
|
|
74
|
-
return Response.json({ ok: true, people: Object.keys(live).length });
|
|
110
|
+
return Response.json({ ok: true, people: Object.keys(live).length, unmatched, previews: Object.keys(previews).length });
|
|
75
111
|
}
|
|
76
112
|
|
|
77
|
-
// The Social tab's "once a day"
|
|
113
|
+
// The Social tab's switches: "once a day" for the live messages sync, and
|
|
114
|
+
// Keep my messages. Only the ones sent change. Turning Keep my messages off
|
|
115
|
+
// deletes the kept messages here too, so the words can't outlive the switch
|
|
116
|
+
// whatever the page did first.
|
|
78
117
|
export async function PATCH(request) {
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
118
|
+
const files = socialFiles();
|
|
119
|
+
if (!files) return Response.json({ error: 'Pick a profile first.' }, { status: 400 });
|
|
81
120
|
let body = {};
|
|
82
121
|
try { body = await request.json(); } catch { /* nothing to change */ }
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return Response.json({ ok: true, autoSync: next.autoSync });
|
|
122
|
+
const next = { ...(readJson(files.social, {}) || {}) };
|
|
123
|
+
if (typeof body?.autoSync === 'boolean') next.autoSync = body.autoSync;
|
|
124
|
+
if (typeof body?.keepMessages === 'boolean') next.keepMessages = body.keepMessages;
|
|
125
|
+
if (next.keepMessages !== true) deleteMessages(files);
|
|
126
|
+
writeJsonAtomic(files.social, next);
|
|
127
|
+
return Response.json({ ok: true, autoSync: next.autoSync === true, keepMessages: next.keepMessages === true });
|
|
90
128
|
}
|
|
91
129
|
|
|
130
|
+
// Forget it: both files, the numbers and the words.
|
|
92
131
|
export async function DELETE() {
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
132
|
+
const files = socialFiles();
|
|
133
|
+
if (files) {
|
|
134
|
+
rmSync(files.social, { force: true });
|
|
135
|
+
deleteMessages(files);
|
|
136
|
+
}
|
|
95
137
|
return Response.json({ ok: true });
|
|
96
138
|
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// pacing and the caps live in the scanner (scripts/scrape.py, lib/linkedin-limits.js).
|
|
9
9
|
|
|
10
10
|
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
11
|
+
import { createPortal } from 'react-dom';
|
|
12
|
+
import { usePathname } from 'next/navigation';
|
|
11
13
|
import Link from 'next/link';
|
|
12
14
|
import { watchScanner, scannerNow, stopScrape } from '../../lib/scraper-client';
|
|
13
15
|
import { watchAllDay, allDayNow } from '../../lib/experimental-client';
|
|
@@ -37,19 +39,54 @@ export default function ScanStatusBar() {
|
|
|
37
39
|
// The Scan page's experimental all-day Auto-Bridge switch: with it on, the bar
|
|
38
40
|
// stays under the tabs even while nothing runs, so you can see the mode is on.
|
|
39
41
|
const allDay = useSyncExternalStore(watchAllDay, allDayNow, () => false);
|
|
42
|
+
// Where it sits: in the page's own slot under its header when the page has one
|
|
43
|
+
// (it then pushes the page down rather than covering it), otherwise fixed just
|
|
44
|
+
// under the header's buttons, measured, since a header can wrap to two rows.
|
|
45
|
+
const pathname = usePathname();
|
|
46
|
+
const [spot, setSpot] = useState({ slot: null, top: null });
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
// The header can load late or be swapped for a new one (a page that shows
|
|
49
|
+
// "Loading…" first), so it's found again whenever the page's elements change,
|
|
50
|
+
// and its size is watched for a wrap to two rows.
|
|
51
|
+
let header = null;
|
|
52
|
+
let ro = null;
|
|
53
|
+
let queued = 0;
|
|
54
|
+
const measure = () => {
|
|
55
|
+
queued = 0;
|
|
56
|
+
const slot = document.getElementById('scan-bar-slot');
|
|
57
|
+
const now = slot ? null : document.querySelector('header');
|
|
58
|
+
if (now !== header) {
|
|
59
|
+
ro?.disconnect();
|
|
60
|
+
ro = null;
|
|
61
|
+
header = now;
|
|
62
|
+
if (header) { ro = new ResizeObserver(later); ro.observe(header); }
|
|
63
|
+
}
|
|
64
|
+
const top = header ? Math.round(header.getBoundingClientRect().bottom) + 6 : null;
|
|
65
|
+
setSpot((was) => (was.slot === slot && was.top === top ? was : { slot, top }));
|
|
66
|
+
};
|
|
67
|
+
const later = () => { if (!queued) queued = requestAnimationFrame(measure); };
|
|
68
|
+
later();
|
|
69
|
+
const mo = new MutationObserver(later);
|
|
70
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
71
|
+
window.addEventListener('resize', later);
|
|
72
|
+
return () => { cancelAnimationFrame(queued); ro?.disconnect(); mo.disconnect(); window.removeEventListener('resize', later); };
|
|
73
|
+
}, [pathname]);
|
|
40
74
|
useEffect(() => watchScanner(() => {
|
|
41
75
|
const now = scannerNow();
|
|
42
76
|
setJob(now);
|
|
43
77
|
if (!now.running) setStopping(false);
|
|
44
78
|
}), []);
|
|
45
79
|
const place = {
|
|
46
|
-
|
|
80
|
+
...(spot.slot
|
|
81
|
+
? { position: 'relative', margin: '8px auto 12px', width: 'fit-content' }
|
|
82
|
+
: { position: 'fixed', left: '50%', top: spot.top != null ? spot.top : 'var(--scan-bar-top, 94px)', transform: 'translateX(-50%)', zIndex: 60 }),
|
|
47
83
|
maxWidth: 'calc(100vw - 32px)', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'nowrap', whiteSpace: 'nowrap',
|
|
48
84
|
padding: '8px 12px', borderRadius: 12, fontSize: 12, color: '#cfd8d8', background: 'rgba(14,16,32,0.94)',
|
|
49
85
|
};
|
|
86
|
+
const put = (bar) => (spot.slot && document.body.contains(spot.slot) ? createPortal(bar, spot.slot) : bar);
|
|
50
87
|
if (!job?.running) {
|
|
51
88
|
if (!allDay) return null;
|
|
52
|
-
return (
|
|
89
|
+
return put(
|
|
53
90
|
<div role="status" style={{ ...place, opacity: 0.55, border: '1px solid rgba(255,255,255,0.1)' }}>
|
|
54
91
|
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#556', flexShrink: 0 }} />
|
|
55
92
|
<b style={{ color: '#b8c4c4' }}>Auto scan</b>
|
|
@@ -65,7 +102,7 @@ export default function ScanStatusBar() {
|
|
|
65
102
|
const b = job.budget;
|
|
66
103
|
const last = [...(job.log || [])].reverse().find((l) => l && l.trim())?.trim();
|
|
67
104
|
|
|
68
|
-
return (
|
|
105
|
+
return put(
|
|
69
106
|
<div role="status" aria-live="polite" style={{ ...place, border: '1px solid rgba(0,255,136,0.25)' }}>
|
|
70
107
|
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#00ff88', flexShrink: 0 }} />
|
|
71
108
|
<b style={{ color: '#fff' }}>{WHAT[job.action] || 'Scanning'}</b>
|
|
@@ -334,7 +334,8 @@ export default function DataSection() {
|
|
|
334
334
|
<Body style={small}>
|
|
335
335
|
The copy holds the names, headlines and photos of the people in your network. Keep it private: don’t
|
|
336
336
|
post it or share it, and delete it once it has been imported. A network opened from a LinkedIn CSV lives
|
|
337
|
-
only in its browser tab, so it isn’t in the copy; import the CSV again on the new computer.
|
|
337
|
+
only in its browser tab, so it isn’t in the copy; import the CSV again on the new computer. The Social
|
|
338
|
+
tab’s findings, and any messages you keep there, stay on this computer and aren’t in the copy either.
|
|
338
339
|
</Body>
|
|
339
340
|
|
|
340
341
|
<h3 style={subhead}>Bring in a network from another computer</h3>
|
|
@@ -302,6 +302,8 @@ function PathsInner() {
|
|
|
302
302
|
)}
|
|
303
303
|
</div>
|
|
304
304
|
</header>
|
|
305
|
+
{/* The scan status bar sits here, under the header's buttons (ScanStatusBar.js). */}
|
|
306
|
+
<div id="scan-bar-slot" />
|
|
305
307
|
|
|
306
308
|
{!selectedCompany && tab !== 'companies' ? (
|
|
307
309
|
<PathsAnalyzer d1={d1Data} d2={d2Data} d3={d3Data} tab={tab}
|
|
@@ -164,6 +164,8 @@ function ProfileInner() {
|
|
|
164
164
|
WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
|
|
165
165
|
}}>Profile</h1>
|
|
166
166
|
</header>
|
|
167
|
+
{/* The scan status bar sits here, under the header's buttons (ScanStatusBar.js). */}
|
|
168
|
+
<div id="scan-bar-slot" />
|
|
167
169
|
|
|
168
170
|
<div style={{ flex: 1, overflow: 'auto', padding: '30px 20px' }}>
|
|
169
171
|
<div style={{ maxWidth: 700, margin: '0 auto' }}>
|
|
@@ -301,6 +301,8 @@ function QueueInner() {
|
|
|
301
301
|
))}
|
|
302
302
|
</div>
|
|
303
303
|
</header>
|
|
304
|
+
{/* The scan status bar sits here, under the header's buttons (ScanStatusBar.js). */}
|
|
305
|
+
<div id="scan-bar-slot" />
|
|
304
306
|
|
|
305
307
|
{/* Batch action bar — recs view only */}
|
|
306
308
|
{view === 'recs' && selected.size > 0 && (
|
|
@@ -56,6 +56,8 @@ function ScoresInner() {
|
|
|
56
56
|
WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
|
|
57
57
|
}}>Scores</h1>
|
|
58
58
|
</header>
|
|
59
|
+
{/* The scan status bar sits here, under the header's buttons (ScanStatusBar.js). */}
|
|
60
|
+
<div id="scan-bar-slot" />
|
|
59
61
|
<main style={{ maxWidth: 980, margin: '0 auto', padding: '8px 24px 64px' }}>
|
|
60
62
|
<Body style={{ marginTop: 16 }}>
|
|
61
63
|
How someone’s power score is worked out, and the three things you can change about it:{' '}
|
|
@@ -73,6 +73,8 @@ function SettingsInner() {
|
|
|
73
73
|
WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
|
|
74
74
|
}}>Settings</h1>
|
|
75
75
|
</header>
|
|
76
|
+
{/* The scan status bar sits here, under the header's buttons (ScanStatusBar.js). */}
|
|
77
|
+
<div id="scan-bar-slot" />
|
|
76
78
|
|
|
77
79
|
<main style={{ maxWidth: 720, margin: '0 auto', padding: '8px 24px 64px' }}>
|
|
78
80
|
{error && <Body style={{ color: '#ff7676', marginTop: 16 }}>{error}</Body>}
|
|
@@ -203,6 +203,8 @@ function SetupInner() {
|
|
|
203
203
|
WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
|
|
204
204
|
}}>Scan your network</h1>
|
|
205
205
|
</header>
|
|
206
|
+
{/* The scan status bar sits here, under the header's buttons (ScanStatusBar.js). */}
|
|
207
|
+
<div id="scan-bar-slot" />
|
|
206
208
|
|
|
207
209
|
<div style={{ maxWidth: 720, margin: '0 auto', padding: '32px 24px 64px' }}>
|
|
208
210
|
|
|
@@ -448,9 +450,10 @@ function SetupInner() {
|
|
|
448
450
|
<input type="checkbox" checked={experimental} disabled={running} style={{ marginTop: 3 }}
|
|
449
451
|
onChange={(e) => { setExperimental(e.target.checked); window.dispatchEvent(new Event('six-degrees:experimental')); }} />
|
|
450
452
|
<span>
|
|
451
|
-
<b style={{ color: '#FFD700' }}>Experimental:</b> all-day pacing. Up to
|
|
452
|
-
then
|
|
453
|
-
|
|
453
|
+
<b style={{ color: '#FFD700' }}>Experimental:</b> all-day pacing. Up to 8 pages in a sitting,
|
|
454
|
+
then an hour’s rest; searches only from 9:00 to 18:00; never more than 40 searches in a day
|
|
455
|
+
or 200 in a week, however high your budget; two days’ rest after any check from LinkedIn; at
|
|
456
|
+
the budget it waits instead of stopping; and it saves every page. It also reads LinkedIn’s own data beside
|
|
454
457
|
the page text, to fill gaps and measure how the two compare. It keeps running while the app is open.
|
|
455
458
|
</span>
|
|
456
459
|
</label>
|