[object Object]

← back to Rentv 2026

PR intel: CRM name/title hygiene tool (byline + trailing-punct cleanup)

1663154b6f494832bd80c1a8920462933ce30e47 · 2026-08-05 14:38:03 -0700 · Steve

src/pr/tools/cleanup-names.js strips byline prefixes (By/With/Photos by) and
trailing punctuation on titles via audited PUT (recomputes normalized_name +
re-classifies). Dry-run default. First apply: 92 fixes across 2490 contacts.

Files touched

Diff

commit 1663154b6f494832bd80c1a8920462933ce30e47
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 5 14:38:03 2026 -0700

    PR intel: CRM name/title hygiene tool (byline + trailing-punct cleanup)
    
    src/pr/tools/cleanup-names.js strips byline prefixes (By/With/Photos by) and
    trailing punctuation on titles via audited PUT (recomputes normalized_name +
    re-classifies). Dry-run default. First apply: 92 fixes across 2490 contacts.
---
 src/pr/tools/cleanup-names.js | 63 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 63 insertions(+)

diff --git a/src/pr/tools/cleanup-names.js b/src/pr/tools/cleanup-names.js
new file mode 100644
index 00000000..94644531
--- /dev/null
+++ b/src/pr/tools/cleanup-names.js
@@ -0,0 +1,63 @@
+'use strict';
+// CRM name/title hygiene — strips byline prefixes ("By Peter Fabris" → "Peter Fabris")
+// and trailing punctuation on titles ("Editor-in-Chief:" → "Editor-in-Chief"), through
+// the audited PUT /api/pr/people/:id (which recomputes normalized_name + re-classifies
+// the title). Dry-run by default; pass --apply to write. Idempotent.
+//
+// Usage: node src/pr/tools/cleanup-names.js [--apply]
+// Env: PR_API (default prod), PR_AUTH (user:pass)
+const API = process.env.PR_API || 'https://rentv.agentabrams.com';
+const AUTH = 'Basic ' + Buffer.from(process.env.PR_AUTH || 'admin:DW2024!').toString('base64');
+const APPLY = process.argv.includes('--apply');
+
+async function api(p, opts = {}) {
+  const r = await fetch(API + '/api/pr' + p, {
+    ...opts,
+    headers: { Authorization: AUTH, 'Content-Type': 'application/json', ...(opts.headers || {}) },
+    body: opts.body ? JSON.stringify(opts.body) : undefined,
+  });
+  if (!r.ok) throw new Error(p + ' → ' + r.status + ': ' + (await r.text()).slice(0, 120));
+  return r.json();
+}
+
+const BYLINE = /^(by|with|photos?\s+by|words?\s+by|story\s+by|reporting\s+by|edited\s+by)\s+/i;
+function cleanName(n) {
+  let s = String(n || '').replace(/\s+/g, ' ').trim();
+  s = s.replace(BYLINE, '');
+  s = s.replace(/[,:;|]+$/, '').trim();
+  return s;
+}
+function cleanTitle(t) {
+  if (t == null) return t;
+  let s = String(t).replace(/\s+/g, ' ').trim();
+  s = s.replace(/[-–—:;,\s]+$/, '').replace(/^[-–—:;,\s]+/, '').trim();
+  return s || null;
+}
+
+async function main() {
+  let offset = 0, scanned = 0, nameFix = 0, titleFix = 0, applied = 0;
+  const samples = [];
+  for (;;) {
+    const page = await api(`/people?limit=500&offset=${offset}`);
+    const rows = page.rows || page.people || [];
+    if (!rows.length) break;
+    for (const p of rows) {
+      scanned++;
+      const patch = {};
+      const nn = cleanName(p.full_name);
+      if (nn && nn !== p.full_name) { patch.full_name = nn; nameFix++; }
+      const nt = cleanTitle(p.exact_title);
+      if ((p.exact_title || null) !== nt) { patch.exact_title = nt; titleFix++; }
+      if (!Object.keys(patch).length) continue;
+      if (samples.length < 12) samples.push(`${p.full_name} | ${p.exact_title || ''}  ⇒  ${patch.full_name || p.full_name} | ${patch.exact_title !== undefined ? patch.exact_title : p.exact_title || ''}`);
+      if (APPLY) { try { await api('/people/' + p.id, { method: 'PUT', body: patch }); applied++; } catch (e) { /* skip */ } }
+    }
+    offset += rows.length;
+    if (rows.length < 500) break;
+  }
+  console.log(`[cleanup-names] scanned ${scanned} · name-fixes ${nameFix} · title-fixes ${titleFix} · ${APPLY ? 'APPLIED ' + applied : 'DRY-RUN (pass --apply to write)'}`);
+  console.log('samples:\n  ' + samples.join('\n  '));
+  process.exit(0);
+}
+
+main().catch((e) => { console.error('fatal:', e.message); process.exit(1); });

← 1fd2ec07 Cody-gate: posts public-status now published|live in lockste  ·  back to Rentv 2026  ·  closings: buyer/seller/broker are now clickable → filter the 279335d4 →