Matthew Meszaros
|
6bc6bf4345
|
feat: reword the no-eligible-mailbox pause on the start endpoint, the task auto-pause, and its test to name the only cause left (a sending behaviour profile with no working days) now that budget and window gates defer
|
2026-09-03 20:18:43 -07:00 |
|
Matthew Meszaros
|
701dc404a2
|
Merge remote-tracking branch 'origin/main' into fix/click-without-open-tracking
|
2026-09-03 05:22:05 -07:00 |
|
Matthew Meszaros
|
d9527ad98a
|
feat: reject an overflowing commit distance in the update version parser instead of clamping it, with a regression case, so a malformed build string can never suppress an available update
|
2026-09-03 05:04:30 -07:00 |
|
Matthew Meszaros
|
3d5eb36ae5
|
feat: second review pass on self-hosted updates: git describe suffixes after a prerelease (rc.1-2-gabc1234) now parse as prerelease plus commit distance, Apply validates the updater for every target including explicit tags, a DNS failure only reads as the compose profile being off when UPDATER_URL names the updater service and stays unreachable for custom hosts, the admin dialog re-checks that the update can still start before launching it and drops the confirmation when it cannot, the bare-metal installer refuses symlinks anywhere inside a build directory, restores config.js with --remove-destination and bounds every health probe, and the bare-metal docs add the single sudoers rule before the upgrade command
|
2026-09-03 05:04:30 -07:00 |
|
Matthew Meszaros
|
bb35de0455
|
feat: address review on self-hosted updates: the updater no longer re-locks its mutex when a job finishes (every job used to deadlock at completion and freeze the status API), the backend caches the updater view so the member version pill, the health checks and the admin poll share one read and an absent updater is reported as not running rather than broken, the bare-metal upgrade builds unprivileged and hands off to a root-owned fixed-path installer that refuses symlinks so sudoers allows one command instead of install/cp/rm/chown/chmod/systemctl/ln, the installer fails when the backend does not come back, the seed image gets the version build args, the dashboard gates the update action on manage_settings and stops polling a backend that answers 404, and revived timestamps are typed as Date
|
2026-09-03 05:04:30 -07:00 |
|
Matthew Meszaros
|
b2ea1f1961
|
feat: add self-hosted update awareness and one-click updates: every binary is stamped with its version and commit, the backend polls GitHub Releases and a new host-side updater (cmd/updater, compose profile or systemd unit) reports the checkout's commit distance, the admin panel's top bar shows a version pill that turns into an update indicator and opens a dialog with confirmation, live step progress and log, restart tracking and result, the dashboard header shows the same pill to every member of a self-hosted instance with the full update flow for platform admins, Setup and health gains update_available and updater_unreachable checks, warmblyctl status prints the version, make upgrade and scripts/upgrade-bare-metal.sh cover the by-hand paths, and docs gain an Updates page plus configuration, health, deployment and API reference updates
|
2026-09-03 05:04:30 -07:00 |
|
Matthew Meszaros
|
4c7229c7d6
|
feat: make a held-back click announcement retryable until its effects ran: the claim leases the click row for one attempt via announce_claimed_at and the flag clears only after the effects complete, the sweep retries expired leases as well as clicks a restart cut off, and the partial index for the sweep moves to its own concurrent migration 000125 so the live click table is never write-blocked
|
2026-09-03 04:34:30 -07:00 |
|
Matthew Meszaros
|
a3264f0d39
|
feat: address the review on the rebuilt engagement branch: a deferred human click's effects are now durable and once-only (announce_pending on the click row written before the event is marked processed, a claim in finishHumanClick and a minute-by-minute sweep that finishes what a restart cut off, a burst relabel clears the flag), the tracking service keys the source-address token with TRACKING_IP_HASH_KEY so it cannot be enumerated back into an IPv4 address, the migration no longer adds a blocking index to the existing click table, and timeline click and open rows carry task_id
|
2026-09-03 04:13:39 -07:00 |
|
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
|
579b0ac04c
|
feat: merge main into the unsubscribe branch again, carrying the UTM campaign columns alongside unsubscribe_mode in every campaign scanner, keeping unsubscribe links out of the moved link tracker and its UTM tagging, and renumbering the opt-out migration to 000124 because main released 000123 for link clicks
|
2026-09-03 03:43:47 -07:00 |
|
Matthew Meszaros
|
78c4021053
|
Merge remote-tracking branch 'origin/main' into feat/link-click-attribution
# Conflicts:
# docs/content/docs/api/reference/campaigns.mdx
# docs/content/docs/guides/campaigns.mdx
# internal/repository/pg_campaign.go
# internal/repository/pg_campaign_lifecycle.go
# web/src/components/app/campaigns/NewCampaignDialog.tsx
|
2026-09-03 03:14:19 -07:00 |
|
Matthew Meszaros
|
64f2d1637b
|
feat: address the review on the unsubscribe PR: register the suppression family in warmblyctl, require the confirm field on the browser unsubscribe POST and cap its body, render a chosen A/B variant through the template engine so its merge fields and unsubscribe link resolve, fold curly apostrophes before opt-out phrase matching, write pasted suppression lists in one transaction, clamp copy by runes instead of bytes, add the constraints NOT VALID plus a lower(email) index in migration 000123, scope the unsubscribe link type-ahead to email bodies, and document DELETE /suppressions/:id
|
2026-09-03 02:20:47 -07:00 |
|
Matthew Meszaros
|
9485503e03
|
feat: merge main into the unsubscribe branch, keeping the new campaigns.kind column alongside unsubscribe_mode in every campaign scanner and renumbering the opt-out migration to 000123 because main took 000122 for campaign kind
|
2026-09-03 01:56:38 -07:00 |
|
Matthew Meszaros
|
5eb92c601c
|
feat: give every campaign email a working opt-out: a reply-to-opt-out line by default or an unsubscribe link (workspace setting under Settings > Sending with a per-campaign override and a {{.UnsubscribeLink}} variable), signed per-recipient unsubscribe links served on the API origin so the List-Unsubscribe header no longer points at a dead warmbly.com page, a confirm page on GET with RFC 8058 one-click on POST and a resubscribe button, reply opt-out detection through the whole-word compliance lexicon with quoted history stripped, a first-class suppression list (Contacts tab, GET/POST/DELETE /suppressions with address and domain entries, audited removal, contact drawer action), the contact Subscribed flag enforced in campaign routing, migration 000122 with a shared recipient_suppressed() predicate, and docs for all of it
|
2026-09-03 01:52:40 -07:00 |
|
Matthew Meszaros
|
1cb2b65cd7
|
Merge remote-tracking branch 'origin/main' into feat/link-click-attribution
|
2026-09-03 01:42:44 -07:00 |
|
Matthew Meszaros
|
6b1ddd8bb8
|
feat: address the CodeRabbit review on the one-time email preset: rate-limit POST /campaigns-estimate as a read, count a mailbox whose sent-today counter fails as having nothing left today instead of untouched, lock the campaign row FOR UPDATE before counting email steps so concurrent inserts cannot give a one-time campaign two messages, keep a plain-text test email free of the HTML signature, and make the wizard's estimate panel say an audience beyond the two-year horizon cannot be projected instead of reading the null as one sending day
|
2026-09-03 01:42:00 -07:00 |
|
Matthew Meszaros
|
24b7e26fbb
|
feat: add the one-time email campaign preset from #288: campaigns.kind column with sequence and one_time values and a kind field on POST /campaigns, GET /campaigns?kind= and an one_time count on /campaigns-overview, a POST /campaigns-estimate endpoint projecting deduplicated segment recipients against the mailbox pool's per-day capacity and finish date, a wizard type picker whose one-time flow runs Basics, Email, Audience, Sending and Send with send-now or scheduled start and the estimate panel and then creates, links segments and starts the campaign, a guard refusing a second email step on a one-time campaign, draft/scheduled/sending/sent wording with a One-time badge and Type filter in the campaigns list, kind on the AI create_campaign_draft tool, honouring text_only in the send and test-email paths so plain-text campaigns ship no HTML part or tracking, and the campaigns, segments, API reference and endpoint docs
|
2026-09-03 01:42:00 -07:00 |
|
Matthew Meszaros
|
e8393eaeb8
|
feat: identify links by ticket in the click burst check so two tickets sharing a destination still count as a scanner walking the email, re-read a deferred human click's classification with retries and skip its effects entirely when the read keeps failing instead of assuming it was a person, and document the burst window plus one second wait
|
2026-09-03 01:36:37 -07:00 |
|
Matthew Meszaros
|
8096ca047c
|
feat: hold a human click's side effects (evidence, instant actions, webhook, live event) until the click burst window has passed and re-read its classification first, so the first click of a security scanner's burst can no longer fire a clicked automation or webhook before the second click reveals the burst; the stamp, log row and dedupe mark are still written immediately and walked back as before
|
2026-09-03 01:29:54 -07:00 |
|
Matthew Meszaros
|
b785d3b86b
|
feat: address review on per-link click attribution: read bare href values and ignore data-href when tracking anchors, compare the destination host instead of substring-matching the tracking domain, tag bare URLs in plain-text bodies when UTM tagging is on, count UTM limits in characters, expose the UTM overrides in the AI campaign tool, dedupe clicks by ticket so two links sharing a destination are two clicks, count machine_clicks only for steps with machine and no human clicks, never withdraw a click stamp that predates per-link logging, keep the coarse timeline click unless a logged click stands for it, resolve the link once per event, give auto-clicks their own tooltip and make the wizard's discard guard notice toggled settings
|
2026-09-03 01:20:48 -07:00 |
|
Matthew Meszaros
|
f579497c81
|
feat: make profile and workspace avatar changes survive a refresh: user avatar upload and remove go through the user service so the cached /auth/me copy is dropped instead of serving the old avatar for 30 minutes, the org list mapper carries avatar_url into the persisted workspace pointer it previously discarded, the avatar mutation hooks patch the cached user, org pointer and org queries with the server's answer before invalidating, all four handlers delete the replaced or removed object under our own avatars/ prefix with millisecond keys so quick replacements cannot collide on an immutable URL, the org avatar removal is audited, the uploader copy stops promising WebP and GIF, and the API docs note that remove deletes the stored file (fixes #295)
|
2026-09-03 01:06:36 -07:00 |
|
Matthew Meszaros
|
99be92f159
|
feat: attribute every campaign click to the exact link with a per-link click log (email_link_clicks) behind the contact activity timeline, campaign live feed, recent activity and the email_clicked webhook, add per-campaign automatic UTM tagging (utm_tracking with source, medium and campaign overrides, utm_content from the link text) applied at send time to the stored ticket destination, and classify opens and clicks as machine when they arrive within ten seconds of dispatch or when several links of one email are followed within five seconds, so scanner clicks are logged but never count as engagement, fire automations or send webhooks
|
2026-09-03 01:05:34 -07:00 |
|
Matthew Meszaros
|
53b932292b
|
fix: address review findings on forms v2: the list aggregates query passed a Go int into a text concatenation so pgx could not encode it and starts, identified and the trend sparkline were always empty behind a silent degrade; the forms tables sat above campaigns in the org transfer order despite carrying a campaign_id foreign key, which would fail a full import; an imported forms domain inherited the destination workspace's stale verified flag; a form whose creator was offboarded stopped creating contacts because created_by is ON DELETE SET NULL, now falling back to the workspace owner; the forms mirror check never ran when only the web-side copy changed; and the bare metal guide asked for five DNS names while certbot requested a sixth
|
2026-09-01 09:49:06 -07:00 |
|
Matthew Meszaros
|
290f1f00aa
|
Merge remote-tracking branch 'origin/main' into feature/public-forms
# Conflicts:
# cmd/backend/main.go
# docs/content/docs/guides/workspace-export-import.mdx
|
2026-09-01 09:29:53 -07:00 |
|
Matthew Meszaros
|
05d74dbe9f
|
feat: forms v2, a full redesign of the hosted form and its builder: eight one-click themes, card/wide/split layouts with a brand cover panel, classic paged and Typeform-style focus modes split on a new page_break block, logo/cover/background uploads with size, fit and veil controls, an optional header bar that can span the page or sit with the form, a real HSV colour picker and font preview, per-contact personalized links that prefill and attribute without email verification, a render-token gate so the form JSON cannot be scraped without loading the page, funnel analytics with per-page drop-off and identified visitors, a leads-style forms list and responses table, and an organization custom forms domain verified by CNAME with hourly re-checks
|
2026-09-01 09:27:28 -07:00 |
|
Matthew Meszaros
|
271b4b1625
|
feat: coalesce org segment syncs instead of dropping them, so an import's pinned contacts reach their linked campaigns in the same request: SyncOrgLinkedCampaigns now records a follow-up when a request lands while a pass is running (ImportCommit writes segment membership after its chunked Add already started one, so that pass read the old membership and left the contacts to the 2-minute sweep), the per-org state moves from a sync.Map to a mutex-guarded map so running, queued and released are one atomic transition, and two hermetic tests cover the mid-pass coalesce and the idle re-run
|
2026-09-01 04:19:59 -07:00 |
|
Matthew Meszaros
|
128e314103
|
Merge remote-tracking branch 'origin/main' into fix/issue-277
|
2026-09-01 04:03:37 -07:00 |
|
Matthew Meszaros
|
d6ddf1f170
|
feat: fix implicit-TLS SMTP on 465 and IMAP STARTTLS on 143 behind a stored per-mailbox security mode that accepts any port, stop worker ID churn orphaning mailbox assignments via flock-claimed persistent worker ids, give the unibox a standard mail-folder sidebar (inbox/sent/drafts/archive/spam/trash) backed by a provider-derived folder column, and expose the AI tool registry over REST for non-MCP function-calling agents (#283)
|
2026-09-01 03:53:19 -07:00 |
|
Matthew Meszaros
|
103145e970
|
feat: address the CodeRabbit review on the segment pin and campaign selection bar: the include override for a contact created inside a segment is now written inside the contact repository's own transaction (parsed and deduped alongside campaigns and categories, org-scoped, ON CONFLICT upsert) so a failed override write rolls the contact back instead of answering 200 with a membership that never happened, the service keeps only the pre-write existence check that turns an unknown segment into a 400, and the leads selection bar drops its bulk Delete inside a campaign so the destructive bulk action matches the row action and cannot delete workspace contacts from a Leads tab
|
2026-09-01 03:07:39 -07:00 |
|
Matthew Meszaros
|
55307b0ad9
|
feat: fix the segment dialog UI and make a contact created inside a segment join it (issue #285): the add-to-campaign picker now shows a humanised, colour-toned campaign status instead of the raw PAUSED_NO_ACCOUNTS enum, its footer wraps instead of clipping the hint mid-sentence and the Add leads button no longer breaks across two lines, campaign status labels move to a shared components/app/campaigns/status module, the Segments, Categories and Segment pages plus every unmapped settings and onboarding route get a document title so they stop reading Page not found, and POST /contacts takes a segments array that pins the new contacts in as include overrides (validated before the write, best-effort after it) which the New contact dialog sends when opened from a segment page
|
2026-09-01 02:57:30 -07:00 |
|
Matthew Meszaros
|
5e87b1bbbf
|
Merge remote-tracking branch 'origin/main' into feature/public-forms
|
2026-09-01 01:17:54 -07:00 |
|
Matthew Meszaros
|
60c9e316d9
|
feat: hosted lead-capture forms end to end: drag-and-drop builder with field settings, design panel, embed/share and submissions tabs in the dashboard, a public TanStack form app (forms/) served by the new standalone forms service (cmd/forms + internal/formserver) on FORMS_DOMAIN with per-form frame-ancestors CSP, honeypot/fill-time/Turnstile/per-IP submit protection and a same-origin JSON API proxying the backend internal API, form submissions creating contacts with categories and campaign enrollment plus realtime, audit, webhook and org-transfer coverage, migration 000114, seed forms, CI jobs, Dockerfile, systemd/nginx/compose manifests and docs
|
2026-09-01 01:17:51 -07:00 |
|
Matthew Meszaros
|
8db032a656
|
feat: address review on segment-campaign linking: a campaign_lead_removals table (migration 000115, orgtransfer-registered) records hand-removed leads so the automatic segment sync never re-adds them while a manual add or the one-shot enrol clears the record, PUT /campaigns/:id/segments rejects an omitted segment_ids so {} cannot detach everything, rows.Err checks on the four new link queries so a truncated read cannot commit as success, write-path enrolment syncs detached from the request goroutine with a per-org in-flight dedupe, per-campaign 30s deadlines in the sweep instead of one shared budget, and an error-with-retry state in the linked-segments dialog
|
2026-09-01 00:33:42 -07:00 |
|
Matthew Meszaros
|
66a105fb9f
|
feat: make segments a live campaign audience and round out lead management (issue #277): a campaign_segments link table with GET/PUT /campaigns/:id/segments (max 20, replace semantics), immediate enrolment of linked-segment members as leads plus targeted re-syncs on contact writes, segment edits, pin-ins and imports and a 2-minute backend sweep for drift, waking active campaigns and restarting completed ones through the full launch checks; a Segments manager dialog and toolbar button on the campaign Leads tab; Remove from campaign as the row action and a selection-bar bulk action so leads can leave a campaign without deleting the contact; campaign and segment pickers in the CSV import wizard backed by a new segment_ids import option that pins imported rows as include overrides; a delete guard naming linked campaigns; orgtransfer registration, live repo tests and docs for all of it
|
2026-08-31 08:45:27 -07:00 |
|
Matthew Meszaros
|
773a3ecaf2
|
Merge remote-tracking branch 'origin/main' into fix/account-reverification-button-missing
|
2026-08-31 06:26:40 -07:00 |
|
Matthew Meszaros
|
c2f5cc4e9e
|
feat: address review on the reconnect flow: a reauth whose repeat consent omits the refresh token now refuses (typed error) when the stored one cannot be read instead of sealing an empty string over it, and credential errors resolve only after the reactivation Update succeeds so a failed reactivation keeps the banner and its reconnect button
|
2026-08-31 06:26:39 -07:00 |
|
Matthew Meszaros
|
70ca90a3e0
|
feat: fix the reconnect reactivation 500 found in live testing: reconnectAccount now loads the row via GetByID itself because the org-scoped Get selects no user_id and the owner-scoped Update was handed an empty uuid; UpdateSMTPIMAPCredentials switches to GetByID with an explicit tenancy check, and the test stub's Get now mimics the real partial row
|
2026-08-31 05:53:12 -07:00 |
|
Matthew Meszaros
|
6918ee06b0
|
Merge remote-tracking branch 'origin/main' into fix/issue-273
|
2026-08-31 05:49:38 -07:00 |
|
Matthew Meszaros
|
5abfb69882
|
feat: address Greptile review on #282: re-measure useFlipPlacement/useFlipAlignment on window resize so an open popover keeps a valid side when the toolbar reflows, and trim the unibox body-key and hook comments down to their one-line invariants
|
2026-08-31 05:36:24 -07:00 |
|
Matthew Meszaros
|
896fc60f4d
|
feat: add the missing mailbox re-verification flow (issue #274): POST /emails/onboarding/oauth/reauth/:id re-runs the provider consent for an existing Gmail/Outlook mailbox (login_hint preselect, same-address check, token rewrite keeping the stored refresh token) and PUT /emails/onboarding/smtp-imap/:id replaces SMTP/IMAP credentials after live validation; both resolve the credential-class error rows and reactivate the mailbox onto its worker, the drawer's Needs attention banner gains Re-authorize and Update credentials buttons, and the mailboxes guide documents reconnecting
|
2026-08-31 05:29:35 -07:00 |
|
Matthew Meszaros
|
760cb5651c
|
feat: fix issue 273: read unibox message bodies from the key the worker actually writes (users/<owner>/emails/<mailbox>/<message>.emsg via config.StorageEndpointEmailBody) instead of the never-written emails/<owner>/<id> key, so opened messages render the full stored plain+HTML body with its line breaks instead of degrading to the collapsed one-line snippet with the truncated-message notice; thread the mailbox account id through GetBody, GetByID and the body-text search backfill, drop the dead PutBody/GetEmailKey, and in the contact activity detail stop content escaping the drawer: measured left/right alignment for the date-range popover (new useFlipAlignment hook) and wrap-anywhere on expanded detail values so long URLs cannot widen the grid past the card
|
2026-08-31 03:54:59 -07:00 |
|
Matthew Meszaros
|
d7a17a0149
|
feat: make the per-mailbox daily campaign cap configurable up to 5000 (issue #276): raise campaign_limit, campaign daily_limit and ramp start/ceiling validation to config.LimitMax, warn in the dashboard above 100/day, and update aitools, zapier and docs copy to match
|
2026-08-31 03:50:36 -07:00 |
|
Matthew Meszaros
|
7a7c6051bc
|
Merge remote-tracking branch 'origin/main' into feature/contact-segments
|
2026-08-30 02:07:56 -07:00 |
|
Matthew Meszaros
|
3bd9325bee
|
feat: address review on segments: gate the add-to-campaign and from-segment actions behind the manage-campaigns permission on the segment page, the segments list and the campaign Leads tab so the dashboard never offers an enrolment the API would refuse, trim the segment package and migration comments to the one invariant they carry, and drop the em dash from the editor comment
|
2026-08-30 00:11:19 -07:00 |
|
Matthew Meszaros
|
52916ab60d
|
feat: verification evidence engine and Greptile fixes for #264: contact_verification_evidence ledger (migration 000111) fed by clean deliveries, human opens, clicks, replies and recipient-naming bounces from the send, tracking, reply and bounce paths, a decaying score that lets real mail outrank a probe and a newer bounce outrank older engagement, verification_confidence on every contact with a reasons list and animated Deliverability card in the contact drawer, per-organization probe breakers, undeliverable counts that ignore finished leads, tighter wiring comments, and docs
|
2026-08-29 23:49:33 -07:00 |
|
Matthew Meszaros
|
daf946fc45
|
feat: add the add_to_segment and remove_from_segment sequence action steps, executed on both the scheduled campaign path and the instant reply path, wired into the backend and consumer
|
2026-08-29 23:45:05 -07:00 |
|
Matthew Meszaros
|
87b3a00aa2
|
feat: add the segment service with create/update validation, nested reference and loop checks, delete refusal when another segment depends on it, preview, member overrides, campaign enrolment that wakes the campaign, and the field catalog with the org's custom fields
|
2026-08-29 23:45:05 -07:00 |
|
Matthew Meszaros
|
3391f6a4ec
|
feat: add the segments and segment_members tables (migration 000110) and register both in the workspace export/import spec under the Contacts group
|
2026-08-29 23:45:05 -07:00 |
|
Matthew Meszaros
|
a02ff7c936
|
feat: address verification overhaul for #264: MillionVerifier as a pay-as-you-go integration plugin with org-sealed key and automatic fallback to the built-in check, built-in prober gains domain cache, Microsoft/Yahoo fingerprinting, MX fallback, disposable/role sub-statuses and a self-check breaker, imports and POST /contacts accept verification results in any known provider vocabulary with auto-detected columns, verdict provenance and expiry columns (migration 000110), campaigns park at paused_undeliverable with re-verify/send-anyway instead of finishing, POST/GET /contacts/verification bulk actions and overview, launch gate override via acknowledge_list_risk, animated verification marks, banner and settings card in the dashboard, and docs
|
2026-08-29 23:11:22 -07:00 |
|
Matthew Meszaros
|
44b2c18906
|
feat: address review on cloud-managed mailboxes: the consumer now asks the cloud to vouch for a warmup token in a mailbox it warms (GET /pool-link/instance/mailboxes/:id/warmup-tokens/:token) and files anything unverified as ordinary mail instead of dropping on a sender-controlled header, disconnect keeps local mirrors and the link until the cloud confirms the instance is released so managed mailboxes cannot be stranded, and long narrative comments are cut to one line
|
2026-08-29 10:07:36 -07:00 |
|