Matthew Meszaros
|
dc1746aa2d
|
feat: fix the dashboard socket layer's channel bookkeeping so a topic held by two surfaces survives one of them unmounting and a workspace switch actually leaves the old organization's channel: leaveChannel deleted the whole channelsRef entry, and that entry owns the handler map, so every other subscribeToChannel registration on that topic died with it and the unsubscribe closures they held pointed at an orphaned Set, meaning the first component out silently deafened the second; it now refcounts holders in holdersRef (incremented before joinChannel's already-joined bail-out, so the second surface to ask for a live topic is actually counted) and only the last holder out leaves, resets the entry in place instead of dropping it while subscribers remain, discards it from the unsubscribe closure once nothing is listening and nobody holds the join, sends phx_leave only for a channel that was really joined or joining, and drops the pointless 'leaving' state flip that was set and nulled in the same tick and read by nothing; refcounting alone would have been actively harmful, because RealtimeManager's org effect joined with no cleanup and re-ran on every isConnected false->true cycle, so the count climbed on every reconnect and never returned to zero and switching workspaces would decrement without leaving and keep feeding the client the previous organization's events, so that effect is now balanced with a cleanup matching the user-channel effect above it and prevOrgIdRef is gone, which also fixes a latent defect of its own where switching organizations while the socket was down left the old topic in desiredTopicsRef and rejoinChannels brought the abandoned workspace back on reconnect; covered by 11 new tests over the fake-socket rig, including a real RealtimeManager mounted against the real provider, and each combination of half-fixes fails the matrix
|
2026-08-27 02:53:33 -07:00 |
|
Matthew Meszaros
|
ec0a6f3b2f
|
Merge pull request #208 from warmbly/fix/195-warmup-health-evaluation-silent-failure
fix: warmup health evaluation has never run — pgx's ErrNoRows is not sql.ErrNoRows, and UpdateParticipantHealth was never preparable (#195)
|
2026-08-27 01:24:55 -07:00 |
|
Matthew Meszaros
|
0bbfa4b2ac
|
feat: make warmup health evaluation actually run, which it never has: GetParticipantHealth compared pgx's not-found error with == sql.ErrNoRows but pgx.ErrNoRows is a proxy that wraps it rather than being it, so "this account is not in this pool" surfaced as a hard error and the premium-first probe failed before ever reaching a free-pool account's own row; UpdateParticipantHealth used a bare $1 as both a health_state assignment and an equality test, so Postgres deduced character varying from one and text from the other and refused the statement with 42P08, meaning no health state was ever persisted for anyone in any pool; both are now fixed along with the other 18 == sql.ErrNoRows comparisons in the repository layer and the same unpreparable-parameter defect in the contacts bulk custom-field writes, the worker install-state update and the referral earnings upsert, all found by a new live test that asks Postgres to PREPARE every parameterised statement in the package; evaluation failures now log their real cause instead of collapsing into errx.InternalError, the hourly sweep reports how many participants it could not evaluate instead of skipping them in silence, ApplyInvalidTokenAttempt no longer reports failure once the attempt is persisted so the caller cannot double-count it, and migration 000096 adds a per-participant signal floor so the first working evaluation cannot block a mailbox on history gathered while nothing was watching
|
2026-08-27 01:04:50 -07:00 |
|
Matthew Meszaros
|
dd1119e302
|
Merge pull request #204 from joaoppa/fix/195-invalid-token-double-count
fix: stop double-counting invalid warmup token attempts, and log the health evaluation failure that was being discarded (#195)
|
2026-08-27 00:46:59 -07:00 |
|
Matthew Meszaros
|
c6448fecbe
|
Merge branch 'main' into fix/195-invalid-token-double-count
|
2026-08-27 00:41:23 -07:00 |
|
Matthew Meszaros
|
08a91b39d0
|
Merge pull request #206 from warmbly/fix/realtime-join-throttle-and-channel-refcount
fix: throttle channel joins on the realtime service, and make both clients handle a refused join without storming it
|
2026-08-27 00:16:39 -07:00 |
|
Matthew Meszaros
|
42ddb78e60
|
feat: make the realtime service survive a client that spams channel joins, and stop both clients from being that client: ws_join is now spent per phx_join in a shared RealtimeWeb.ChannelGuard that every channel runs BEFORE its Auth lookup (it was only ever spent on the socket handshake, so an established socket could issue unlimited joins and each one cost a Postgres query), handshakes move to their own ws_connect bucket so a reconnect storm cannot eat the budget a client needs to rejoin with, the retry_after_ms hint now points at the next window instead of shrinking as overage grew, every channel's join rejection carries the numeric code the API reference already promised (plus 4005 for a malformed topic), BulkChannel only pushes an event to the user who owns the operation instead of to anyone who guesses the id, and the web and admin clients treat a rate_limited join as transient (wait out the hint, keep the socket) while backing off per-channel rejoins on a decaying schedule rather than reconnecting on the 120ms floor forever; adds an ExUnit suite for realtime and a vitest suite for admin, both wired into CI
|
2026-08-26 21:14:34 -07:00 |
|
Matthew Meszaros
|
49e1696011
|
Merge pull request #205 from joaoppa/fix/socket-channel-rejoin-loop
fix: stop the realtime socket rejoining a channel on every render (~400 joins/second from one open campaign page)
|
2026-08-26 20:51:42 -07:00 |
|
joao-crm
|
f82e3c3402
|
feat: stop the realtime socket tearing down and rejoining a channel on every render, which made one open campaign page issue about 400 channel joins per second and saturate the realtime service: the provider's context value is now memoised instead of an inline object literal, and useChannel/useChannelEvent depend on the stable joinChannel/leaveChannel/subscribeToChannel callbacks and on params BY VALUE rather than on the whole context, so channelStates re-rendering the provider no longer re-runs the effect that owns the subscription
|
2026-08-27 03:05:15 +00:00 |
|
joao-crm
|
9130498ade
|
feat: stop double-counting every invalid warmup token attempt by treating a failed health evaluation as non-fatal inside ApplyInvalidTokenAttempt, since the attempt and its spam score are already persisted when it runs and the caller's degraded path recorded both a second time, and log the evaluation failure that was being discarded so the band that never fired can finally be diagnosed
|
2026-08-26 23:05:52 +00:00 |
|
Matthew Meszaros
|
28fbde314d
|
Merge pull request #203 from warmbly/fix/issue-200-investigation
fix: one undeliverable address no longer wedges a whole campaign, and the pre-send verifier stops inventing the verdict that caused it
|
2026-08-26 06:10:04 -07:00 |
|
Matthew Meszaros
|
0a8c3314d0
|
feat: stop one undeliverable address from wedging a whole campaign, and stop the pre-send verifier from inventing the verdict that caused it: campaign routing in FindNextRoutedPair now excludes contacts whose address verification refused them (invalid, or risky with the campaign's send-to-risky toggle off) because the pre-send gates in campaign_task.go skip those without recording progress, so the finder re-served the same lead every tick and the campaign never reached the healthy leads behind it; the SMTP prober no longer reads every 5xx reply to RCPT TO as a dead mailbox, since Postfix defers HELO/sender/policy rejections to RCPT time and the old default greeting of localhost made them arrive as 504 5.5.2 Helo command rejected against the recipient, so a 5xx now becomes invalid only when the reply names the recipient and degrades to unknown otherwise; the HELO name falls back to the APP_URL host and the probe is skipped outright without a public FQDN; migration 000095 resets the verdicts the corrected classifier would no longer reach so poisoned contacts are re-verified; and skipped leads now report as undeliverable in the Leads view and in the campaign completion log instead of sitting at Queued forever
|
2026-08-26 06:03:20 -07:00 |
|
Matthew Meszaros
|
a9ff56d2c5
|
Merge pull request #202 from warmbly/chore/remove-stray-repo-files
chore: remove stray, unreferenced files from the repository
|
2026-08-26 05:02:22 -07:00 |
|
Matthew Meszaros
|
8953b2a132
|
chore: delete repository junk that was never referenced by anything: the paseo worktree-tool config, the root .astro type output an astro run from the repo root left behind, the empty schema.sql, the stray root package.json plus pnpm-lock.yaml from an accidental pnpm add motion (every frontend tree owns its own manifest and lockfile, and CI only ever reads those), the zero-byte cmd/consumer/envsample, the empty web CampaignSearchProvider.tsx, six Go files holding nothing but a package clause, and models.WMailAdd which had no callers; root .gitignore now covers each of them so they cannot drift back in
|
2026-08-26 04:55:45 -07:00 |
|
Matthew Meszaros
|
51fec3dd44
|
Merge pull request #201 from warmbly/placid-ridge
fix: verify warmup mail whose verify header Graph stripped in transit
|
2026-08-26 04:40:43 -07:00 |
|
Matthew Meszaros
|
b77664cd09
|
feat: verify warmup mail that arrives without its verify header, because Microsoft Graph strips custom headers in transit and re-stamps the Message-ID, so every warmup email sent from an Outlook or Microsoft 365 mailbox reached its recipient unmarked, consumed no token, earned no engagement and was filed in the recipient's unibox as ordinary mail; the Graph client now creates the message as a draft and sends that so it can read the internetMessageId Exchange assigned, the consumer records that delivered id on the task and its warmup token alongside the sent subject, and the recipient resolves an unmarked warmup email by delivered Message-ID or by the pending sender/recipient/subject pair
|
2026-08-26 04:31:19 -07:00 |
|
Matthew Meszaros
|
6b00ab23cb
|
Merge pull request #199 from warmbly/fix/graph-spam-rescue-guard
fix: Graph rescues a message from Junk that was never in Junk, undoing the Warmbly foldering and re-ingesting the message
|
2026-08-25 20:40:07 -07:00 |
|
Matthew Meszaros
|
f1856749e4
|
feat: only rescue a Graph message from Junk when it is actually in Junk, because engagementPlan folders into the untracked Warmbly folder first and the unconditional move undid that foldering and dropped the message back into the tracked Inbox under a new id, where live sync re-ingested it as new mail and burned its already-consumed warmup token as an invalid-token attempt
|
2026-08-25 20:14:22 -07:00 |
|
Matthew Meszaros
|
4c9d0b1c87
|
Merge pull request #198 from joaoppa/fix/186-warmup-selfmove-and-namespace
fix: stop banning a warmup recipient for the foldering we ordered, and create the Warmbly folder inside the server's IMAP namespace (#186)
|
2026-08-25 20:07:15 -07:00 |
|
Matthew Meszaros
|
c65d083545
|
Merge branch 'main' into fix/186-warmup-selfmove-and-namespace
|
2026-08-25 20:01:11 -07:00 |
|
Matthew Meszaros
|
35ca9967cf
|
Merge pull request #197 from joaoppa/fix/181-warmup-generation-schema
fix: make warmup AI generation work at all, and report the provider's real reason when a batch fails (#181)
|
2026-08-25 19:57:48 -07:00 |
|
joao-crm
|
8752b02217
|
feat: create and move into the Warmbly folder inside the server's personal IMAP namespace instead of the root, so Dovecot hosts that keep user folders under INBOX. stop failing every warmup foldering action with "nonexistent namespace" and silently losing that engagement signal
|
2026-08-25 20:47:15 +00:00 |
|
joao-crm
|
135e0c8c94
|
feat: stop banning a warmup recipient for the foldering the platform itself ordered, by marking each move_to_warmbly before it is published and excusing the single removal it produces, since Microsoft Graph reports a move out of a tracked folder exactly like a deletion and the tampering threshold of one blocked every Outlook mailbox on its first warmup email
|
2026-08-25 20:46:37 +00:00 |
|
joao-crm
|
ac40eca920
|
feat: read a fully failed batch's error file instead of reporting "empty output file id", so a generation job that had every request refused names the provider's actual reason in the admin panel rather than an internal guard message
|
2026-08-25 20:46:27 +00:00 |
|
joao-crm
|
93a2fa0962
|
feat: reflect the structured-output schema inline and anonymously so OpenAI stops rejecting every warmup generation request with "$ref cannot have keywords {'$id'}", which made AI warmup content impossible to generate on any install
|
2026-08-25 20:46:27 +00:00 |
|
Matthew Meszaros
|
c9f831c59a
|
Merge pull request #192 from warmbly/feature/issue-185-implementation
Add Delete and Duplicate campaign actions to the dashboard
|
2026-08-25 09:08:36 -07:00 |
|
Matthew Meszaros
|
6ebc925a93
|
feat: fix the campaigns guide so the docs site builds again: the new Duplicate and delete section wrote the derived copy name as "<name> (copy)", which MDX parses as an unclosed JSX tag and fails the Cloudflare Pages build; it is now inline code
|
2026-08-25 09:02:05 -07:00 |
|
Matthew Meszaros
|
d396f53fc4
|
feat: add Delete and Duplicate campaign actions to the dashboard (issue #185): every campaign row and the detail header get a ⋯ menu (Edit, Duplicate, Start/Pause, Delete) plus a Delete card at the bottom of Settings, all permission-gated with a confirm that spells out what goes; DELETE /campaigns/:id is now organization-scoped instead of user_id-scoped so teammates can delete, runs in one transaction that also deletes the campaign's pending tasks and cancels a wakeup tick claimed at that moment (campaign_tasks only nulls its link, so those rows kept firing), removes attachment objects and publishes CAMPAIGN_DELETED so a teammate's open detail page is sent back to the list; new POST /campaigns/:id/duplicate copies the campaign row as a draft with steps and their branch graph rewired onto new step ids, tags, folders, senders with rotation reset, A/B variants, advanced settings and attachments (quota-checked, blobs undone if the copy fails) and none of the leads, progress, logs, counters, ramp level, guardrail trip or past dates, naming it (copy)/(copy N) inside the 50 byte cap without splitting runes; a claimed campaign tick whose campaign vanished now ends the chain instead of staying active forever; covered by TestLiveCampaignLifecycle* against real SQL, RemapBranchTargets and duplicateName unit tests and a react-query vitest for the list cache, with API reference, endpoint map and campaigns guide updated
|
2026-08-25 08:54:51 -07:00 |
|
Matthew Meszaros
|
5da840ba72
|
Merge pull request #191 from warmbly/fix/issue-187
fix: adding a contact to a campaign actually creates the lead and shows it on the Leads tab
|
2026-08-25 08:04:24 -07:00 |
|
Matthew Meszaros
|
560776619c
|
feat: fix adding a contact to a campaign so the lead is actually created and shows up on the campaign Leads tab: contact writes matched campaign membership on campaigns.user_id, so a member adding a contact to a campaign a teammate created selected no rows and the API answered 200 having done nothing, and GetDetail rendered the contact 360 through the same filter so existing memberships read as empty; Update, Add and the GetDetail badge subselect now scope on organization_id, the membership diff runs as one statement per direction with UUID-validated ids, and the response always re-reads membership so an unrelated field edit no longer returns campaigns:null; on the dashboard the Leads tab is a ["contacts","list"] search scoped to one campaign, and useUpdateContactsBulk patched rows under a ["campaigns","list"] key that holds no contacts and invalidated nothing, so the cached lead-less result survived its 5 minute staleTime and only a search (a new query key) appeared to find the contact, so every contact mutation hook now invalidates ["contacts"] and ["campaigns"] from onSuccess, covered by TestLiveContact* against real SQL and a react-query test that fails on the old hooks
|
2026-08-25 07:55:15 -07:00 |
|
Matthew Meszaros
|
9f5b4499b1
|
Merge pull request #190 from warmbly/fix/issue-189-image-review-fixes
fix: campaign stuck at "Queued / Not started" with nothing sending (#189)
|
2026-08-25 07:45:08 -07:00 |
|
Matthew Meszaros
|
efa914025c
|
feat: stop an active campaign sitting at "Queued / Not started" with nothing sending: a campaign is one self-perpetuating task, so a tick that found nothing due parked its successor at the literal next-due moment (three days out for a "wait 3 days" step) and that parked task was also the next time anything re-read the campaign, so leads imported meanwhile stayed invisible until it fired and the reconciler never noticed because it only re-seeds chains with no pending task; deferral parks are now capped at config.CampaignMaxDeferMinutes via scheduler.DeferSlot at all three enqueue sites (a tick that actually sent still parks at its paced interval, so send spacing is untouched), the reconciler re-checks any active campaign parked beyond CampaignStaleParkHours and pulls its wakeup forward when the real next slot is CampaignReparkMarginMinutes sooner, attaching leads to a running campaign wakes it immediately through one CampaignWaker seam in the contact service that covers add/update/bulk-edit/import/Sheets-sync, even distribution now paces across the whole sender pool via poolRemainingOn instead of the one mailbox the tick picked (a three-mailbox campaign was sending at one mailbox's rate), the flat +/-20 minute jitter that was wider than the interval it perturbed is scaled to half the distance to the slot so it stops landing slots in the past where notBefore collapsed them onto the min-gap, and on the dashboard a full-day window renders "12am-midnight" instead of "12am-12pm", the campaign lead strip uses the server's campaign-wide lead_counts instead of counting the 50 loaded rows, and channel state moves out of a ref into React state so a live campaign's panel stops reading "Disconnected" forever
|
2026-08-25 07:39:18 -07:00 |
|
Matthew Meszaros
|
2e5ea3b3af
|
Merge pull request #183 from warmbly/fix/release-notes-formatting
fix: release notes render as one crammed paragraph with a duplicate What's Changed section
|
2026-08-24 20:33:54 -07:00 |
|
Matthew Meszaros
|
4589f32634
|
Merge remote-tracking branch 'origin/main' into fix/release-notes-formatting
|
2026-08-24 20:29:06 -07:00 |
|
Matthew Meszaros
|
07fe5e7f34
|
feat: fix the release notes so What's Changed renders as grouped sections instead of one crammed paragraph: the changelog step encoded newlines as %0A for the set-output command GitHub disabled in 2022, so all 50 commits collapsed into a single run-on line with literal %0A separators, and generate_release_notes:true alongside a hand-written body made GitHub append a second duplicate What's Changed section (44KB body on v0.2.1); a new .github/scripts/release-notes.sh now walks --first-parent so each PR is one entry, takes the PR title from the merge commit body, groups by conventional-commit type into Features/Fixes/Performance/Security/Documentation/Maintenance/Other, trims this repo's deliberately long subjects at a clause boundary, links the PR (or the commit for direct pushes), picks the previous tag via git tag --sort=-v:refname --merged so stable releases skip prereleases and still list what an rc covered, states truncation instead of silently cutting at 50, and the workflow passes the assembled body via body_path so no escaping layer remains
|
2026-08-24 20:23:41 -07:00 |
|
Matthew Meszaros
|
13d52cf76b
|
Merge pull request #182 from warmbly/fix/imap-live-lane-denial-stops-pass
Stop the IMAP live pass once a lane is denied, so unfreezing a mailbox cannot deactivate it
|
2026-08-24 20:20:01 -07:00 |
|
Matthew Meszaros
|
430a645858
|
feat: stop the IMAP incremental pass fetching further batches once a sync lane is denied, so a mailbox unfrozen by the LIST-STATUS release fix does not walk its whole invisible backlog into the flood detector and deactivate itself: imapIncremental now returns not-complete on the first batch that could not be fully stored (holding the folder's mod-sequence for the next tick instead of setting stats.aborted, which would also skip the backfill and every other folder), ReleaseMailbox takes Client.mu like every other selected-state command so it cannot interleave with a warmup MOVE/STORE, every SELECT is funnelled through Client.selectMailbox so UNSELECT is skipped when nothing is selected and a strict server never answers BAD, and SmtpImapData.ImapClient plus WMail.gov become the narrow ImapConn and syncBudget interfaces so a full IMAP pass can be driven against a fake and its fetch round trips counted in TestImapSyncStopsFetchingOnceTheLiveLaneIsDenied, TestImapSyncKeepsWhatFitBeforeTheDenial and TestImapSyncWalksEveryBatchWithinBudget
|
2026-08-24 20:16:03 -07:00 |
|
Matthew Meszaros
|
4fa20644c9
|
Merge pull request #180 from joaoppa/fix/165-imap-release-mailbox
fix: release the selected IMAP mailbox before the LIST-STATUS poll so live sync keeps detecting new mail on Dovecot
|
2026-08-24 20:06:53 -07:00 |
|
joao-crm
|
afdeb9622e
|
feat: release the selected IMAP mailbox before the LIST-STATUS poll so Dovecot servers stop reporting a frozen HIGHESTMODSEQ and live sync keeps detecting new mail after the first fetch
|
2026-08-24 23:43:51 +00:00 |
|
Matthew Meszaros
|
2f5fc52ec6
|
Merge pull request #177 from warmbly/fix/167-sender-org-scoping
fix: resolve campaign senders by organization, not by the campaign owner (#167)
v0.2.1
|
2026-08-24 09:24:54 -07:00 |
|
Matthew Meszaros
|
222c9d2554
|
feat: scope campaign sender resolution to the campaign's organization instead of its owner so a multi-org user can no longer send organization A's campaign from an organization B mailbox: GetByTags/GetAllActiveInScope/GetByCampaignSenders now take a repository.AccountScope keyed on organization_id where a scope with no organization resolves to no mailboxes rather than widening to the owner (tags carry no organization of their own, so one user's tag legitimately spans workspaces and the predicate is the only thing holding the boundary), the campaign scheduler and the preflight tracking-domain check build that scope from campaign.OrganizationID, unibox compose scores only the current workspace's mailboxes, the 'all' lanes of AccountHasActiveCampaign/CountActiveCampaignsForAccount join ea.organization_id = c.organization_id instead of ea.user_id = c.user_id, dead and broken PauseAllByUserID (which wrote the reason string into status and had no callers) is removed, the campaigns/unibox guides now state that senders resolve inside the campaign's workspace, and TestLiveOrglessCampaignDoesNotSendToSuppressedRecipient is updated because an orgless campaign now finds no senders before routing is consulted while the send gate it covers still refuses; live-tested in TestLiveSenderResolutionStaysInsideTheCampaignOrg, TestLiveSenderSchedulerNeverPicksAnotherOrgMailbox, TestLiveSenderSchedulerPicksTheCampaignOrgMailbox, TestLiveSenderScopeWithoutAnOrganizationReachesNothing and TestLiveActiveCampaignLookupIsOrgScoped
|
2026-08-24 09:20:41 -07:00 |
|
Matthew Meszaros
|
05c8334a11
|
Merge pull request #179 from warmbly/fix/issue-169-campaign-send-loop
fix: a campaign email can be sent twice when the progress write after dispatch is lost
|
2026-08-24 09:19:02 -07:00 |
|
Matthew Meszaros
|
15e139e15d
|
feat: stop a campaign email going out twice when the progress write after dispatch is lost: a step is now RESERVED before its SEND_EMAIL reaches the bus (migration 000093 adds campaign_contact_progress.dispatched_at + dispatch_task_id, and ReserveSend takes the claim and the day's counters in one transaction) and routing treats a step as attempted on sent_at OR dispatched_at, so a crash or a failed stamp in the dispatch window can no longer read as "never sent" and email the same person again; the ON CONFLICT claim is exactly-once so two ticks racing the same pair cannot both send (the loser ends skipped_duplicate), the stamp is retried and escalated to the campaign feed instead of warned and swallowed, HandleEmailSent repairs a lost stamp from the worker's own confirmation, ReleaseSend gives a reservation back only when the command provably never left (a publish failure is ambiguous via ErrSendDispatchUnknown and keeps it), and StartStuckSendReclaimer walks back a reservation nobody answered after 30 minutes so a worker that died mid-send cannot park a lead in flight forever; live-tested in TestLiveLostProgressWriteDoesNotResend, TestLiveDispatchedSendIsNeverOfferedTwice, TestLiveConcurrentTicksSendOnce, TestLiveStuckDispatchIsReclaimed, TestLiveReclaimBelievesADeliveredSend and TestLiveInFlightSendIsNotOfferedAgain
|
2026-08-24 09:15:06 -07:00 |
|
Matthew Meszaros
|
ce678bd8e6
|
Merge pull request #178 from warmbly/fix/issue-173-end-to-end
fix: make a custom tracking domain verifiable instead of permanently "Pending DNS" (#173)
|
2026-08-24 09:09:30 -07:00 |
|
Matthew Meszaros
|
cbf4190f42
|
feat: make a custom tracking domain verifiable instead of permanently "Pending DNS": the CNAME target is now this install's TRACKING_DOMAIN rather than a hardcoded t.warmbly.com that resolves nowhere, matching is exact on the label boundary (or on shared addresses, so a provider-flattened CNAME stops reading as no record at all) instead of strings.Contains, and every outcome carries the reason plus what DNS actually returned, including when the tracking host the customer is told to point at has no record of its own; a pasted URL is normalized to its host and a malformed one is rejected up front instead of saved and left pending forever; only a VERIFIED mailbox domain is used at send time with the shared host as the fallback and a campaign-feed entry saying why; pixels and click tickets are built from the configured host, and with none configured mail ships untracked rather than carrying links to another deployment's tracking service; adds GET /emails/:id/track and POST /emails/:id/track/verify plus an hourly re-resolution sweep so a record that propagates later starts being used and one that breaks stops routing links; and scopes the tracking-domain write by organization like the read, which also fixes GET /emails/:id passing a user id to an org-scoped query and 404ing for every caller
|
2026-08-24 09:02:29 -07:00 |
|
Matthew Meszaros
|
e3e1137877
|
Merge pull request #176 from warmbly/fix/orgtransfer-import-blockers
Workspace import fails on any archive with a webhook endpoint, a notification, or a campaign-scoped suppression
|
2026-08-24 09:01:58 -07:00 |
|
Matthew Meszaros
|
019b45fca7
|
feat: unblock workspace import for any archive carrying a webhook endpoint, a notification, or a campaign-scoped suppression: ResetOnImport columns are now left out of the insert so the destination's own DEFAULT applies instead of writing NULL, which aborted the whole transaction on the four NOT NULL ones (webhook_endpoints.consecutive_failures, notifications.email_state and email_attempts, ai_mcp_servers.last_error) and made a webhook endpoint arrive carrying the source's failure streak and auto-disable state, and three tables that sat above something they reference are moved below it (webhook_endpoints under oauth_applications, suppressed_recipients under campaigns, reply_intents under tasks) because referencePlan only clears a reference the run does not write at all, so a forward reference to a table written later landed as a foreign-key violation; both classes are now guarded against the live schema in TestLiveResetColumnsHaveSomethingToFallBackOn and TestLiveTablesAreInDependencyOrder, and a cross-instance export/import round trip carrying all three previously fatal tables completes with failure counters back at their defaults and the suppression rows still attributed to their campaign
|
2026-08-24 08:57:05 -07:00 |
|
Matthew Meszaros
|
7d13b6ff46
|
Merge pull request #175 from warmbly/fix/issue-168-campaign-send-loop
Fail closed when a campaign has no organization, and remove the state entirely
|
2026-08-24 08:51:08 -07:00 |
|
Matthew Meszaros
|
1018068942
|
feat: stop recipient suppression and the entitlement gate being skipped when a campaign has no organization: the send path now fails closed (an orgless campaign is paused with the reason in its activity feed instead of mailing an unsubscribed, bounced or complained address, since routing's own suppression filter joins on the campaign's organization_id and matches nothing when it is NULL), the same tenancy gate covers warmup and unibox sends and an orgless mailbox resolves to the free pool rather than defaulting into the paid one, the state is no longer creatable (sequenceRepository.Create inherits organization_id from its campaign instead of inserting NULL, campaign creation and mailbox onboarding refuse without a workspace via errx.ErrNoOrganization, guardInboxLimit / guardMailboxThrottle / the campaign creation throttle no longer treat a missing org as exempt, and a new session resolves the user's default workspace so the orgless session that produced these rows cannot exist), and migration 000092 backfills then sets organization_id NOT NULL on campaigns, contacts, email_accounts and sequences plus sessions.current_organization_id, provisioning a recovery workspace for any user with none so no row is deleted to satisfy the constraint (live-tested in TestLiveOrglessCampaignDoesNotSendToSuppressedRecipient, TestLiveSuppressedRecipientIsSkipped, TestLiveCampaignRequiresAnOrganization and TestLiveHealthyCampaignStillSends)
|
2026-08-24 08:47:08 -07:00 |
|
Matthew Meszaros
|
2e905d4fa0
|
Merge pull request #174 from warmbly/fix/campaign-stuck-processing-recurrence
fix: one waiting lead no longer parks the whole campaign (#171)
|
2026-08-24 07:53:18 -07:00 |
|