- Aug 28, 2026
- 4 min read
A Weekly ntfy Digest With Counts Only
My lead pipeline already pings my phone for every real enquiry. What it didn’t tell me was the shape of a week: how many people subscribed and actually confirmed, whether the welcome emails went out, whether the spam filter is catching anything, whether the homelab sync is healthy.
I didn’t want a dashboard to remember to open. I wanted one quiet notification on Monday morning. So the homelab job that mirrors my leads (a pull-only homelab mirror) now sends a weekly digest through the same self-hosted ntfy.
What it looks like
Leads weekly - 2026-W41
New leads: 9
fossengineer.com enquiry: 3
fossengineer.com newsletter: 1
jalcocertech.com enquiry: 1
jalcocertech.com newsletter: 1
multibodysystemsdynamics.com enquiry: 3
Confirmed subscriptions: 3 (fossengineer.com 1, jalcocertech.com 2)
Unsubscribes: 0
Drips sent: 1 (jt-welcome 1)
Do-not-send list: 0 (+0)
Spam filtered (last 30 days): 0
Pulls: 530 (6 failed)
Every line is a count. That’s the rule: no addresses, names or messages, ever. A notification ends up on a lock screen, in a phone backup, maybe on a smartwatch — it’s the wrong place for personal data. A test enforces it:
title, body = digest.build(conn, NOW)
assert "@" not in body, "counts only"
Where the numbers come from
Everything is a query against the SQLite copy the hub already keeps:
| Line | Source |
|---|---|
| New leads | leads rows received in the last 7 days, grouped by site and kind |
| Confirmed / Unsubscribes | confirmedAt / unsubscribedAt inside each lead’s stored JSON |
| Drips sent | the drip engine’s sends table, status sent (and failed if any) |
| Do-not-send list | suppressions: total, and how many appeared this week |
| Spam filtered | the number of spam:* keys seen in KV at the last pull |
| Pulls | the pulls log: runs this week and how many failed |
The spam line needed a small schema change. Blocklisted submissions live in
KV under their own spam: prefix for 30 days and are never mirrored as leads
(why they’re stored, not dropped),
so the pull now just counts them and records the number on its log row. A
new column on an existing database meant a migration — CREATE TABLE IF NOT EXISTS doesn’t add columns to a table that already exists:
columns = {row["name"] for row in conn.execute("PRAGMA table_info(pulls)")}
if "spam" not in columns:
conn.execute("ALTER TABLE pulls ADD COLUMN spam INTEGER")
Once a week, exactly once
The hub already runs a loop every 15 minutes (pull, daily backup, drips). The digest hooks into the same loop instead of adding a cron job:
def due(conn, now, weekday=0, hour_utc=7):
"""True once per ISO week, from `weekday` (0 = Monday) at `hour_utc` on."""
if (now.weekday(), now.hour) < (weekday, hour_utc):
return False
return conn.execute("SELECT 1 FROM digests WHERE week = ?", (week_id(now),)).fetchone() is None
week_id is the ISO week (2026-W41), and a tiny digests table keyed by it
records what was sent. Consequences I like:
- If the homelab is down on Monday morning, the digest goes out on the first loop after it’s back — still once.
- Sending manually (
leadshub digest --send) marks the week too, so I don’t get a second one on Monday. - Weekday, hour and an on/off switch are environment variables, defaulting to Monday 07:00 UTC.
The notification is sent with Priority: low and a chart tag. It’s a summary
to read with coffee, not something that should vibrate.
Two bugs the first run found
The first digest --send from the homelab printed a perfectly good digest
and then failed with 403. ntfy sits behind Cloudflare, and Cloudflare
rejects Python’s default Python-urllib user agent with error 1010 before
the request ever reaches ntfy. Which also meant the hub’s failure alerts had
never been delivered — the full story is in
Cloudflare error 1010 blocked my homelab alerts.
The same run claimed 24 new leads. KV held 9. The other 15 were test and
spam leads I had just deleted: the mirror keeps deleted keys for 30 days,
marked kv_gone_at, as a safety net. The digest counted them anyway. The fix
was one condition on each query:
SELECT site, kind, COUNT(*) AS n FROM leads
WHERE received_at >= ? AND kv_gone_at IS NULL
GROUP BY site, kind
and a test that deletes a lead from the fake KV, pulls, and expects
New leads: 1. After both fixes the digest matched the live inbox exactly.
Takeaways
- A weekly summary beats a dashboard you have to remember to open.
- Counts only — and a test that fails if an
@ever appears. - Hang it on a loop you already run, keyed by ISO week, so it’s sent exactly once even after downtime.
- Count only what still exists; safety copies of deleted data shouldn’t show up in your numbers.
- The first real send is a test of the whole alert path. Mine found two bugs.