· XingAI Opportunity Radar
Empty Tables, Full Issues: Fixing the Opportunity Radar Newsletter Body
We finally wired Resend secrets into the Opportunity Radar GitHub Actions job and forced a send. The mail arrived. It also looked like a shell: date line, empty tables, a canned thesis box. The real issue — Executive Summary, Top Opportunity, Build This Week — never showed up.
That's a content ↔ renderer mismatch, not a Resend problem.
Two shapes of "issue"
The July 3 newsletter post and ADR-002 assumed a Chinese ranking table:
| 排名 | 产品机会 | 分类 | 优先级 |
What we actually publish under issues/ follows TEMPLATE.md: narrative sections in English (and the occasional bilingual note). Issue #2, When Agents Start Hiring Agents, has no ranking table at all.
The worker used regex on the table headers. No match → empty rows → "success" email with no substance.
What we changed
newsletter_worker.py now:
- Parses TEMPLATE section headings (Executive Summary → Sources).
- Renders those as the primary HTML/text body.
- Still attaches ranking/signal tables when they exist.
- Falls back to a full markdown dump only if both paths fail.
Ops side, same day:
- Actions secrets:
RESEND_API_KEY,NEWSLETTER_TO(reused the Invest AI Resend account). NEWSLETTER_FROM=onboarding@resend.devuntilradar@xingai.appis verified — same Resend 403 lesson as Invest OTP.
Documented as ADR-004.
Lessons worth stealing
Match the renderer to the template you actually ship. Spec sheets and TEMPLATE.md diverge fast if nobody re-reads both.
Fly secrets ≠ GitHub Action secrets. Invest already mailed via Resend; Radar cron still failed with empty env until we copied the key by hand.
Verified From addresses are product infrastructure. Unverified alerts@ / radar@ looks branded and dies with 403. Test domain first, brand later.
Not a July 14 issue
There still isn't a fresh issues/2026-07-14-… draft. The fixed pipeline re-sent the latest real issue (2026-06-28) with full body. Editorial cadence and weekday cron are still different clocks — weekday only fires when the issue date is new (or you pass --force).