six-degrees 0.4.9 → 0.4.11
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 +4 -4
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.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/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 → [root-of-the-server]__05_3-22._.js} +7 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +4 -4
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +7 -7
- package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +1 -1
- 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 +2 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +1 -0
- package/.next/standalone/.next/static/chunks/{054zdthcnyd2h.js → 160wjb73ddwqd.js} +1 -1
- package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +1 -0
- package/.next/standalone/.next/static/chunks/{04bbi55urkrd6.js → 1c_n0973t07l0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0bo1dxn5gwzpk.css → 1x_ysly42-teq.css} +1 -1
- package/.next/standalone/.next/static/chunks/{1_mohm2643nxp.js → 236f0jtp68j_h.js} +1 -1
- package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +1 -0
- package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
- package/.next/standalone/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
- package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
- package/.next/{static/chunks/1z_rt4jteewqp.js → standalone/.next/static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/.next/standalone/CHANGELOG.md +57 -0
- package/.next/standalone/app/api/data/export/route.js +2 -1
- package/.next/standalone/app/api/scraper/route.js +5 -1
- package/.next/standalone/app/api/social/crm/route.js +37 -0
- package/.next/standalone/app/api/social/messages/route.js +44 -8
- package/.next/standalone/app/api/social/route.js +80 -22
- package/.next/standalone/app/components/GalaxyLab.js +4 -1
- package/.next/standalone/app/components/ScanStatusBar.js +97 -71
- package/.next/standalone/app/components/settings/DataSection.js +12 -3
- package/.next/standalone/app/globals.css +15 -0
- package/.next/standalone/app/paths/page.js +0 -2
- package/.next/standalone/app/profile/page.js +0 -2
- package/.next/standalone/app/queue/page.js +0 -2
- package/.next/standalone/app/scores/page.js +0 -2
- package/.next/standalone/app/settings/page.js +0 -2
- package/.next/standalone/app/setup/page.js +0 -2
- package/.next/standalone/app/social/Crm.js +683 -0
- package/.next/standalone/app/social/Thread.js +90 -0
- package/.next/standalone/app/social/page.js +71 -62
- package/.next/standalone/lib/data-export.js +10 -4
- package/.next/standalone/lib/data-folder.js +9 -0
- package/.next/standalone/lib/data-import.js +13 -3
- package/.next/standalone/lib/island.js +36 -0
- package/.next/standalone/lib/linkedin-export.js +267 -46
- package/.next/standalone/lib/social-crm.js +415 -0
- package/.next/standalone/lib/social-store.js +13 -6
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +126 -22
- package/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/static/chunks/15sjx07xhpn6p.js +1 -0
- package/.next/static/chunks/{054zdthcnyd2h.js → 160wjb73ddwqd.js} +1 -1
- package/.next/static/chunks/1a74dky22dxd6.js +1 -0
- package/.next/static/chunks/{04bbi55urkrd6.js → 1c_n0973t07l0.js} +1 -1
- package/.next/static/chunks/{0bo1dxn5gwzpk.css → 1x_ysly42-teq.css} +1 -1
- package/.next/static/chunks/{1_mohm2643nxp.js → 236f0jtp68j_h.js} +1 -1
- package/.next/static/chunks/297e1z34yi--z.js +1 -0
- package/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
- package/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
- package/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
- package/.next/{standalone/.next/static/chunks/1z_rt4jteewqp.js → static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/CHANGELOG.md +57 -0
- package/lib/data-export.js +10 -4
- package/lib/data-folder.js +9 -0
- package/lib/data-import.js +13 -3
- package/lib/island.js +36 -0
- package/lib/linkedin-export.js +267 -46
- package/lib/social-crm.js +415 -0
- package/lib/social-store.js +13 -6
- package/package.json +1 -1
- package/scripts/scrape.py +126 -22
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +0 -3
- package/.next/standalone/.next/static/chunks/2bftn9gubwf0w.js +0 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/standalone/.next/static/chunks/39csdcx7yv227.js +0 -1
- package/.next/standalone/.next/static/chunks/3yzlf70j5t0-o.js +0 -1
- package/.next/standalone/.next/static/chunks/3z-swu5pet507.js +0 -1
- package/.next/standalone/app/social/Conversations.js +0 -246
- package/.next/static/chunks/2bftn9gubwf0w.js +0 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/static/chunks/39csdcx7yv227.js +0 -1
- package/.next/static/chunks/3yzlf70j5t0-o.js +0 -1
- package/.next/static/chunks/3z-swu5pet507.js +0 -1
- /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_ssgManifest.js +0 -0
- /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
- /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_ssgManifest.js +0 -0
|
@@ -1,47 +1,81 @@
|
|
|
1
1
|
import { rmSync } from 'node:fs';
|
|
2
2
|
import { db } from '../../../lib/db';
|
|
3
3
|
import { resolveProfile } from '../../../lib/profile';
|
|
4
|
-
import { matchLive } from '../../../lib/linkedin-export';
|
|
4
|
+
import { matchLive, matchLiveGroups, mergeLive } from '../../../lib/linkedin-export';
|
|
5
5
|
import { socialFiles, readJson, writeJsonAtomic, readMessages, deleteMessages } from '../../../lib/social-store';
|
|
6
6
|
|
|
7
7
|
// The Social tab's saved findings (lib/linkedin-export.js): numbers and dates
|
|
8
8
|
// from your own LinkedIn export and the live messages sync, one file per
|
|
9
|
-
// profile in the data folder
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// profile in the data folder, and the names of people who aren't your
|
|
10
|
+
// connections (nothing else in the app says who they are). No message text is
|
|
11
|
+
// kept in this file: the words, when Keep my messages is on, go to their own
|
|
12
|
+
// file (lib/social-store.js, app/api/social/messages). GET reads it, POST
|
|
13
|
+
// replaces it, PUT takes a live sync, PATCH flips a switch, DELETE forgets it
|
|
14
|
+
// all. Your CRM notes are a file of their own (app/api/social/crm), which
|
|
15
|
+
// Forget it leaves alone: the page asks about them separately.
|
|
13
16
|
|
|
14
17
|
// A person's key: their profile link, as lib/separation.js keyFor makes it.
|
|
15
18
|
const KEY_OK = (k) => typeof k === 'string' && k.length > 0 && k.length < 300;
|
|
19
|
+
const num = (n) => (Number.isFinite(Number(n)) && n !== null ? Number(n) : null);
|
|
20
|
+
const name = (s) => (typeof s === 'string' ? s.replace(/[\u0000-\u001f]/g, ' ').trim().slice(0, 120) : '');
|
|
16
21
|
|
|
17
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* An import's list of conversations, with nothing but the fields the list
|
|
24
|
+
* shows. A conversation with no one but you in it (the export didn't say to
|
|
25
|
+
* whom) stays, with no people: nothing is dropped.
|
|
26
|
+
*/
|
|
18
27
|
function cleanConversations(list) {
|
|
19
28
|
if (!Array.isArray(list)) return [];
|
|
20
29
|
const out = [];
|
|
21
30
|
for (const c of list) {
|
|
22
31
|
if (!c || typeof c.id !== 'string' || !c.id || c.id.length > 200 || !Array.isArray(c.people)) continue;
|
|
23
32
|
const people = c.people.filter(KEY_OK).slice(0, 200);
|
|
24
|
-
if (!people.length) continue;
|
|
25
33
|
out.push({
|
|
26
34
|
id: c.id,
|
|
27
35
|
people,
|
|
28
36
|
group: c.group === true,
|
|
37
|
+
kind: ['sponsored', 'inmail'].includes(c.kind) ? c.kind : null,
|
|
38
|
+
folder: typeof c.folder === 'string' && /^[\w -]{0,30}$/.test(c.folder) ? c.folder.toLowerCase() : '',
|
|
29
39
|
last: Number(c.last) || null,
|
|
30
40
|
lastFromThem: typeof c.lastFromThem === 'boolean' ? c.lastFromThem : null,
|
|
31
|
-
count:
|
|
41
|
+
count: num(c.count),
|
|
42
|
+
mine: num(c.mine),
|
|
32
43
|
});
|
|
33
44
|
}
|
|
34
45
|
return out;
|
|
35
46
|
}
|
|
36
47
|
|
|
48
|
+
/** An import's requests (buildInvitations), without their notes: those go with the messages, and only while they're kept. */
|
|
49
|
+
function cleanInvitations(list) {
|
|
50
|
+
if (!Array.isArray(list)) return [];
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const i of list.slice(0, 50000)) {
|
|
53
|
+
if (!i || !['in', 'out'].includes(i.dir) || !KEY_OK(i.key) || typeof i.id !== 'string' || i.id.length > 400) continue;
|
|
54
|
+
out.push({ id: i.id, dir: i.dir, key: i.key, name: name(i.name), t: Number(i.t) || null });
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Names by key, as the export spells them. */
|
|
60
|
+
function cleanNames(names) {
|
|
61
|
+
const out = {};
|
|
62
|
+
if (!names || typeof names !== 'object') return out;
|
|
63
|
+
for (const [k, v] of Object.entries(names).slice(0, 200000)) {
|
|
64
|
+
const n = name(v);
|
|
65
|
+
if (KEY_OK(k) && n) out[k] = n;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
37
70
|
export async function GET() {
|
|
38
71
|
const files = socialFiles();
|
|
39
72
|
if (!files) return Response.json({ social: null });
|
|
40
73
|
const social = readJson(files.social, null);
|
|
41
74
|
if (!social) return Response.json({ social: null });
|
|
42
|
-
// The
|
|
43
|
-
//
|
|
44
|
-
|
|
75
|
+
// The lists (conversations, requests, names, what the sync found beyond your
|
|
76
|
+
// connections) can run to thousands; the Galaxy and the tab's counts don't
|
|
77
|
+
// need them, and GET /api/social/messages serves them.
|
|
78
|
+
const { conversations, invitations, names, liveOthers, liveGroups, ...rest } = social;
|
|
45
79
|
return Response.json({ social: { ...rest, conversationCount: Array.isArray(conversations) ? conversations.length : 0 } });
|
|
46
80
|
}
|
|
47
81
|
|
|
@@ -55,6 +89,8 @@ export async function POST(request) {
|
|
|
55
89
|
const clean = {
|
|
56
90
|
live: before.live || {},
|
|
57
91
|
liveAt: before.liveAt || null,
|
|
92
|
+
liveOthers: before.liveOthers || {},
|
|
93
|
+
liveGroups: before.liveGroups || [],
|
|
58
94
|
autoSync: before.autoSync === true,
|
|
59
95
|
keepMessages: before.keepMessages === true,
|
|
60
96
|
importedAt: new Date().toISOString(),
|
|
@@ -64,6 +100,8 @@ export async function POST(request) {
|
|
|
64
100
|
chapters: Array.isArray(body?.chapters) ? body.chapters : [],
|
|
65
101
|
posts: body?.posts && typeof body.posts === 'object' ? body.posts : null,
|
|
66
102
|
invites: body?.invites && typeof body.invites === 'object' ? body.invites : null,
|
|
103
|
+
invitations: cleanInvitations(body?.invitations),
|
|
104
|
+
names: cleanNames(body?.names),
|
|
67
105
|
emails: body?.emails === true,
|
|
68
106
|
};
|
|
69
107
|
for (const p of Object.values(clean.people)) {
|
|
@@ -73,14 +111,18 @@ export async function POST(request) {
|
|
|
73
111
|
}
|
|
74
112
|
}
|
|
75
113
|
writeJsonAtomic(files.social, clean);
|
|
76
|
-
return Response.json({ ok: true, people: Object.keys(clean.people).length, conversations: clean.conversations.length });
|
|
114
|
+
return Response.json({ ok: true, people: Object.keys(clean.people).length, conversations: clean.conversations.length, invitations: clean.invitations.length });
|
|
77
115
|
}
|
|
78
116
|
|
|
79
117
|
// From the scanner's experimental messages sync (scripts/scrape.py sync_messages):
|
|
80
118
|
// per person, when your 1:1 conversation was last active, unread count, the
|
|
81
|
-
// thread's link, who wrote last and the newest message
|
|
82
|
-
//
|
|
83
|
-
//
|
|
119
|
+
// thread's link, who wrote last and the newest message; and the group
|
|
120
|
+
// conversations, without any words. Kept beside what the export gave: your
|
|
121
|
+
// connections under `live`, everyone else (with their name) under
|
|
122
|
+
// `liveOthers`, groups under `liveGroups`, each merged with what earlier syncs
|
|
123
|
+
// found, so a quick sync doesn't forget what a full-history one read further
|
|
124
|
+
// down. The newest message's words go to the messages file, and only while
|
|
125
|
+
// Keep my messages is on.
|
|
84
126
|
export async function PUT(request) {
|
|
85
127
|
const files = socialFiles();
|
|
86
128
|
if (!files) return Response.json({ error: 'Pick a profile first.' }, { status: 400 });
|
|
@@ -95,19 +137,34 @@ export async function PUT(request) {
|
|
|
95
137
|
if (me?.id) q.eq('user_id', me.id);
|
|
96
138
|
conns = (await q).data || [];
|
|
97
139
|
} catch { /* no connections yet */ }
|
|
98
|
-
const { live: matched, unmatched } = matchLive(body?.live || {}, conns);
|
|
140
|
+
const { live: matched, others: found, unmatched } = matchLive(body?.live || {}, conns);
|
|
99
141
|
const live = {};
|
|
142
|
+
const others = {};
|
|
100
143
|
const previews = {};
|
|
101
|
-
for (const [
|
|
102
|
-
|
|
103
|
-
|
|
144
|
+
for (const [into, from] of [[live, matched], [others, found]]) {
|
|
145
|
+
for (const [key, { preview, ...rest }] of Object.entries(from)) {
|
|
146
|
+
into[key] = rest;
|
|
147
|
+
if (saved.keepMessages === true && preview?.text) previews[key] = preview;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const groups = {};
|
|
151
|
+
for (const g of saved.liveGroups || []) groups[g.threadUrl || g.people.join(',')] = g;
|
|
152
|
+
for (const g of matchLiveGroups(body?.groups || [], conns)) {
|
|
153
|
+
const k = g.threadUrl || g.people.join(',');
|
|
154
|
+
if (!groups[k] || (g.last || 0) >= (groups[k].last || 0)) groups[k] = g;
|
|
104
155
|
}
|
|
105
|
-
writeJsonAtomic(files.social, {
|
|
156
|
+
writeJsonAtomic(files.social, {
|
|
157
|
+
...saved,
|
|
158
|
+
live: mergeLive(saved.live, live),
|
|
159
|
+
liveOthers: mergeLive(saved.liveOthers, others),
|
|
160
|
+
liveGroups: Object.values(groups),
|
|
161
|
+
liveAt: new Date().toISOString(),
|
|
162
|
+
});
|
|
106
163
|
if (Object.keys(previews).length) {
|
|
107
164
|
const kept = readJson(files.messages, {}) || {};
|
|
108
165
|
writeJsonAtomic(files.messages, { ...kept, previews: { ...(kept.previews || {}), ...previews } });
|
|
109
166
|
}
|
|
110
|
-
return Response.json({ ok: true, people: Object.keys(live).length, unmatched, previews: Object.keys(previews).length });
|
|
167
|
+
return Response.json({ ok: true, people: Object.keys(live).length, unmatched, groups: Object.keys(groups).length, previews: Object.keys(previews).length });
|
|
111
168
|
}
|
|
112
169
|
|
|
113
170
|
// The Social tab's switches: "once a day" for the live messages sync, and
|
|
@@ -127,7 +184,8 @@ export async function PATCH(request) {
|
|
|
127
184
|
return Response.json({ ok: true, autoSync: next.autoSync === true, keepMessages: next.keepMessages === true });
|
|
128
185
|
}
|
|
129
186
|
|
|
130
|
-
// Forget it: both files, the numbers and the words.
|
|
187
|
+
// Forget it: both files, the numbers and the words. Not your CRM notes: the
|
|
188
|
+
// page asks about those on their own (DELETE /api/social/crm).
|
|
131
189
|
export async function DELETE() {
|
|
132
190
|
const files = socialFiles();
|
|
133
191
|
if (files) {
|
|
@@ -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,18 +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';
|
|
11
|
-
import { createPortal } from 'react-dom';
|
|
12
14
|
import { usePathname } from 'next/navigation';
|
|
13
15
|
import Link from 'next/link';
|
|
14
16
|
import { watchScanner, scannerNow, stopScrape } from '../../lib/scraper-client';
|
|
15
17
|
import { watchAllDay, allDayNow } from '../../lib/experimental-client';
|
|
18
|
+
import { watchActivities, activitiesNow, noActivities } from '../../lib/island';
|
|
16
19
|
|
|
17
20
|
const WHAT = {
|
|
18
21
|
full: 'Scanning your network',
|
|
@@ -28,6 +31,7 @@ const WHAT = {
|
|
|
28
31
|
company: 'Scanning a company',
|
|
29
32
|
photos: 'Saving photos',
|
|
30
33
|
messages: 'Reading your messages list',
|
|
34
|
+
'messages-full': 'Reading your whole messages history',
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
// Green while there's plenty left, amber past 60%, red past 90%.
|
|
@@ -36,33 +40,35 @@ const tone = (used, cap) => (!cap ? '#8b9a9a' : used / cap >= 0.9 ? '#ff6b6b' :
|
|
|
36
40
|
export default function ScanStatusBar() {
|
|
37
41
|
const [job, setJob] = useState(() => scannerNow());
|
|
38
42
|
const [stopping, setStopping] = useState(false);
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
const [pinned, setPinned] = useState(false);
|
|
41
45
|
const allDay = useSyncExternalStore(watchAllDay, allDayNow, () => false);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
const others = useSyncExternalStore(watchActivities, activitiesNow, noActivities);
|
|
47
|
+
useEffect(() => watchScanner(() => {
|
|
48
|
+
const now = scannerNow();
|
|
49
|
+
setJob(now);
|
|
50
|
+
if (!now.running) setStopping(false);
|
|
51
|
+
}), []);
|
|
52
|
+
|
|
53
|
+
// Hangs from the header's bottom edge, measured: the header can load late,
|
|
54
|
+
// be swapped for a new one, or wrap to two rows.
|
|
45
55
|
const pathname = usePathname();
|
|
46
|
-
const [
|
|
56
|
+
const [top, setTop] = useState(null);
|
|
47
57
|
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
58
|
let header = null;
|
|
52
59
|
let ro = null;
|
|
53
60
|
let queued = 0;
|
|
54
61
|
const measure = () => {
|
|
55
62
|
queued = 0;
|
|
56
|
-
const
|
|
57
|
-
const now = slot ? null : document.querySelector('header');
|
|
63
|
+
const now = document.querySelector('header');
|
|
58
64
|
if (now !== header) {
|
|
59
65
|
ro?.disconnect();
|
|
60
66
|
ro = null;
|
|
61
67
|
header = now;
|
|
62
68
|
if (header) { ro = new ResizeObserver(later); ro.observe(header); }
|
|
63
69
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
70
|
+
const at = header ? Math.round(header.getBoundingClientRect().bottom) : null;
|
|
71
|
+
setTop((was) => (was === at ? was : at));
|
|
66
72
|
};
|
|
67
73
|
const later = () => { if (!queued) queued = requestAnimationFrame(measure); };
|
|
68
74
|
later();
|
|
@@ -71,63 +77,83 @@ export default function ScanStatusBar() {
|
|
|
71
77
|
window.addEventListener('resize', later);
|
|
72
78
|
return () => { cancelAnimationFrame(queued); ro?.disconnect(); mo.disconnect(); window.removeEventListener('resize', later); };
|
|
73
79
|
}, [pathname]);
|
|
74
|
-
useEffect(() => watchScanner(() => {
|
|
75
|
-
const now = scannerNow();
|
|
76
|
-
setJob(now);
|
|
77
|
-
if (!now.running) setStopping(false);
|
|
78
|
-
}), []);
|
|
79
|
-
const place = {
|
|
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 }),
|
|
83
|
-
maxWidth: 'calc(100vw - 32px)', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'nowrap', whiteSpace: 'nowrap',
|
|
84
|
-
padding: '8px 12px', borderRadius: 12, fontSize: 12, color: '#cfd8d8', background: 'rgba(14,16,32,0.94)',
|
|
85
|
-
};
|
|
86
|
-
const put = (bar) => (spot.slot && document.body.contains(spot.slot) ? createPortal(bar, spot.slot) : bar);
|
|
87
|
-
if (!job?.running) {
|
|
88
|
-
if (!allDay) return null;
|
|
89
|
-
return put(
|
|
90
|
-
<div role="status" style={{ ...place, opacity: 0.55, border: '1px solid rgba(255,255,255,0.1)' }}>
|
|
91
|
-
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#556', flexShrink: 0 }} />
|
|
92
|
-
<b style={{ color: '#b8c4c4' }}>Auto scan</b>
|
|
93
|
-
<span style={{ color: '#8b9a9a' }}>ready · press Auto scan beside Scan to start</span>
|
|
94
|
-
</div>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
80
|
|
|
98
|
-
const
|
|
81
|
+
const running = !!job?.running;
|
|
82
|
+
const other = others[others.length - 1] || null;
|
|
83
|
+
if (!running && !other && !allDay) return null;
|
|
84
|
+
|
|
85
|
+
const p = job?.progress;
|
|
99
86
|
const step = p?.kind === 'batch' && p.total ? `${p.current || p.done || 0} of ${p.total}`
|
|
100
|
-
: p?.kind === 'walk' && p.total ? `${(p.done || 0).toLocaleString()} of ${p.total.toLocaleString()}
|
|
87
|
+
: p?.kind === 'walk' && p.total ? `${(p.done || 0).toLocaleString()} of ${p.total.toLocaleString()}`
|
|
101
88
|
: p?.kind === 'saving' ? 'saving' : null;
|
|
102
|
-
const
|
|
103
|
-
const
|
|
89
|
+
const fraction = p?.total ? Math.min(1, (p.current || p.done || 0) / p.total) : other?.progress ?? null;
|
|
90
|
+
const b = job?.budget;
|
|
91
|
+
const last = running ? [...(job.log || [])].reverse().find((l) => l && l.trim())?.trim() : null;
|
|
92
|
+
const expanded = open || pinned;
|
|
104
93
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
94
|
+
// What the pill says, smallest first.
|
|
95
|
+
const dot = running ? '#00ff88' : other ? (other.tone === 'warn' ? '#FFD700' : '#3498DB') : '#556';
|
|
96
|
+
const label = running ? (WHAT[job.action] || 'Scanning') : other ? other.label : 'Auto scan';
|
|
97
|
+
const short = running ? step : other ? other.detail : null;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
role="status"
|
|
102
|
+
aria-live="polite"
|
|
103
|
+
onMouseEnter={() => setOpen(true)}
|
|
104
|
+
onMouseLeave={() => setOpen(false)}
|
|
105
|
+
style={{
|
|
106
|
+
position: 'fixed', left: '50%', top: top ?? 'var(--scan-bar-top, 94px)', transform: 'translateX(-50%)', zIndex: 60,
|
|
107
|
+
maxWidth: 'calc(100vw - 32px)', minWidth: running || other ? 180 : 0,
|
|
108
|
+
padding: expanded ? '8px 14px 10px' : '4px 12px 6px',
|
|
109
|
+
borderRadius: '0 0 16px 16px', borderTop: 'none',
|
|
110
|
+
border: `1px solid ${running ? 'rgba(0,255,136,0.25)' : 'rgba(255,255,255,0.1)'}`,
|
|
111
|
+
background: 'rgba(8,10,22,0.96)', color: '#cfd8d8', fontSize: 12,
|
|
112
|
+
opacity: running || other || expanded ? 1 : 0.55,
|
|
113
|
+
boxShadow: running ? '0 6px 20px rgba(0,0,0,0.35)' : 'none',
|
|
114
|
+
transition: 'padding 0.18s ease, opacity 0.18s ease',
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => setPinned((v) => !v)}
|
|
119
|
+
aria-expanded={expanded}
|
|
120
|
+
title={expanded ? 'Close' : 'Open'}
|
|
121
|
+
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', background: 'none', border: 'none', color: 'inherit', font: 'inherit', cursor: 'pointer', padding: 0, whiteSpace: 'nowrap' }}
|
|
122
|
+
>
|
|
123
|
+
<span className={running ? 'notch-pulse' : undefined} style={{ width: 7, height: 7, borderRadius: '50%', background: dot, flexShrink: 0 }} />
|
|
124
|
+
<b style={{ color: running || other ? '#fff' : '#b8c4c4', fontWeight: 600 }}>{label}</b>
|
|
125
|
+
{short && <span style={{ color: '#8b9a9a' }}>{short}</span>}
|
|
126
|
+
{!running && !other && expanded && <span style={{ color: '#8b9a9a' }}>ready · press Auto scan beside Scan to start</span>}
|
|
127
|
+
</button>
|
|
128
|
+
{fraction != null && (
|
|
129
|
+
<div style={{ height: 2, marginTop: 5, borderRadius: 2, background: 'rgba(255,255,255,0.08)', overflow: 'hidden' }}>
|
|
130
|
+
<div style={{ width: `${Math.round(fraction * 100)}%`, height: '100%', background: running ? '#00ff88' : '#3498DB', transition: 'width 0.4s ease' }} />
|
|
131
|
+
</div>
|
|
115
132
|
)}
|
|
116
|
-
{
|
|
117
|
-
<
|
|
118
|
-
{
|
|
119
|
-
|
|
133
|
+
{expanded && running && (
|
|
134
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 8, flexWrap: 'wrap' }}>
|
|
135
|
+
{job.target?.name && <span>{job.target.name}</span>}
|
|
136
|
+
{b && (
|
|
137
|
+
<span style={{ color: tone(b.searches, b.cap) }} title="Searches on your LinkedIn account in the last 24 hours, and your daily budget">
|
|
138
|
+
{b.searches}{b.cap ? ` of ${b.cap}` : ''} searches today
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
141
|
+
{last && (
|
|
142
|
+
<span style={{ color: '#667', maxWidth: 280, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={last}>{last}</span>
|
|
143
|
+
)}
|
|
144
|
+
<Link href="/setup" style={{ color: '#3498DB', textDecoration: 'none' }}>Details</Link>
|
|
145
|
+
<button
|
|
146
|
+
onClick={async () => { setStopping(true); try { await stopScrape(); } catch { setStopping(false); } }}
|
|
147
|
+
disabled={stopping}
|
|
148
|
+
title="Stops after saving what's been read; Resume on the Scan page carries on from the same page"
|
|
149
|
+
style={{
|
|
150
|
+
padding: '3px 10px', borderRadius: 8, fontSize: 12, cursor: stopping ? 'default' : 'pointer',
|
|
151
|
+
background: 'rgba(255,107,107,0.12)', color: '#ff9b9b', border: '1px solid rgba(255,107,107,0.35)',
|
|
152
|
+
}}
|
|
153
|
+
>{stopping ? 'Stopping…' : 'Stop'}</button>
|
|
154
|
+
</div>
|
|
120
155
|
)}
|
|
121
|
-
<
|
|
122
|
-
<button
|
|
123
|
-
onClick={async () => { setStopping(true); try { await stopScrape(); } catch { setStopping(false); } }}
|
|
124
|
-
disabled={stopping}
|
|
125
|
-
title="Stops after saving what's been read; Resume on the Scan page carries on from the same page"
|
|
126
|
-
style={{
|
|
127
|
-
padding: '4px 10px', borderRadius: 8, fontSize: 12, cursor: stopping ? 'default' : 'pointer',
|
|
128
|
-
background: 'rgba(255,107,107,0.12)', color: '#ff9b9b', border: '1px solid rgba(255,107,107,0.35)',
|
|
129
|
-
}}
|
|
130
|
-
>{stopping ? 'Stopping…' : 'Stop'}</button>
|
|
156
|
+
{expanded && !running && other?.detail && <div style={{ marginTop: 6, color: '#8b9a9a' }}>{other.detail}</div>}
|
|
131
157
|
</div>
|
|
132
158
|
);
|
|
133
159
|
}
|
|
@@ -79,6 +79,7 @@ export default function DataSection() {
|
|
|
79
79
|
const [exportNote, setExportNote] = useState(null);
|
|
80
80
|
const [importNote, setImportNote] = useState(null);
|
|
81
81
|
const [photos, setPhotos] = useState(true);
|
|
82
|
+
const [social, setSocial] = useState(true);
|
|
82
83
|
const [file, setFile] = useState(null);
|
|
83
84
|
const [confirmed, setConfirmed] = useState(false);
|
|
84
85
|
const [restarting, setRestarting] = useState(false);
|
|
@@ -137,7 +138,7 @@ export default function DataSection() {
|
|
|
137
138
|
const res = await fetch('/api/data/export', {
|
|
138
139
|
method: 'POST',
|
|
139
140
|
headers: { 'Content-Type': 'application/json' },
|
|
140
|
-
body: JSON.stringify({ photos }),
|
|
141
|
+
body: JSON.stringify({ photos, social }),
|
|
141
142
|
});
|
|
142
143
|
if (!res.ok) await answer(res, 'The copy could not be made.');
|
|
143
144
|
const blob = await res.blob();
|
|
@@ -324,6 +325,13 @@ export default function DataSection() {
|
|
|
324
325
|
They can’t be downloaded again without a new scan.
|
|
325
326
|
</span>
|
|
326
327
|
</label>
|
|
328
|
+
<label style={check}>
|
|
329
|
+
<input type="checkbox" checked={social} onChange={(e) => setSocial(e.target.checked)} style={{ marginTop: 3 }} />
|
|
330
|
+
<span>
|
|
331
|
+
Include the Social tab: its findings, your CRM notes, stages and follow-ups, and any messages you keep
|
|
332
|
+
there.
|
|
333
|
+
</span>
|
|
334
|
+
</label>
|
|
327
335
|
<div style={row}>
|
|
328
336
|
<Btn primary onClick={exportNow} disabled={!!busy || restarting || info.people === 0}>
|
|
329
337
|
{busy === 'export' ? 'Saving…' : 'Save a copy of my network'}
|
|
@@ -334,8 +342,9 @@ export default function DataSection() {
|
|
|
334
342
|
<Body style={small}>
|
|
335
343
|
The copy holds the names, headlines and photos of the people in your network. Keep it private: don’t
|
|
336
344
|
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.
|
|
338
|
-
tab
|
|
345
|
+
only in its browser tab, so it isn’t in the copy; import the CSV again on the new computer. With the
|
|
346
|
+
Social tab included, the copy also holds your CRM notes and any messages you keep, so it’s as private
|
|
347
|
+
as your inbox.
|
|
339
348
|
</Body>
|
|
340
349
|
|
|
341
350
|
<h3 style={subhead}>Bring in a network from another computer</h3>
|
|
@@ -42,3 +42,18 @@ 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; } }
|
|
49
|
+
|
|
50
|
+
/* The Social tab's CRM (app/social/Crm.js): the list and the person side by
|
|
51
|
+
side, stacked on a phone. In Pipeline the board takes the whole width and
|
|
52
|
+
the person sits under it. */
|
|
53
|
+
.crm-grid { display: grid; grid-template-columns: minmax(280px, 400px) minmax(0, 1fr); gap: 10px; align-items: start; }
|
|
54
|
+
.crm-grid > .crm-wide { grid-column: 1 / -1; }
|
|
55
|
+
.crm-board { display: grid; grid-template-columns: repeat(6, minmax(150px, 1fr)); gap: 10px; padding: 10px; overflow-x: auto; }
|
|
56
|
+
@media (max-width: 760px) {
|
|
57
|
+
.crm-grid { grid-template-columns: minmax(0, 1fr); }
|
|
58
|
+
.crm-board { grid-template-columns: minmax(0, 1fr); }
|
|
59
|
+
}
|
|
@@ -302,8 +302,6 @@ 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" />
|
|
307
305
|
|
|
308
306
|
{!selectedCompany && tab !== 'companies' ? (
|
|
309
307
|
<PathsAnalyzer d1={d1Data} d2={d2Data} d3={d3Data} tab={tab}
|
|
@@ -164,8 +164,6 @@ 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" />
|
|
169
167
|
|
|
170
168
|
<div style={{ flex: 1, overflow: 'auto', padding: '30px 20px' }}>
|
|
171
169
|
<div style={{ maxWidth: 700, margin: '0 auto' }}>
|
|
@@ -301,8 +301,6 @@ 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" />
|
|
306
304
|
|
|
307
305
|
{/* Batch action bar — recs view only */}
|
|
308
306
|
{view === 'recs' && selected.size > 0 && (
|
|
@@ -56,8 +56,6 @@ 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" />
|
|
61
59
|
<main style={{ maxWidth: 980, margin: '0 auto', padding: '8px 24px 64px' }}>
|
|
62
60
|
<Body style={{ marginTop: 16 }}>
|
|
63
61
|
How someone’s power score is worked out, and the three things you can change about it:{' '}
|
|
@@ -73,8 +73,6 @@ 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" />
|
|
78
76
|
|
|
79
77
|
<main style={{ maxWidth: 720, margin: '0 auto', padding: '8px 24px 64px' }}>
|
|
80
78
|
{error && <Body style={{ color: '#ff7676', marginTop: 16 }}>{error}</Body>}
|
|
@@ -203,8 +203,6 @@ 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" />
|
|
208
206
|
|
|
209
207
|
<div style={{ maxWidth: 720, margin: '0 auto', padding: '32px 24px 64px' }}>
|
|
210
208
|
|