diff --git a/docs/content/docs/guides/deliverability.mdx b/docs/content/docs/guides/deliverability.mdx index 58daf5139..fa6af803d 100644 --- a/docs/content/docs/guides/deliverability.mdx +++ b/docs/content/docs/guides/deliverability.mdx @@ -30,7 +30,7 @@ Use **Customize** to pick which sections show. Your choices and time window pers Placement figures only appear once seed data exists, and a small sample is a hint rather than a verdict. Warmbly waits for a meaningful sample before acting. -**Placement by provider is the check before scaling a campaign.** An aggregate rate can look fine while one provider quietly junks everything: Gmail at `95%` inbox with Outlook at `40%` is a provider-specific problem, usually authentication or content, not volume. +**Seed test placement by provider is the check before scaling a campaign.** An aggregate rate can look fine while one provider quietly junks everything: Gmail at `95%` inbox with Outlook at `40%` is a provider-specific problem, usually authentication or content, not volume. **Warmup placement is your continuous signal.** Seed tests are point-in-time probes, but every verified warmup delivery reports inbox, tab or spam, so drift appears here before it becomes complaints on a campaign. Click a mailbox in **Warmup inbox placement** to open its own history; see [seeing where warmup lands](/guides/warmup/#seeing-where-warmup-lands) for how the rate is counted. diff --git a/internal/repository/warmup_placement_live_test.go b/internal/repository/warmup_placement_live_test.go index 2cb47b657..bcfe0a390 100644 --- a/internal/repository/warmup_placement_live_test.go +++ b/internal/repository/warmup_placement_live_test.go @@ -2,6 +2,7 @@ package repository import ( "context" + "math/rand/v2" "testing" "time" @@ -121,9 +122,10 @@ func TestLiveWarmupPlacementRollup(t *testing.T) { t.Fatalf("rate = %+v, want 4 delivered, below the floor", r) } - // The sweep leaves receipts the live path counted alone, and a fresh one to it. - if n, err := repo.SweepUnplaced(ctx, time.Now().Add(time.Hour), 100); err != nil || n != 0 { - t.Fatalf("sweep over counted receipts = %d, %v; want 0", n, err) + // Every receipt the live path counted is marked, so the sweep never counts it again. + var unplaced int + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM warmup_received WHERE sender_account_id = $1 AND NOT placed`, sender).Scan(&unplaced); err != nil || unplaced != 0 { + t.Fatalf("receipts left unplaced after the live count = %d, %v; want 0", unplaced, err) } // The partner's organization sees none of it. @@ -198,7 +200,9 @@ func TestLiveWarmupPlacementSweep(t *testing.T) { t.Errorf("cleanup: %v", err) } }) - day := time.Date(2026, time.August, 3, 12, 0, 0, 0, time.UTC) + // The sweep claims the oldest receipts below its cutoff across the whole + // database, so the fixture sits decades back where nothing else lives. + day := time.Date(1975, time.January, 1, 12, 0, 0, 0, time.UTC).AddDate(0, 0, rand.IntN(3650)) for i, msg := range []string{"", "", "", ""} { exec(`INSERT INTO warmup_received (email_account_id, internal_id, message_id, sender_account_id, created_at) VALUES ($1, gen_random_uuid(), $2, $3, $4)`, recipient, msg, sender, day.Add(time.Duration(i)*time.Minute)) diff --git a/web/src/components/ui/scroll-strip.tsx b/web/src/components/ui/scroll-strip.tsx index 2d377b1ad..c63d28183 100644 --- a/web/src/components/ui/scroll-strip.tsx +++ b/web/src/components/ui/scroll-strip.tsx @@ -41,6 +41,10 @@ export default function ScrollStrip({ // A vertical wheel over an overflowing strip scrolls it sideways. const onWheel = (e: WheelEvent) => { if (el.scrollWidth <= el.clientWidth || Math.abs(e.deltaX) > Math.abs(e.deltaY)) return; + // At the end it cannot move toward, the wheel scrolls the page instead. + const atStart = el.scrollLeft <= 0 && e.deltaY < 0; + const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1 && e.deltaY > 0; + if (atStart || atEnd) return; e.preventDefault(); el.scrollLeft += e.deltaY; };