six-degrees 0.4.8 → 0.4.10
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 +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.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 +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__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_page_1fazqbf.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/standalone/.next/static/chunks/07dyt47mp4s9-.js +1 -0
- package/.next/standalone/.next/static/chunks/{3q3pmt5vv-4pi.js → 160wjb73ddwqd.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1z-zp93cyoxvz.js → 1c_n0973t07l0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0bo1dxn5gwzpk.css → 1xptlvtcz6654.css} +1 -1
- package/.next/standalone/.next/static/chunks/{2_wcobfqry2uj.js → 206rm6j42gl_a.js} +1 -1
- package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.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/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
- package/.next/standalone/CHANGELOG.md +48 -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/GalaxyLab.js +4 -1
- package/.next/standalone/app/components/ScanStatusBar.js +112 -50
- package/.next/standalone/app/components/settings/DataSection.js +2 -1
- package/.next/standalone/app/globals.css +4 -0
- package/.next/standalone/app/setup/page.js +4 -3
- package/.next/standalone/app/social/Conversations.js +246 -0
- package/.next/standalone/app/social/page.js +88 -9
- package/.next/standalone/lib/galaxy-lab.js +6 -1
- package/.next/standalone/lib/island.js +36 -0
- 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/static/chunks/07dyt47mp4s9-.js +1 -0
- package/.next/static/chunks/{3q3pmt5vv-4pi.js → 160wjb73ddwqd.js} +1 -1
- package/.next/static/chunks/{1z-zp93cyoxvz.js → 1c_n0973t07l0.js} +1 -1
- package/.next/static/chunks/{0bo1dxn5gwzpk.css → 1xptlvtcz6654.css} +1 -1
- package/.next/static/chunks/{2_wcobfqry2uj.js → 206rm6j42gl_a.js} +1 -1
- package/.next/static/chunks/2coxzjm_k6ilr.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/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
- package/CHANGELOG.md +48 -0
- package/lib/galaxy-lab.js +6 -1
- package/lib/island.js +36 -0
- 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/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/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 → 7zWJnSkfSoRhRsqY6ZKtD}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_ssgManifest.js +0 -0
- /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_buildManifest.js +0 -0
- /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_ssgManifest.js +0 -0
|
@@ -6,6 +6,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.10] - 2026-09-29
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **The scan status bar is now a notch.** A small pill hangs from the bottom of the header,
|
|
13
|
+
centred, on every page. It shows nothing while nothing runs, only a tiny dimmed *Auto scan*
|
|
14
|
+
while the all-day mode waits, and while a scan runs, what it's doing with a thin progress
|
|
15
|
+
line. Hover or click to open it: who, today's searches, the latest line, *Details* and
|
|
16
|
+
*Stop*. Other long jobs show there too, starting with recording the Galaxy's replay.
|
|
17
|
+
|
|
18
|
+
## [0.4.9] - 2026-09-29
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Conversations, in the Social tab.** Every conversation with one of your connections, from your
|
|
22
|
+
export and the live messages sync: who it's with (name and tier), when it was last active, who
|
|
23
|
+
wrote last, unread, how many messages, and *Open on LinkedIn* (the thread when the sync found
|
|
24
|
+
its link, else their profile). Waiting on you comes first, then S and A tier, then the most
|
|
25
|
+
recent. Group conversations are shown and marked, and aren't counted in warmth. A search box
|
|
26
|
+
finds people by name.
|
|
27
|
+
- **Keep my messages on this computer**, a switch that's off unless you turn it on. When it's on,
|
|
28
|
+
the messages themselves, what other people wrote to you included, are kept in the app's data
|
|
29
|
+
folder (a file of their own beside the Social tab's), and a conversation opens as a thread,
|
|
30
|
+
newest at the bottom, a page at a time so a thread of thousands opens as fast as a short one.
|
|
31
|
+
The search box then finds what was said, too. They're never sent anywhere, and "Save a copy of
|
|
32
|
+
my network" doesn't carry them (Settings says so). Switching it off asks first, then deletes
|
|
33
|
+
them; *Forget it* deletes them with everything else.
|
|
34
|
+
- **The live messages sync reads a little more of the list.** Each conversation's link, whether
|
|
35
|
+
it's a group, and its newest message: who wrote last, which now feeds warmth and Waiting on you
|
|
36
|
+
where your export is missing or older, and its words, which go to the app only while Keep my
|
|
37
|
+
messages is on. It still opens no conversation, and its saved samples still keep no words.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- **Auto scan is slower and has its own ceilings.** Sittings of 8 pages, then an hour's rest;
|
|
41
|
+
searches only from 9:00 to 18:00; never more than 40 searches in 24 hours or 200 in 7 days,
|
|
42
|
+
however high the daily budget is set (the account restricted on 28 September did 373 in a
|
|
43
|
+
day with the budget at 500); and two days with no searches after any check from LinkedIn.
|
|
44
|
+
- **The scan status bar sits under the buttons.** On each page it now sits in its own row
|
|
45
|
+
under the header, pushing the page down instead of covering it. On Network Circle it floats
|
|
46
|
+
just under the header, measured, so a header that wraps to two rows no longer hides it.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
- **The live messages sync keeps what it finds.** It read your conversations but matched none of
|
|
50
|
+
them: the pattern for a profile link never matched, and LinkedIn's messaging gives a member
|
|
51
|
+
link your connections list doesn't have. It now matches each person to your connections by
|
|
52
|
+
link, or by a name only one connection has (the name isn't kept), and scrolls the list's own
|
|
53
|
+
panel so more than the first 20 conversations load.
|
|
54
|
+
- **Messaging samples keep no words.** The sync's saved samples of LinkedIn's data (for tuning
|
|
55
|
+
the reader) now drop every piece of writing and every name before they're saved.
|
|
56
|
+
|
|
9
57
|
## [0.4.8] - 2026-09-29
|
|
10
58
|
|
|
11
59
|
### 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
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
layoutsNow, watchLayouts, saveLayout, applyLayout, forgetLayout, milestones,
|
|
13
13
|
} from '../../lib/galaxy-lab';
|
|
14
14
|
import { savePicture, recordReplay, canRecord } from '../../lib/galaxy-export';
|
|
15
|
+
import { showActivity } from '../../lib/island';
|
|
15
16
|
|
|
16
17
|
const noLayouts = [];
|
|
17
18
|
|
|
@@ -89,7 +90,9 @@ export default function GalaxyLab() {
|
|
|
89
90
|
const run = async (what, job) => {
|
|
90
91
|
setBusy(what);
|
|
91
92
|
setNote(null);
|
|
92
|
-
|
|
93
|
+
// A recording runs for the whole replay: the notch under the header shows it.
|
|
94
|
+
const done = what === 'video' ? showActivity({ id: 'replay-video', label: 'Recording the replay', detail: 'The video saves when it ends' }) : () => {};
|
|
95
|
+
try { await job(); } catch (e) { setNote(e.message || 'That didn’t work.'); } finally { setBusy(null); done(); }
|
|
93
96
|
};
|
|
94
97
|
const canReplay = clock.min != null && clock.max != null && clock.max > clock.min;
|
|
95
98
|
const preset = FORCES.every((f) => lab[f.key] === LAB_DEFAULTS[f.key]) ? 'rings'
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// The notch: a small pill hanging from the bottom of the header, centred, on
|
|
4
|
+
// every page (Blake, 2026-09-29: "a notch ui of the thing scanning"). It stays
|
|
5
|
+
// out of the way: nothing at all while nothing runs; a tiny dimmed "Auto scan"
|
|
6
|
+
// when the all-day mode is on but idle; while a scan runs, what it's doing and
|
|
7
|
+
// how far it's got. Hover or click it to open it: who, today's LinkedIn budget,
|
|
8
|
+
// the latest line, Details and Stop. Stop saves what was read, and the Scan
|
|
9
|
+
// page's Resume carries on from that page. Other long jobs can show here too
|
|
10
|
+
// (lib/island.js). It only reports: the pacing and the caps live in the scanner
|
|
11
|
+
// (scripts/scrape.py, lib/linkedin-limits.js).
|
|
9
12
|
|
|
10
13
|
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
14
|
+
import { usePathname } from 'next/navigation';
|
|
11
15
|
import Link from 'next/link';
|
|
12
16
|
import { watchScanner, scannerNow, stopScrape } from '../../lib/scraper-client';
|
|
13
17
|
import { watchAllDay, allDayNow } from '../../lib/experimental-client';
|
|
18
|
+
import { watchActivities, activitiesNow, noActivities } from '../../lib/island';
|
|
14
19
|
|
|
15
20
|
const WHAT = {
|
|
16
21
|
full: 'Scanning your network',
|
|
@@ -34,63 +39,120 @@ const tone = (used, cap) => (!cap ? '#8b9a9a' : used / cap >= 0.9 ? '#ff6b6b' :
|
|
|
34
39
|
export default function ScanStatusBar() {
|
|
35
40
|
const [job, setJob] = useState(() => scannerNow());
|
|
36
41
|
const [stopping, setStopping] = useState(false);
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
const [open, setOpen] = useState(false);
|
|
43
|
+
const [pinned, setPinned] = useState(false);
|
|
39
44
|
const allDay = useSyncExternalStore(watchAllDay, allDayNow, () => false);
|
|
45
|
+
const others = useSyncExternalStore(watchActivities, activitiesNow, noActivities);
|
|
40
46
|
useEffect(() => watchScanner(() => {
|
|
41
47
|
const now = scannerNow();
|
|
42
48
|
setJob(now);
|
|
43
49
|
if (!now.running) setStopping(false);
|
|
44
50
|
}), []);
|
|
45
|
-
const place = {
|
|
46
|
-
position: 'fixed', left: '50%', top: 'var(--scan-bar-top, 94px)', transform: 'translateX(-50%)', zIndex: 60,
|
|
47
|
-
maxWidth: 'calc(100vw - 32px)', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'nowrap', whiteSpace: 'nowrap',
|
|
48
|
-
padding: '8px 12px', borderRadius: 12, fontSize: 12, color: '#cfd8d8', background: 'rgba(14,16,32,0.94)',
|
|
49
|
-
};
|
|
50
|
-
if (!job?.running) {
|
|
51
|
-
if (!allDay) return null;
|
|
52
|
-
return (
|
|
53
|
-
<div role="status" style={{ ...place, opacity: 0.55, border: '1px solid rgba(255,255,255,0.1)' }}>
|
|
54
|
-
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#556', flexShrink: 0 }} />
|
|
55
|
-
<b style={{ color: '#b8c4c4' }}>Auto scan</b>
|
|
56
|
-
<span style={{ color: '#8b9a9a' }}>ready · press Auto scan beside Scan to start</span>
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
51
|
|
|
61
|
-
|
|
52
|
+
// Hangs from the header's bottom edge, measured: the header can load late,
|
|
53
|
+
// be swapped for a new one, or wrap to two rows.
|
|
54
|
+
const pathname = usePathname();
|
|
55
|
+
const [top, setTop] = useState(null);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
let header = null;
|
|
58
|
+
let ro = null;
|
|
59
|
+
let queued = 0;
|
|
60
|
+
const measure = () => {
|
|
61
|
+
queued = 0;
|
|
62
|
+
const now = document.querySelector('header');
|
|
63
|
+
if (now !== header) {
|
|
64
|
+
ro?.disconnect();
|
|
65
|
+
ro = null;
|
|
66
|
+
header = now;
|
|
67
|
+
if (header) { ro = new ResizeObserver(later); ro.observe(header); }
|
|
68
|
+
}
|
|
69
|
+
const at = header ? Math.round(header.getBoundingClientRect().bottom) : null;
|
|
70
|
+
setTop((was) => (was === at ? was : at));
|
|
71
|
+
};
|
|
72
|
+
const later = () => { if (!queued) queued = requestAnimationFrame(measure); };
|
|
73
|
+
later();
|
|
74
|
+
const mo = new MutationObserver(later);
|
|
75
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
76
|
+
window.addEventListener('resize', later);
|
|
77
|
+
return () => { cancelAnimationFrame(queued); ro?.disconnect(); mo.disconnect(); window.removeEventListener('resize', later); };
|
|
78
|
+
}, [pathname]);
|
|
79
|
+
|
|
80
|
+
const running = !!job?.running;
|
|
81
|
+
const other = others[others.length - 1] || null;
|
|
82
|
+
if (!running && !other && !allDay) return null;
|
|
83
|
+
|
|
84
|
+
const p = job?.progress;
|
|
62
85
|
const step = p?.kind === 'batch' && p.total ? `${p.current || p.done || 0} of ${p.total}`
|
|
63
|
-
: p?.kind === 'walk' && p.total ? `${(p.done || 0).toLocaleString()} of ${p.total.toLocaleString()}
|
|
86
|
+
: p?.kind === 'walk' && p.total ? `${(p.done || 0).toLocaleString()} of ${p.total.toLocaleString()}`
|
|
64
87
|
: p?.kind === 'saving' ? 'saving' : null;
|
|
65
|
-
const
|
|
66
|
-
const
|
|
88
|
+
const fraction = p?.total ? Math.min(1, (p.current || p.done || 0) / p.total) : other?.progress ?? null;
|
|
89
|
+
const b = job?.budget;
|
|
90
|
+
const last = running ? [...(job.log || [])].reverse().find((l) => l && l.trim())?.trim() : null;
|
|
91
|
+
const expanded = open || pinned;
|
|
92
|
+
|
|
93
|
+
// What the pill says, smallest first.
|
|
94
|
+
const dot = running ? '#00ff88' : other ? (other.tone === 'warn' ? '#FFD700' : '#3498DB') : '#556';
|
|
95
|
+
const label = running ? (WHAT[job.action] || 'Scanning') : other ? other.label : 'Auto scan';
|
|
96
|
+
const short = running ? step : other ? other.detail : null;
|
|
67
97
|
|
|
68
98
|
return (
|
|
69
|
-
<div
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
<div
|
|
100
|
+
role="status"
|
|
101
|
+
aria-live="polite"
|
|
102
|
+
onMouseEnter={() => setOpen(true)}
|
|
103
|
+
onMouseLeave={() => setOpen(false)}
|
|
104
|
+
style={{
|
|
105
|
+
position: 'fixed', left: '50%', top: top ?? 'var(--scan-bar-top, 94px)', transform: 'translateX(-50%)', zIndex: 60,
|
|
106
|
+
maxWidth: 'calc(100vw - 32px)', minWidth: running || other ? 180 : 0,
|
|
107
|
+
padding: expanded ? '8px 14px 10px' : '4px 12px 6px',
|
|
108
|
+
borderRadius: '0 0 16px 16px', borderTop: 'none',
|
|
109
|
+
border: `1px solid ${running ? 'rgba(0,255,136,0.25)' : 'rgba(255,255,255,0.1)'}`,
|
|
110
|
+
background: 'rgba(8,10,22,0.96)', color: '#cfd8d8', fontSize: 12,
|
|
111
|
+
opacity: running || other || expanded ? 1 : 0.55,
|
|
112
|
+
boxShadow: running ? '0 6px 20px rgba(0,0,0,0.35)' : 'none',
|
|
113
|
+
transition: 'padding 0.18s ease, opacity 0.18s ease',
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<button
|
|
117
|
+
onClick={() => setPinned((v) => !v)}
|
|
118
|
+
aria-expanded={expanded}
|
|
119
|
+
title={expanded ? 'Close' : 'Open'}
|
|
120
|
+
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', background: 'none', border: 'none', color: 'inherit', font: 'inherit', cursor: 'pointer', padding: 0, whiteSpace: 'nowrap' }}
|
|
121
|
+
>
|
|
122
|
+
<span className={running ? 'notch-pulse' : undefined} style={{ width: 7, height: 7, borderRadius: '50%', background: dot, flexShrink: 0 }} />
|
|
123
|
+
<b style={{ color: running || other ? '#fff' : '#b8c4c4', fontWeight: 600 }}>{label}</b>
|
|
124
|
+
{short && <span style={{ color: '#8b9a9a' }}>{short}</span>}
|
|
125
|
+
{!running && !other && expanded && <span style={{ color: '#8b9a9a' }}>ready · press Auto scan beside Scan to start</span>}
|
|
126
|
+
</button>
|
|
127
|
+
{fraction != null && (
|
|
128
|
+
<div style={{ height: 2, marginTop: 5, borderRadius: 2, background: 'rgba(255,255,255,0.08)', overflow: 'hidden' }}>
|
|
129
|
+
<div style={{ width: `${Math.round(fraction * 100)}%`, height: '100%', background: running ? '#00ff88' : '#3498DB', transition: 'width 0.4s ease' }} />
|
|
130
|
+
</div>
|
|
78
131
|
)}
|
|
79
|
-
{
|
|
80
|
-
<
|
|
81
|
-
{
|
|
82
|
-
|
|
132
|
+
{expanded && running && (
|
|
133
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 8, flexWrap: 'wrap' }}>
|
|
134
|
+
{job.target?.name && <span>{job.target.name}</span>}
|
|
135
|
+
{b && (
|
|
136
|
+
<span style={{ color: tone(b.searches, b.cap) }} title="Searches on your LinkedIn account in the last 24 hours, and your daily budget">
|
|
137
|
+
{b.searches}{b.cap ? ` of ${b.cap}` : ''} searches today
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
{last && (
|
|
141
|
+
<span style={{ color: '#667', maxWidth: 280, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={last}>{last}</span>
|
|
142
|
+
)}
|
|
143
|
+
<Link href="/setup" style={{ color: '#3498DB', textDecoration: 'none' }}>Details</Link>
|
|
144
|
+
<button
|
|
145
|
+
onClick={async () => { setStopping(true); try { await stopScrape(); } catch { setStopping(false); } }}
|
|
146
|
+
disabled={stopping}
|
|
147
|
+
title="Stops after saving what's been read; Resume on the Scan page carries on from the same page"
|
|
148
|
+
style={{
|
|
149
|
+
padding: '3px 10px', borderRadius: 8, fontSize: 12, cursor: stopping ? 'default' : 'pointer',
|
|
150
|
+
background: 'rgba(255,107,107,0.12)', color: '#ff9b9b', border: '1px solid rgba(255,107,107,0.35)',
|
|
151
|
+
}}
|
|
152
|
+
>{stopping ? 'Stopping…' : 'Stop'}</button>
|
|
153
|
+
</div>
|
|
83
154
|
)}
|
|
84
|
-
<
|
|
85
|
-
<button
|
|
86
|
-
onClick={async () => { setStopping(true); try { await stopScrape(); } catch { setStopping(false); } }}
|
|
87
|
-
disabled={stopping}
|
|
88
|
-
title="Stops after saving what's been read; Resume on the Scan page carries on from the same page"
|
|
89
|
-
style={{
|
|
90
|
-
padding: '4px 10px', borderRadius: 8, fontSize: 12, cursor: stopping ? 'default' : 'pointer',
|
|
91
|
-
background: 'rgba(255,107,107,0.12)', color: '#ff9b9b', border: '1px solid rgba(255,107,107,0.35)',
|
|
92
|
-
}}
|
|
93
|
-
>{stopping ? 'Stopping…' : 'Stop'}</button>
|
|
155
|
+
{expanded && !running && other?.detail && <div style={{ marginTop: 6, color: '#8b9a9a' }}>{other.detail}</div>}
|
|
94
156
|
</div>
|
|
95
157
|
);
|
|
96
158
|
}
|
|
@@ -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>
|
|
@@ -42,3 +42,7 @@ html:has([data-field-question]) {
|
|
|
42
42
|
/* Where the scan status bar sits: just under the header (app/components/ScanStatusBar.js). */
|
|
43
43
|
:root { --scan-bar-top: 94px; }
|
|
44
44
|
@media (max-width: 767px) { :root { --scan-bar-top: 66px; } }
|
|
45
|
+
|
|
46
|
+
/* The notch's running dot (app/components/ScanStatusBar.js). */
|
|
47
|
+
@keyframes notch-pulse { 50% { opacity: 0.35; } }
|
|
48
|
+
@media (prefers-reduced-motion: no-preference) { .notch-pulse { animation: notch-pulse 1.6s ease-in-out infinite; } }
|
|
@@ -448,9 +448,10 @@ function SetupInner() {
|
|
|
448
448
|
<input type="checkbox" checked={experimental} disabled={running} style={{ marginTop: 3 }}
|
|
449
449
|
onChange={(e) => { setExperimental(e.target.checked); window.dispatchEvent(new Event('six-degrees:experimental')); }} />
|
|
450
450
|
<span>
|
|
451
|
-
<b style={{ color: '#FFD700' }}>Experimental:</b> all-day pacing. Up to
|
|
452
|
-
then
|
|
453
|
-
|
|
451
|
+
<b style={{ color: '#FFD700' }}>Experimental:</b> all-day pacing. Up to 8 pages in a sitting,
|
|
452
|
+
then an hour’s rest; searches only from 9:00 to 18:00; never more than 40 searches in a day
|
|
453
|
+
or 200 in a week, however high your budget; two days’ rest after any check from LinkedIn; at
|
|
454
|
+
the budget it waits instead of stopping; and it saves every page. It also reads LinkedIn’s own data beside
|
|
454
455
|
the page text, to fill gaps and measure how the two compare. It keeps running while the app is open.
|
|
455
456
|
</span>
|
|
456
457
|
</label>
|