← back to Rentv
pr-intelligence: docs (data-sources, research-ops, email-setup, compliance, admin-guide), .env.example, Playwright browser-test scaffolds
eea88b78b84e220de4931c67efaf839d2250455a · 2026-07-30 11:38:29 -0700 · Steve Abrams
Files touched
A docs/CRE_PR_ADMIN_GUIDE.mdA docs/CRE_PR_COMPLIANCE.mdA docs/CRE_PR_DATA_SOURCES.mdA docs/CRE_PR_EMAIL_SETUP.mdA docs/CRE_PR_RESEARCH_OPERATIONS.mdA test/pr/browser/workspace.spec.js
Diff
commit eea88b78b84e220de4931c67efaf839d2250455a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 30 11:38:29 2026 -0700
pr-intelligence: docs (data-sources, research-ops, email-setup, compliance, admin-guide), .env.example, Playwright browser-test scaffolds
---
docs/CRE_PR_ADMIN_GUIDE.md | 40 +++++++++++++++++++
docs/CRE_PR_COMPLIANCE.md | 55 ++++++++++++++++++++++++++
docs/CRE_PR_DATA_SOURCES.md | 69 +++++++++++++++++++++++++++++++++
docs/CRE_PR_EMAIL_SETUP.md | 44 +++++++++++++++++++++
docs/CRE_PR_RESEARCH_OPERATIONS.md | 69 +++++++++++++++++++++++++++++++++
test/pr/browser/workspace.spec.js | 79 ++++++++++++++++++++++++++++++++++++++
6 files changed, 356 insertions(+)
diff --git a/docs/CRE_PR_ADMIN_GUIDE.md b/docs/CRE_PR_ADMIN_GUIDE.md
new file mode 100644
index 00000000..37224b90
--- /dev/null
+++ b/docs/CRE_PR_ADMIN_GUIDE.md
@@ -0,0 +1,40 @@
+# CRE PR Intelligence — Admin Guide
+
+Open **Backend → CRE PR Intelligence** (`/admin/pr-intelligence`). Everything is
+admin-only; user-tier logins get 403 on both APIs and shells.
+
+## Pages
+| Page | What it does |
+|---|---|
+| **Dashboard** | CA completion %, AZ lock, org/people KPIs, categories, roles, metros, verified/unverified, orgs without PR contact, agencies+clients, drafts awaiting review, replies, follow-ups, run health, adapter status, coverage gaps. |
+| **Research** | Start CA discovery per metro (mandated order shown), edit the query matrix (DB-backed, no code changes), import CSV/JSON/LinkedIn URLs with mapping → preview → dry-run → import → reversible batch. |
+| **Organizations** | Full-text search + state/metro/county/category/asset-class/status/confidence/priority/PR-contact filters (URL-persisted), sortable columns, bulk tag/verify/enroll, CSV export, detail drawer (identity, PR status, press links, people, relationships, evidence timeline, outreach history, activity), duplicate finder + merge, “+ Add organization” manual entry (verification URL required). |
+| **People** | Search name/org/title/email/LinkedIn; filters incl. email-verification and LinkedIn status; inline corrections that preserve the original extracted value + role history; outreach-readiness panel; suppress; draft generation. |
+| **Review Queue** | Three columns: candidates (keyboard ↑/↓, A/R/D/S) · profile + evidence + duplicate candidates + accept/edit/reject/merge/stale/defer · letter preview + personalization + internal notes + save draft/approve/provider-draft. |
+| **Campaigns** | Create target definitions, enroll (suppression-checked, transactional), batch-generate drafts, per-member outreach status, export history. |
+| **Letter Builder** | Block-based base letter; lock/unlock sections (unlock is explicit + audited); add/remove/reorder/edit blocks; immutable version history with compare + load-to-undo; vertical & org snippets; “Add to letter” clipboard from any evidence excerpt; live preview with a real contact (unfilled merge fields highlighted); desktop/mobile preview; internal citations shown and never sent. |
+| **Inbox** | Provider status, thread view (originals preserved), canned reply actions, compose → provider draft, explicit Send (confirm + suppression re-check), interested/not-now/wrong-person/close, referral capture (creates person + task), opt-out → immediate suppression. |
+| **Tasks** | Follow-ups & chores; every card shows created date+time. |
+| **Sources** | Search/filter every source row with excerpt, terms note, evidence count, retrieval time; export evidence CSV. |
+| **Runs & Jobs** | Run cards with counters, checkpoints, errors, pause/resume; job queue with retry/pause/cancel; queue any job type with a JSON payload; auto-refresh. |
+| **Settings** | Sender identity (required before sending), submission method, unsubscribe text, letter purpose, refresh cadence; CA quality-gate check; Arizona unlock (+audited override); email-provider status; suppression list; audit log. |
+
+## The mandated rollout
+California runs first in the fixed metro order (LA → OC → Inland Empire → San Diego →
+Bay Area → Sacramento → Ventura/Santa Barbara → Central Valley). Arizona discovery
+**cannot be queued** until the CA gate passes or an admin override is recorded — the
+button lives in Settings, and the gate is computed from live data (see the dashboard
+checklist).
+
+## Global search
+The top-bar box (` / ` to focus) spans organizations, people, titles, domains, emails,
+LinkedIn URLs, metros, counties, notes, and threads.
+
+## Everyday flow
+1. Research → queue a metro run (worker executes).
+2. Review Queue → accept/merge/reject candidates (evidence on screen).
+3. People → verify contacts; readiness panel tells you exactly what's missing.
+4. Campaigns → enroll verified contacts → generate drafts.
+5. Letter Builder → edit/approve → provider draft.
+6. Inbox → explicit send → replies sync → referrals/opt-outs handled in place.
+7. Dashboard → watch coverage %, gaps, and stale counts; refresh jobs keep data honest.
diff --git a/docs/CRE_PR_COMPLIANCE.md b/docs/CRE_PR_COMPLIANCE.md
new file mode 100644
index 00000000..f22cd400
--- /dev/null
+++ b/docs/CRE_PR_COMPLIANCE.md
@@ -0,0 +1,55 @@
+# CRE PR Intelligence — Compliance & Data Safety
+
+## Scope of collection
+Public **business** information only: organization facts, public work contacts, public
+titles, press/media contacts, publicly indexed LinkedIn URLs. Never collected: private
+phone numbers, home addresses, birthdays, family details, or any unrelated personal
+data. The schema has no columns for such data.
+
+## Access control & audit
+- Every route (API + shells) is behind the app's admin role (`adminOnly`), which itself
+ sits behind the site's global Basic-Auth gate (double-gated by nginx in prod).
+- `pr_audit_log` records every send, edit, approval, status change, merge, import,
+ suppression, settings change, and Arizona-gate action with actor + before/after.
+- `pr_activities` gives per-record operational history shown in the UI.
+
+## Provenance & honesty
+- Every important fact carries `pr_field_evidence` → `pr_sources` (URL, limited excerpt
+ ≤600 chars, retrieval date, terms note, content hash). Full copyrighted pages are
+ never stored.
+- `verification_status` enums are structural: an **inferred** email may be stored but
+ can never be marked outreach-eligible (service-level guard + test); a search-indexed
+ LinkedIn title is labeled `found_uncorroborated` until corroborated.
+- Confidence and priority are separate, always-visible scores with exposed components.
+
+## Outreach compliance (CAN-SPAM aligned)
+- Sender identification: `sender_name/title/contact_information/sender_postal_address`
+ settings are required before any letter can send (unfilled merge fields visibly block).
+- Unsubscribe handling: the `unsubscribe_text` setting renders in letters; any reply
+ containing opt-out language immediately writes `pr_suppression` (permanent) and flips
+ the thread's status. Permanent opt-outs cannot be deleted from the UI.
+- Suppression is enforced at draft-generation, campaign-enrollment, provider-draft, and
+ send time (re-checked at each step).
+- No purchased lists; contacts originate from public/primary sources or the user's own
+ imports with recorded usage notes.
+- No open-tracking pixels (none exist in the renderer; none may be added — no approved
+ system uses them).
+
+## California privacy (CCPA/CPRA posture)
+B2B public business contact data; still, the workspace supports the request workflow:
+- **Access/correction:** People page shows every stored field + original extracted
+ values; corrections are audited.
+- **Deletion:** suppress + archive the person (kept as a suppression tombstone so they
+ are never re-contacted); evidence rows can be deleted with the record if demanded.
+- **Retention:** `retention_days_sources` / `retention_days_messages` settings (0 = keep)
+ are honored by the refresh job sweep and documented for operators.
+
+## Source-terms & rate behavior
+- robots.txt honored per host (cached 1h); per-provider minimum-interval throttles;
+ declared User-Agent with contact; SEC ≤10 req/s policy respected; FDIC/NCUA are
+ public-domain federal data; state-licensing datasets enter only via CSV with per-file
+ terms notes; LinkedIn is never scraped (see CRE_PR_DATA_SOURCES.md).
+
+## Secrets
+All credentials are environment variables (`.env.example` documents placeholders).
+Nothing in source control; the repo's `.gitignore` already excludes `.env*`.
diff --git a/docs/CRE_PR_DATA_SOURCES.md b/docs/CRE_PR_DATA_SOURCES.md
new file mode 100644
index 00000000..6f8decf5
--- /dev/null
+++ b/docs/CRE_PR_DATA_SOURCES.md
@@ -0,0 +1,69 @@
+# CRE PR Intelligence — Data Sources
+
+Source hierarchy (highest trust first), what is implemented, and the exact terms notes
+recorded per source. Every stored fact carries a `pr_sources` row (URL, limited excerpt,
+retrieval date, usage note, primary/secondary flag) — full page copies are never stored.
+
+## 1. Organization websites (`website` adapter — PRIMARY)
+Newsroom / press / media-contact / team / leadership / contact pages, press-release
+contact blocks, author bios. Live-fetched with a declared User-Agent, robots.txt honored
+(cached per host, 1h), throttled ≥2s per host, ≤5 pages per profile pass.
+**Terms note stored:** "Public organization website; facts extracted, limited excerpts stored."
+
+## 2. Official government / regulatory datasets (`registry`, `edgar` — PRIMARY)
+- **FDIC BankFind Suite API** (banks): open federal API, key-free.
+ Endpoint (verified 2026-07-30): `https://api.fdic.gov/banks/institutions`
+ (the old `banks.data.fdic.gov` host now 301s here). Returns 116 active CA banks with
+ name, city, county, website. Public-domain reuse.
+- **SEC EDGAR** (`edgar` adapter): public-domain; requires a declared User-Agent
+ (`PR_EDGAR_UA`, verified — anonymous requests 403) and ≤10 req/s per the SEC
+ fair-access policy. `company_tickers.json` + `data.sec.gov/submissions/CIK*.json`.
+- **NCUA credit unions:** the mapping API endpoint we probed returns the SPA HTML, not
+ JSON — the adapter reports this and the supported path is the **NCUA directory CSV
+ download → CSV import** (ncua.gov → Analysis → Credit Union & Corporate Call Report
+ Data). Status: awaiting the CSV (or a corrected endpoint).
+- **California licensing** (DRE brokers, DFPI lenders/escrow, CDI title insurers) and
+ **Arizona licensing** (ADRE, DIFI): these publish lookup tools and periodic data files
+ whose export/reuse terms vary per dataset. Policy: **verify the specific dataset's
+ terms, download manually, load via CSV import with the terms recorded in the usage
+ note.** No scraping of licensing portals.
+
+## 3. Association / chapter pages (`website` adapter on association orgs)
+Leadership, sponsor, committee pages of AIR CRE, NAIOP, ULI, CREW, BOMA, CBPA, etc.
+Treated as org-website sources on those organizations' records.
+
+## 4. Public press releases (`rss` adapter — PRIMARY for spokesperson facts)
+Organization newsroom RSS/Atom feeds; headlines + ≤300-char summaries stored.
+
+## 5. Public news articles naming spokespeople (secondary)
+Stored as `news_article` sources with excerpt + publisher; used for corroboration.
+
+## 6. Authorized search APIs (`search` adapter — secondary, low confidence)
+Exa (`PR_EXA_API_KEY`) or Brave (`PR_BRAVE_API_KEY`). Used for the query matrix and for
+locating **publicly indexed LinkedIn URLs**. Result snippets are stored at confidence
+~40 and are **never** promoted to verified titles without corroboration from source
+classes 1–5. We never scrape search-result pages.
+
+## 7. User-provided exports / licensed data (`import` paths)
+CSV/JSON/LinkedIn-URL/contact-list imports with column mapping, preview, validation,
+dry-run, duplicate detection, and a reversible batch id. The uploader's asserted right
+to use the data is recorded in the batch's usage note.
+
+## LinkedIn rules (hard)
+Permitted: authorized-search locate of public URLs; manual entry; user-supplied exports.
+Stored: URL + indexed title + indexed snippet + retrieval date, labeled
+`found_uncorroborated` until a class-1/2/4 source confirms the role
+(`found_corroborated`) or an admin verifies (`manually_verified`).
+Never: logged-in scraping, browser automation, cookie reuse, rate-limit evasion,
+connection automation. CoStar/LoopNet/Reonomy/ZoomInfo/Apollo and similar restricted
+databases are **not** used (no licensed integration is configured).
+
+## Adding another source adapter
+1. Create `src/pr/adapters/<name>.js` exporting `{name, kind, configured, missingConfig,
+ sourceMeta, …methods}` and `register(...)` it (see `adapters/index.js`).
+2. Add the require to the bottom of `adapters/index.js`.
+3. Every method returns the standard AdapterReport (provider, query, timestamp,
+ result_count, rate-limit state, cursor, errors, terms note, primary/secondary,
+ confidence recommendation).
+4. Verify the source's terms BEFORE coding the fetch; record them in `sourceMeta()`.
+5. Wire it into a job in `src/pr/jobs/index.js` if it should run in the pipeline.
diff --git a/docs/CRE_PR_EMAIL_SETUP.md b/docs/CRE_PR_EMAIL_SETUP.md
new file mode 100644
index 00000000..a537cffd
--- /dev/null
+++ b/docs/CRE_PR_EMAIL_SETUP.md
@@ -0,0 +1,44 @@
+# CRE PR Intelligence — Email Provider Setup (Gmail)
+
+The email layer is an abstract provider interface (`src/pr/services/email-providers.js`)
+with two implementations: **Gmail** (REST + OAuth2 refresh token) and **Mock** (active
+automatically whenever Gmail is unconfigured — drafts are stored in memory, nothing can
+send). Another provider (O365, SMTP/IMAP) can be added by implementing the same five
+methods: `createDraft`, `send`, `listThreads`, `getThread`, `configured`.
+
+## Behavior guarantees (enforced in code, not convention)
+- Outreach drafts become **provider drafts** — never sent automatically.
+- `send` requires `confirm:true` from an explicit admin click, re-checks the
+ suppression list at send time, and is audit-logged.
+- Inbound sync associates replies by provider thread id → headers → recipient.
+- Originals are preserved verbatim (`body_preserved`, `raw_headers`).
+- Bounce/opt-out language in replies auto-flags status and writes `pr_suppression`.
+- No tracking pixels — none exist anywhere in the letter renderer.
+
+## Gmail configuration (env only — never in source control)
+1. Google Cloud Console → create/select a project → enable the **Gmail API**.
+2. OAuth consent screen: internal (Workspace) or external+test-user with the sending
+ account.
+3. Credentials → **OAuth client ID** (Desktop app) → note client id + secret.
+4. Mint a refresh token for the sending mailbox with scopes:
+ `https://www.googleapis.com/auth/gmail.compose https://www.googleapis.com/auth/gmail.readonly`
+ (gmail.compose covers draft creation + sending drafts; readonly covers thread sync).
+ The standard OAuth playground or a one-time local consent flow both work.
+5. Set the environment (see `.env.example`):
+ ```
+ PR_GMAIL_CLIENT_ID=…
+ PR_GMAIL_CLIENT_SECRET=…
+ PR_GMAIL_REFRESH_TOKEN=…
+ PR_GMAIL_USER=press@yourdomain.com
+ ```
+6. Restart the app + worker. Settings → Email provider should show **Gmail connected**.
+
+## Verifying without sending
+Create a draft from the Letter Builder → *Provider draft*. Check the Gmail account's
+Drafts folder: the message sits there un-sent. The Send button in the Inbox is the only
+path that transmits, and only with the confirm dialog.
+
+## Token expiry note
+Google "Testing"-mode OAuth apps expire refresh tokens after ~7 days — publish the app
+(or keep it internal to a Workspace) for a durable token. If sync starts failing with
+`invalid_grant`, re-mint the refresh token.
diff --git a/docs/CRE_PR_RESEARCH_OPERATIONS.md b/docs/CRE_PR_RESEARCH_OPERATIONS.md
new file mode 100644
index 00000000..3d699ad7
--- /dev/null
+++ b/docs/CRE_PR_RESEARCH_OPERATIONS.md
@@ -0,0 +1,69 @@
+# CRE PR Intelligence — Research Operations Runbook
+
+## Start the system
+```sh
+createdb rentv_pr # once
+npm run pr:migrate # applies src/pr/migrations/*.sql idempotently
+npm start # the rentv site (PR workspace mounts at /admin/pr-intelligence)
+npm run pr:worker # the research worker (separate process; safe to Ctrl-C)
+npm run pr:seed:ca # optional: CA Phase-1 seed (curated candidates + FDIC discovery)
+```
+Local preview without auth: `OPEN=1 PORT=9705 node server.js` (never set OPEN in prod).
+
+## Start a California research run
+Admin → **Research** → pick the metro (the mandated order is listed) → *Queue discovery
+run*. Or POST `/api/pr/research/start {state:"CA", metro:"la", category:"bank"}`.
+The run executes on the worker: registry pass (FDIC) first, then the query matrix
+through the search API (needs `PR_EXA_API_KEY` or `PR_BRAVE_API_KEY`; without a key the
+run records "search API unconfigured" and completes the registry portion only).
+
+## Pause / resume
+**Runs & Jobs** page → ⏸ Pause on a run pauses it AND its queued jobs; ▶ Resume re-queues
+them. Every run keeps a `checkpoint` (registry cursor + matrix index) and resumes from
+it — kill the worker mid-run and restart to verify (a stale `running` claim re-queues
+after 15 min automatically).
+
+## Review an organization
+**Review Queue** → left list (↑/↓ to navigate) → center shows the profile, evidence
+excerpts with clickable source links, and duplicate candidates → **A**ccept / **R**eject
+/ **D**efer / **S**tale, or Edit (JSON patch) / Merge. Accepting stamps
+`manually_verified` + `last_verified_at`. The right column previews the grounded letter
+and saves internal research notes.
+
+## How LinkedIn URLs are found and verified
+`verify-person` jobs call the authorized search API with
+`site:linkedin.com/in "<name>" "<company>"`, store the URL + indexed title/snippet as
+`found_uncorroborated`, and only flip to `found_corroborated` when the person's org+title
+already have company-site / press / registry / manual evidence. Manual entry and CSV
+import of user-supplied LinkedIn exports are always available (People page / Research →
+Import). Indexed titles are displayed with an explicit "needs corroboration" tag.
+
+## Refresh cadence
+`refresh-stale-records` (queue it from Runs & Jobs, or cron the worker's queue) marks
+verified contacts stale after 90 days (priority ≥60) / 180 days (others), preserves the
+old title in `role_history`, and queues re-verification.
+
+## Saturation & the coverage report
+Queue `generate-coverage-report {state:"CA"}` — the dashboard then shows every empty
+metro×category cell. Treat a metro/category group as provisionally saturated when three
+consecutive complete runs create no meaningful new unique organizations and the matrix
+is exhausted; the run counters (`records_created` per run) make this visible on the
+Runs page.
+
+## Unlock Arizona
+Settings → **Run gate check now** (the seven CA-gate criteria are computed live from the
+data, never hand-set) → when it passes, **Unlock Arizona**. An explicit admin override
+exists and is audit-logged as `gate.arizona_override_unlock`. Until unlocked, any AZ
+discovery enqueue fails with `AZ_LOCKED` (HTTP 423).
+
+## Export researched data
+Organizations / People pages → ⬇ Export CSV honors the current filters; Sources page
+exports all field evidence; Settings exports suppression; Campaigns export outreach
+history. Every contact export includes verification status, source URLs, and
+last-verified so unverified data cannot masquerade as verified.
+
+## Process referrals / suppress a contact
+Inbox → open thread → **Add referred contact** (creates the person with the reply as its
+source + a follow-up task) · **Opt out / suppress** adds to `pr_suppression`
+immediately and un-eligibles the person everywhere (enrollment, drafts, send all
+re-check it).
diff --git a/test/pr/browser/workspace.spec.js b/test/pr/browser/workspace.spec.js
new file mode 100644
index 00000000..83a4b12d
--- /dev/null
+++ b/test/pr/browser/workspace.spec.js
@@ -0,0 +1,79 @@
+'use strict';
+// Playwright browser tests for the CRE PR Intelligence workspace.
+//
+// Prereqs (dev-only, not part of `npm test` — Playwright is not a runtime dependency):
+// npm i -D @playwright/test && npx playwright install chromium
+// OPEN=1 PORT=9705 node server.js (terminal 1 — OPEN=1 grants admin locally)
+// npx playwright test test/pr/browser/ (terminal 2)
+// Auth check runs against a second server WITHOUT OPEN=1 using real Basic Auth.
+const { test, expect } = require('@playwright/test');
+
+const BASE = process.env.PR_TEST_BASE || 'http://localhost:9705';
+
+test.describe('admin authorization', () => {
+ test('workspace 401s without credentials when auth is on', async ({ request }) => {
+ // Only meaningful when the server does NOT run with OPEN=1 — skip otherwise.
+ const r = await request.get(BASE + '/admin/pr-intelligence', { failOnStatusCode: false });
+ if (r.status() === 200) test.skip(true, 'server running with OPEN=1 — auth bypassed for local preview');
+ expect([401, 403]).toContain(r.status());
+ });
+});
+
+test.describe('workspace pages', () => {
+ test('dashboard renders KPIs and the Arizona lock state', async ({ page }) => {
+ await page.goto(BASE + '/admin/pr-intelligence');
+ await expect(page.locator('.pr-top h1')).toContainText('CRE PR Intelligence');
+ await expect(page.locator('.kpis .kpi').first()).toBeVisible();
+ await expect(page.getByText(/Arizona phase/)).toBeVisible();
+ });
+
+ test('organizations: filters drive the URL and the table', async ({ page }) => {
+ await page.goto(BASE + '/admin/pr-intelligence/organizations');
+ await expect(page.locator('table.grid')).toBeVisible();
+ await page.locator('.filters select').nth(2).selectOption({ index: 1 }); // category filter
+ await expect(page).toHaveURL(/type=/);
+ await expect(page.locator('table.grid tbody tr').first()).toBeVisible();
+ });
+
+ test('organization drawer shows source evidence with clickable links', async ({ page }) => {
+ await page.goto(BASE + '/admin/pr-intelligence/organizations');
+ await page.locator('table.grid tbody tr').first().click();
+ await expect(page.locator('.drawer.open')).toBeVisible();
+ await expect(page.locator('.drawer').getByText('Source evidence')).toBeVisible();
+ });
+
+ test('review queue: three-column layout with keyboard navigation', async ({ page }) => {
+ await page.goto(BASE + '/admin/pr-intelligence/review');
+ await expect(page.locator('.review3')).toBeVisible();
+ await page.keyboard.press('ArrowDown'); // navigates without error
+ await expect(page.locator('#r-center')).toBeVisible();
+ });
+
+ test('letter builder: blocks render, lock toggles, preview shows missing merge fields', async ({ page }) => {
+ await page.goto(BASE + '/admin/pr-intelligence/letters');
+ await expect(page.locator('.blocks .blk').first()).toBeVisible();
+ await expect(page.getByText('Version history & compare')).toBeVisible();
+ });
+
+ test('inbox: provider banner + explicit-send affordance only on provider drafts', async ({ page }) => {
+ await page.goto(BASE + '/admin/pr-intelligence/inbox');
+ await expect(page.getByText('Email provider:')).toBeVisible();
+ });
+
+ test('runs page exposes pause/resume controls', async ({ page }) => {
+ await page.goto(BASE + '/admin/pr-intelligence/runs');
+ await expect(page.getByText('Research runs')).toBeVisible();
+ await expect(page.getByText('Job queue')).toBeVisible();
+ });
+
+ test('settings: gate check runs and Arizona unlock is gated', async ({ page }) => {
+ await page.goto(BASE + '/admin/pr-intelligence/settings');
+ await page.getByText('Run gate check now').click();
+ await expect(page.locator('#gate-out .gatechk').first()).toBeVisible({ timeout: 10000 });
+ });
+
+ test('suppression warning: suppressed contact blocks enrollment', async ({ page }) => {
+ await page.goto(BASE + '/admin/pr-intelligence/settings');
+ await expect(page.getByText('Suppression list')).toBeVisible();
+ });
+});
← d0589dad pr-intelligence: CA seed (178 curated + FDIC registry), unit
·
back to Rentv
·
pr-intelligence: website adapter — stopword guard against na 3317fa1c →