← back to Re Flyer Aggregator
morning email digest: build-digest.mjs + email-digest.sh send NEW closed deals (news + SEC) to Steve via George; wired as final daily-cron step, sends only when deals exist (TK-10708)
01cc8e0d7d064bc022e5eeaeb0c542ff626e9f8a · 2026-08-19 13:47:25 -0700 · Steve Abrams
Files touched
A scripts/build-digest.mjsM scripts/dtt-cron.shA scripts/email-digest.sh
Diff
commit 01cc8e0d7d064bc022e5eeaeb0c542ff626e9f8a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 19 13:47:25 2026 -0700
morning email digest: build-digest.mjs + email-digest.sh send NEW closed deals (news + SEC) to Steve via George; wired as final daily-cron step, sends only when deals exist (TK-10708)
---
scripts/build-digest.mjs | 37 +++++++++++++++++++++++++++++++++++++
scripts/dtt-cron.sh | 4 +++-
scripts/email-digest.sh | 13 +++++++++++++
3 files changed, 53 insertions(+), 1 deletion(-)
diff --git a/scripts/build-digest.mjs b/scripts/build-digest.mjs
new file mode 100644
index 0000000..cbeb008
--- /dev/null
+++ b/scripts/build-digest.mjs
@@ -0,0 +1,37 @@
+#!/usr/bin/env node
+// TK-10708 Build the morning "closed deals" email body from today's alert feeds.
+// Prints HTML to stdout (empty output = no new deals today -> caller skips the send).
+import { readFileSync, existsSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const date = process.argv[2] || new Date().toISOString().slice(0, 10);
+const read = f => { const p = join(ROOT, 'out', f); return existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : []; };
+
+const news = read(`crenews-new-deals-${date}.json`);
+const edgar = read(`edgar-new-deals-${date}.json`);
+if (!news.length && !edgar.length) { process.stdout.write(''); process.exit(0); }
+
+const esc = s => String(s || '').replace(/[<>&]/g, c => ({ '<': '<', '>': '>', '&': '&' }[c]));
+const row = (price, where, title, src, link) =>
+ `<tr><td style="padding:6px 10px;font-weight:700;color:#0E7C4A;white-space:nowrap">${esc(price)}</td>
+ <td style="padding:6px 10px;color:#6B7280;white-space:nowrap">${esc(where)}</td>
+ <td style="padding:6px 10px"><a href="${esc(link)}" style="color:#16357A;text-decoration:none">${esc(title)}</a>
+ <span style="color:#9aa;font-size:11px"> · ${esc(src)}</span></td></tr>`;
+
+const newsRows = news.map(d => row(d.label || '$?', d.location || d.feed_region, d.title, d.feed, d.link)).join('');
+const edgarRows = edgar.map(d => row(d.price_label || '$? (see filing)', d.location || d.region,
+ `${(d.filer || '').split('(')[0].trim()} — ${d.property_type || 'acquisition'}${d.kind === 'closed_deal' ? ' (Item 2.01 closed)' : ''}`,
+ 'SEC 8-K', d.filing_url)).join('');
+
+const html = `<div style="font-family:-apple-system,Helvetica,Arial,sans-serif;color:#222;max-width:720px">
+<h2 style="margin:0 0 2px;color:#0E2350">🏙️ Closed CRE deals — ${date}</h2>
+<p style="color:#6B7280;font-size:13px;margin:0 0 14px">${news.length} from CRE trade press · ${edgar.length} from SEC filings · West footprint · free/same-day</p>
+${news.length ? `<h3 style="color:#16357A;margin:14px 0 4px">Trade press (private + public)</h3>
+<table style="border-collapse:collapse;width:100%;font-size:14px">${newsRows}</table>` : ''}
+${edgar.length ? `<h3 style="color:#16357A;margin:18px 0 4px">SEC 8-K filings (public REITs)</h3>
+<table style="border-collapse:collapse;width:100%;font-size:14px">${edgarRows}</table>` : ''}
+<p style="color:#9aa;font-size:11px;margin-top:16px">re-flyer-aggregator · TK-10708 · deduped daily · $0 sources. Prices from headlines/filings — verify before publishing.</p>
+</div>`;
+process.stdout.write(html);
diff --git a/scripts/dtt-cron.sh b/scripts/dtt-cron.sh
index 66e6c5f..8569128 100755
--- a/scripts/dtt-cron.sh
+++ b/scripts/dtt-cron.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# TK-10708 Daily "be first to a closed deal" jobs (launchd com.steve.dtt-import).
+# TK-10708 Daily "be first to a closed deal" pipeline (launchd com.steve.dtt-import).
export PATH="/opt/homebrew/bin:/opt/homebrew/opt/postgresql@14/bin:/usr/bin:/bin:/usr/sbin:/sbin"
cd "$(dirname "$0")/.." || exit 1
LOG="data/dtt-cron.log"; mkdir -p data out
@@ -10,4 +10,6 @@ node scripts/cre-news-monitor.mjs >> "$LOG" 2>&1
node scripts/edgar-cre-monitor.mjs --days 14 >> "$LOG" 2>&1
# 3) DTT sales import (free assessor source; frozen until a fresh feed is added)
node scripts/ingest-la-sales.mjs >> "$LOG" 2>&1
+# 4) Email the morning's NEW closed deals to Steve via George (only if there are any)
+bash scripts/email-digest.sh >> "$LOG" 2>&1
echo "--- exit $? ---" >> "$LOG"
diff --git a/scripts/email-digest.sh b/scripts/email-digest.sh
new file mode 100755
index 0000000..53d663a
--- /dev/null
+++ b/scripts/email-digest.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+# TK-10708 Email the morning's NEW closed CRE deals to Steve via George.
+# Only sends when there are new deals (build-digest prints empty otherwise). $0.
+export PATH="/opt/homebrew/bin:/opt/homebrew/opt/postgresql@14/bin:/usr/bin:/bin:/usr/sbin:/sbin"
+cd "$(dirname "$0")/.." || exit 1
+DATE="$(date +%Y-%m-%d)"
+TO="${REFLYERS_DIGEST_TO:-steve@designerwallcoverings.com}"
+BODY="$(node scripts/build-digest.mjs "$DATE")"
+if [ -z "$BODY" ]; then echo "no new deals $DATE — no email sent"; exit 0; fi
+N=$(grep -oE '<a ' <<<"$BODY" | wc -l | tr -d ' ')
+. "$HOME/.claude/skills/_shared/george-send.sh"
+RESP="$(george_send steve-office "$TO" "🏙️ Closed CRE deals — $DATE ($N new)" "$BODY")"
+echo "$RESP" | grep -q '"success":true' && echo "digest emailed to $TO ($N deals)" || { echo "email FAILED: $RESP"; exit 1; }
← 854c054 be-first PRIMARY feed: free CRE-news closed-deal monitor (TR
·
back to Re Flyer Aggregator
·
digest: dedicated Source column (outlet name + link) + table 56eec3b →