Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions apps/site/assets/js/bulk-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@
var selection = new Map(); // id -> true
var allMatchingMode = false; // true after 2nd header click
var currentSignature = cfg.getFilterSignature ? cfg.getFilterSignature() : "";
var headerCheck = document.getElementById("ads-bulk-header-check");

// Rehydrate selection on init iff the persisted signature matches
// the current filter signature. Drop otherwise.
Expand Down Expand Up @@ -211,15 +210,28 @@
}
}

if (headerCheck) {
var headerClicks = 0;
headerCheck.addEventListener("click", function () {
headerClicks += 1;
if (headerClicks === 1) { selectPage(headerCheck.checked); }
else { headerClicks = 0; selectAllMatching(); }
setTimeout(function () { headerClicks = 0; }, 2500);
});
}
// Delegated, not bound to the element. On the investors and companies
// pages the header checkbox is emitted inside the async row render, so it
// does not exist when init() runs and it is replaced wholesale on every
// non-append load. A direct listener resolved at init() caught neither
// case: "select page" and "select all matching" were dead on both pages,
// and the checkbox still ticked, so it looked like it had worked.
// (It did work on leads and accounts, whose header cell is static HTML —
// which is why the two pages behaved differently for the same code.)
var headerClicks = 0;
var headerResetTimer = null;
document.addEventListener("click", function (e) {
var hc = e.target && e.target.closest ? e.target.closest("#ads-bulk-header-check") : null;
if (!hc) return;
headerClicks += 1;
if (headerClicks === 1) { selectPage(hc.checked); }
else { headerClicks = 0; selectAllMatching(); }
// Cleared rather than stacked: the old code queued a fresh 2.5 s reset
// on every click, so an earlier timer could fire between the two clicks
// of a deliberate double-click and reset the counter mid-gesture.
if (headerResetTimer) clearTimeout(headerResetTimer);
headerResetTimer = setTimeout(function () { headerClicks = 0; }, 2500);
});

// Rebind row checks after each list refresh — pages call this manually
// (or use a MutationObserver as a fallback).
Expand Down
19 changes: 18 additions & 1 deletion apps/site/assets/js/leads.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@
}).join("");
}

// Where a lead's name should go.
//
// This used to be an unconditional /dashboard/people/?id=<l.id>. That page
// hands the value to /api/profilers/:entity_id/*, which is keyed on
// u_entities — and `l.id` is the legacy `leads` primary key, a different id
// space entirely. So the link never resolved: every name on the Leads page
// opened a person profile with every panel empty, which reads as "we have
// no data on this person" rather than "this link is wrong".
//
// The listing now carries `entity_id` when the lead has been mapped into
// u_entities. Rows that have not been mapped yet go to the lead detail
// page, which is keyed on exactly the id we have.
function nameHref(l) {
if (l.entity_id) return "/dashboard/people/?id=" + encodeURIComponent(l.entity_id);
return "/dashboard/lead/?id=" + encodeURIComponent(l.id);
}

function render(items) {
var tbody = document.getElementById("ads-leads-tbody");
if (!items.length) {
Expand All @@ -81,7 +98,7 @@
tbody.innerHTML = items.map(function (l) {
return '<tr data-id="' + esc(l.id) + '">'
+ '<td style="padding:8px;vertical-align:middle"><input type="checkbox" class="ads-bulk-check" data-id="' + esc(l.id) + '"></td>'
+ '<td style="padding:8px"><a href="/dashboard/people/?id=' + esc(l.id) + '">' + esc(l.name || "(no name)") + '</a>' + rolesBadges(l.roles) + '</td>'
+ '<td style="padding:8px"><a href="' + esc(nameHref(l)) + '">' + esc(l.name || "(no name)") + '</a>' + rolesBadges(l.roles) + '</td>'
+ '<td style="padding:8px">' + esc(l.org || "—") + '</td>'
+ '<td style="padding:8px">' + esc(l.email || "—") + '</td>'
+ '<td style="padding:8px">' + esc(l.status || "—") + '</td>'
Expand Down
17 changes: 14 additions & 3 deletions apps/site/assets/js/profile-tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
// window.ADS.Profile.mount({ rootId, entityId }) — embed mode
// window.ADS.Profile.mountStandalone() — /dashboard/profile/
//
// Resolves entity via ?entity=<id> or ?table=&ref= (same convention as DD/News).
// Resolves entity via ?entity=<id> (or ?id=<id>) or ?table=&ref= (same
// convention as DD/News).

(function () {
if (window.ADS && window.ADS.Profile) return;
Expand All @@ -27,9 +28,19 @@
sec_rel: "Secular ↔ Religious",
};

// `?id=` is accepted alongside `?entity=` because seven navigation entry
// points across six pages link here with `?id=` — ops-garbage-review.js,
// predictions.html (twice), watchlists.html, power-nodes.html,
// dossiers.html and ops-quality.html. Reading only `entity` made every one
// of those land on "No entity selected." Fixing the reader rather than the
// seven call sites also covers any future link written the same way.
function qs() {
var p = new URLSearchParams(window.location.search);
return { entity: p.get("entity"), table: p.get("table"), ref: p.get("ref") };
return {
entity: p.get("entity") || p.get("id"),
table: p.get("table"),
ref: p.get("ref"),
};
}
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
Expand Down Expand Up @@ -399,7 +410,7 @@
if (!entityId) {
if (titleEl) titleEl.textContent = "Profile";
var host = document.getElementById("ads-profile-root");
if (host) host.innerHTML = '<div class="ads-muted">No entity selected. Pass <code>?entity=</code> or <code>?table=&ref=</code>.</div>';
if (host) host.innerHTML = '<div class="ads-muted">No entity selected. Pass <code>?entity=</code>, <code>?id=</code> or <code>?table=&ref=</code>.</div>';
return;
}
if (titleEl) titleEl.textContent = "Loading…";
Expand Down
20 changes: 18 additions & 2 deletions apps/site/dashboard/crawlers.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ <h2 class="ads-h2">Recent runs</h2>
<th>Inserted</th>
<th>Skipped</th>
<th>Accts ↑</th>
<th>Notes</th>
<th>Error</th>
</tr>
</thead>
<tbody><tr><td colspan="8">Loading…</td></tr></tbody>
<tbody><tr><td colspan="9">Loading…</td></tr></tbody>
</table>
</div>
</div>
Expand All @@ -54,6 +55,20 @@ <h2 class="ads-h2">Recent runs</h2>
(async function () {
const tbody = document.querySelector("#ads-crawlers-table tbody");
const runsBody = document.querySelector("#ads-crawler-runs tbody");
// crawler_runs.meta_json holds the per-source counters. Rendering them is
// the difference between "this source had nothing to scan" and "this source
// scanned everything and found nothing new" — both of which otherwise show
// as a run with 0 emitted and status ok. The seven ATS sources are seeded
// from accounts.meta_json keys that nothing sets automatically, so
// seeded_accounts=0 is the answer to "why is this always empty?".
function runNotes(metaJson) {
if (!metaJson) return "";
var m;
try { m = JSON.parse(metaJson); } catch (e) { return ""; }
if (!m || typeof m !== "object") return "";
return Object.keys(m).map(function (k) { return k + "=" + m[k]; }).join(" · ");
}

function fmt(s) { return s ? new Date(s).toLocaleString() : "—"; }
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]; }); }
async function refresh() {
Expand Down Expand Up @@ -87,11 +102,12 @@ <h2 class="ads-h2">Recent runs</h2>
<td>${esc(x.signals_inserted)}</td>
<td>${esc(x.signals_skipped)}</td>
<td>+${esc(x.accounts_created)} / ${esc(x.accounts_resolved)}</td>
<td class="ads-muted" style="font-size:11px">${esc(runNotes(x.meta_json))}</td>
<td>${esc(x.error || "")}</td>
</tr>
`).join("");
} catch (e) {
runsBody.innerHTML = `<tr><td colspan="8">Failed to load: ${esc(e && e.message ? e.message : e)}</td></tr>`;
runsBody.innerHTML = `<tr><td colspan="9">Failed to load: ${esc(e && e.message ? e.message : e)}</td></tr>`;
}
}
document.addEventListener("change", async (e) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "eslint 'src/**/*.ts'",
"lint:fix": "eslint 'src/**/*.ts' --fix",
"gates": "bash scripts/gates.sh",
"test": "tsc -p tsconfig.test.json && node --test test/profile.test.mjs test/agent.test.mjs test/monitoring.test.mjs test/osint.test.mjs test/profile-helpers.test.mjs test/profile-db.test.mjs test/csv_person_import.test.mjs test/firm_geo_backfill.test.mjs test/profilers.test.mjs test/access_guard.test.mjs test/markMap.security.test.mjs test/profileWorkflows.test.mjs test/vc_sources.test.mjs src/crawler/adapters/__tests__/*.test.mjs src/crawler/adapters/deals/__tests__/*.test.mjs src/services/deals/__tests__/*.test.mjs src/ai/__tests__/*.test.mjs src/services/capTable/__tests__/*.test.mjs src/services/valuation/__tests__/*.test.mjs src/services/documents/__tests__/*.test.mjs test/verification.test.mjs test/verification.verifiers.test.mjs test/preflight.test.mjs test/proxyPool.test.mjs test/fetcher_proxy.test.mjs test/sweeper.test.mjs src/services/termSheets/__tests__/*.test.mjs src/services/fundReturns/__tests__/*.test.mjs src/services/edgeQuality/__tests__/*.test.mjs src/services/intros/__tests__/*.test.mjs src/services/diligence/__tests__/*.test.mjs src/services/founderCrm/__tests__/*.test.mjs src/services/mlOps/__tests__/*.test.mjs test/people.test.mjs test/overrides.test.mjs test/overrides_integration.test.mjs test/relationships_inference.test.mjs src/services/compute/__tests__/*.test.mjs src/services/systemHealth/__tests__/*.test.mjs test/investor_portfolio.test.mjs test/error_surfacing.test.mjs test/subrequest_budget.test.mjs test/errors_classify.test.mjs test/robots_skip.test.mjs test/do_merge_consistency.test.mjs test/search_sync_index.test.mjs test/comp_panel_snapshot.test.mjs test/account_score_batch.test.mjs test/accounts_sort_fallback.test.mjs test/dd_scores_by_ref.test.mjs test/job_state_transitions.test.mjs test/simple_request.test.mjs test/simple_request_client.test.mjs test/pagination_guard.test.mjs test/enrichment_budget.test.mjs test/org_entity_merge.test.mjs test/import_entity_mapping.test.mjs test/schema_drift.test.mjs test/garbage.test.mjs test/crawler_seeds.test.mjs test/identity_harvest.test.mjs test/personaMatching.test.mjs test/personaMatchingAcceptance.test.mjs test/personaMatchingDbContract.test.mjs test/profile_comments.test.mjs test/profiler_batch.test.mjs test/schema_drift_repo.test.mjs test/migrations_apply.test.mjs test/merge_repoints.test.mjs test/ci_wrangler_version.test.mjs test/soft_delete_roundtrip.test.mjs",
"test": "tsc -p tsconfig.test.json && node --test test/profile.test.mjs test/agent.test.mjs test/monitoring.test.mjs test/osint.test.mjs test/profile-helpers.test.mjs test/profile-db.test.mjs test/csv_person_import.test.mjs test/firm_geo_backfill.test.mjs test/profilers.test.mjs test/access_guard.test.mjs test/markMap.security.test.mjs test/profileWorkflows.test.mjs test/vc_sources.test.mjs src/crawler/adapters/__tests__/*.test.mjs src/crawler/adapters/deals/__tests__/*.test.mjs src/services/deals/__tests__/*.test.mjs src/ai/__tests__/*.test.mjs src/services/capTable/__tests__/*.test.mjs src/services/valuation/__tests__/*.test.mjs src/services/documents/__tests__/*.test.mjs test/verification.test.mjs test/verification.verifiers.test.mjs test/preflight.test.mjs test/proxyPool.test.mjs test/fetcher_proxy.test.mjs test/sweeper.test.mjs src/services/termSheets/__tests__/*.test.mjs src/services/fundReturns/__tests__/*.test.mjs src/services/edgeQuality/__tests__/*.test.mjs src/services/intros/__tests__/*.test.mjs src/services/diligence/__tests__/*.test.mjs src/services/founderCrm/__tests__/*.test.mjs src/services/mlOps/__tests__/*.test.mjs test/people.test.mjs test/overrides.test.mjs test/overrides_integration.test.mjs test/relationships_inference.test.mjs src/services/compute/__tests__/*.test.mjs src/services/systemHealth/__tests__/*.test.mjs test/investor_portfolio.test.mjs test/error_surfacing.test.mjs test/subrequest_budget.test.mjs test/errors_classify.test.mjs test/robots_skip.test.mjs test/do_merge_consistency.test.mjs test/search_sync_index.test.mjs test/comp_panel_snapshot.test.mjs test/account_score_batch.test.mjs test/accounts_sort_fallback.test.mjs test/dd_scores_by_ref.test.mjs test/job_state_transitions.test.mjs test/simple_request.test.mjs test/simple_request_client.test.mjs test/pagination_guard.test.mjs test/enrichment_budget.test.mjs test/org_entity_merge.test.mjs test/import_entity_mapping.test.mjs test/schema_drift.test.mjs test/garbage.test.mjs test/crawler_seeds.test.mjs test/identity_harvest.test.mjs test/personaMatching.test.mjs test/personaMatchingAcceptance.test.mjs test/personaMatchingDbContract.test.mjs test/profile_comments.test.mjs test/profiler_batch.test.mjs test/schema_drift_repo.test.mjs test/migrations_apply.test.mjs test/merge_repoints.test.mjs test/ci_wrangler_version.test.mjs test/soft_delete_roundtrip.test.mjs test/site_deeplinks.test.mjs test/persona_headcount.test.mjs test/crawler_run_meta.test.mjs test/diligence_founder_link.test.mjs test/edge_quality_sectors.test.mjs test/sector_resolution.test.mjs test/team_snapshot_eligibility.test.mjs",
"cf:provision": "node scripts/provision-cf.mjs"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion apps/worker/src/entities/backfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export async function backfillAccounts(env: Env, offset = 0, limit = BATCH): Pro
`SELECT id, name, legal_name, website, domain, industry, industries_json,
hq_country_iso2, hq_region, hq_city, funding_stage,
linkedin_url, twitter_handle, github_org, crunchbase_url,
fit_score, intent_score
fit_score, intent_score, employees
FROM accounts ORDER BY created_at LIMIT ? OFFSET ?`,
).bind(limit + 1, offset).all<Record<string, unknown>>();
const rows = r.results ?? [];
Expand Down
8 changes: 8 additions & 0 deletions apps/worker/src/entities/dualwrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ interface AccountLikeInput {
crunchbase_url?: string | null;
fit_score?: number | null;
intent_score?: number | null;
employees?: number | null;
}

interface BuyerLikeInput {
Expand Down Expand Up @@ -408,6 +409,13 @@ export async function syncAccountToEntity(env: Env, a: AccountLikeInput, source
{ predicate: "funding_stage", value_text: a.funding_stage ?? null },
{ predicate: "fit_max_score", value_number: numOrNull(a.fit_score) },
{ predicate: "intent_score", value_number: numOrNull(a.intent_score) },
// `accounts.employees` was the one sizing column dualwrite dropped, so
// no account entity carried a headcount fact and persona matching
// scored every one of them "company size unknown". `employees` is the
// predicate the registry declares (entities/profile-predicates.ts) and
// the one secEdgar/persist.ts already writes, so this converges on the
// name that exists rather than adding a fourth spelling.
{ predicate: "employees", value_number: numOrNull(a.employees) },
];
await insertFactsBatch(env, entityId, patches, source, "scrape");
await Promise.all([
Expand Down
Loading
Loading