feat: outreach follow-up scheduling, activity timeline, and pSEO noindex (migration 0025)

Feature A — Outreach follow-up + activity timeline:
- follow_up_at column on suppliers (migration 0025)
- HTMX date picker on outreach rows, POST /admin/outreach/<id>/follow-up
- Amber due-today banner on /admin/outreach with ?follow_up=due filter
- get_follow_up_due_count() for dashboard widget
- Activity timeline on /admin/suppliers/<id>: merges sent + received emails by contact_email

Feature B — pSEO article noindex:
- noindex column on articles (migration 0025)
- NOINDEX_THRESHOLDS per-template lambdas in content/__init__.py
- generate_articles() evaluates threshold and stores noindex=1 for thin-data articles
- <meta name="robots" content="noindex, follow"> in article_detail.html
- Sitemap excludes noindex articles (AND noindex = 0)
- pSEO dashboard noindex count card + article row badge

Tests: 49 new tests (29 outreach, 20 noindex), 1377 total, 0 failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-25 17:51:38 +01:00
16 changed files with 605 additions and 14 deletions

View File

@@ -15,6 +15,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **`analytics.execute_user_query()`** — new function returning `(columns, rows, error, elapsed_ms)` for admin query editor
- **`worker.run_extraction` task** — background handler shells out to `uv run extract` from repo root (2h timeout)
- 29 new tests covering all routes, data access helpers, security checks, and `execute_user_query()`
- **Outreach follow-up scheduling + activity timeline** — extends the outreach pipeline (migration 0024):
- **Migration 0025** — adds `follow_up_at TEXT DEFAULT NULL` to `suppliers` and `noindex INTEGER NOT NULL DEFAULT 0` to `articles`
- **Follow-up date picker** (`POST /admin/outreach/<id>/follow-up`) — HTMX date input on each outreach row; sets/clears `follow_up_at`; returns updated row via outerHTML swap
- **Follow-up due banner** on `/admin/outreach` — amber alert banner shows count of overdue follow-ups with "Show them" link (`?follow_up=due` filter)
- **`?follow_up=due` / `?follow_up=set` filters** in `get_outreach_suppliers()` — querystring params passed through dashboard and results partial
- **`get_follow_up_due_count()`** query function counts suppliers with `follow_up_at <= date('now')`
- **Activity timeline** on `/admin/suppliers/<id>` — merges sent outreach emails (`email_log WHERE email_type='outreach'`) and received emails (`inbound_emails`) matched by `contact_email`; sorted by date descending; max 50 entries; empty state shown when no history
- 29 new tests (follow-up CRUD, due count, due filter, timeline with sent+received, timeline empty state)
- **pSEO article noindex** — prevents thin-data articles from diluting crawl budget and index quality:
- **`NOINDEX_THRESHOLDS` dict** in `content/__init__.py` — per-template lambda: `city-pricing` (venue_count < 3), `city-cost-de` (data_confidence < 1.0), `country-overview` (total_venues < 5)
- **`generate_articles()` upsert** now evaluates the threshold and stores `noindex = 1` for articles that fail it; existing articles are updated on re-generation
- **`<meta name="robots" content="noindex, follow">`** injected in `article_detail.html` head block when `article.noindex` is truthy
- **Sitemap exclusion** — `sitemap.py` articles query adds `AND noindex = 0`; thin-data articles excluded from `sitemap.xml`
- **pSEO dashboard noindex card** — 4th summary card shows count of noindex articles (amber highlight when > 0)
- **Article row noindex badge** — amber pill badge on `partials/article_row.html` when `a.noindex`
- 20 new tests (threshold unit tests per template, sitemap exclusion, article detail robots meta tag)
- **Outreach pipeline** — cold B2B supplier outreach isolated from transactional emails:
- **Separate sending domain** (`hello.padelnomics.io`) — added `"outreach"` key to `EMAIL_ADDRESSES`; reputation isolated from `notifications.padelnomics.io` magic-link/lead-forward traffic (manual DNS step: add domain in Resend dashboard)
- **Migration 0024** — 4 new columns on `suppliers`: `outreach_status`, `outreach_notes`, `last_contacted_at`, `outreach_sequence_step`; `NULL` status = not in pipeline (no backfill needed for existing suppliers)