Matthew Meszaros
dee54417a3
Merge pull request #612 from warmbly/feature/campaign-daily-send-view
...
feat: add a Today's sending plan to the campaign overview and feed the sidebar meter and wizard estimate from the scheduler's own clamps (issue #606 )
2026-09-19 17:04:19 +00:00
Matthew Meszaros
150dc7df6e
feat: add a Today's sending plan to the campaign overview (GET /campaigns/:id/send-plan, derived through the scheduler's own gates: per-mailbox cap clamps, warmup graduation, health bands, other campaigns on the same mailbox, hours, spacing, window, plan allowance, new-lead cap and leads due, as a waterfall that adds up), feed the sidebar meter and the wizard estimate from the same clamps instead of summing configured caps, floor the campaign chain's next tick at the pool's spacing rather than one mailbox's whole gap, fold the compact Advisor strip to one line, add warmbly campaign plan and warmblyctl campaign plan, and document it (issue #606 )
2026-09-19 09:31:46 -07:00
Matthew Meszaros
62201a2dd3
feat: let each member choose, reorder and persist the contact list's columns (custom fields included) and sort on any of them: a user_view_preferences table (migration 000187) behind GET/PUT/DELETE /v1/me/views/:view, a column registry that renders the contacts and campaign Leads tables from a saved layout, a Columns chooser with drag reorder and a Sort menu on both toolbars, click-to-sort headers, sort_by custom:<key> plus company and phone sorts in POST /contacts/search resolved once for Search and SearchIDs with a nullable keyset cursor, and the contacts guide, API reference, openapi, error codes and export-import docs updated
2026-09-19 09:24:17 -07:00
Matthew Meszaros
274ff888a5
feat: rebuild two-factor setup in Settings > Security as a three-step wizard with a QR code, manual setup key and TOTP parameters, inline code errors, and recovery codes with download, copy and print; show enable date and remaining recovery codes, add POST /auth/2fa/recovery-codes to regenerate them, return two_fa_invalid_code on a mismatched code, and update the security guide, API reference, error codes and OpenAPI
2026-09-19 08:15:31 -07:00
Matthew Meszaros
e668a2a36b
feat: complete the ADA CASA v2.1.1 AL1 control set across authentication, sessions, access control, cryptography, input validation and configuration, adding a breached-password denylist and per-account login throttling, enforced multi-factor authentication on the admin panel, step-up confirmation before an action that mints a lasting credential, purpose-scoped session tokens, single-use TOTP steps, tenant verification on every cross-referenced identifier, security headers on every surface, encrypted webhook signing secrets, per-organization idempotency, PKCE and a minimal two-scope Gmail consent on the mailbox OAuth flow, bounded spreadsheet and archive decoding, a patched Go toolchain with govulncheck in CI, and the evidence pack under compliance/casa
2026-09-19 08:18:35 +02:00
Matthew Meszaros
49acd51b64
feat: stop one recurring fault burying error tracking by reporting it once per five minutes with the count it stands for, keep a cache outage from answering every signed-in request with a 500 and from taking realtime down by treating an unreachable Redis as a miss and the websocket handshake nonce nothing reads as best-effort, answer a 5xx with a sentence the reader can act on while the call site's own words go to the log against the same request id, prefer the API's own message over the HTTP class in the admin and dashboard clients, and name the fix on a schema registry refusal, an SES sandbox rejection and a mailbox check that could not be run
2026-09-19 07:39:40 +02:00
Matthew Meszaros
dcf26a2361
feat: make automatic inbox tagging atomic live and reviewable in production
2026-09-17 04:57:24 -07:00
SUMAN JANA
260898bf20
feat(inbox): automatic tagging and relevance scoring for inbound mail, optional and off by default
2026-09-17 04:57:24 -07:00
Matthew Meszaros
ae1a324801
Merge pull request #562 from rocker1166/feat/direct-mail-analytics
...
feat: add accurate direct-mail analytics and opt-in engagement tracking
2026-09-17 11:47:50 +00:00
Matthew Meszaros
93a0e39371
Merge pull request #568 from warmbly/fix/reported-issue-resolution
...
feat: preserve binary dashboard API responses so campaign lead exports remain downloadable
2026-09-17 11:28:29 +00:00
Matthew Meszaros
7402898c0d
feat: preserve binary dashboard API responses so campaign lead exports remain downloadable ( #565 )
2026-09-17 04:24:52 -07:00
SUMAN JANA
2134c7a143
feat(analytics): report on mail written by hand, with opt-in open and click tracking per mailbox
2026-09-17 10:26:25 +00:00
SUMAN JANA
ebbe432d2c
feat(unibox): search people and partial words, and offer to widen a search that found nothing in the current scope
2026-09-17 10:20:17 +00:00
Matthew Meszaros
0f5ca71155
feat: correct mailbox sending metrics and workspace analytics across dashboard surfaces
2026-09-16 21:44:42 -07:00
Matthew Meszaros
d40a95dff5
fix: give the dashboard's auth errors a real stack and stop reporting an ended session as a crash, by building AuthError per throw instead of sharing two module-level instances whose stack was captured at module evaluation, so every report pointed at "module code" rather than the call that failed, and by dropping AuthError in before_send since normalizeError already turns it into a redirect and UserProvider sends the user to sign in ( #527 )
2026-09-15 01:52:35 -07:00
Matthew Meszaros
50711a8e66
fix: a campaign's linked segments are its audience, so detaching one withdraws its leads ( #510 ) ( #523 )
...
* fix: make a campaign's linked segments the audience rather than an accumulator, so detaching one withdraws the leads it enrolled instead of leaving the old list mixed in with the new, tracked by a new campaign_leads.source that keeps a hand-picked lead, an overlapping segment's member and anyone the campaign has already emailed out of the withdrawal, and reported back as withdrawn/contacted counts the dialog confirms and explains (issue #510 )
* fix: serialize the linked-segment sweep against a link replacement by taking the same campaign lock, so a sweep that read the old set cannot re-enrol the audience the replacement just withdrew, and word the dialog's confirm and toast so the campaign, not the segment, is what has already emailed a lead
* fix: stop the one-shot segment enrol from re-stamping leads that are already in the campaign, since the Leads tab's Add back runs through it and pinning a whole linked audience as hand-picked because one held-out member was restored is the accumulation this change exists to end
* fix: lock the leads a detachment is about to withdraw before deciding, because a send is reserved by stamping campaign_contact_progress and only then locking the lead row, so a reservation committing mid-pass was invisible to the delete's snapshot and could withdraw a lead whose first email had already gone; and report the already-emailed count on every toast branch, since a detach where the whole audience had been emailed changed no count and said nothing after confirming a removal
* fix: add the campaign_leads.source check constraint NOT VALID, which still enforces every insert and update while skipping a full scan of the largest table in the product under ACCESS EXCLUSIVE to learn that a one-statement-old column holds its own default everywhere
2026-09-14 22:12:15 -07:00
Matthew Meszaros
ffd27bc46d
fix: stop every out-of-office notice and bounce opening a high-priority CRM follow-up by classifying machine replies from their headers and gating the task on a per-intent setting, and give the Tasks page multi-select with select-all-matching, bulk status, priority and delete over new PATCH and DELETE /crm/tasks endpoints (issue #471 )
2026-09-14 12:20:47 -07:00
Matthew Meszaros
8d790ede6c
feat: send from any address Google has verified a Gmail mailbox to send as and import the signature its owner already wrote in Gmail, reading both through gmail.settings.basic at connect and on demand via GET/POST /emails/:id/identity, validating the choice against the provider's own list in the service and again inside the UPDATE, clearing it when the provider stops verifying it, and never applying it to warmup ( #514 )
2026-09-14 10:13:36 -07:00
Matthew Meszaros
13e9ce8e10
feat: hold a lead whose mailbox answers out of office until they are back, resuming at the return date it names, plus a manual per-contact pause in one campaign that unsubscribing and the suppression list were the only stand-ins for
2026-09-14 09:26:06 -07:00
Matthew Meszaros
adfe4c17aa
Self-hosted pool plan: a price the server resolves, and a checkout that reaches it ( #494 )
...
* feat: make the self-hosted pool plan buyable by resolving its Stripe price server-side behind a new /pool-link/offer and /pool-link/checkout pair, adding the plans.price_yearly column the yearly price id never had, and landing the instance's Unlimited button on a dialog that names the workspace and the billing period instead of a plans grid the non-public plan never appears in
* feat: apply the pool dialog's yearly default once per opening rather than on every offer result, so a background refetch cannot move the billing period out from under someone who already chose monthly
2026-09-13 21:22:12 -07:00
Matthew Meszaros
7300b3b021
feat: full PostHog coverage: identify the signed-in user and workspace in the dashboard and admin panel with autocapture, heatmaps, dead and rage clicks, web vitals, network timing, console capture and session replay masking only password fields, send server-side signup, trial and subscription events under the user id with the organization as a group, keep the marketing site and form pages cookieless while capturing everything stateless plus a form funnel, upload the form app's source maps, and add WARMBLY_POSTHOG_SESSION_REPLAY
2026-09-13 20:58:22 -07:00
Matthew Meszaros
6b6efca865
fix: campaign follow-ups opened a new conversation instead of replying in the contact's thread, so carry In-Reply-To/References and the Gmail threadId from the previous send, give every step a reply-in-thread switch, and let a threading step inherit the conversation's subject (issue #472 ) ( #489 )
2026-09-13 20:51:41 -07:00
Matthew Meszaros
47defafa09
feat: fix the six self-host defects reported in issue #439 ( #456 )
...
* feat: fix the six defects reported in issue #439 by mapping the IMAP UNAVAILABLE, INUSE and NONEXISTENT response codes to retry-level errors instead of a critical reconnect prompt, synthesising a stable no-msgid key so one message with no Message-ID header can no longer 400 the internal map endpoint and wedge every later sync pass with its cursors held, adding mailhtml.FromText and HasContent so an API or agent-created step with a plain body stops shipping the composer's empty div placeholder as its text/html part (derived on create and plain-only update, exposed as body_html on update_campaign_step, dropped at send and preview time, and refused at campaign start with empty_step_body), honouring sender_strategy='explicit' in ResolveCampaignSenderPool and ValidateCampaignReady so an emptied explicit pool parks the campaign instead of widening it to every mailbox in the workspace, making the paused_no_accounts auto-pause loud with an error log line, an error-level activity-feed entry and an org-scoped CAMPAIGN_PAUSED realtime pulse, gating the admin sign-in's Turnstile widget on GET /v1/auth/config so a self-host with CAPTCHA_PROVIDER=none is not locked out, and parsing NATS_URL down to its host:port so a credentialed bus URL no longer reports NATS down
* feat: act on the self-review of the issue #439 fixes by dropping the campaign wizard's own escapeHtml body_html builder, which entity-escaped the quotes in a conditional and made the template fail to parse at send time, and letting the backend's FromText render that part instead so wizard-written steps also get their bare URLs linked for click tracking, correcting the docs and openapi description that claimed an explicit sender pool never falls back when it still unions its tags as migration 000013 designed, extracting the duplicated blank-HTML-part guard into dropBlankHTMLPart shared by the send path and the preview, and recording why the no-msgid key keeps the folder name despite a RENAME changing it
* feat: address the CodeRabbit review on the issue #439 fixes by holding the admin sign-in's Turnstile widget unmounted until /v1/auth/config resolves so an instance with no route to Cloudflare cannot raise a widget error on a screen nobody submitted, failing StartCampaign closed when the sequence read errors rather than skipping both the malformed-template and empty-body refusals, giving TCPCheck the default port its protocol assumes so a portless NATS_URL is no longer reported down, leaving a URL that carries a merge field unanchored because the send path renders bodies with text/template and a quoted contact value would break out of the href, and correcting the sequences guide and the Campaign and CampaignUpdate openapi descriptions that named the wrong tag field
2026-09-12 03:13:38 -07:00
Matthew Meszaros
9698052569
Merge branch 'main' into feat/cleanmylist-verification
2026-09-11 06:03:55 -07:00
Matthew Meszaros
9f61d070d4
feat: give the unibox thread header a way back out of a filing mistake, with Undo on the Archive and Delete toasts, Move to inbox while reading the Trash or Archive folder, and the actions disabled while one is in flight, plus stop the invite page offering Accept before it knows which account the browser is signed in as
2026-09-11 03:23:23 -07:00
SUMAN JANA
727ddb1482
feat: wire the unibox thread header's Mark as unread, Archive and Delete to a new PATCH /unibox/folder, add an Add as contact action for senders outside the CRM, and replace six private From-header parsers with one shared lib/helper/emailAddress that also understands the parenthesised form the IMAP sync stores, which left the reply composer's seeded To failing its own validator
2026-09-11 03:10:18 -07:00
Chris Edington
59122e6c8a
feat: add CleanMyList contact verification with API key setup and built-in fallback
2026-09-10 20:06:29 +01:00
Matthew Meszaros
a84ab48729
Merge branch 'main' into feature/posthog-error-tracking
2026-09-10 10:25:21 -07:00
Matthew Meszaros
ced741e352
feat: make PostHog the default error tracker across every runtime while keeping Sentry fully supported alongside or instead of it, by turning internal/observability/errs into a two-sink fan-out with a local-log fallback, adding $exception capture to the Go services, the Rust tracking service, the Elixir realtime service and the dashboard, admin and form apps, reporting gin panics with their route, request id, workspace and user, attaching that identity plus a route and failed-request trail to browser exceptions, and wiring POSTHOG_ERROR_TRACKING, the node join env, compose, source-map upload and the docs to match
2026-09-10 19:11:32 +02:00
Matthew Meszaros
331db196d8
feat: locate every content-check issue in the subject or the body with the exact fragments that caused it and a one-line fix, add POST /templates/analyze running the configured LLM over a campaign template for located spam findings quoted verbatim from the copy plus a rewritten subject and an overall score, verify every model quote against the draft so an invented sentence is dropped rather than shown, pin the analysis temperature so re-checking unchanged copy returns the same number, and give the editor panel a Re-check button that re-runs both passes and reports the movement since the last check
2026-09-10 09:28:00 -07:00
Matthew Meszaros
9e37ea73a8
feat: add a permanent delete for API keys, DELETE /api-keys/:id/permanent plus a Delete key button under a revoked key in the dashboard drawer and warmbly key purge, taking the key's usage logs with it and refusing any key that could still authenticate so revoking stays the step that records why a credential ended (issue #414 )
2026-09-10 05:02:05 -07:00
Matthew Meszaros
426ea45013
feat: full HTML and CSS support for campaign bodies and mailbox signatures, adding a send-time CSS inliner and a structure-aware plain-text renderer in internal/pkg/mailhtml, persisting HTML mode on a step through the previously unused sequences.body_code so a designed email is no longer gutted by the editor schema on reopen, widening the TipTap schema to hold tables, div containers, colours, fonts, sizes and alignment with toolbar controls for each, folding alignment into a single style producer because TipTap merges two style attributes by splitting on the first colon and truncated background-image url(https://...) to url("https"), replacing the regex plain-text stripper that put a whole stylesheet into the text/plain part and cost a content score eight points for a .free-trial-banner class, locating </body> past Outlook conditional comments so the signature and opt-out footer stop landing inside one, forcing the signature source view for markup a contentEditable cannot host safely, and reporting per-client compatibility findings from the preview endpoint
2026-09-09 06:34:47 -07:00
Matthew Meszaros
9ce1e060dd
feat: fix the campaign step body editor for issue #380 by registering TipTap's UndoRedo and HardBreak so Ctrl+Z and Shift+Enter work at all, normalising pasted HTML from Gmail/Outlook/Word so their blank-line scaffolding stops rendering a second gap on top of our paragraph margins, adding a workspace email image library (migration 000139, public email-images/ objects sharing the attachment storage quota, keyset-paginated /email-images endpoints) with toolbar upload, URL and library insertion, drag-drop, clipboard paste and a size/alignment/alt bubble, and adding an HTML source view that names the tags the schema would drop before switching back
2026-09-08 21:00:09 -07:00
Matthew Meszaros
0e7525866a
feat: merge main into the select-all branch and rebuild the contact selection on top of its new keyset pagination, dropping this branch's own cursor repair now that Search carries the sort key and boundary value in a SortCursor, re-extracting the shared WHERE builder from that version, and pointing SearchIDs at the contactSorts spec and the conditional campaign-count lateral so a resolved selection orders and joins exactly the way the list does
2026-09-08 20:26:34 -07:00
Matthew Meszaros
4e52e9476b
Merge remote-tracking branch 'origin/main' into feat/campaign-entry-delay
2026-09-08 07:31:31 -07:00
Matthew Meszaros
a6862477b3
feat: add a 'select all matching' selection to every contact list so a bulk action can cover the whole filtered set instead of only the loaded pages: a ContactSelection body (all + filters + exclude) on bulk update, bulk delete, verification, batch research, segment members and CRM push resolved server-side through a new SearchIDs that shares the search's WHERE builder, a Gmail-style banner and shared selection module in the contacts table, campaign Leads and segment members views, an uncapped select-all in the From contacts picker, and fixes for the keyset cursor comparing a row against itself (contact pagination repeated and skipped rows past page one) and the dashboard sending custom-field filters under the wrong wire name
2026-09-08 05:19:59 -07:00
Matthew Meszaros
99c4ec9e21
feat: answer the CodeRabbit review on #386 by making ContactImportResult.SegmentsPinned a *bool so omitempty stops collapsing a failed membership write into the same absent value as an import that asked for none, warning in the result step when a pin did not land instead of leaving it to a note under a green Import complete, holding the pinned-contacts truncation notice until the overrides listing has actually arrived so it cannot read the newest 0 of 5,000 while it loads, and pointing that notice at the contact drawer, since a pinned-out contact can never be reached from the member list it used to name
2026-09-08 05:19:03 -07:00
Matthew Meszaros
ed50ad9f2f
feat: add a campaign entry delay so a contact's first email can wait a set time after they enter the campaign, with campaigns.entry_delay_minutes and a campaign_leads.added_at anchor (migration 000136), the delay applied in the router's per-lead due check and floored into the placer through ContactSequencePair.NotBefore, a distinct entry_delay constraint in the contact next-action preview, and the control surfaced on the Schedule tab, a new Trigger card at the top of the Steps canvas, the campaign wizard's Schedule step, the launch dialog, the update_campaign AI tool and the iOS schedule page, plus guides, API reference and live scheduler and repository tests
2026-09-08 05:06:04 -07:00
Matthew Meszaros
5f665a3208
feat: teach the contact import wizard a lockedSegment that always travels with the upload, named in the header and as a fixed always-applied row above the segment picker, confirmed on the result step from the API's segments_pinned flag, and refresh the segments query alongside contacts so the page behind the wizard is right before the realtime spine event lands
2026-09-08 04:59:25 -07:00
Matthew Meszaros
c9781a1c57
feat: surface the sheet-sync segment targets in the dashboard: an Add to segments picker on the sync wizard's options step and in the source edit drawer, a lockedSegment the wizard applies on every run and shows as a fixed row, and a SyncSourcesPanel that scopes its list and its New sync to the segment it was opened from
2026-09-08 04:59:18 -07:00
Matthew Meszaros
5d60fe9a21
feat: fix campaign restart dead end (issue #340 ): resolve preflight tracking senders through the same explicit/tags/all pool the scheduler uses so a connected mailbox is never reported missing, fetch the launch dialog's step count from the steps endpoint instead of a field the campaign API never returns, turn on Keep running for new leads when a form or an automation feeds a campaign (migration 000131 backfills existing ones) and when a member starts a campaign whose every lead has finished so it goes active and waits for leads instead of answering 400, return waiting_for_leads from the start endpoint for the dialog's success screen, and document the no_leads and no_remaining_leads codes and the new behaviour in the campaigns, forms, automations and API docs
2026-09-07 02:26:50 -07:00
Matthew Meszaros
7b11489c85
feat: keep a campaign running for new leads (issue #336 ): add a continuous campaign setting (migration 000130, on by default once a segment is linked) under which a campaign that runs out of leads stays active and idle with idle_since set instead of finishing, is startable with no leads, is re-checked by the reconciler every pass and shows as waiting for leads in the dashboard list, header and live activity panel with a CAMPAIGN_IDLE realtime event; make WakeCampaigns restart a finished campaign through the full launch checks so a lead added by the contacts API, a bulk update, an import or the add-to-campaign automation action reopens it exactly like a linked segment does, and write a refused restart to the campaign activity log once an hour per reason instead of only the backend log; expose continuous on the campaign API and preferences page and document it in the campaigns, segments, forms, automations, API reference and realtime docs
2026-09-06 02:50:16 -07:00
Matthew Meszaros
3bf8d4bdfa
Merge remote-tracking branch 'origin/main' into feature/mailbox-fair-use-allowance
2026-09-04 21:25:53 -07:00
Matthew Meszaros
3596a9770f
feat: make mailboxes truly unlimited under a fair-use allowance of one mailbox per daily send the plan includes (15,000 on Business), drop the 200 hard cap and the 5-per-day connect throttle, open a request-more dialog with inline limit requests and plan upgrade when the allowance is reached, add bulk CSV mailbox import with live progress and a downloadable failed-rows file, reserve attachment storage atomically under a per-org lock so concurrent uploads cannot pass the quota ( #326 ), report storage and the allowance on the billing usage meters, and honor approved daily-send overrides in the sender
2026-09-04 21:00:26 -07:00
Matthew Meszaros
f68918d7f6
Merge remote-tracking branch 'origin/main' into feature/warmbly-cli
2026-09-04 20:14:46 -07:00
Matthew Meszaros
028689fd2e
feat: device-code sign-in for the CLI, with a browser approval page at /cli that mints a scoped API key, self-revocation at DELETE /api-keys/self so a read-only credential can always end itself, and app_url plus websocket_url on /auth/config so a client can find the dashboard and the realtime gateway on a self-hosted layout
2026-09-04 20:14:27 -07:00
Matthew Meszaros
5fd9aedb80
Merge remote-tracking branch 'origin/main' into feature/plan-upgrade-paywall-modal
2026-09-04 09:11:51 -07:00
Matthew Meszaros
d78e4423e4
fix: address the review of the notifications work — bound detached alert delivery to a fixed pool that drops and logs rather than spawning a goroutine per event, never echo a transport error that embeds the webhook URL, treat an emptied channel target as cleared so switching transport cannot post Slack payloads to a Discord URL, emit user.registered for invited signups and the worker alert after a successful reassignment, format Stripe amounts by currency exponent, refuse an upgrade while the plan list is still loading instead of silently opening the portal, apply a fixed discount to the annual invoice rather than to each month, convert proration from minor units, contain focus in the nested enterprise dialog, hide the odometer wheels from assistive tech, and drop the last dedicated-IP row from the pricing matrix
2026-09-04 06:33:20 -07:00
Matthew Meszaros
85f3dad215
feat: send the files a step actually carries by honouring campaign_attachments.sequence_id in the send, test-send and preview paths instead of attaching every campaign file to every step, count them per step in the preflight content score, and refuse an upload naming another campaign's step
2026-09-04 06:16:16 -07:00
Matthew Meszaros
81d6ba4520
Merge remote-tracking branch 'origin/main' into feature/plan-upgrade-paywall-modal
2026-09-04 06:09:14 -07:00