-
Notifications
You must be signed in to change notification settings - Fork 1
Notifications
Get pinged when a new bill shows up. Off by default, exactly-once, and structurally unable to slow down or fail a scrape.
Only on a SCHEDULED check that discovers a bill newer than the last one notified. Manual
refreshes stay silent. Each new bill produces exactly one notification with the amount (current
charges), the service period, the statement date, and a dashboard link if APP_BASE_URL is set.
Pick one explicitly with NOTIFY_CHANNEL, or leave it unset and the app infers from whichever
block you filled in (precedence webhook > ntfy > smtp); off disables.
| Channel | Env |
|---|---|
| webhook |
NOTIFY_WEBHOOK_URL — HTTP POST a JSON body (no extra dependency). |
| ntfy |
NTFY_URL (default https://ntfy.sh), NTFY_TOPIC, optional NTFY_TOKEN. |
| smtp |
SMTP_HOST/SMTP_FROM/SMTP_TO required; SMTP_PORT/SMTP_SECURE/SMTP_USER/SMTP_PASS optional (465 ⇒ implicit TLS unless overridden). Via nodemailer. |
APP_BASE_URL is the public dashboard URL used to build the link.
The dedupe watermark is an AppSetting row, key lastNotifiedStatementDate (an ISO
YYYY-MM-DD). The app notifies for each bill strictly newer than the watermark, then advances
it to the newest notified date — exactly-once across restarts and across multiple new bills in
one scrape. On the first run after enabling (watermark unset), it seeds the watermark to
the current max statement date without notifying, so turning notifications on never replays
your whole bill history.
-
Pure helpers in
lib/notifyFormat.ts—formatBillNotification()(content),selectBillsToNotify()(watermark dedupe),resolveChannel()(env → channel). DB-/network-free so the unit suite runs without a Prisma client, and they ship hand-calculated tests. -
Impure dispatcher
notifyNewBills()reads env, sends, advances the watermark — and is wrapped intry/catchby its caller (run.ts). A misconfigured/unset channel is a no-op, and a send failure can never fail an otherwise-good scrape.
See Architecture for where this sits in the scrape orchestrator.