Bug: runJob computed since = sub.last_digest_at, fetched events with
received_at > since, sent the digest (slow), then deleted ALL events
with received_at > since. Any /notify event that arrived between the
fetch and the delete was also received_at > since, so it was deleted
without ever being sent in a digest.
Two changes:
1. Delete by exact event IDs (src/database.ts, src/worker/email.ts):
Added deleteEventsByIds(subscriptionId, eventIds) which deletes
only the events that were actually fetched + sent. The old
timestamp-based delete is retained but no longer called from runJob.
2. Re-fetch subscription on each cron tick (src/worker/email.ts):
createJob's closure captured the original sub, so sub.last_digest_at
stayed stale in memory. Every subsequent tick recomputed since from
the old value, re-fetching and re-sending duplicate events. Now each
tick re-fetches the subscription from the DB via getSubscriptionById
before calling runJob.
Fixes bead mailship-091
Switch the subscribe endpoint from POST to an idempotent PUT so clients can
always upsert without a stateful lookup first.
- Add GET /subscription/email?pubkey= lookup so clients can avoid re-POSTing.
- insertSubscription no longer clears confirmed_at unless the email address
changes; a frequency change keeps the existing confirmation.
- registerSubscription only sends a confirmation email when the subscription
is new, unconfirmed, or the email address changed.
- Update integration test and README for the PUT endpoint.
- test/integration.sh: runs full E2E flow against fresh server
- pnpm test and pnpm test:server scripts added
- Root endpoint handles missing web UI gracefully (returns JSON)
- database.ts reads DATA_DIR env var for test isolation
- .gitignore updated for test artifacts