Commit Graph
10 Commits
Author SHA1 Message Date
Matthew Meszaros 31dabea0a4 feat: rebuild the click-without-open fix on top of the per-link click attribution from #298: a person's click now also counts as an open and a burst that withdraws the click withdraws the open it implied unless a real open is on record, routing readers ignore machine opens as the docs promised, every open gets its own log row and every open and click records the mail client or proxy, browser, device, OS, country, region and city (migration 000124: origin columns on email_link_clicks plus an email_opens table), the tracking service publishes only the address's network in a nullable client_ip field which the consumer resolves with GeoLite and drops, the contact Activity tab shows each open and the origin of opens and clicks, the campaign overview gains a who-engaged-from-where breakdown exposed as engagement in campaign analytics, live open and click events carry occurred_at, client and location, the leads table explains why an open is not always counted, both logs are pruned daily after a year, email_opens joins the export registry, the consumer reads GEODB_PATH optionally, and the guides and API references are updated (fixes #294) 2026-09-03 03:49:44 -07:00
Matthew Meszaros 9f23c663c4 feat: give a cold mailbox a rotation lifecycle so a tired one can rest (#237)
* feat: give a cold mailbox a rotation lifecycle so a tired one can rest and come back, instead of running at full volume until a hard band trips: send_lifecycle is warming, active, resting or reserve and decides whether cold sender resolution offers the mailbox at all, which is a different axis from risk_band deciding which worker and IP host it, so a resting mailbox is still a clean-band mailbox that keeps its warmup traffic and its reputation; the hourly rebalancer rests a mailbox at throttled and worse but never at watch, since watch is defined as the band that changes nothing a customer can feel and leaving cold rotation is very much something they feel, and a rested mailbox returns only after three clean days so one good hour cannot bounce it back to full volume; reserve is the owner's hold and is never overridden, the default is active so no existing mailbox changes on deploy, and the state never travels in a workspace archive because it is this instance's decision about sending it watched

* feat: stop a query error re-admitting rested mailboxes, make probation measure healthy time, and rotate the candidate window so no mailbox starves: sendLifecycles returned a nil map on failure and an unresolved state reads as active, so one bad query quietly put every resting and reserved mailbox back into cold rotation, and the gate is now applied only when the states were actually read, with the skip logged rather than silent; ReadyToResume measured total time resting, so a mailbox that sat unhealthy for three days resumed on its first healthy tick having served no clean time, and an unhealthy evaluation now restarts the streak; and ordering candidates by send_lifecycle_since put every never-moved mailbox equal-first, so on an install with more than one page of them the same page was re-examined forever, which a checked-at stamp and its index fix
2026-08-28 12:08:52 -07:00
Matthew Meszaros b9cce2fbdc feat: ease a graduating mailbox into cold volume (#231)
* feat: ease a graduating mailbox into cold volume instead of handing it the full cap the day it joins a campaign: cold sending read warmup HEALTH but never whether the mailbox had actually warmed enough, so one at its 40/day warmup ceiling could send 50 cold the next morning, which is the post-warmup spike providers penalise; effectiveCap gains a min() term that starts the mailbox at 5, 10 or 20 a day by how long it warmed and adds 5 per clean day toward its own cap, freezing on a spam placement through the same union-of-freezes the warmup ramp uses, anchored by a new cold_ramp_started_at stamped idempotently on the first cold send and reset on org import because it raises a ceiling the destination never watched being earned; mailboxes that never warmed are not gated, since capping senders who never opted into warmup is a different decision from smoothing the transition out of it

* feat: anchor the graduation ramp on a CONFIRMED send, and make the hold the drawer reports the hold the scheduler applies: stamping cold_ramp_started_at at dispatch started the clock on a send the worker then failed, so a mailbox climbed on days it had not proven anything, and the stamp moves to the worker's EMAIL_SENT; separately the drawer computed its hold over every placement while ColdCeiling only counts placements after the first cold send, so a placement predating the ramp read as paused while the scheduler kept climbing, and both now go through one ColdHeldUntil

* fix: restore the Callout closing tag my conflict resolution dropped, which types:check and lint both pass and only pnpm build catches
2026-08-28 10:47:03 -07:00
Matthew Meszaros 7a8406e35a feat: close the warmup ramp loop with an early-signal hold (#227)
* feat: close the warmup ramp loop so an early spam placement holds the mailbox instead of letting it climb into the wall: every band in evaluateMetrics needs a sample floor before it can trip (20 warmup sends in 7 days, 100 delivered in 30), which a mailbox in its first fortnight never reaches, so one landing in junk on day three kept adding an email a day until it had sent enough to be judged; a placement inside 48h now cuts the day about a quarter and holds the ramp where it stood, and the resume subtracts the frozen days rather than catching them up because climbing three steps in one morning is the spike the hold exists to prevent; the ramp arithmetic moves into internal/app/warmupramp so the mailbox drawer reports the target the scheduler will actually act on instead of its own private copy, and says which signal cut it and when it resumes

* feat: fix three defects the review found in the warmup ramp hold: the ramp is now elapsed days minus frozen days rather than a level held at the newest placement, because MAX(created_at) meant a second placement arriving mid-hold RAISED the held level and a mailbox landing in junk repeatedly ramped up; the early-signal cut and the health band are resolved in one shared warmupramp.Resolve so the scheduler's health gate and the dashboard's ungated copy can no longer disagree about the same mailbox; and the hold is reported for the whole 72h freeze instead of only the 48h cut window, so a mailbox between the two no longer shows a ramp that has stopped climbing with nothing to explain it
2026-08-28 10:06:19 -07:00
Matthew Meszaros b465f30d42 fix: scope account analytics by org id not user id 2026-07-21 17:34:59 +02:00
Matthew Meszaros 88e241d536 fix: scope the analytics dashboard (overall stats, recent activity, top campaigns, daily trend, account health) by organization_id instead of user_id, so every workspace member sees the org's analytics rather than an empty dashboard 2026-06-11 17:49:39 +02:00
Matthew Meszaros a04d7450bc feat: guard warmup reconciliation by access
Make the warmup task reconciler re-check account state and org warmup access before scheduling replacement tasks. Remove pool membership for accounts whose org can no longer use warmup to avoid repeated no-access task churn.
2026-06-01 11:19:44 +02:00
Matthew Meszaros 878e8d921e feat: enforce api key email scopes 2026-05-30 04:28:25 +00:00
Máté Mészáros (Laptop) 41624a6f79 Analytics & Tracking 2026-01-29 05:59:04 +01:00
Máté Mészáros (Laptop) 5ac159d2f8 Realtime, api keys & more 2026-01-26 16:04:42 +01:00