← back to Rentv 2026
PR intel: daily media/PR refresh crawl ($0 local)
9da8b6b0c8c75136e22cd9785ddcbae95cbe263b · 2026-08-05 13:46:44 -0700 · Steve
- src/pr/tools/daily-media-crawl.js (npm run pr:crawl:media): enqueues
re-verify for CRE media/association/PR-agency orgs that are stale or have
no contact yet; verify re-profiles the site and chains discover-people.
Standing rentv-pr-worker processes it. Per-day dedupe keys; no paid APIs/LLM.
- for cron 0 4,14 * * * on prod (4am + 2pm)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M package.jsonA src/pr/tools/daily-media-crawl.js
Diff
commit 9da8b6b0c8c75136e22cd9785ddcbae95cbe263b
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 5 13:46:44 2026 -0700
PR intel: daily media/PR refresh crawl ($0 local)
- src/pr/tools/daily-media-crawl.js (npm run pr:crawl:media): enqueues
re-verify for CRE media/association/PR-agency orgs that are stale or have
no contact yet; verify re-profiles the site and chains discover-people.
Standing rentv-pr-worker processes it. Per-day dedupe keys; no paid APIs/LLM.
- for cron 0 4,14 * * * on prod (4am + 2pm)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
package.json | 3 ++-
src/pr/tools/daily-media-crawl.js | 57 +++++++++++++++++++++++++++++++++++++++
2 files changed, 59 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index fc7361d3..cc2566f4 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
"pr:seed:ca": "node src/pr/seed/ca-seed.js",
"test": "node --test test/pr/*.test.js",
"pr:seed:az": "node src/pr/seed/az-seed.js",
- "pr:seed:national": "node src/pr/seed/national-media-seed.js"
+ "pr:seed:national": "node src/pr/seed/national-media-seed.js",
+ "pr:crawl:media": "node src/pr/tools/daily-media-crawl.js"
},
"dependencies": {
"express": "^4.21.2",
diff --git a/src/pr/tools/daily-media-crawl.js b/src/pr/tools/daily-media-crawl.js
new file mode 100644
index 00000000..3c5743b5
--- /dev/null
+++ b/src/pr/tools/daily-media-crawl.js
@@ -0,0 +1,57 @@
+'use strict';
+// Daily media/PR crawl — $0, LOCAL compute only (no paid search APIs, no LLM).
+//
+// Keeps the CRE media + association-media + PR-agency records fresh: enqueues a
+// re-verify for every media/PR org that is STALE (last verified > MEDIA_STALE_DAYS
+// ago, or never) OR has no press contact yet. verify-organization re-profiles the
+// live site (refreshing press / submission / editorial-calendar / awards / events /
+// media-kit URLs + press email) and chains discover-people to pick up NEWLY published
+// editors/reporters. The standing rentv-pr-worker processes the queue with the same
+// plain-fetch adapter — zero API spend. Idempotent within a day via per-day dedupe keys.
+//
+// Cron (prod, Kamatera): 0 4,14 * * * → 4am + 2pm, processed by rentv-pr-worker.
+// Run manually: node src/pr/tools/daily-media-crawl.js
+const db = require('../db');
+const jobs = require('../jobs');
+
+const MEDIA_TYPES = ['trade_publication', 'association', 'pr_agency', 'marketing_agency', 'public_affairs'];
+const STALE_DAYS = Number(process.env.MEDIA_STALE_DAYS || 3);
+
+async function main() {
+ const h = await db.health();
+ if (!h.ok) { console.error('DB unavailable: ' + h.reason); process.exit(1); }
+ await db.runMigrations({});
+ const orgs = await db.rows(
+ `SELECT o.id, o.display_name,
+ (o.last_verified_at IS NULL OR o.last_verified_at < now() - ($1 || ' days')::interval) AS stale,
+ NOT EXISTS (
+ SELECT 1 FROM pr_people p
+ WHERE p.organization_id = o.id
+ AND p.lifecycle_status NOT IN ('duplicate','wrong_person','left_company')
+ ) AS no_contact
+ FROM pr_organizations o
+ WHERE o.organization_type = ANY($2)
+ AND o.lifecycle_status <> 'duplicate'
+ AND o.website_url IS NOT NULL`,
+ [String(STALE_DAYS), MEDIA_TYPES]);
+
+ const day = new Date().toISOString().slice(0, 10);
+ let queued = 0, skipped = 0;
+ for (const o of orgs) {
+ if (!o.stale && !o.no_contact) { skipped++; continue; }
+ // verify-organization re-profiles the site (URL fields + press email) and chains
+ // discover-people with the team pages it finds — one primitive covers both needs.
+ const j = await jobs.enqueue('verify-organization', { organization_id: o.id },
+ { priority: 6, actor: 'system:daily-media-crawl', dedupe_key: `media-verify-${o.id}-${day}` });
+ if (!j.deduped) queued++;
+ }
+ // housekeeping after the wave
+ await jobs.enqueue('detect-duplicates', {}, { priority: 8, actor: 'system:daily-media-crawl', dedupe_key: `media-dedupe-${day}` });
+ await jobs.enqueue('calculate-scores', {}, { priority: 9, actor: 'system:daily-media-crawl', dedupe_key: `media-scores-${day}` });
+
+ console.log(`[daily-media-crawl] ${day} media-orgs=${orgs.length} re-verify-queued=${queued} fresh-skipped=${skipped} ($0 local, no paid APIs/LLM)`);
+ console.log('[daily-media-crawl] processed by the standing rentv-pr-worker.');
+ process.exit(0);
+}
+
+main().catch((e) => { console.error(e); process.exit(1); });
← 3c345857 Consulting portal: RENTV brand film — Boom avatar (headshot)
·
back to Rentv 2026
·
deploy: rentv Review Queue narrow-viewport fix + national CR 784b3fe6 →